├── .eslintignore ├── app.js ├── server ├── service │ ├── login │ │ ├── default │ │ │ ├── config.js │ │ │ └── index.js │ │ ├── github_qiniu │ │ │ ├── config.js │ │ │ └── index.js │ │ ├── index.js │ │ └── github │ │ │ ├── config.js │ │ │ └── index.js │ └── upload │ │ ├── default │ │ ├── config.js │ │ └── index.js │ │ ├── default_qiniu │ │ ├── config.js │ │ └── index.js │ │ ├── github_default │ │ ├── config.js │ │ └── index.js │ │ ├── github_qiniu │ │ ├── config.js │ │ └── index.js │ │ ├── index.js │ │ └── qiniu │ │ ├── config.js │ │ └── index.js ├── config │ ├── loggerConfig.js │ ├── constant.js │ ├── redisConfig.js │ ├── dbConfig.js │ ├── config.js │ └── avatarConfig.js ├── router │ ├── indexRouter.js │ ├── index.js │ ├── iconRouter.js │ ├── userRouter.js │ ├── iconDraftRouter.js │ └── repoRouter.js ├── database │ ├── model │ │ ├── counter.js │ │ ├── repoRecommend.js │ │ ├── icon.js │ │ ├── iconBelongToRepo.js │ │ ├── iconDraft.js │ │ ├── user.js │ │ └── iconRepo.js │ ├── connect.js │ ├── redisStorage.js │ └── index.js ├── middleware │ ├── getUserInfo.js │ ├── auth.js │ └── install.js ├── validation │ ├── userLoginRules.js │ ├── userRegisterRules.js │ └── repoInfoRules.js ├── util │ ├── incUtil.js │ ├── stringUtil.js │ ├── responseFormat.js │ ├── log.js │ ├── cryptoUtil.js │ ├── userUtil.js │ ├── validator.js │ └── fileUtil.js ├── tool │ ├── svgSprite.js │ ├── iconTemplate │ │ ├── iconTemplate.css │ │ └── demoTemplate.html │ └── icon.js └── controller │ ├── iconDraftController.js │ ├── iconController.js │ ├── userController.js │ └── repoController.js ├── logs └── readme.md ├── gulpfile.js ├── public ├── resource │ └── docs │ │ ├── 图标管理平台结构设计.png │ │ ├── README.md │ │ └── README.html ├── static │ ├── fonts │ │ ├── ionicons.05acfdb.woff │ │ ├── ionicons.24712f6.ttf │ │ └── ionicons.2c2ae06.eot │ └── js │ │ ├── manifest.7c73e181da5eacf1a236.js │ │ └── manifest.7c73e181da5eacf1a236.js.map └── index.html ├── .npmrc ├── changelog.md ├── README.md ├── .gitignore ├── .eslintrc.js ├── bin └── start_example.sh ├── package.json ├── index.js └── LICENSE /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/*.js -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | console.log('debug') 2 | -------------------------------------------------------------------------------- /server/service/login/default/config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /logs/readme.md: -------------------------------------------------------------------------------- 1 | ## logs 文件夹必须存在, 否则在linux中运行sh ./bin/start.sh 时会报错 -------------------------------------------------------------------------------- /server/service/upload/default/config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /server/service/upload/default_qiniu/config.js: -------------------------------------------------------------------------------- 1 | module.export = {}; -------------------------------------------------------------------------------- /server/service/upload/github_default/config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /server/service/upload/github_qiniu/config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /server/config/loggerConfig.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | level: 'debug', 3 | root: '../logs/' 4 | }; 5 | -------------------------------------------------------------------------------- /server/service/upload/github_default/index.js: -------------------------------------------------------------------------------- 1 | let login = require('../default/index'); 2 | module.exports = login; -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | 3 | require('./server/tool/icon'); 4 | 5 | module.exports = gulp; 6 | -------------------------------------------------------------------------------- /server/service/login/github_qiniu/config.js: -------------------------------------------------------------------------------- 1 | let config = require('../github/config'); 2 | module.exports = config; 3 | -------------------------------------------------------------------------------- /server/service/login/github_qiniu/index.js: -------------------------------------------------------------------------------- 1 | let login = require('../github/index'); 2 | module.exports = login; 3 | -------------------------------------------------------------------------------- /server/service/upload/default_qiniu/index.js: -------------------------------------------------------------------------------- 1 | let upload = require('../qiniu/index'); 2 | module.exports = upload; 3 | -------------------------------------------------------------------------------- /server/service/upload/github_qiniu/index.js: -------------------------------------------------------------------------------- 1 | let upload = require('../qiniu/index'); 2 | module.exports = upload; 3 | -------------------------------------------------------------------------------- /public/resource/docs/图标管理平台结构设计.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-L/nicon/HEAD/public/resource/docs/图标管理平台结构设计.png -------------------------------------------------------------------------------- /public/static/fonts/ionicons.05acfdb.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-L/nicon/HEAD/public/static/fonts/ionicons.05acfdb.woff -------------------------------------------------------------------------------- /public/static/fonts/ionicons.24712f6.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-L/nicon/HEAD/public/static/fonts/ionicons.24712f6.ttf -------------------------------------------------------------------------------- /public/static/fonts/ionicons.2c2ae06.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolin-L/nicon/HEAD/public/static/fonts/ionicons.2c2ae06.eot -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | sass_binary_site = https://npmmirror.com/mirrors/node-sass/ 2 | registry = https://registry.npmmirror.com 3 | cache = ./.cache/.npm 4 | #dev = true -------------------------------------------------------------------------------- /server/router/indexRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | 3 | let router = new Router(); 4 | router.get('/', ctx => { 5 | ctx.body = 'index'; 6 | }); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /server/database/model/counter.js: -------------------------------------------------------------------------------- 1 | // 计数集合实现 2 | 3 | module.exports = { 4 | model: String, // model的名称,modelName是保留字 5 | field: String, // 自增的字段 6 | seq: {type: Number, default: 0} // 该字段的计数 7 | }; 8 | -------------------------------------------------------------------------------- /server/service/login/index.js: -------------------------------------------------------------------------------- 1 | let config = require('../../config/config'); 2 | 3 | module.exports = { 4 | login: require(`./${config.productType}`), 5 | config: require(`./${config.productType}/config`) 6 | }; 7 | -------------------------------------------------------------------------------- /server/service/upload/index.js: -------------------------------------------------------------------------------- 1 | let config = require('../../config/config') 2 | 3 | module.exports = { 4 | upload: require(`./${config.productType}`), 5 | config: require(`./${config.productType}/config`) 6 | }; 7 | -------------------------------------------------------------------------------- /server/service/login/default/index.js: -------------------------------------------------------------------------------- 1 | class DefaultLogin { 2 | async login () { 3 | return null; 4 | } 5 | } 6 | 7 | let loginIns = new DefaultLogin(); 8 | module.exports = loginIns.login.bind(loginIns); 9 | -------------------------------------------------------------------------------- /server/database/model/repoRecommend.js: -------------------------------------------------------------------------------- 1 | // index page recommend repo 2 | 3 | module.exports = { 4 | id: {type: Number, unique: true}, // recommend id 5 | repoId: {type: Number, unique: true}, // repo id 6 | repoName: String // repo name 7 | }; 8 | -------------------------------------------------------------------------------- /server/service/upload/qiniu/config.js: -------------------------------------------------------------------------------- 1 | let pe = process.env; 2 | 3 | module.exports = { 4 | accessKey: pe.QINIU_UPLOAD_ACCESS_KEY, 5 | secretKey: pe.QINIU_UPLOAD_SECRET_KEY, 6 | bucket: pe.QINIU_UPLOAD_BUCKET, 7 | cdnHost: pe.QINIU_UPLOAD_CDN_HOST 8 | }; 9 | -------------------------------------------------------------------------------- /server/config/constant.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /* 添加成员输入类型-用户ID */ 3 | MEMBER_ACCOUNT_TYPE_USER_ID: 1, 4 | 5 | /* 添加成员输入类型-邮箱 */ 6 | MEMBER_ACCOUNT_TYPE_USER_NAME: 5, 7 | 8 | /* 图标仓库列表图标库返回图标个数 */ 9 | REPO_LIST_CONTAIN_ICON_COUNT_PER_REPO: 15 10 | }; 11 | -------------------------------------------------------------------------------- /server/config/redisConfig.js: -------------------------------------------------------------------------------- 1 | let pe = process.env; 2 | 3 | module.exports = { 4 | port: pe.REDIS_PORT || 6379, 5 | host: pe.REDIS_HOST || '127.0.0.1', 6 | family: pe.REDIS_FAMILY || 4, 7 | password: pe.REDIS_PASSWORD || '', 8 | db: pe.REDIS_DB || 0 9 | }; 10 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | ## 2018-05-27 2 | 3 | 1. 添加4种默认登录&上传服务类型productType 4 | 2. 支持登录&上传服务类型自定义,代码自定义功能 5 | 3. 图标源文件在本地保留,实时添加、删除图标 6 | 4. .gitignore中保留默认登录&上传服务文件夹,用户自定义生成的忽略 7 | 5. 添加installApp 中间件,启动程序时判断启动脚本是否存在而决定是否链接数据库 8 | 9 | ## 2018-06-07 10 | 11 | 1. 添加可视化配置、自动重启 12 | 2. 更新添加可视化配置readme -------------------------------------------------------------------------------- /server/config/dbConfig.js: -------------------------------------------------------------------------------- 1 | let pe = process.env; 2 | 3 | module.exports = { 4 | dbName: pe.MONGODB_NAME || 'iconRepo', 5 | host: pe.MONGODB_HOST || '127.0.0.1', 6 | port: pe.MONGODB_PORT || 27017, 7 | username: pe.MONGODB_USERNAME || '', 8 | password: pe.MONGODB_PASSWORD || '' 9 | }; 10 | -------------------------------------------------------------------------------- /server/database/model/icon.js: -------------------------------------------------------------------------------- 1 | // 字体图标信息表 2 | 3 | module.exports = { 4 | iconId: {type: Number, unique: true}, // 字体图标Id, 唯一 5 | iconName: {type: String}, // 字体图标名称 6 | iconContent: String, // 字体图标内容 7 | ownerId: Number, // 归属者用户Id 8 | createTime: Date, // 创建时间 9 | updateTime: Date // 最后更新时间 10 | }; 11 | -------------------------------------------------------------------------------- /server/database/model/iconBelongToRepo.js: -------------------------------------------------------------------------------- 1 | // icon and repo relationship, for delete operation 2 | 3 | module.exports = { 4 | iconId: {type: Number, unique: true}, 5 | iconName: {type: String}, 6 | repos: [ 7 | { 8 | repoId: Number, 9 | repoName: String 10 | } 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /server/service/login/github/config.js: -------------------------------------------------------------------------------- 1 | let pe = process.env; 2 | let config = require('../../../config/config'); 3 | 4 | module.exports = { 5 | clientId: pe.GITHUB_LOGIN_CLIENT_ID, 6 | clientSecret: pe.GITHUB_LOGIN_CLIENT_SECRET, 7 | loginUrl: `https://github.com/login/oauth/authorize?client_id=${pe.GITHUB_LOGIN_CLIENT_ID}&redirect_uri=http://${config.host}/api/user/openid&scope=user` 8 | }; 9 | -------------------------------------------------------------------------------- /server/database/model/iconDraft.js: -------------------------------------------------------------------------------- 1 | // 字体图标信息表 2 | 3 | module.exports = { 4 | iconId: {type: Number, unique: true}, // 字体图标Id, 唯一 5 | iconName: String, // 字体图标名称 6 | iconOriginContent: String, // 字体图标原内容 7 | iconContent: String, // 字体图标显示内容 8 | svgPath: String, // svg绘制path 9 | ownerId: Number, // 归属者用户Id 10 | createTime: Date, // 创建时间 11 | updateTime: Date // 最后更新时间 12 | }; 13 | -------------------------------------------------------------------------------- /server/middleware/getUserInfo.js: -------------------------------------------------------------------------------- 1 | const userUtil = require('../util/userUtil'); 2 | let redis = require('../database/redisStorage'); 3 | 4 | module.exports = function () { 5 | return async function (ctx, next) { 6 | const sessionId = userUtil.getIconSessionCookie(ctx); 7 | let userInfo = await redis.get(sessionId); 8 | ctx.userInfo = userInfo || {}; 9 | await next(); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /server/validation/userLoginRules.js: -------------------------------------------------------------------------------- 1 | let validator = { 2 | userName: [ 3 | { 4 | type: 'isRequired', 5 | message: '请填写手机号码' 6 | }, 7 | { 8 | type: 'isMobilePhone', 9 | message: '请填写正确的手机号码' 10 | } 11 | ], 12 | password: [ 13 | { 14 | type: 'isRequired', 15 | message: '请填写密码' 16 | } 17 | ] 18 | } 19 | 20 | module.exports = validator; 21 | -------------------------------------------------------------------------------- /server/database/model/user.js: -------------------------------------------------------------------------------- 1 | // 用户个人信息表 2 | 3 | module.exports = { 4 | userId: {type: Number, unique: true}, // 用户Id, 唯一 5 | userName: {type: String, unique: true}, // 用户名称, 唯一,账号 6 | nickName: String, // 昵称 7 | fullName: String, // 全名 8 | password: String, // 密码 9 | email: String, // 邮箱 10 | avatar: String, // 头像链接 11 | repos: [ 12 | { 13 | repoId: {type: Number}, // 拥有的图标库Id 14 | repoName: String 15 | } 16 | ], 17 | createTime: Date, // 创建时间 18 | updateTime: Date // 最后更新时间 19 | }; 20 | -------------------------------------------------------------------------------- /server/config/config.js: -------------------------------------------------------------------------------- 1 | let path = require('path'); 2 | let host = process.env.productHost || 'icon.bolin.site'; 3 | module.exports = { 4 | productType: process.env.productType || 'default', 5 | url: `http://${host}`, 6 | host: `${host}`, 7 | ICON_APP_PORT: process.env.ICON_APP_PORT || 4843, 8 | rootRepoPath: path.resolve(__dirname, '../../public/resource/repository'), 9 | salt: 'NXArUDVwNlg1cGl2NUxpcTVhU241YmlGNllDOA==', 10 | autoLoginSessionExpires: 7 * 24 * 60 * 60 * 1000, // 7天 11 | defaultExpiresTime: +new Date('01-Jan-1970 00:00:10') 12 | }; 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Nicon 2 | 3 | Nicon 是一个集图标上传、展示、使用于一身的字体图标管理平台,流程简单,符合日常开发使用习惯,适合企业在内部部署使用。采用 Iconfont 字体图标替换项目中图片图标的使用,以达到缩减体积、风格统一、提高开发效率等目的。若配合设计师使用,设计师可在平台上管理图标,复用图标,减少设计图标耗费的时间,而开发只负责使用设计师维护好的图标库,减少了与设计师的交流成本。 4 | 5 | ## 优势 6 | 与其他字体图标管理平台相比,它拥有以下优势: 7 | 8 | * 使用流程简单,符合日常开发使用习惯,无需在审核管理流程中耗费时间 9 | * 部署简单,支持可视化配置部署,平台自带注册、登录功能,还有静态资源路由,只需数据库配置就可部署使用 10 | * 支持接入三方登录、资源上传到三方CDN服务器。使用更安全,资源更稳定 11 | * 支持导出资源多样化,符合多种使用场合,更有配套的导出工具[nicon-tookit](https://github.com/bolin-L/nicon-toolkit), 方便快捷 12 | 13 | ## DOCS 14 | 15 | [使用文档](http://icon.bolin.site/resource/docs/README.html) 16 | 17 | ## License 18 | MIT -------------------------------------------------------------------------------- /server/validation/userRegisterRules.js: -------------------------------------------------------------------------------- 1 | let validator = { 2 | userName: [ 3 | { 4 | type: 'isRequired', 5 | message: '请填写手机号码' 6 | }, 7 | { 8 | type: 'isMobilePhone', 9 | message: '请填写正确的手机号码' 10 | } 11 | ], 12 | password: [ 13 | { 14 | type: 'isRequired', 15 | message: '请填写密码' 16 | } 17 | ], 18 | RePassword: [ 19 | { 20 | type: 'isRequired', 21 | message: '请填写密码' 22 | } 23 | ] 24 | } 25 | 26 | module.exports = validator; 27 | -------------------------------------------------------------------------------- /server/middleware/auth.js: -------------------------------------------------------------------------------- 1 | const responseFormat = require('../util/responseFormat'); 2 | const userUtil = require('../util/userUtil'); 3 | let redis = require('../database/redisStorage'); 4 | 5 | module.exports = function () { 6 | return async function (ctx, next) { 7 | const sessionId = userUtil.getIconSessionCookie(ctx); 8 | let userInfo = await redis.get(sessionId); 9 | if (!userInfo) { 10 | ctx.body = responseFormat.responseFormat(401, '无权限', null); 11 | } else { 12 | ctx.userInfo = userInfo; 13 | await next(); 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /server/util/incUtil.js: -------------------------------------------------------------------------------- 1 | let db = require('../database/index'); 2 | let _ = require('lodash'); 3 | 4 | /** 5 | * 获取自增Id 6 | * 7 | * @param {Object} query 查询字段对象 8 | * @return {Integer} 自增Id 9 | * @return {Object} promise 10 | */ 11 | async function getIncId (query) { 12 | // 新版文档参数new为returnNewDoc, 返回更改后的对象 13 | let result = await db.counter.findOneAndUpdate(query, {$inc: {seq: _.random(10, 100)}}, {upsert: true, new: true, lean: true}); 14 | if (!result) { 15 | // 随机返回一个数,防止错误 16 | return _.random(10000, 100000); 17 | } else { 18 | return result.seq; 19 | } 20 | } 21 | 22 | module.exports = { getIncId }; 23 | -------------------------------------------------------------------------------- /server/validation/repoInfoRules.js: -------------------------------------------------------------------------------- 1 | let validator = { 2 | repoName: [ 3 | { 4 | type: 'isRequired', 5 | message: '请填写仓库名称' 6 | } 7 | ], 8 | repoPrefix: [ 9 | { 10 | type: 'isRequired', 11 | message: '请填写字体图标类型前缀' 12 | }, 13 | { 14 | type: 'is', 15 | reg: new RegExp('\\w+'), 16 | message: '请填写正确格式的字体图标类型前缀' 17 | } 18 | ], 19 | repoDescription: [ 20 | { 21 | type: 'isLength', 22 | message: '描述不能超过300字', 23 | min: 0, 24 | max: 300 25 | } 26 | ] 27 | } 28 | 29 | module.exports = validator; 30 | -------------------------------------------------------------------------------- /server/router/index.js: -------------------------------------------------------------------------------- 1 | const indexRouter = require('./indexRouter'); 2 | const repoRouter = require('./repoRouter'); 3 | const userRouter = require('./userRouter'); 4 | const iconRouter = require('./iconRouter'); 5 | const iconDraftRouter = require('./iconDraftRouter'); 6 | 7 | module.exports = class AppRouter { 8 | constructor (app) { 9 | // 首页 操作路由 10 | app.use(indexRouter.routes()); 11 | 12 | // 仓库CRUD 13 | app.use(repoRouter.routes()); 14 | 15 | // 图标CRUD 16 | app.use(iconRouter.routes()); 17 | 18 | // 图标草稿CRUD 19 | app.use(iconDraftRouter.routes()); 20 | 21 | // 用户相关操作 22 | app.use(userRouter.routes()); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /server/util/stringUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 字符串相关方法 3 | * 4 | */ 5 | module.exports = { 6 | /** 7 | * 替换字符串中指定的标记 8 | * 9 | * @param {Object} param 数据对象 10 | * @param {String} str 需要替换的字符串 11 | * @return {String} 替换后的字符串 12 | */ 13 | replaceParams (str = '', param = {}) { 14 | let reg0 = /\{(.*?)}/gi; 15 | let reg1 = /:([^-/|.]*)/gi; 16 | 17 | return str.replace(reg0, function ($0, $1) { 18 | return param[$1] ? param[$1] : $0; 19 | }).replace(reg1, function ($0, $1) { 20 | return param[$1] ? param[$1] : $0; 21 | }); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /server/tool/svgSprite.js: -------------------------------------------------------------------------------- 1 | module.exports = function (clazz, icons) { 2 | let svgSpriteStr = ` 10 | `; 11 | let symbolStr = ''; 12 | for (let icon of icons) { 13 | symbolStr += icon.iconContent.replace(/', ''); 15 | } 16 | return svgSpriteStr.replace('{placeholder}', symbolStr); 17 | }; 18 | -------------------------------------------------------------------------------- /server/database/model/iconRepo.js: -------------------------------------------------------------------------------- 1 | // 字体图标库信息表 2 | 3 | module.exports = { 4 | repoId: {type: Number, unique: true}, // 字体图标库Id, 唯一 5 | repoUrl: String, // 字体图标库git地址 6 | repoName: String, // 字体图标库名称 7 | repoDescription: String, // 字体图标库描述 8 | iconPrefix: {type: String}, // 字体图标前缀 9 | fontPath: String, // css文件中引用字体文件的路径 10 | createTime: Date, // 创建时间 11 | updateTime: Date, // 最后更新时间 12 | isPublic: Boolean, // 是否公开 13 | unSync: Boolean, // 是否有未更新 14 | cssUrl: String, // css的nos链接 15 | cssContent: String, // css的内容 16 | svgSpriteContent: String, // svgSprite的内容 17 | iconIds: [ 18 | { 19 | iconId: {type: Number}, 20 | iconName: {type: String} 21 | } 22 | ], 23 | ownerId: Number // 归属者用户Id 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | lib/ 9 | logs/*.log 10 | .cache/ 11 | public/resource/repository/ 12 | .vscode/ 13 | 14 | # reserve default service 15 | #login service 16 | server/service/login/ 17 | !server/service/login/default/ 18 | !server/service/login/github/ 19 | !server/service/login/github_qiniu/ 20 | server/service/login/index.js 21 | 22 | 23 | #upload service 24 | server/upload/login/ 25 | !server/upload/login/default/ 26 | !server/upload/login/qiniu/ 27 | !server/upload/login/github_qiniu/ 28 | server/upload/login/index.js 29 | 30 | # personal start script 31 | start.sh 32 | start.bat 33 | 34 | # Editor directories and files 35 | .idea 36 | *.suo 37 | *.ntvs* 38 | *.njsproj 39 | *.sln 40 | -------------------------------------------------------------------------------- /server/router/iconRouter.js: -------------------------------------------------------------------------------- 1 | let Router = require('koa-router'); 2 | let auth = require('../middleware/auth'); 3 | let IconController = require('../controller/iconController'); 4 | let iconControllerIns = new IconController(); 5 | 6 | let router = new Router({ 7 | prefix: '/api/icon' 8 | }); 9 | 10 | /** 11 | * 获取图标列表 12 | * 13 | */ 14 | router.get('/list/:userId', async (ctx) => { 15 | await iconControllerIns.getIconList(ctx); 16 | }); 17 | 18 | /** 19 | * delete user icon 20 | * 21 | */ 22 | router.post('/delete/:iconId', auth(), async (ctx) => { 23 | await iconControllerIns.deleteIcon(ctx); 24 | }); 25 | 26 | /** 27 | * 下载图标 28 | * 29 | */ 30 | router.get('/download/:iconId', async (ctx) => { 31 | await iconControllerIns.downloadIcon(ctx); 32 | }); 33 | 34 | module.exports = router; 35 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parser: 'babel-eslint', 6 | parserOptions: { 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: 'standard', 14 | // add your custom rules here 15 | 'rules': { 16 | // allow paren-less arrow functions 17 | 'arrow-parens': 0, 18 | 'indent': ["error", 4], 19 | // allow async-await 20 | 'generator-star-spacing': 0, 21 | // allow debugger during development 22 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 23 | 'no-return-await': 'off', 24 | 'no-new': 'off', 25 | "semi": ["off", "never"], // 禁止使用分号 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /bin/start_example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # DB config 4 | 5 | # mongodb 6 | export MONGODB_NAME=iconRepo; 7 | export MONGODB_HOST=127.0.0.1; 8 | export MONGODB_PORT=27017; 9 | export MONGODB_USERNAME=''; 10 | export MONGODB_PASSWORD=''; 11 | 12 | 13 | # redis 14 | export REDIS_FAMILY=4; 15 | export REDIS_HOST=127.0.0.1; 16 | export REDIS_PORT=6379; 17 | export REDIS_PASSWORD=''; 18 | export REDIS_DB=0; 19 | 20 | 21 | # config your website host 22 | export productHost='icon.bolin.site'; 23 | 24 | 25 | # if you want login by github and upload by qiniu, set productType 26 | export productType='github_qiniu'; 27 | 28 | 29 | # Login config 30 | 31 | # github openid login 32 | export GITHUB_LOGIN_CLIENT_ID=''; 33 | export GITHUB_LOGIN_CLIENT_SECRET=''; 34 | export GITHUB_LOGIN_REDIRECT_URI=''; 35 | 36 | 37 | 38 | # Upload config 39 | 40 | # qiniu 41 | export QINIU_UPLOAD_ACCESS_KEY=''; 42 | export QINIU_UPLOAD_SECRET_KEY=''; 43 | export QINIU_UPLOAD_BUCKET=''; 44 | export QINIU_UPLOAD_CDN_HOST=''; 45 | 46 | # start command 47 | node index.js -------------------------------------------------------------------------------- /server/tool/iconTemplate/iconTemplate.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "<%= fontName %>"; 3 | src: url('{eot}'); 4 | src: url('{eot}') format('embedded-opentype'), 5 | url('{ttf}') format('truetype'), 6 | url('{woff}') format('woff'), 7 | url('{svg}') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | 12 | [class^="<%= cssClass %>-"], [class*=" <%= cssClass %>-"]{ 13 | font-family: "<%= fontName %>" !important; 14 | -webkit-font-smoothing: antialiased; 15 | -moz-osx-font-smoothing: grayscale; 16 | font-style: normal; 17 | font-variant: normal; 18 | font-weight: normal; 19 | text-decoration: none; 20 | text-transform: none; 21 | line-height: 1; 22 | } 23 | 24 | <% _.each(glyphs, function(glyph) { %> 25 | .<%= cssClass %>-<%= glyph.name %>:before { 26 | content: "\<%= glyph.codePoint %>"; 27 | } 28 | <% }); %> 29 | 30 | <% _.each(glyphs, function(glyph) { %> 31 | .<%= cssClass %>-<%= glyph.name %>-after:after { 32 | content: "\<%= glyph.codePoint %>"; 33 | } 34 | <% }); %> -------------------------------------------------------------------------------- /server/database/connect.js: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose'); 2 | let dbConfig = require('../config/dbConfig'); 3 | let log = require('../util/log'); 4 | 5 | // 启动数据库 6 | module.exports = function () { 7 | return new Promise(function (resolve) { 8 | let dbUrl = `mongodb://${dbConfig.username}:${dbConfig.password}@${dbConfig.host}:${dbConfig.port}/${dbConfig.dbName}`; 9 | if (!dbConfig.username) { 10 | dbUrl = `mongodb://${dbConfig.host}:${dbConfig.port}/${dbConfig.dbName}`; 11 | } 12 | mongoose.connect(dbUrl, { 13 | poolSize: 20 14 | }); 15 | mongoose.connection.on('connected', function () { 16 | log.debug(`connect to mongodb success, dbUrl: ${dbUrl}`); 17 | resolve(); 18 | }); 19 | 20 | mongoose.connection.on('error', function (err) { 21 | log.error(`connect to mongodb error, err: ${err} dbUrl: ${dbUrl}`); 22 | process.exit(1); 23 | }); 24 | 25 | mongoose.connection.on('disconnected', function (err) { 26 | log.debug(`disconnect mongodb, dbUrl: ${dbUrl}, reason: ${err}`); 27 | process.exit(1); 28 | }); 29 | }) 30 | }; 31 | -------------------------------------------------------------------------------- /server/router/userRouter.js: -------------------------------------------------------------------------------- 1 | const Router = require('koa-router'); 2 | let UserController = require('../controller/userController'); 3 | let userControllerIns = new UserController(); 4 | 5 | let router = new Router({ 6 | prefix: '/api/user' 7 | }); 8 | 9 | /** 10 | * user openid login 11 | * 12 | */ 13 | router.get('/openid', async (ctx) => { 14 | await userControllerIns.userOpenIdLogin(ctx); 15 | }); 16 | 17 | /** 18 | * app login 19 | * 20 | */ 21 | router.post('/login', async (ctx) => { 22 | await userControllerIns.userLogin(ctx); 23 | }); 24 | 25 | /** 26 | * app register, login after registered 27 | * 28 | */ 29 | router.post('/register', async (ctx) => { 30 | await userControllerIns.userRegister(ctx); 31 | }); 32 | 33 | /** 34 | * app logout 35 | * 36 | */ 37 | router.post('/logout', async (ctx) => { 38 | await userControllerIns.userLogout(ctx); 39 | }); 40 | 41 | /** 42 | * get userInfo of current login user 43 | * get ICON_AUTO_LOGIN_SESSION cookie from request 44 | * 45 | */ 46 | router.get('/get', async (ctx) => { 47 | await userControllerIns.getCurLoginUserInfo(ctx); 48 | }); 49 | 50 | /** 51 | * get userInfo 52 | * get ICON_AUTO_LOGIN_SESSION cookie from request 53 | * 54 | */ 55 | router.get('/:userId/get', async (ctx) => { 56 | await userControllerIns.getUserInfo(ctx); 57 | }); 58 | module.exports = router; 59 | -------------------------------------------------------------------------------- /server/database/redisStorage.js: -------------------------------------------------------------------------------- 1 | const Redis = require('ioredis'); 2 | const redisConfig = require('../config/redisConfig'); 3 | const { Store } = require('koa-session2'); 4 | const cryptoUtil = require('../util/cryptoUtil'); 5 | const log = require('../util/log'); 6 | 7 | class RedisStore extends Store { 8 | constructor () { 9 | super(); 10 | this.redis = new Redis(redisConfig); 11 | this.redis.on('connect', () => { 12 | log.debug(`connect redis success`); 13 | }) 14 | } 15 | 16 | async get (sid, salt, ctx) { 17 | let data = await this.redis.get(`SESSION:${sid}`) || null; 18 | // log.debug(`[%s.get] get session success, sid: ${sid}`, this.constructor.name); 19 | return JSON.parse(data); 20 | } 21 | 22 | async set (session, { sid = cryptoUtil.encrypt(session), maxAge = 100000000 } = {}, ctx) { 23 | try { 24 | // Use redis set EX to automatically drop expired sessions 25 | await this.redis.set(`SESSION:${sid}`, JSON.stringify(session), 'EX', maxAge / 1000); 26 | log.debug(`[%s.set] set session success, sid: ${sid}`, this.constructor.name); 27 | } catch (e) { 28 | throw new Error(e); 29 | } 30 | return sid; 31 | } 32 | 33 | async destroy (sid, ctx) { 34 | log.debug(`destroy session ${sid}`); 35 | return await this.redis.del(`SESSION:${sid}`); 36 | } 37 | } 38 | 39 | module.exports = new RedisStore(); 40 | -------------------------------------------------------------------------------- /public/static/js/manifest.7c73e181da5eacf1a236.js: -------------------------------------------------------------------------------- 1 | !function(e){function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var r=window.webpackJsonp;window.webpackJsonp=function(t,c,i){for(var u,a,f,s=0,l=[];s { 15 | await iconDraftControllerIns.downloadIcon(ctx); 16 | }); 17 | 18 | /** 19 | * 我的草稿图标列表 20 | * 21 | */ 22 | router.get('/list', auth(), async (ctx) => { 23 | await iconDraftControllerIns.getIconDraftList(ctx); 24 | }); 25 | 26 | /** 27 | * 添加图标草稿 28 | * 29 | */ 30 | router.post('/add', auth(), async (ctx) => { 31 | await iconDraftControllerIns.saveDraftIcon(ctx); 32 | }); 33 | 34 | /** 35 | * collect to transform to draft 36 | * 37 | */ 38 | router.post('/collect', auth(), async (ctx) => { 39 | await iconDraftControllerIns.collectIcon(ctx); 40 | }); 41 | 42 | /** 43 | * 删除图标草稿 44 | * 45 | */ 46 | router.post('/delete', auth(), async (ctx) => { 47 | await iconDraftControllerIns.deleteDraftIcon(ctx); 48 | }); 49 | 50 | /** 51 | * 更新图标草稿 52 | * 53 | */ 54 | router.post('/update', auth(), async (ctx) => { 55 | await iconDraftControllerIns.updateDraftIcon(ctx); 56 | }); 57 | 58 | /** 59 | * 提交草稿转换成正式字体图标并删除 60 | * 61 | */ 62 | router.post('/2icon', auth(), async (ctx) => { 63 | await iconDraftControllerIns.changeDraft2Icon(ctx); 64 | }); 65 | 66 | module.exports = router; 67 | -------------------------------------------------------------------------------- /server/service/login/github/index.js: -------------------------------------------------------------------------------- 1 | require('request'); 2 | let rp = require('request-promise'); 3 | let config = require('./config'); 4 | 5 | class GithubOpenIdLogin { 6 | async login (ctx) { 7 | return this.getUserBaseInfo(ctx); 8 | } 9 | 10 | async getUserBaseInfo (ctx) { 11 | let query = ctx.request.query || {}; 12 | let code = query.code; 13 | 14 | let token = await this.getToken(code); 15 | return await this.getUserInfo(token); 16 | } 17 | 18 | async getToken (code) { 19 | let uri = `https://github.com/login/oauth/access_token`; 20 | let response = await rp({ 21 | uri: uri, 22 | method: 'POST', 23 | body: { 24 | client_id: config.clientId, 25 | client_secret: config.clientSecret, 26 | code: code 27 | }, 28 | json: true 29 | }); 30 | return response.access_token; 31 | } 32 | 33 | async getUserInfo (token) { 34 | let userInfo = {}; 35 | try { 36 | userInfo = JSON.parse(await rp({ 37 | uri: `https://api.github.com/user?access_token=${token}&scope=user`, 38 | method: 'GET', 39 | headers: { 40 | 'User-Agent': 'lbl-dev' 41 | } 42 | })); 43 | } catch (err) { 44 | throw new Error(err); 45 | } 46 | 47 | return { 48 | userName: userInfo.login || userInfo.name 49 | }; 50 | } 51 | } 52 | 53 | let loginIns = new GithubOpenIdLogin(); 54 | module.exports = loginIns.login.bind(loginIns); 55 | -------------------------------------------------------------------------------- /server/util/responseFormat.js: -------------------------------------------------------------------------------- 1 | // 后端返回数据格式定义 2 | // 后端操作成功 3 | // { 4 | // code: 0, 5 | // result: true | Array | Object | ... 6 | // message: "" 7 | // } 8 | 9 | // 后端操作失败|验证不通过 10 | // { 11 | // code: 0, 12 | // result: false | null 13 | // message: "后端返回的错误信息,由前端上层toast或弹框显示或input框报错" 14 | // } 15 | 16 | // 后端操作失败|验证不通过 17 | // { 18 | // code: 0, 19 | // result: false | null 20 | // message: {fieldName: {message: "后端返回的错误信息,由前端上层toast或弹框显示或input框报错", success: false}} 21 | // } 22 | 23 | // 后端操作通用提示 24 | // { 25 | // code: 100, 101, 26 | // result: null 27 | // message: "后端返回的错误信息,统一toast处理" 28 | // } 29 | 30 | // 后端操作通用提示 31 | // { 32 | // code: 500|501|502|503|504|505, // 内部错误 33 | // result: null 34 | // message: "后端返回的错误信息,统一弹框处理" 35 | // } 36 | 37 | // 后端操作通用提示 38 | // { 39 | // code: 401, 40 | // result: null 41 | // message: "后端返回的错误信息,统一弹登录框处理" 42 | // } 43 | 44 | // 后端操作通用提示 45 | // { 46 | // code: 403, 47 | // result: null 48 | // message: "后端返回的错误信息,统一弹框处理" 49 | // } 50 | 51 | function responseFormat (code, message, result) { 52 | return { 53 | code: code, 54 | message: message, 55 | result: result 56 | } 57 | } 58 | 59 | function responseFormatList (code, message, result, query) { 60 | let ret = { 61 | list: result, 62 | query: {} 63 | }; 64 | Object.assign(ret.query, query, { 65 | totalPageCount: Math.ceil(query.totalCount / query.pageSize) 66 | }); 67 | return { 68 | code: code, 69 | message: message, 70 | result: ret 71 | } 72 | } 73 | 74 | module.exports = { responseFormat, responseFormatList }; 75 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | icon-图标管理平台
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icon-server", 3 | "version": "1.0.0", 4 | "description": "字体图标管理平台 http://icon.bolin.site/#/", 5 | "main": "index.js", 6 | "scripts": { 7 | "publish": "./node_modules/pm2/bin/pm2 start index.js --name iconServer -f -e ./logs/icon.err.log -o ./logs/icon.log", 8 | "stop": "./node_modules/pm2/bin/pm2 delete iconServer", 9 | "lint": "eslint --ext .js ./ --fix", 10 | "start": "node index.js", 11 | "reset": "./node_modules/pm2/bin/pm2 reset iconServer && ./node_modules/pm2/bin/pm2 updatePM2", 12 | "restart": "./node_modules/pm2/bin/pm2 restart iconServer --update-env" 13 | }, 14 | "author": "", 15 | "license": "MIT", 16 | "dependencies": { 17 | "async": "^2.5.0", 18 | "crypto": "^1.0.1", 19 | "crypto-js": "^3.1.9-1", 20 | "ejs": "^2.5.7", 21 | "fs-extra": "^4.0.1", 22 | "gulp": "^3.9.1", 23 | "gulp-consolidate": "^0.2.0", 24 | "gulp-iconfont": "^9.0.0", 25 | "gulp-iconfont-css": "^2.1.0", 26 | "gulp-rename": "^1.2.2", 27 | "gulp-sass": "^3.1.0", 28 | "gulp-sourcemaps": "^2.6.0", 29 | "ioredis": "^3.2.1", 30 | "jwt-decode": "^2.2.0", 31 | "koa": "^2.3.0", 32 | "koa-body": "^2.5.0", 33 | "koa-bodyparser": "^2.5.0", 34 | "koa-cookie": "^1.0.0", 35 | "koa-cors": "0.0.16", 36 | "koa-router": "^7.2.1", 37 | "koa-session": "^5.5.0", 38 | "koa-session2": "^2.2.5", 39 | "koa-static": "^5.0.0", 40 | "koa-static-cache": "^5.1.2", 41 | "loadash": "^1.0.0", 42 | "mkdirp": "^0.5.1", 43 | "mongoose": "^4.12.1", 44 | "mongoose-unique-array": "^0.2.0", 45 | "pinyin": "^2.8.3", 46 | "qiniu": "^7.1.2", 47 | "querystring": "^0.2.0", 48 | "request": "^2.83.0", 49 | "request-promise": "^4.2.2", 50 | "rimraf": "^2.6.2", 51 | "safe-buffer": "^5.1.1", 52 | "svgo": "^1.0.3", 53 | "thunkify-wrap": "^1.0.4", 54 | "winston": "^2.4.0", 55 | "winston-daily-rotate-file": "^1.7.2" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "git@github.com:lbl-dev/icon.git" 60 | }, 61 | "devDependencies": { 62 | "babel-eslint": "^8.2.1", 63 | "eslint": "^4.18.0", 64 | "eslint-config-standard": "^11.0.0-beta.0", 65 | "eslint-plugin-import": "^2.8.0", 66 | "eslint-plugin-node": "^6.0.0", 67 | "eslint-plugin-promise": "^3.6.0", 68 | "eslint-plugin-standard": "^3.0.1", 69 | "gulp-rev": "^8.1.1", 70 | "lodash": "^4.17.4", 71 | "node-debug": "^0.1.0", 72 | "pm2": "^2.10.4" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/router/repoRouter.js: -------------------------------------------------------------------------------- 1 | let Router = require('koa-router'); 2 | let auth = require('../middleware/auth'); 3 | let getUserInfo = require('../middleware/getUserInfo'); 4 | let router = new Router({ 5 | prefix: '/api/repo' 6 | }); 7 | let RepoController = require('../controller/repoController'); 8 | let repoControllerIns = new RepoController(); 9 | 10 | /** 11 | * add icon repo 12 | * 13 | */ 14 | router.post('/add', auth(), async (ctx) => { 15 | await repoControllerIns.addRepo(ctx); 16 | }); 17 | 18 | /** 19 | * my icon repo list 20 | * 21 | */ 22 | router.get('/list/:userId', async (ctx) => { 23 | await repoControllerIns.getRepoList(ctx); 24 | }); 25 | 26 | /** 27 | * add icon to repo 28 | * 29 | */ 30 | router.post('/add/icon', auth(), async (ctx) => { 31 | await repoControllerIns.addIcon2Repo(ctx); 32 | }); 33 | 34 | /** 35 | * delete icon from repo 36 | * 37 | */ 38 | router.post('/:repoId/:iconId/delete', auth(), async (ctx) => { 39 | await repoControllerIns.deleteIconFromRepo(ctx); 40 | }); 41 | 42 | /** 43 | * get info of repo 44 | * 45 | */ 46 | router.get('/:repoId/get', getUserInfo(), async (ctx) => { 47 | await repoControllerIns.getRepoInfo(ctx); 48 | }); 49 | 50 | /** 51 | * update repo info 52 | * 53 | */ 54 | router.post('/:repoId/update', auth(), async (ctx) => { 55 | await repoControllerIns.updateRepoInfo(ctx); 56 | }); 57 | 58 | /** 59 | * update repo info 60 | * 61 | */ 62 | router.get('/:repoId/resource', async (ctx) => { 63 | await repoControllerIns.getRepoResource(ctx); 64 | }); 65 | 66 | /** 67 | * sync repo of database and css file 68 | * 69 | */ 70 | router.post('/:repoId/sync', auth(), async (ctx) => { 71 | await repoControllerIns.syncRepo(ctx); 72 | }); 73 | 74 | /** 75 | * add member of repo 76 | * 77 | */ 78 | router.post('/:repoId/member/add', auth(), async (ctx) => { 79 | await repoControllerIns.addMember(ctx); 80 | }); 81 | 82 | /** 83 | * get recommend repo list 84 | * 85 | */ 86 | router.get('/recommend/list', async (ctx) => { 87 | await repoControllerIns.getRecommendRepoList(ctx); 88 | }); 89 | 90 | /** 91 | * update recommend repo list 92 | * 93 | */ 94 | router.post('/recommend/add', async (ctx) => { 95 | await repoControllerIns.addRecommendRepo(ctx); 96 | }); 97 | 98 | /** 99 | * update recommend repo list 100 | * 101 | */ 102 | router.post('/recommend/delete', async (ctx) => { 103 | await repoControllerIns.deleteRecommendRepo(ctx); 104 | }); 105 | 106 | module.exports = router; 107 | -------------------------------------------------------------------------------- /server/middleware/install.js: -------------------------------------------------------------------------------- 1 | const responseFormat = require('../util/responseFormat'); 2 | let fileUtil = require('../util/fileUtil'); 3 | let log = require('../util/log'); 4 | const childProcess = require('child_process'); 5 | let path = require('path'); 6 | let startFilePath = path.resolve(__dirname, '../../bin/start.sh'); 7 | 8 | module.exports = function () { 9 | return async function (ctx) { 10 | if (ctx.originalUrl === '/api/install') { 11 | ctx.body = responseFormat.responseFormat(200, 'restart application', true); 12 | let params = ctx.request.body; 13 | let existService = ['default', 'default_qiniu', 'github_default', 'github_qiniu']; 14 | let productType = 'default'; 15 | let paramStr = ''; 16 | // 环境变量配置 17 | params.config.forEach(item => { 18 | if (item.name === 'productType') { 19 | productType = item.value; 20 | } 21 | paramStr += `export ${item.name}='${item.value}'; #${item.description} \n`; 22 | }); 23 | await fileUtil.createFile(startFilePath, `${paramStr}npm run restart;\n`); 24 | 25 | // 登录上传服务 26 | if (existService.indexOf(productType) === -1) { 27 | let loginRootPath = path.resolve(__dirname, `../service/login/${productType}`); 28 | let uploadRootPath = path.resolve(__dirname, `../service/upload/${productType}`); 29 | 30 | // 登录 31 | if (params.login.index && params.login.config) { 32 | await fileUtil.createDirector(loginRootPath); 33 | await fileUtil.createFile(path.resolve(loginRootPath, './index.js'), params.login.index); 34 | await fileUtil.createFile(path.resolve(loginRootPath, './config.js'), params.login.config); 35 | } 36 | 37 | // 上传 38 | if (params.upload.index && params.upload.config) { 39 | await fileUtil.createDirector(uploadRootPath); 40 | await fileUtil.createFile(path.resolve(uploadRootPath, './index.js'), params.upload.index); 41 | await fileUtil.createFile(path.resolve(uploadRootPath, './config.js'), params.upload.config); 42 | } 43 | } 44 | childProcess.exec(`sh ${startFilePath}`, (err) => { 45 | if (err) { 46 | log.error(`exec error: ${err}`); 47 | } 48 | }) 49 | } else { 50 | ctx.body = responseFormat.responseFormat(0, 'config params to install application', {}); 51 | } 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /server/service/upload/default/index.js: -------------------------------------------------------------------------------- 1 | let config = require('../../../config/config'); 2 | let path = require('path'); 3 | let fileUtil = require('../../../util/fileUtil'); 4 | let stringUtil = require('../../../util/stringUtil'); 5 | 6 | class DefaultUpload { 7 | async upload (dirPath) { 8 | let fontMap = await this.uploadFonts(dirPath); 9 | // 上传font完毕后替换css中的引用 10 | let cssContent = await this.replaceFontsInCss(dirPath, fontMap); 11 | let cssNosUrl = await this.uploadCss(dirPath); 12 | return {url: cssNosUrl, cssContent: cssContent}; 13 | } 14 | 15 | /** 16 | * 读取图标库生成的font文件到nos 17 | * 18 | * @param {String} dirPath 图标库文件夹路径 19 | * @return {Object} 字体文件nos链接的key=value对象 20 | */ 21 | async uploadFonts (dirPath = '') { 22 | let fontDirPath = path.join(dirPath, './fonts'); 23 | let fontFiles = await fileUtil.readDirector(fontDirPath); 24 | let fontMap = {}; 25 | 26 | for (let font of fontFiles) { 27 | let fileExt = font.match(/.*\.(\w+)$/)[1]; 28 | let fontPath = path.join(fontDirPath, font).replace(__dirname, ''); 29 | fontMap[fileExt] = fontPath.replace(`${process.cwd()}/public`, `//${config.host}`); 30 | } 31 | return fontMap; 32 | } 33 | 34 | /** 35 | * 替换css文件中的字体文件引用 36 | * 37 | * @param {Object} fontMap 字体文件nos链接的key=value对象 38 | * @param {String} dirPath 图标库文件夹路径 39 | * @return {void} 40 | */ 41 | async replaceFontsInCss (dirPath = '', fontMap = {}) { 42 | let cssDirPath = path.join(dirPath, './css'); 43 | let cssFiles = await fileUtil.readDirector(cssDirPath); 44 | let cssPath = path.join(dirPath, './css/' + (cssFiles[0] || 'icons.css')); 45 | let cssContent = await fileUtil.readFile(cssPath, {encoding: 'utf8'}); 46 | cssContent = stringUtil.replaceParams(cssContent, fontMap); 47 | await fileUtil.createFile(cssPath, cssContent); 48 | return cssContent; 49 | } 50 | 51 | /** 52 | * 图标库生成的css链接 53 | * 54 | * @param {String} dirPath 图标库文件夹路径 55 | * @return {String} css文件的nos链接 56 | */ 57 | async uploadCss (dirPath) { 58 | let cssDirPath = path.join(dirPath, './css'); 59 | let cssFiles = await fileUtil.readDirector(cssDirPath); 60 | let cssPath = path.join(dirPath, './css/' + (cssFiles[0] || 'icons.css')); 61 | return cssPath.replace(`${process.cwd()}/public`, `//${config.host}`); 62 | } 63 | } 64 | 65 | let uploadIns = new DefaultUpload(); 66 | module.exports = uploadIns.upload.bind(uploadIns); 67 | -------------------------------------------------------------------------------- /server/tool/icon.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 工程中根据res/icon/svg/*.svg 3 | * 自动生成字体图标-cqh 4 | */ 5 | 6 | let gulp = require('gulp'); 7 | let iconfont = require('gulp-iconfont'); 8 | let consolidate = require('gulp-consolidate'); 9 | let rename = require('gulp-rename'); 10 | let rev = require('gulp-rev'); 11 | let path = require('path'); 12 | let fileUtil = require('../util/fileUtil'); 13 | 14 | module.exports = { 15 | async compileSvg2Icon (repoPath, iconPrefix, fontPath = '../fonts/') { 16 | let svgPath = path.join(repoPath, './svg/*.svg'); 17 | let templatePath = path.join(__dirname, './iconTemplate/'); 18 | // 先清除文件夹,防止缓存 19 | await fileUtil.deleteDirector(path.join(repoPath, './css')); 20 | await fileUtil.deleteDirector(path.join(repoPath, './fonts')); 21 | 22 | await this.compileIcon(iconPrefix, fontPath, svgPath, templatePath, repoPath); 23 | }, 24 | compileIcon (iconPrefix, fontPath, svgPath, templatePath, repoPath) { 25 | return new Promise((resolve, reject) => { 26 | gulp.src([svgPath]) 27 | .pipe(iconfont({ 28 | fontName: iconPrefix, 29 | // prependUnicode: true, 30 | // startUnicode: 0xE001, 31 | formats: ['svg', 'ttf', 'eot', 'woff'], 32 | normalize: true, 33 | centerHorizontally: true, 34 | fontHeight: 1024 // must need for perfect icon 35 | })) 36 | .on('error', function (e) { 37 | reject(e); 38 | throw new Error(e); 39 | }) 40 | .on('glyphs', function (glyphs, options) { 41 | glyphs.forEach(function (glyph, idx, arr) { 42 | arr[idx].codePoint = glyph.unicode[0].charCodeAt(0).toString(16).toUpperCase() 43 | }); 44 | gulp.src(path.join(templatePath, './iconTemplate.css')) 45 | .pipe(consolidate('lodash', { 46 | glyphs: glyphs, 47 | fontName: iconPrefix, 48 | fontPath: '../fonts/', 49 | cssClass: iconPrefix 50 | })) 51 | // css 给demo文件用 52 | .pipe(rename('icons.css')) 53 | .pipe(rev()) 54 | .pipe(gulp.dest(path.join(repoPath, './css/'))) 55 | .on('finish', function () { 56 | console.log('css file generation over!'); 57 | }) 58 | }) 59 | .pipe(rev()) 60 | .pipe(gulp.dest(path.join(repoPath, './fonts/'))) 61 | .on('finish', function () { 62 | console.log('font generation over'); 63 | resolve(); 64 | }); 65 | }) 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /server/util/log.js: -------------------------------------------------------------------------------- 1 | /** 2 | * logger api 3 | * 4 | */ 5 | 6 | // logger config 7 | let path = require('path'); 8 | let util = require('util'); 9 | let winston = require('winston'); 10 | let fs = require('fs'); 11 | let config = process.appConfig || {}; 12 | let options = require('../config/loggerConfig'); 13 | require('winston-daily-rotate-file'); 14 | 15 | // DEFAULT config 16 | const DEFAULT = { 17 | json: !1, 18 | timestamp: function () { 19 | let time = new Date(); 20 | return util.format( 21 | '%s-%s-%s %s:%s:%s.%s', 22 | time.getFullYear(), 23 | String('0' + (time.getMonth() + 1)).slice(-2), 24 | String('0' + time.getDate()).slice(-2), 25 | String('0' + time.getHours()).slice(-2), 26 | String('0' + time.getMinutes()).slice(-2), 27 | String('0' + time.getSeconds()).slice(-2), 28 | String('00' + time.getMilliseconds()).slice(-3) 29 | ); 30 | }, 31 | formatter: function (options) { 32 | let arr = [util.format( 33 | '[%s] [%s] - %s', 34 | options.level.toUpperCase().charAt(0), 35 | options.timestamp(), 36 | options.message || '' 37 | )]; 38 | if (options.meta && Object.keys(options.meta).length > 0) { 39 | arr.push(util.format( 40 | 'meta -> %s', 41 | JSON.stringify( 42 | options.meta, function (key, value) { 43 | if (value === undefined) { 44 | return '__NOT_DEFINED__'; 45 | } 46 | return value; 47 | } 48 | )) 49 | ); 50 | } 51 | return arr.join('\n'); 52 | } 53 | }; 54 | 55 | let appRoot = config.appRoot || path.join(__dirname, '../'); 56 | let logRoot = path.join(appRoot, options.root); 57 | try { 58 | fs.accessSync(logRoot); 59 | } catch (err) { 60 | fs.mkdirSync(logRoot); 61 | } 62 | // export Logger instance 63 | module.exports = new (winston.Logger)({ 64 | level: options.level || 'debug', 65 | transports: [ 66 | new winston.transports.Console(DEFAULT), 67 | new winston.transports.File( 68 | Object.assign({}, DEFAULT, { 69 | level: 'warn', 70 | filename: path.join( 71 | logRoot, 72 | '/icon.error.log' 73 | ) 74 | }) 75 | ), 76 | new (winston.transports.DailyRotateFile)( 77 | Object.assign({}, DEFAULT, { 78 | datePattern: '.yyyyMMdd.log', 79 | filename: path.join( 80 | logRoot, 81 | '/icon' 82 | ) 83 | }) 84 | ) 85 | ], 86 | exceptionHandlers: [ 87 | new winston.transports.File( 88 | Object.assign({}, DEFAULT, { 89 | filename: path.join( 90 | logRoot, 91 | '/icon.exception.log' 92 | ) 93 | }) 94 | ) 95 | ], 96 | exitOnError: !1 97 | }); 98 | -------------------------------------------------------------------------------- /server/util/cryptoUtil.js: -------------------------------------------------------------------------------- 1 | let cryptoJs = require('crypto-js'); 2 | let crypto = require('crypto'); 3 | let config = require('../config/config'); 4 | module.exports = { 5 | /** 6 | * 加盐加密 7 | * 8 | * @param {Object|String} data 需要加密的数据 9 | * @param {String} salt 盐 10 | * @return {String} 加密后的自负窜key 11 | */ 12 | encrypt (data, salt) { 13 | if (typeof data === 'object') { 14 | try { 15 | data = JSON.stringify(data); 16 | } catch (error) { 17 | data = 'error_string_' + (+new Date()) 18 | } 19 | } 20 | return cryptoJs.AES.encrypt(data + '', salt || config.salt); 21 | }, 22 | 23 | /** 24 | * 加盐解密 25 | * 26 | * @param {String} data 加密过的密钥 27 | * @param {String} salt 加密时用的盐 28 | * @return {Object|String} 加密前数据 29 | */ 30 | decrypt (data, salt) { 31 | let bytes = cryptoJs.AES.decrypt(data.toString(), salt || config.salt); 32 | try { 33 | data = JSON.parse(bytes.toString(cryptoJs.enc.Utf8)); 34 | } catch (error) { 35 | data = bytes.toString(cryptoJs.enc.Utf8); 36 | } 37 | return data; 38 | }, 39 | 40 | /** 41 | * generate an unique seed 42 | * @return {String} unique seed 43 | */ 44 | seed: (function () { 45 | let seed = +new Date(); 46 | return function () { 47 | return String(seed++); 48 | }; 49 | })(), 50 | 51 | /** 52 | * generate a random string 53 | * @param {Number} length - length of the random string 54 | * @param {Boolean} onlyNum - whether the random string consists of only numbers 55 | * @param {Boolean} isComplex - whether the random string can contain complex chars 56 | * @return {String} generated random string 57 | */ 58 | randString: (function () { 59 | let complexChars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz!@#$%^&*()-=_+,./<>?;:[{}]\'"~`|\\'; 60 | let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz'; 61 | let numchars = '0123456789'; 62 | return function (length, onlyNum, isComplex) { 63 | let strs = isComplex ? complexChars : chars; 64 | strs = onlyNum ? numchars : strs; 65 | length = length || 10; 66 | let ret = []; 67 | for (let i = 0, it; i < length; ++i) { 68 | it = Math.floor(Math.random() * strs.length); 69 | ret.push(strs.charAt(it)); 70 | } 71 | return ret.join(''); 72 | }; 73 | })(), 74 | 75 | /** 76 | * md5 encryption 77 | * @param {String} content - content to be encrpted 78 | * @return {String} encryption 79 | */ 80 | md5 (content) { 81 | return crypto 82 | .createHash('md5') 83 | .update(content) 84 | .digest('hex'); 85 | }, 86 | 87 | /** 88 | * generate an unique key 89 | * @return {String} unique key 90 | */ 91 | uniqueKey () { 92 | let key = 'site' + this.seed() + this.randString(16); 93 | return this.md5(key); 94 | } 95 | }; 96 | -------------------------------------------------------------------------------- /server/util/userUtil.js: -------------------------------------------------------------------------------- 1 | let redis = require('../database/redisStorage'); 2 | let config = require('../config/config') 3 | 4 | module.exports = { 5 | /** 6 | * 登录,生成session 7 | * 使用时间与random参数确保前后两次session不固定 8 | * ICON_SESSION 用与判断每次请求判断用户是否是同一个 9 | * 10 | * @param {Integer} userId 用户ID 11 | * @param {Object} ctx 请求对象 12 | * @return {String} session的key 13 | */ 14 | async setIconSession (userId, ctx) { 15 | return await redis.set({ 16 | userId: userId, 17 | time: global.globalConfig.nowTime, 18 | random: Math.random(), 19 | type: 'ICON_SESSION' 20 | }); 21 | }, 22 | 23 | /** 24 | * 用于自动登录的session 25 | * 26 | * @param {Integer} userId 用户ID 27 | * @param {Object} ctx 请求对象 28 | * @return {String} session的key 29 | */ 30 | async setIconAutoLoginSession (userId, ctx) { 31 | return await redis.set({ 32 | userId: userId, 33 | time: global.globalConfig.nowTime, 34 | random: Math.random(), 35 | type: 'ICON_AUTO_LOGIN_SESSION' 36 | }); 37 | }, 38 | 39 | /** 40 | * 给客户端写会话cookie,每次请求带过来确认身份 41 | * 42 | * @param {String} value session的key 43 | * @param {Boolean} expired 是否过期 44 | * @param {Object} ctx 请求对象 45 | * @return {void} 46 | */ 47 | setIconSessionCookie (value, expired, ctx) { 48 | let expiredTime = expired ? new Date(config.defaultExpiresTime) : null; 49 | ctx.cookies.set('ICON_SESSION', value, { 50 | path: '/', 51 | domain: config.host, 52 | expires: expiredTime, 53 | httpOnly: true 54 | }) 55 | }, 56 | 57 | /** 58 | * 给客户端写自动登录cookie,每次请求带过来当会话cookie失效后实现自动登录 59 | * 60 | * @param {String} value session的key 61 | * @param {Boolean} expired 是否过期 62 | * @param {Object} ctx 请求对象 63 | * @return {void} 64 | */ 65 | setIconAutoLoginSessionCookie (value, expired, ctx) { 66 | let expiredTime = expired ? new Date(config.defaultExpiresTime) : new Date(global.globalConfig.nowTime + config.autoLoginSessionExpires); 67 | ctx.cookies.set('ICON_AUTO_LOGIN_SESSION', value, { 68 | path: '/', 69 | domain: config.host, 70 | expires: expiredTime, 71 | httpOnly: true 72 | }); 73 | }, 74 | 75 | /** 76 | * 获取客户端写会话cookie,每次请求带过来确认身份 77 | * 78 | * @param {Object} ctx 请求对象 79 | * @return {String} cookie值 80 | */ 81 | getIconSessionCookie (ctx) { 82 | return ctx.cookies.get('ICON_SESSION', { 83 | path: '/', 84 | domain: config.host, 85 | httpOnly: true 86 | }) 87 | }, 88 | 89 | /** 90 | * 获取客户端写自动登录cookie 91 | * 92 | * @param {Object} ctx 请求对象 93 | * @return {String} cookie值 94 | */ 95 | getIconAutoLoginSessionCookie (ctx) { 96 | return ctx.cookies.get('ICON_AUTO_LOGIN_SESSION', { 97 | path: '/', 98 | domain: config.host, 99 | httpOnly: true 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | let Koa = require('koa'); 2 | let app = new Koa(); 3 | const KoaStatic = require('koa-static'); 4 | let fileUtil = require('./server/util/fileUtil'); 5 | let installApp = require('./server/middleware/install'); 6 | let log = require('./server/util/log'); 7 | let bodyParser = require('koa-bodyparser'); 8 | let koaBody = require('koa-body'); 9 | let config = require('./server/config/config'); 10 | let responseFormat = require('./server/util/responseFormat'); 11 | let path = require('path'); 12 | let startFilePath = path.resolve(__dirname, './bin/start.sh'); 13 | 14 | /** 15 | * register router and connect db to expose serve 16 | * 17 | * @return {void} 18 | */ 19 | async function startServe () { 20 | if (await fileUtil.exists(startFilePath)) { 21 | let AppRouter = require('./server/router/index'); 22 | let connectDB = require('./server/database/connect'); 23 | let redis = require('./server/database/redisStorage.js'); 24 | let session = require('koa-session2'); 25 | 26 | // redis记录session 27 | app.use(session({ 28 | store: redis 29 | })); 30 | 31 | await connectDB(); 32 | 33 | // register router 34 | new AppRouter(app); 35 | } else { 36 | app.use(installApp()) 37 | } 38 | } 39 | 40 | /** 41 | * entrance 42 | * 43 | * @return {void} 44 | */ 45 | async function start () { 46 | // 处理全局错误 47 | app.use(async (ctx, next) => { 48 | try { 49 | const start = +new Date(); 50 | const result = await next(); 51 | const spendTime = +new Date() - start; 52 | const normalTTL = 350; 53 | const requestStatus = spendTime > normalTTL ? 'optimize' : 'normal'; 54 | log.debug(`[${requestStatus}] request [${ctx.originalUrl}] spend time is ${spendTime}ms ...`); 55 | return result; 56 | } catch (error) { 57 | // todo 做日志处理 58 | // 统一错误出口 59 | let er = error || {}; 60 | ctx.status = 200; 61 | let message = er.message; 62 | if (er.code === 11000) { 63 | message = (message.match(/"(.*)"/) || [])[1] + ' 名称重复,请修改!' 64 | } 65 | let stack = er.stack || er; 66 | log.error(stack); 67 | ctx.body = responseFormat.responseFormat(500, message || stack, false); 68 | } 69 | }); 70 | 71 | // 设置全局变量 72 | app.use(async (ctx, next) => { 73 | global.globalConfig = {}; 74 | Object.assign(global.globalConfig, { 75 | ctx: ctx, 76 | nowTime: +new Date(), 77 | // 数据库能暴露给客户端的字段,当查询条件用 78 | iconRepoExportFields: 'repoId iconPrefix repoName createTime iconIds fontPath ownerId', 79 | iconExportFields: 'iconId iconName iconContent ownerId', 80 | iconDraftExportFields: 'iconId iconName iconContent', 81 | userExportFields: 'userId userName nickName repos avatar' 82 | }); 83 | await next(); 84 | }); 85 | 86 | // 文件上传multiple/from-data 解析到req.body 87 | app.use(koaBody({ 88 | multipart: true, 89 | urlencoded: true 90 | })); 91 | 92 | // 请求参数解析 93 | app.use(bodyParser()); 94 | 95 | app.use(KoaStatic(path.join(__dirname, 'public'), {})); 96 | 97 | await startServe(); 98 | 99 | app.listen(config.ICON_APP_PORT, () => { 100 | log.debug(`app listen on port ${config.ICON_APP_PORT} ...`); 101 | }); 102 | 103 | app.on('error', (err) => { 104 | log.error(err); 105 | process.exit(1); 106 | }) 107 | } 108 | 109 | start(); 110 | -------------------------------------------------------------------------------- /server/util/validator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const validatorUtil = require('./validatorUtil'); 3 | const responseFormat = require('./responseFormat'); 4 | 5 | module.exports = { 6 | validate (value, rules) { 7 | let conclusion = { 8 | success: true, 9 | message: "" 10 | }, 11 | success = true, 12 | rule; 13 | 14 | if(!rules || rules.length == 0){ 15 | return conclusion; 16 | } 17 | 18 | for(let i = 0, len = rules.length; i < len; i++){ 19 | rule = rules[i]; 20 | 21 | switch (rule.type){ 22 | case 'is': 23 | success = rule.reg.test(value);//个性化的正则表达式校验 24 | break; 25 | case 'isRequired': 26 | success = !!validatorUtil.toString(value); 27 | break; 28 | case 'isFilled': 29 | success = !!validatorUtil.toString(value).trim(); 30 | break; 31 | case 'isEmail': 32 | success = validatorUtil.isEmail(value); 33 | break; 34 | case 'isMobilePhone': 35 | success = validatorUtil.isMobilePhone(value, 'zh-CN'); 36 | break; 37 | case 'isURL': 38 | success = validatorUtil.isURL(value); 39 | break; 40 | case 'isNumber': 41 | success = validatorUtil.isInt(value, rule); //同int 42 | break; 43 | 44 | case 'isId': 45 | success = validatorUtil.isId(value); //isInt 的首位不能为0, isID可以 46 | break; 47 | case 'isInt': 48 | success = validatorUtil.isInt(value, rule); 49 | break; 50 | case 'isFloat': 51 | success = validatorUtil.isFloat(value, rule); 52 | break; 53 | case 'isSoftDecimal2': 54 | success = validatorUtil.isSoftDecimal2(value, rule.min, rule.max); 55 | break; 56 | case 'isLength': 57 | success = validatorUtil.isLength(value, rule.min, rule.max); 58 | break; 59 | default: 60 | if(!rule.method){ 61 | conclusion = { 62 | success: false, 63 | message: "找不到此规则的校验方法" 64 | } 65 | }else { 66 | success = rule.method(val); //个性化函数,校验特定的变量+特定规则 67 | } 68 | break; 69 | } 70 | if(!success || !conclusion.success){ 71 | conclusion.message = rule.message || conclusion.message; 72 | conclusion.success = false; 73 | break; // 有错误则跳出 74 | } 75 | } 76 | 77 | return conclusion; 78 | }, 79 | 80 | /** 81 | * 校验参数字段是否完整,验证数据完整性 82 | * 83 | * @param {Object} params post请求参数对象 84 | * @param {Array} rules 校验字段对象 85 | * @param {Object} ctx 请求对象 86 | * @return {void} 87 | */ 88 | validateParamsField (params, rules, ctx) { 89 | for (let key of Object.keys(params)) { 90 | let conslution = this.validate(params[key], rules[key]); 91 | if (!conslution.success) { 92 | ctx.body = responseFormat.responseFormat(200, conslution.message, false); 93 | return; 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /server/tool/iconTemplate/demoTemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | edu-icon-demo 6 | 7 | 8 | 9 | 162 |
163 |

164 | Font Name: ux-icon-edu 165 |       166 | total: <%= total %> 167 |

168 |
169 |
170 |

171 | <% arr.forEach((item) => {%> 172 |
173 |
174 | 175 | <%= item.key %> 176 |
177 |
178 | 179 | 180 |
181 |
182 | <%});%> 183 |
184 | 185 |
186 |

Generated by hzchenqinhui@corp.163.com

187 |
188 | 189 | 190 | -------------------------------------------------------------------------------- /server/service/upload/qiniu/index.js: -------------------------------------------------------------------------------- 1 | let config = require('./config'); 2 | let path = require('path'); 3 | let fileUtil = require('../../../util/fileUtil'); 4 | let cryptoUtil = require('../../../util/cryptoUtil'); 5 | let stringUtil = require('../../../util/stringUtil'); 6 | let qiniu = require('qiniu'); 7 | 8 | class QiniuUpload { 9 | async upload (dirPath) { 10 | let fontMap = await this.uploadFonts(dirPath); 11 | // 上传font完毕后替换css中的引用 12 | let cssContent = await this.replaceFontsInCss(dirPath, fontMap); 13 | let cssUrl = await this.uploadCss(dirPath, cssContent); 14 | return {url: cssUrl, cssContent: cssContent}; 15 | } 16 | 17 | /** 18 | * 读取图标库生成的font文件到nos 19 | * 20 | * @param {String} dirPath 图标库文件夹路径 21 | * @return {Object} 字体文件nos链接的key=value对象 22 | */ 23 | async uploadFonts (dirPath = '') { 24 | let fontDirPath = path.join(dirPath, './fonts'); 25 | let fontFiles = await fileUtil.readDirector(fontDirPath); 26 | let fontMap = {}; 27 | 28 | for (let font of fontFiles) { 29 | let fileExt = font.match(/.*\.(\w+)$/)[1]; 30 | let fontPath = path.join(fontDirPath, font); 31 | let fontContent = await fileUtil.readFile(fontPath); 32 | let uc = this.getUploadConfig(fontPath, cryptoUtil.md5(fontContent) + '.' + fileExt); 33 | fontMap[fileExt] = '//' + config.cdnHost + '/' + await this.qiniuUpload(uc); 34 | } 35 | return fontMap; 36 | } 37 | 38 | getUploadConfig (localFile, key) { 39 | let mac = new qiniu.auth.digest.Mac(config.accessKey, config.secretKey); 40 | let options = { 41 | scope: config.bucket 42 | }; 43 | let putPolicy = new qiniu.rs.PutPolicy(options); 44 | let uploadToken = putPolicy.uploadToken(mac); 45 | let uploadConfig = new qiniu.conf.Config(); 46 | uploadConfig.zone = qiniu.zone.Zone_z0; 47 | let formUploader = new qiniu.form_up.FormUploader(uploadConfig); 48 | let putExtra = new qiniu.form_up.PutExtra(); 49 | return { 50 | localFile, 51 | key, 52 | uploadToken, 53 | formUploader, 54 | putExtra 55 | } 56 | } 57 | 58 | async qiniuUpload (options) { 59 | return new Promise(function (resolve) { 60 | options.formUploader.putFile(options.uploadToken, options.key, options.localFile, options.putExtra, 61 | function (respErr, respBody, respInfo) { 62 | if (respErr) { 63 | throw new Error(respErr); 64 | } 65 | if (respInfo.statusCode === 200) { 66 | resolve(respBody.key); 67 | } else { 68 | console.log(respInfo.statusCode); 69 | console.log(respBody); 70 | } 71 | }); 72 | }) 73 | } 74 | 75 | /** 76 | * 替换css文件中的字体文件引用 77 | * 78 | * @param {Object} fontMap 字体文件nos链接的key=value对象 79 | * @param {String} dirPath 图标库文件夹路径 80 | * @return {void} 81 | */ 82 | async replaceFontsInCss (dirPath = '', fontMap = {}) { 83 | let cssDirPath = path.join(dirPath, './css'); 84 | let cssFiles = await fileUtil.readDirector(cssDirPath); 85 | let cssPath = path.join(dirPath, './css/' + (cssFiles[0] || 'icons.css')); 86 | let cssContent = await fileUtil.readFile(cssPath, {encoding: 'utf8'}); 87 | cssContent = stringUtil.replaceParams(cssContent, fontMap); 88 | await fileUtil.createFile(cssPath, cssContent); 89 | return cssContent; 90 | } 91 | 92 | /** 93 | * 图标库生成的css链接 94 | * 95 | * @param {String} dirPath 图标库文件夹路径 96 | * @return {String} css文件的nos链接 97 | */ 98 | async uploadCss (dirPath, cssContent) { 99 | let cssDirPath = path.join(dirPath, './css'); 100 | let cssFiles = await fileUtil.readDirector(cssDirPath); 101 | let cssPath = path.join(dirPath, './css/' + (cssFiles[0] || 'icons.css')); 102 | let uc = this.getUploadConfig(cssPath, cryptoUtil.md5(cssContent) + '.css'); 103 | return '//' + config.cdnHost + '/' + await this.qiniuUpload(uc); 104 | } 105 | } 106 | 107 | let uploadIns = new QiniuUpload(); 108 | module.exports = uploadIns.upload.bind(uploadIns); 109 | -------------------------------------------------------------------------------- /server/database/index.js: -------------------------------------------------------------------------------- 1 | let mongoose = require('mongoose'); 2 | let Schema = mongoose.Schema; 3 | const userModel = require('./model/user'); 4 | const iconModel = require('./model/icon'); 5 | const iconDraftModel = require('./model/iconDraft'); 6 | const iconLibModel = require('./model/iconRepo'); 7 | const counterModel = require('./model/counter'); 8 | const iconBelongToRepoModel = require('./model/iconBelongToRepo'); 9 | const repoRecommendModel = require('./model/repoRecommend'); 10 | class MongoDB { 11 | /** 12 | * 构造函数 13 | * 14 | * @param {String} name 集合名称 15 | * @param {Object} model 集合model模型 16 | * @return {Object} 集合对象 17 | */ 18 | constructor (name, model) { 19 | if (!name || !model) { 20 | return; 21 | } 22 | this.model = this.schema(name, model); 23 | } 24 | 25 | /** 26 | * 单例模式,获取某一个collection对象 27 | * @param {String} name 集合名称 28 | * @param {String} model 数据模型 29 | * @return {Object} 集合对象 30 | */ 31 | static getModel (name, model) { 32 | if (!!name && !!model) { 33 | return new this(name, model); 34 | } 35 | } 36 | 37 | /** 38 | * 添加记录,返回添加成功对象 39 | * 40 | * @param {Object} data 数据对象 41 | * @return {Object} 集合对象 42 | */ 43 | async add (data) { 44 | return await this.model.create(data); 45 | } 46 | 47 | /** 48 | * 删除记录,返回添加成功对象 49 | * 50 | * @param {Object} data 数据对象 51 | */ 52 | async delete (data) { 53 | return await this.model.remove(data); 54 | } 55 | 56 | /** 57 | * 更新记录,返回添加更新对象 58 | * 59 | * @param {Object} condition 查询条件 60 | * @param {Object} data 数据对象 61 | * @param {Object} options 更新配置 62 | * @return {Object} 集合对象 63 | */ 64 | async update (condition, data, options = {}) { 65 | return await this.model.update(condition, data, options) 66 | } 67 | 68 | /** 69 | * 查找,返回对象数组 70 | * 71 | * @param {Object} data 查询条件 72 | * @param {String} fields 需要返回的字段, 多个字段空格分开,默认全部 73 | * @param {Object} options 选择配置{lean: true} 74 | * @return {Object} 集合对象 75 | */ 76 | async find (data, fields = '', options = {}) { 77 | options = Object.assign({lean: true}, options); 78 | return await this.model.find(data, fields, options); 79 | } 80 | 81 | /** 82 | * 查找,返回第一个数据对象 83 | * 84 | * @param {Object} data 查询条件 85 | * @param {String} fields 需要返回的字段, 多个字段空格分开,默认全部 86 | * @param {Object} options 选择配置{lean: true} 87 | * @return {Object} 集合对象 88 | */ 89 | async findOne (data, fields = '', options = {lean: true}) { 90 | let result = await this.model.find(data, fields, options); 91 | return (result[0] || null) 92 | } 93 | 94 | /** 95 | * 查找并更新,返回第一个数据对象 96 | * 97 | * @param {Object} data 查询条件 98 | * @param {Object} update 更新的字段 99 | * @param {Object} options 选择配置{lean: true} 100 | * @return {Object} 集合对象 101 | */ 102 | async findOneAndUpdate (data, update, options = {lean: true}) { 103 | let result = await this.model.findOneAndUpdate(data, update, options); 104 | return (result || null) 105 | } 106 | 107 | /** 108 | * 查找该条件下的所有条目,返回数量 109 | * 110 | * @param {Object} data 查询条件 111 | * @param {String} fields 需要返回的字段, 多个字段空格分开,默认全部 112 | * @param {Object} options 选择配置{lean: true} 113 | * @return {Object} 集合对象 114 | */ 115 | async count (data) { 116 | return await this.model.count(data); 117 | } 118 | 119 | /** 120 | * 生成mongoose collection实例 121 | * 122 | * @param {String} name 集合名称 123 | * @param {Object} model 集合model模型 124 | * @return {Object} 集合对象 125 | */ 126 | schema (name, model) { 127 | let schema = new Schema(model); 128 | // 保证嵌套数组中 unique起作用 129 | return mongoose.model(name, schema); 130 | } 131 | } 132 | 133 | module.exports = { 134 | user: MongoDB.getModel('user', userModel), 135 | icon: MongoDB.getModel('icon', iconModel), 136 | iconRepo: MongoDB.getModel('iconRepo', iconLibModel), 137 | counter: MongoDB.getModel('counter', counterModel), 138 | iconDraft: MongoDB.getModel('iconDraft', iconDraftModel), 139 | iconBelongToRepo: MongoDB.getModel('iconBelongToRepo', iconBelongToRepoModel), 140 | repoRecommend: MongoDB.getModel('repoRecommend', repoRecommendModel) 141 | }; 142 | -------------------------------------------------------------------------------- /server/util/fileUtil.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const SVGO = require('svgo'); 3 | let rimraf = require('rimraf'); 4 | let mkdirp = require('mkdirp'); 5 | 6 | let svgConfig = { 7 | plugins: [{ 8 | cleanupAttrs: true, 9 | }, { 10 | removeDoctype: true, 11 | }, { 12 | removeXMLProcInst: true, 13 | }, { 14 | removeComments: true, 15 | }, { 16 | removeMetadata: true, 17 | }, { 18 | removeTitle: true, 19 | }, { 20 | removeDesc: true, 21 | }, { 22 | removeUselessDefs: true, 23 | }, { 24 | removeEditorsNSData: true, 25 | }, { 26 | removeEmptyAttrs: true, 27 | }, { 28 | removeHiddenElems: true, 29 | }, { 30 | removeEmptyText: true, 31 | }, { 32 | removeEmptyContainers: true, 33 | }, { 34 | removeViewBox: false, 35 | }, { 36 | cleanUpEnableBackground: true, 37 | }, { 38 | convertStyleToAttrs: true, 39 | }, { 40 | convertColors: false, 41 | }, { 42 | convertPathData: false, 43 | }, { 44 | convertTransform: true, 45 | }, { 46 | removeUnknownsAndDefaults: true, 47 | }, { 48 | removeNonInheritableGroupAttrs: true, 49 | }, { 50 | removeUselessStrokeAndFill: true, 51 | }, { 52 | removeUnusedNS: true, 53 | }, { 54 | cleanupIDs: true, 55 | }, { 56 | cleanupNumericValues: true, 57 | }, { 58 | moveElemsAttrsToGroup: true, 59 | }, { 60 | moveGroupAttrsToElems: true, 61 | }, { 62 | collapseGroups: true, 63 | }, { 64 | removeRasterImages: false, 65 | }, { 66 | mergePaths: false, 67 | }, { 68 | convertShapeToPath: true, 69 | }, { 70 | sortAttrs: true, 71 | }, { 72 | transformsWithOnePath: false, 73 | }, { 74 | removeDimensions: true, 75 | }] 76 | }; 77 | 78 | module.exports = { 79 | /** 80 | * 递归创建文件夹 81 | * 82 | * @param {String} fPath 文件夹路径 83 | * @return {Object} 成功对象 84 | */ 85 | async createDirector (fPath) { 86 | if (!fPath) { 87 | throw new Error('file fPath error'); 88 | } 89 | await mkdirp.sync(fPath); 90 | return true; 91 | }, 92 | 93 | /** 94 | * 读取文件夹中的所有文件 95 | * 96 | * @param {String} fPath 文件夹路径 97 | * @param {Object} options 读取配置 98 | * @return {Array} 文件夹下的文件数组 99 | */ 100 | async readDirector (fPath, options) { 101 | if (!fPath) { 102 | throw new Error('file fPath error') 103 | } 104 | return await fs.readdirSync(fPath, options) 105 | }, 106 | 107 | /** 108 | * 读取文件夹中的文件 109 | * 110 | * @param {String} fPath 文件夹路径 111 | * @param {Object} options 读取配置 112 | * @return {Object} data 文件内容 113 | */ 114 | async readFile (fPath, options = {}) { 115 | if (!fPath || !await this.exists(fPath)) { 116 | throw new Error('file fPath error') 117 | } 118 | return await fs.readFileSync(fPath, options); 119 | }, 120 | 121 | /** 122 | * 删除文件夹中的文件 123 | * 124 | * @param {String} fPath 文件夹路径 125 | * @return {Object} promise 126 | */ 127 | async deleteFile (fPath) { 128 | if (!fPath) { 129 | throw new Error('file fPath error') 130 | } 131 | if (this.exists(fPath)) { 132 | return await fs.unlinkSync(fPath); 133 | } else { 134 | return true; 135 | } 136 | }, 137 | 138 | /** 139 | * 删除文件夹中的文件 140 | * 141 | * @param {String} fPath 文件夹路径 142 | * @return {Object} promise 143 | */ 144 | async deleteDirector (fPath) { 145 | if (!fPath) { 146 | throw new Error('file fPath error') 147 | } 148 | await rimraf.sync(fPath); 149 | return true; 150 | }, 151 | 152 | /** 153 | * 创建文件 154 | * 155 | * @param {String} fPath 文件路径 156 | * @param {String} content 文件内容 157 | * @param {String} encode 文件编码 158 | * @return {void} 159 | */ 160 | async createFile (fPath, content, encode = 'utf8') { 161 | if (!fPath) { 162 | throw new Error('file fPath error') 163 | } 164 | return await fs.writeFileSync(fPath, content, encode); 165 | }, 166 | 167 | /** 168 | * 删除文件夹中的文件 169 | * 170 | * @param {String} fPath 文件夹路径 171 | */ 172 | async exists (fPath) { 173 | let result = await fs.existsSync(fPath); 174 | return !!result; 175 | }, 176 | 177 | /** 178 | * 对svg内容进行处理 179 | * 180 | * @param {String} content 文件内容 181 | * @param {Boolean} resetColor 重置颜色 182 | * @return {Object} 处理后的对象 183 | */ 184 | async formatSvgFile (content, resetColor = false) { 185 | // 添加style 186 | let styleValue = 'width: 1em; height: 1em;vertical-align: middle;overflow: hidden;'; 187 | if (/style=\"\S*\"/.test(content)) { 188 | content = content.replace(/style="(.*?)"/, `style="${styleValue}"`); 189 | } else { 190 | content = content.replace(/ { 96 | return icon.iconName.match(query.q); 97 | }); 98 | } 99 | return iconRepoItem.iconIds.map((icon) => { 100 | return icon.iconId 101 | }); 102 | } 103 | 104 | /** 105 | * 获取当前用户的字体图标列表 106 | * 107 | * @param {Object} userInfo 用户基本信息 108 | * @param {Object} query 请求参数对象 109 | * @return {Array} 字体图标对象数组 110 | */ 111 | async getIconListByUserId (userInfo, query) { 112 | let result = await db.icon.find({ 113 | ownerId: userInfo.userId, 114 | iconName: query.q 115 | }, global.globalConfig.iconExportFields, 116 | { 117 | limit: parseInt(query.pageSize), 118 | skip: parseInt((query.pageIndex - 1) * query.pageSize), 119 | sort: { 120 | createTime: -1 121 | } 122 | } 123 | ); 124 | query.totalCount = await db.icon.count({ 125 | ownerId: userInfo.userId, 126 | iconName: query.q 127 | }); 128 | return result; 129 | } 130 | 131 | /** 132 | * 获取当前用户的字体图标列表 133 | * 134 | * @param {Object} userInfo 用户基本信息 135 | * @param {Object} query 请求参数对象 136 | * @return {Array} 字体图标对象数组 137 | */ 138 | async getAllIconList (query) { 139 | let result = await db.icon.find({ 140 | iconName: query.q 141 | }, global.globalConfig.iconExportFields, 142 | { 143 | limit: parseInt(query.pageSize), 144 | skip: parseInt((query.pageIndex - 1) * query.pageSize), 145 | sort: { 146 | createTime: -1 147 | } 148 | } 149 | ); 150 | query.totalCount = await db.icon.count({ 151 | iconName: query.q 152 | }); 153 | return result; 154 | } 155 | 156 | /** 157 | * 获取当前图标库下的字体图标列表 158 | * 159 | * @param {Number} repoId 图标库Id 160 | * @param {Object} query 请求参数对象 161 | * @param {Array} iconIds 字体图标id数组 162 | * @return {Array} 字体图标对象数组 163 | */ 164 | async getIconListByRepoId (repoId, query, iconIds) { 165 | let result = []; 166 | for (let i = (query.pageIndex - 1) * query.pageSize || 0; i < Math.min(iconIds.length, query.pageIndex * query.pageSize); i++) { 167 | let iconItem = await db.icon.findOne({ 168 | iconId: iconIds[i] 169 | }, global.globalConfig.iconExportFields 170 | ); 171 | result.push(iconItem) 172 | } 173 | query.totalCount = iconIds.length; 174 | return result; 175 | } 176 | 177 | /** 178 | * 获取当前用户的字体图标列表 179 | * 180 | * @param {Object} userInfo 用户基本信息 181 | * @param {Object} query 请求参数对象 182 | * @param {Array} iconIds 字体图标id数组 183 | * @return {Array} 字体图标对象数组 184 | */ 185 | async getIconListNotInRepoByUnique (userInfo, query, iconIds) { 186 | let result = await db.icon.find({ 187 | ownerId: userInfo.userId, 188 | iconId: { 189 | $nin: iconIds 190 | }, 191 | iconName: query.q 192 | }, global.globalConfig.iconExportFields, 193 | { 194 | limit: parseInt(query.pageSize), 195 | skip: parseInt((query.pageIndex - 1) * query.pageSize), 196 | sort: { 197 | createTime: -1 198 | } 199 | } 200 | ); 201 | query.totalCount = await db.icon.count({ 202 | ownerId: userInfo.userId, 203 | iconId: { 204 | $nin: iconIds 205 | }, 206 | iconName: query.q 207 | }); 208 | return result; 209 | } 210 | 211 | /** 212 | * 删除字体图标 213 | * 214 | * @param {Object} ctx 请求对象 215 | * @return {void} 216 | */ 217 | async deleteIcon (ctx) { 218 | let userInfo = ctx.userInfo; 219 | let params = ctx.params; 220 | let iconItem = await db.icon.findOne({ 221 | iconId: params.iconId 222 | }); 223 | if (!iconItem) { 224 | ctx.body = responseFormat.responseFormat(500, '无此图标!', false); 225 | return; 226 | } 227 | // check privilege 228 | if (userInfo.userId !== iconItem.ownerId) { 229 | ctx.body = responseFormat.responseFormat(403, '无权限', false); 230 | return; 231 | } 232 | 233 | // check dependence 234 | let iconRelationshipItem = await db.iconBelongToRepo.findOne({ 235 | iconId: params.iconId 236 | }); 237 | 238 | if (iconRelationshipItem && iconRelationshipItem.repos.length > 0) { 239 | let message = '该图标已经加入图标库: '; 240 | for (let repo of iconRelationshipItem.repos) { 241 | message += repo.repoName + '、'; 242 | } 243 | message += ', 请移除后再删除!'; 244 | ctx.body = responseFormat.responseFormat(500, message, false); 245 | return; 246 | } 247 | 248 | // delete icon 249 | await db.icon.delete({ 250 | ownerId: userInfo.userId, 251 | iconId: params.iconId 252 | }); 253 | log.debug(`user ${userInfo.userId} delete icon ${params.iconId}`); 254 | ctx.body = responseFormat.responseFormat(200, '删除成功!', false); 255 | } 256 | 257 | /** 258 | * 下载字体图标 259 | * 260 | * @param {Object} ctx 请求对象 261 | * @return {void} 262 | */ 263 | async downloadIcon (ctx) { 264 | let params = ctx.params || {}; 265 | let iconItem = await db.icon.findOne({ 266 | iconId: params.iconId 267 | }); 268 | if (!iconItem) { 269 | ctx.body = responseFormat.responseFormat(200, '无此图标!', false); 270 | return; 271 | } 272 | // 强制客户端直接下载svg headers 273 | ctx.set('Content-Type', 'application/force-download'); 274 | ctx.set('Content-disposition', 'attachment; filename=' + iconItem.iconName + '.svg'); 275 | ctx.body = iconItem.iconContent; 276 | } 277 | }; 278 | 279 | module.exports = IconController; 280 | -------------------------------------------------------------------------------- /server/controller/userController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * user login & register & logout operation 3 | * 4 | */ 5 | 6 | let config = require('../config/config'); 7 | let log = require('../util/log'); 8 | let responseFormat = require('../util/responseFormat'); 9 | let userRegisterRules = require('../validation/userRegisterRules'); 10 | let userLoginRules = require('../validation/userLoginRules'); 11 | let validator = require('../util/validator'); 12 | let db = require('../database'); 13 | let redis = require('../database/redisStorage'); 14 | let incUtil = require('../util/incUtil'); 15 | let cryptoUtil = require('../util/cryptoUtil'); 16 | let userUtil = require('../util/userUtil'); 17 | let avatarConfig = require('../config/avatarConfig'); 18 | let loginService = require('../service/login'); 19 | 20 | class UserController { 21 | /** 22 | * user openid login 23 | * 24 | * @param {Object} ctx request object 25 | */ 26 | async userOpenIdLogin (ctx) { 27 | let userInfo = await loginService.login(ctx) 28 | if (!userInfo) { 29 | throw new Error('login fail, no userInfo return'); 30 | } 31 | if (!userInfo.userName) { 32 | throw new Error('login fail, userInfo.userName is required'); 33 | } 34 | await this.appThirdLogin(ctx, userInfo); 35 | } 36 | 37 | /** 38 | * app login 39 | * 40 | * @param {Object} ctx request object 41 | * @param {Object} userInfo user info 42 | * @return {void} 43 | */ 44 | async appThirdLogin (ctx, userInfo) { 45 | let existUser = await db.user.findOne({ 46 | userName: userInfo.userName 47 | }, global.globalConfig.userExportFields); 48 | // exist go login or go register 49 | if (existUser) { 50 | await this.userThirdLogin(ctx, existUser) 51 | } else { 52 | await this.userThirdRegister(ctx, { 53 | userName: userInfo.userName, 54 | password: userInfo.password, 55 | email: userInfo.email, 56 | nickName: userInfo.nickname, 57 | fullName: userInfo.fullname 58 | }) 59 | } 60 | } 61 | 62 | /** 63 | * app third user login 64 | * 65 | * @param {Object} ctx request object 66 | * @param {Object} userInfo user info from app database 67 | */ 68 | async userThirdLogin (ctx, userInfo) { 69 | let sessionId = await userUtil.setIconSession(userInfo.userId, ctx); 70 | let autoLoginSessionId = await userUtil.setIconAutoLoginSession(userInfo.userId, ctx); 71 | // generate session to redis and set cookie to client 72 | userUtil.setIconSessionCookie(sessionId, false, ctx); 73 | userUtil.setIconAutoLoginSessionCookie(autoLoginSessionId, false, ctx); 74 | // redirect to index page after login success 75 | // todo redirect to where request come from 76 | ctx.redirect(config.url); 77 | } 78 | 79 | /** 80 | * app third user register use user info from openid login 81 | * 82 | * @param {Object} ctx request object 83 | * @param {Object} params openid userInfo 84 | */ 85 | async userThirdRegister (ctx, params) { 86 | // get increment userId 87 | let userId = await incUtil.getIncId({model: 'user', field: 'userId'}); 88 | // set random avatar 89 | let index = parseInt(Math.random() * 105); 90 | let avatar = avatarConfig[index]; 91 | 92 | // build register userInfo 93 | Object.assign(params, { 94 | createTime: global.globalConfig.nowTime, 95 | updateTime: global.globalConfig.nowTime, 96 | avatar: avatar, 97 | userId: userId 98 | }); 99 | log.debug(`user register and info: ${JSON.stringify(params)}`); 100 | 101 | // save userInfo to app database 102 | await db.user.add(params); 103 | let sessionId = await userUtil.setIconSession(userId, ctx); 104 | let autoLoginSessionId = await userUtil.setIconAutoLoginSession(userId, ctx); 105 | // generate session to redis and set cookie to client 106 | userUtil.setIconSessionCookie(sessionId, false, ctx); 107 | userUtil.setIconAutoLoginSessionCookie(autoLoginSessionId, false, ctx); 108 | ctx.redirect(config.url); 109 | } 110 | 111 | /** 112 | * 用户登录 113 | * 114 | * @param {Object} ctx 请求对象 115 | */ 116 | async userLogin (ctx) { 117 | let params = ctx.request.body || {}; 118 | // 验证数据完整性 119 | validator.validateParamsField(params, userLoginRules, ctx); 120 | 121 | // 查询数据库,判断唯一性 122 | let userInfo = await db.user.findOne({ 123 | userName: params.userName 124 | }); 125 | if (userInfo) { 126 | // 密码校验,正确则生成session 127 | if (userInfo.password === params.password) { 128 | let sessionId = await userUtil.setIconSession(userInfo.userId, ctx); 129 | let autoLoginSessionId = await userUtil.setIconAutoLoginSession(userInfo.userId, ctx); 130 | // 生成session后给给客户端写cookie 131 | userUtil.setIconSessionCookie(sessionId, false, ctx); 132 | userUtil.setIconAutoLoginSessionCookie(autoLoginSessionId, false, ctx); 133 | delete userInfo.password; 134 | ctx.body = responseFormat.responseFormat(200, '', userInfo); 135 | } else { 136 | ctx.body = responseFormat.responseFormat(200, {password: { 137 | message: `密码错误`, 138 | success: false 139 | }}, false); 140 | } 141 | } else { 142 | ctx.body = responseFormat.responseFormat(200, {userName: { 143 | message: `账号不存在!`, 144 | success: false 145 | }}, false); 146 | } 147 | } 148 | 149 | /** 150 | * 用户注册 151 | * 152 | * @param {Object} ctx 请求对象 153 | */ 154 | async userRegister (ctx) { 155 | let params = ctx.request.body || {}; 156 | // 验证数据完整性 157 | validator.validateParamsField(params, userRegisterRules, ctx); 158 | 159 | // 查询数据库,判断唯一性 160 | let userResult = await db.user.findOne({ 161 | userName: params.userName 162 | }); 163 | if (userResult) { 164 | ctx.body = responseFormat.responseFormat(200, {userName: { 165 | message: `用户名已经存在, 请直接登录`, 166 | success: false 167 | }}, false); 168 | return; 169 | } 170 | 171 | // 获取唯一自增Id 172 | let userId = await incUtil.getIncId({model: 'user', field: 'userId'}); 173 | // set random avatar 174 | let index = parseInt(Math.random() * 105); 175 | let avatar = avatarConfig[index]; 176 | 177 | // 构建完整用户注册数据 178 | Object.assign(params, { 179 | createTime: global.globalConfig.nowTime, 180 | updateTime: global.globalConfig.nowTime, 181 | avatar: avatar, 182 | userId: userId, 183 | userName: String(params.userName) 184 | }); 185 | 186 | // 保存用户信息到数据库 187 | await db.user.add(params); 188 | let sessionId = await userUtil.setIconSession(userId, ctx); 189 | let autoLoginSessionId = await userUtil.setIconAutoLoginSession(userId, ctx); 190 | // 生成session后给给客户端写cookie 191 | userUtil.setIconSessionCookie(sessionId, false, ctx); 192 | userUtil.setIconAutoLoginSessionCookie(autoLoginSessionId, false, ctx); 193 | 194 | delete params.password; 195 | ctx.body = responseFormat.responseFormat(200, '', params); 196 | } 197 | 198 | /** 199 | * app user logout 200 | * 201 | * @param {Object} ctx request object 202 | */ 203 | async userLogout (ctx) { 204 | const sessionId = ctx.cookies.get('ICON_SESSION', { 205 | domain: config.host, 206 | path: '/', 207 | httpOnly: true 208 | }); 209 | const autoLoginSessionId = ctx.cookies.get('ICON_AUTO_LOGIN_SESSION', { 210 | domain: config.host, 211 | path: '/', 212 | httpOnly: true 213 | }); 214 | await redis.destroy(sessionId); 215 | await redis.destroy(autoLoginSessionId); 216 | userUtil.setIconSessionCookie(sessionId, true, ctx); 217 | userUtil.setIconAutoLoginSessionCookie(autoLoginSessionId, true, ctx); 218 | ctx.body = responseFormat.responseFormat(200, '', { 219 | loginUrl: loginService.config.loginUrl 220 | }); 221 | } 222 | 223 | /** 224 | * get current login userInfo 225 | * 226 | * @param {Object} ctx request object 227 | */ 228 | async getCurLoginUserInfo (ctx) { 229 | const autoLoginSessionId = ctx.cookies.get('ICON_AUTO_LOGIN_SESSION', { 230 | domain: config.host, 231 | path: '/', 232 | httpOnly: true 233 | }); 234 | // return loginUrl if not login 235 | let userInfo = { 236 | loginUrl: loginService.config.loginUrl 237 | }; 238 | if (autoLoginSessionId) { 239 | // return userInfo and cookie ICON_SESSION if user exist 240 | let exist = await redis.get(autoLoginSessionId); 241 | // decrypt cookie for get userId 242 | let userInfoCur = cryptoUtil.decrypt(autoLoginSessionId, ''); 243 | userInfoCur = await db.user.findOne({userId: userInfoCur.userId}, global.globalConfig.userExportFields); 244 | if (userInfoCur) { 245 | userInfo = userInfoCur; 246 | } 247 | if (exist && userInfoCur && userInfoCur.userId) { 248 | // set cookie to client 249 | let sessionId = await userUtil.setIconSession(userInfoCur.userId, ctx); 250 | userUtil.setIconSessionCookie(sessionId, false, ctx); 251 | } else if (userInfoCur && userInfoCur.userId) { 252 | // user exist but session expired in redis, need to generate session to redis then auto login 253 | let autoLoginSessionId = await userUtil.setIconAutoLoginSession(userInfoCur.userId, ctx); 254 | let sessionId = await userUtil.setIconSession(userInfoCur.userId, ctx); 255 | userUtil.setIconSessionCookie(sessionId, false, ctx); 256 | userUtil.setIconAutoLoginSessionCookie(autoLoginSessionId, false, ctx); 257 | } 258 | } 259 | ctx.body = responseFormat.responseFormat(200, '', userInfo); 260 | } 261 | 262 | /** 263 | * get specific userInfo 264 | * 265 | * @param {Object} ctx request object 266 | */ 267 | async getUserInfo (ctx) { 268 | let params = ctx.params; 269 | let userInfo = await db.user.findOne({userId: params.userId}, global.globalConfig.userExportFields); 270 | if (!userInfo) { 271 | ctx.body = responseFormat.responseFormat(500, 'user not exist', false); 272 | } else { 273 | ctx.body = responseFormat.responseFormat(200, '', userInfo); 274 | } 275 | } 276 | }; 277 | 278 | module.exports = UserController; 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /public/static/js/manifest.7c73e181da5eacf1a236.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack:///static/js/manifest.7c73e181da5eacf1a236.js","webpack:///webpack/bootstrap d3c4cbeff33ceac1bd8c"],"names":["modules","__webpack_require__","moduleId","installedModules","exports","module","i","l","call","parentJsonpFunction","window","chunkIds","moreModules","executeModules","chunkId","result","resolves","length","installedChunks","push","Object","prototype","hasOwnProperty","shift","s","2","e","onScriptComplete","script","onerror","onload","clearTimeout","timeout","chunk","Error","undefined","installedChunkData","Promise","resolve","promise","reject","head","document","getElementsByTagName","createElement","type","charset","async","nc","setAttribute","src","p","0","1","setTimeout","appendChild","m","c","value","d","name","getter","o","defineProperty","configurable","enumerable","get","n","__esModule","object","property","oe","err","console","error"],"mappings":"CAAS,SAAUA,GCuCnB,QAAAC,GAAAC,GAGA,GAAAC,EAAAD,GACA,MAAAC,GAAAD,GAAAE,OAGA,IAAAC,GAAAF,EAAAD,IACAI,EAAAJ,EACAK,GAAA,EACAH,WAUA,OANAJ,GAAAE,GAAAM,KAAAH,EAAAD,QAAAC,IAAAD,QAAAH,GAGAI,EAAAE,GAAA,EAGAF,EAAAD,QA1DA,GAAAK,GAAAC,OAAA,YACAA,QAAA,sBAAAC,EAAAC,EAAAC,GAIA,IADA,GAAAX,GAAAY,EAAAC,EAAAT,EAAA,EAAAU,KACQV,EAAAK,EAAAM,OAAoBX,IAC5BQ,EAAAH,EAAAL,GACAY,EAAAJ,IACAE,EAAAG,KAAAD,EAAAJ,GAAA,IAEAI,EAAAJ,GAAA,CAEA,KAAAZ,IAAAU,GACAQ,OAAAC,UAAAC,eAAAd,KAAAI,EAAAV,KACAF,EAAAE,GAAAU,EAAAV,GAIA,KADAO,KAAAE,EAAAC,EAAAC,GACAG,EAAAC,QACAD,EAAAO,SAEA,IAAAV,EACA,IAAAP,EAAA,EAAYA,EAAAO,EAAAI,OAA2BX,IACvCS,EAAAd,IAAAuB,EAAAX,EAAAP,GAGA,OAAAS,GAIA,IAAAZ,MAGAe,GACAO,EAAA,EA6BAxB,GAAAyB,EAAA,SAAAZ,GA+BA,QAAAa,KAEAC,EAAAC,QAAAD,EAAAE,OAAA,KACAC,aAAAC,EACA,IAAAC,GAAAf,EAAAJ,EACA,KAAAmB,IACAA,GACAA,EAAA,MAAAC,OAAA,iBAAApB,EAAA,aAEAI,EAAAJ,OAAAqB,IAvCA,GAAAC,GAAAlB,EAAAJ,EACA,QAAAsB,EACA,UAAAC,SAAA,SAAAC,GAA0CA,KAI1C,IAAAF,EACA,MAAAA,GAAA,EAIA,IAAAG,GAAA,GAAAF,SAAA,SAAAC,EAAAE,GACAJ,EAAAlB,EAAAJ,IAAAwB,EAAAE,IAEAJ,GAAA,GAAAG,CAGA,IAAAE,GAAAC,SAAAC,qBAAA,WACAf,EAAAc,SAAAE,cAAA,SACAhB,GAAAiB,KAAA,kBACAjB,EAAAkB,QAAA,QACAlB,EAAAmB,OAAA,EACAnB,EAAAI,QAAA,KAEA/B,EAAA+C,IACApB,EAAAqB,aAAA,QAAAhD,EAAA+C,IAEApB,EAAAsB,IAAAjD,EAAAkD,EAAA,aAAArC,EAAA,KAAwEsC,EAAA,uBAAAC,EAAA,wBAAsDvC,GAAA,KAC9H,IAAAkB,GAAAsB,WAAA3B,EAAA,KAgBA,OAfAC,GAAAC,QAAAD,EAAAE,OAAAH,EAaAc,EAAAc,YAAA3B,GAEAW,GAIAtC,EAAAuD,EAAAxD,EAGAC,EAAAwD,EAAAtD,EAGAF,EAAAK,EAAA,SAAAoD,GAA2C,MAAAA,IAG3CzD,EAAA0D,EAAA,SAAAvD,EAAAwD,EAAAC,GACA5D,EAAA6D,EAAA1D,EAAAwD,IACAxC,OAAA2C,eAAA3D,EAAAwD,GACAI,cAAA,EACAC,YAAA,EACAC,IAAAL,KAMA5D,EAAAkE,EAAA,SAAA9D,GACA,GAAAwD,GAAAxD,KAAA+D,WACA,WAA2B,MAAA/D,GAAA,SAC3B,WAAiC,MAAAA,GAEjC,OADAJ,GAAA0D,EAAAE,EAAA,IAAAA,GACAA,GAIA5D,EAAA6D,EAAA,SAAAO,EAAAC,GAAsD,MAAAlD,QAAAC,UAAAC,eAAAd,KAAA6D,EAAAC,IAGtDrE,EAAAkD,EAAA,IAGAlD,EAAAsE,GAAA,SAAAC,GAA8D,KAApBC,SAAAC,MAAAF,GAAoBA","file":"static/js/manifest.7c73e181da5eacf1a236.js","sourcesContent":["/******/ (function(modules) { // webpackBootstrap\n/******/ \t// install a JSONP callback for chunk loading\n/******/ \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n/******/ \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n/******/ \t\t// add \"moreModules\" to the modules object,\n/******/ \t\t// then flag all \"chunkIds\" as loaded and fire callback\n/******/ \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n/******/ \t\tfor(;i < chunkIds.length; i++) {\n/******/ \t\t\tchunkId = chunkIds[i];\n/******/ \t\t\tif(installedChunks[chunkId]) {\n/******/ \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n/******/ \t\t\t}\n/******/ \t\t\tinstalledChunks[chunkId] = 0;\n/******/ \t\t}\n/******/ \t\tfor(moduleId in moreModules) {\n/******/ \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n/******/ \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n/******/ \t\t\t}\n/******/ \t\t}\n/******/ \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n/******/ \t\twhile(resolves.length) {\n/******/ \t\t\tresolves.shift()();\n/******/ \t\t}\n/******/ \t\tif(executeModules) {\n/******/ \t\t\tfor(i=0; i < executeModules.length; i++) {\n/******/ \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n/******/ \t\t\t}\n/******/ \t\t}\n/******/ \t\treturn result;\n/******/ \t};\n/******/\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// objects to store loaded and loading chunks\n/******/ \tvar installedChunks = {\n/******/ \t\t2: 0\n/******/ \t};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId]) {\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\ti: moduleId,\n/******/ \t\t\tl: false,\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.l = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/ \t// This file contains only the entry chunk.\n/******/ \t// The chunk loading function for additional chunks\n/******/ \t__webpack_require__.e = function requireEnsure(chunkId) {\n/******/ \t\tvar installedChunkData = installedChunks[chunkId];\n/******/ \t\tif(installedChunkData === 0) {\n/******/ \t\t\treturn new Promise(function(resolve) { resolve(); });\n/******/ \t\t}\n/******/\n/******/ \t\t// a Promise means \"currently loading\".\n/******/ \t\tif(installedChunkData) {\n/******/ \t\t\treturn installedChunkData[2];\n/******/ \t\t}\n/******/\n/******/ \t\t// setup Promise in chunk cache\n/******/ \t\tvar promise = new Promise(function(resolve, reject) {\n/******/ \t\t\tinstalledChunkData = installedChunks[chunkId] = [resolve, reject];\n/******/ \t\t});\n/******/ \t\tinstalledChunkData[2] = promise;\n/******/\n/******/ \t\t// start chunk loading\n/******/ \t\tvar head = document.getElementsByTagName('head')[0];\n/******/ \t\tvar script = document.createElement('script');\n/******/ \t\tscript.type = 'text/javascript';\n/******/ \t\tscript.charset = 'utf-8';\n/******/ \t\tscript.async = true;\n/******/ \t\tscript.timeout = 120000;\n/******/\n/******/ \t\tif (__webpack_require__.nc) {\n/******/ \t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n/******/ \t\t}\n/******/ \t\tscript.src = __webpack_require__.p + \"static/js/\" + chunkId + \".\" + {\"0\":\"0340c7d1b0e2e05b8117\",\"1\":\"99b5077633f395f72be1\"}[chunkId] + \".js\";\n/******/ \t\tvar timeout = setTimeout(onScriptComplete, 120000);\n/******/ \t\tscript.onerror = script.onload = onScriptComplete;\n/******/ \t\tfunction onScriptComplete() {\n/******/ \t\t\t// avoid mem leaks in IE.\n/******/ \t\t\tscript.onerror = script.onload = null;\n/******/ \t\t\tclearTimeout(timeout);\n/******/ \t\t\tvar chunk = installedChunks[chunkId];\n/******/ \t\t\tif(chunk !== 0) {\n/******/ \t\t\t\tif(chunk) {\n/******/ \t\t\t\t\tchunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));\n/******/ \t\t\t\t}\n/******/ \t\t\t\tinstalledChunks[chunkId] = undefined;\n/******/ \t\t\t}\n/******/ \t\t};\n/******/ \t\thead.appendChild(script);\n/******/\n/******/ \t\treturn promise;\n/******/ \t};\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// identity function for calling harmony imports with the correct context\n/******/ \t__webpack_require__.i = function(value) { return value; };\n/******/\n/******/ \t// define getter function for harmony exports\n/******/ \t__webpack_require__.d = function(exports, name, getter) {\n/******/ \t\tif(!__webpack_require__.o(exports, name)) {\n/******/ \t\t\tObject.defineProperty(exports, name, {\n/******/ \t\t\t\tconfigurable: false,\n/******/ \t\t\t\tenumerable: true,\n/******/ \t\t\t\tget: getter\n/******/ \t\t\t});\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t__webpack_require__.n = function(module) {\n/******/ \t\tvar getter = module && module.__esModule ?\n/******/ \t\t\tfunction getDefault() { return module['default']; } :\n/******/ \t\t\tfunction getModuleExports() { return module; };\n/******/ \t\t__webpack_require__.d(getter, 'a', getter);\n/******/ \t\treturn getter;\n/******/ \t};\n/******/\n/******/ \t// Object.prototype.hasOwnProperty.call\n/******/ \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"/\";\n/******/\n/******/ \t// on error function for async loading\n/******/ \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n/******/ })\n/************************************************************************/\n/******/ ([]);\n\n\n// WEBPACK FOOTER //\n// static/js/manifest.7c73e181da5eacf1a236.js"," \t// install a JSONP callback for chunk loading\n \tvar parentJsonpFunction = window[\"webpackJsonp\"];\n \twindow[\"webpackJsonp\"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [], result;\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n \t\tif(executeModules) {\n \t\t\tfor(i=0; i < executeModules.length; i++) {\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = executeModules[i]);\n \t\t\t}\n \t\t}\n \t\treturn result;\n \t};\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// objects to store loaded and loading chunks\n \tvar installedChunks = {\n \t\t2: 0\n \t};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n \t// This file contains only the entry chunk.\n \t// The chunk loading function for additional chunks\n \t__webpack_require__.e = function requireEnsure(chunkId) {\n \t\tvar installedChunkData = installedChunks[chunkId];\n \t\tif(installedChunkData === 0) {\n \t\t\treturn new Promise(function(resolve) { resolve(); });\n \t\t}\n\n \t\t// a Promise means \"currently loading\".\n \t\tif(installedChunkData) {\n \t\t\treturn installedChunkData[2];\n \t\t}\n\n \t\t// setup Promise in chunk cache\n \t\tvar promise = new Promise(function(resolve, reject) {\n \t\t\tinstalledChunkData = installedChunks[chunkId] = [resolve, reject];\n \t\t});\n \t\tinstalledChunkData[2] = promise;\n\n \t\t// start chunk loading\n \t\tvar head = document.getElementsByTagName('head')[0];\n \t\tvar script = document.createElement('script');\n \t\tscript.type = 'text/javascript';\n \t\tscript.charset = 'utf-8';\n \t\tscript.async = true;\n \t\tscript.timeout = 120000;\n\n \t\tif (__webpack_require__.nc) {\n \t\t\tscript.setAttribute(\"nonce\", __webpack_require__.nc);\n \t\t}\n \t\tscript.src = __webpack_require__.p + \"static/js/\" + chunkId + \".\" + {\"0\":\"0340c7d1b0e2e05b8117\",\"1\":\"99b5077633f395f72be1\"}[chunkId] + \".js\";\n \t\tvar timeout = setTimeout(onScriptComplete, 120000);\n \t\tscript.onerror = script.onload = onScriptComplete;\n \t\tfunction onScriptComplete() {\n \t\t\t// avoid mem leaks in IE.\n \t\t\tscript.onerror = script.onload = null;\n \t\t\tclearTimeout(timeout);\n \t\t\tvar chunk = installedChunks[chunkId];\n \t\t\tif(chunk !== 0) {\n \t\t\t\tif(chunk) {\n \t\t\t\t\tchunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));\n \t\t\t\t}\n \t\t\t\tinstalledChunks[chunkId] = undefined;\n \t\t\t}\n \t\t};\n \t\thead.appendChild(script);\n\n \t\treturn promise;\n \t};\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// identity function for calling harmony imports with the correct context\n \t__webpack_require__.i = function(value) { return value; };\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, {\n \t\t\t\tconfigurable: false,\n \t\t\t\tenumerable: true,\n \t\t\t\tget: getter\n \t\t\t});\n \t\t}\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \t// on error function for async loading\n \t__webpack_require__.oe = function(err) { console.error(err); throw err; };\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap d3c4cbeff33ceac1bd8c"],"sourceRoot":""} -------------------------------------------------------------------------------- /server/controller/repoController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * icon repo Controller 3 | * 4 | */ 5 | let config = require('../config/config'); 6 | let constant = require('../config/constant'); 7 | let responseFormat = require('../util/responseFormat'); 8 | let repoInfoRules = require('../validation/repoInfoRules'); 9 | let validator = require('../util/validator'); 10 | let db = require('../database'); 11 | let log = require('../util/log'); 12 | let path = require('path'); 13 | let fileUtil = require('../util/fileUtil'); 14 | let incUtil = require('../util/incUtil'); 15 | let icon = require('../tool/icon'); 16 | let svgSprite = require('../tool/svgSprite'); 17 | let uploadService = require('../service/upload'); 18 | 19 | class RepoController { 20 | /** 21 | * add icon repo 22 | * 23 | * @param {Object} ctx request object 24 | * @return {void} 25 | */ 26 | async addRepo (ctx) { 27 | let params = ctx.request.body || {}; 28 | let userInfo = ctx.userInfo; 29 | 30 | // validate completely 31 | validator.validateParamsField(params, repoInfoRules, ctx); 32 | 33 | // validate unique 34 | await this.iconRepoExist(params, userInfo); 35 | 36 | // get increment repoId 37 | let repoId = await incUtil.getIncId({model: 'iconRepo', field: 'repoId'}); 38 | 39 | // build repo data 40 | Object.assign(params, { 41 | createTime: global.globalConfig.nowTime, 42 | updateTime: global.globalConfig.nowTime, 43 | ownerId: userInfo.userId, 44 | repoId: repoId 45 | }); 46 | log.debug(`add repo data: ${JSON.stringify(params)}`); 47 | 48 | // save icon repo data to database 49 | await db.iconRepo.add(params); 50 | 51 | // add repoId to owner repos field 52 | await db.user.update({ 53 | userId: userInfo.userId 54 | }, { 55 | $push: { 56 | 'repos': { 57 | repoId: repoId, 58 | repoName: params.repoName 59 | } 60 | } 61 | }); 62 | 63 | ctx.body = responseFormat.responseFormat(200, 'save success!', true); 64 | } 65 | 66 | /** 67 | * add icon repo 68 | * 69 | * @param {Object} ctx request object 70 | * @return {void} 71 | */ 72 | async updateRepoInfo (ctx) { 73 | let data = ctx.request.body || {}; 74 | let params = ctx.params; 75 | let userInfo = ctx.userInfo; 76 | 77 | // validate completely 78 | validator.validateParamsField(params, repoInfoRules, ctx); 79 | 80 | // validate privilege, only owner 81 | let repoItem = await db.iconRepo.findOne({ 82 | ownerId: userInfo.userId, 83 | repoId: params.repoId 84 | }); 85 | 86 | if (!repoItem) { 87 | throw new Error('no privilege!'); 88 | } 89 | 90 | // validate unique 91 | // await this.iconRepoExist(data, userInfo); 92 | 93 | // build repo data 94 | data = Object.assign(repoItem, data, { 95 | updateTime: global.globalConfig.nowTime, 96 | ownerId: userInfo.userId, 97 | unSync: true, 98 | repoId: parseInt(params.repoId) 99 | }); 100 | log.debug(`user ${userInfo.userId} update repo data: ${JSON.stringify(params)}`); 101 | 102 | // update icon repo data to database 103 | await db.iconRepo.update({repoId: params.repoId}, data); 104 | 105 | // add repoId to owner repos field, todo 106 | // await db.user.update({ 107 | // userId: userInfo.userId 108 | // }, { 109 | // "$set": { 110 | // "repos.$[element]": { 111 | // repoId: params.repoId, 112 | // repoName: data.repoName 113 | // } 114 | // } 115 | // }, { 116 | // arrayFilters: [{ 117 | // repoId: params.repoId, 118 | // repoName: repoItem.repoName 119 | // }] 120 | // }); 121 | 122 | ctx.body = responseFormat.responseFormat(200, 'save success!', true); 123 | } 124 | 125 | /** 126 | * get repo list 127 | * 128 | * @param {Object} ctx request object 129 | * @return {void} 130 | */ 131 | async getRepoList (ctx) { 132 | let params = ctx.params; 133 | let query = ctx.request.query || {}; 134 | let repoList = []; 135 | 136 | if (parseInt(params.userId)) { 137 | repoList = await this.getRepoListByUserId(params, query, ctx); 138 | } else { 139 | repoList = await this.getAllRepoList(query) 140 | } 141 | ctx.body = responseFormat.responseFormatList(200, '', repoList, query); 142 | } 143 | 144 | /** 145 | * get all repo list 146 | * 147 | * @param {Object} query pagination object 148 | * @return {void} 149 | */ 150 | async getAllRepoList (query) { 151 | let result = await db.iconRepo.find( 152 | {}, global.globalConfig.iconRepoExportFields, 153 | { 154 | limit: parseInt(query.pageSize), 155 | skip: parseInt((query.pageIndex - 1) * query.pageSize) 156 | } 157 | ); 158 | query.totalCount = await db.iconRepo.count({}); 159 | log.debug(`get all repos and count: ${query.totalCount}`); 160 | // traverse repo list for finding icon that belong to repo 161 | for (let i = 0; i < (result || []).length; i++) { 162 | result[i].icons = []; 163 | result[i].iconCount = (result[i].iconIds || []).length; 164 | for (let j = 0; j < Math.min(result[i].iconIds.length, constant.REPO_LIST_CONTAIN_ICON_COUNT_PER_REPO); j++) { 165 | let iconItem = await db.icon.findOne( 166 | { 167 | iconId: result[i].iconIds[j].iconId 168 | }, global.globalConfig.iconExportFields); 169 | result[i].icons.push(iconItem || {}); 170 | } 171 | } 172 | return result; 173 | } 174 | 175 | /** 176 | * get repo list of specific user 177 | * 178 | * @param {Object} params query object of url 179 | * @param {Object} query pagination object 180 | * @return {void} 181 | */ 182 | async getRepoListByUserId (params, query) { 183 | let userItem = await db.user.findOne( 184 | { 185 | userId: params.userId 186 | } 187 | ); 188 | 189 | query.totalCount = userItem.repos.length; 190 | log.debug(`get user ${params.userId} repo list and count: ${query.totalCount}`); 191 | let repoList = []; 192 | for (let r = (query.pageIndex - 1) * query.pageSize; r < Math.min((query.pageIndex) * query.pageSize, userItem.repos.length); r++) { 193 | let repoItem = await db.iconRepo.findOne({ 194 | repoId: userItem.repos[r].repoId 195 | }, global.globalConfig.iconRepoExportFields); 196 | 197 | // traverse repo list for finding icon that belong to repo 198 | repoItem.icons = []; 199 | repoItem.iconCount = (repoItem.iconIds || []).length; 200 | for (let j = 0; j < Math.min(repoItem.iconIds.length, constant.REPO_LIST_CONTAIN_ICON_COUNT_PER_REPO); j++) { 201 | let iconItem = await db.icon.findOne( 202 | { 203 | iconId: repoItem.iconIds[j].iconId 204 | }, global.globalConfig.iconExportFields); 205 | repoItem.icons.push(iconItem || {}); 206 | } 207 | repoList.push(repoItem); 208 | } 209 | return repoList; 210 | } 211 | 212 | /** 213 | * get repo info 214 | * 215 | * @param {Object} ctx request object 216 | * @return {void} 217 | */ 218 | async getRepoInfo (ctx) { 219 | let userInfo = ctx.userInfo; 220 | let params = ctx.params; 221 | // find info first 222 | let result = await db.iconRepo.findOne( 223 | { 224 | repoId: params.repoId 225 | } 226 | ); 227 | // pre login 228 | if (userInfo.userId) { 229 | // check user has repoId in repos field 230 | let userItem = await db.user.findOne({ 231 | userId: userInfo.userId 232 | }); 233 | // isMember if exist repoId 234 | userItem.repos.forEach((item) => { 235 | if (item.repoId === parseInt(params.repoId)) { 236 | result.isMember = true; 237 | } 238 | }); 239 | // is owner if repo's ownerId equal to userId 240 | if (result.ownerId === userInfo.userId) { 241 | result.isOwner = true; 242 | } 243 | } 244 | 245 | ctx.body = responseFormat.responseFormat(200, '', result); 246 | } 247 | 248 | /** 249 | * get repo css or svg resource 250 | * 251 | * @param {Object} ctx request object 252 | * @return {void} 253 | */ 254 | async getRepoResource (ctx) { 255 | let params = ctx.params; 256 | let type = (ctx.request.query || {}).type; 257 | // find info first 258 | let repoItem = await db.iconRepo.findOne( 259 | { 260 | repoId: params.repoId 261 | } 262 | ); 263 | // pre login 264 | let result = null; 265 | if (repoItem) { 266 | // type can be cssUrl or cssContent 267 | if (repoItem[type]) { 268 | result = repoItem[type] 269 | } else { 270 | let iconIds = await this.getRepoIconIds(repoItem.repoId); 271 | result = []; 272 | for (let iconId of iconIds) { 273 | let iconItem = await db.icon.findOne({iconId: iconId}); 274 | result.push({ 275 | iconId: iconItem.iconId, 276 | iconContent: iconItem.iconContent, 277 | iconName: iconItem.iconName 278 | }) 279 | } 280 | } 281 | } 282 | 283 | ctx.body = responseFormat.responseFormat(200, '', result); 284 | } 285 | 286 | /** 287 | * get iconIds that belong to repo 288 | * 289 | * @param {Number} repoId repoId 290 | * @return {Array} all iconIds that belong to repo 291 | */ 292 | async getRepoIconIds (repoId) { 293 | let iconRepoItem = await db.iconRepo.findOne({ 294 | repoId: repoId 295 | }); 296 | let iconIds = []; 297 | iconRepoItem.iconIds.map((icon) => { 298 | iconIds.push(icon.iconId) 299 | }); 300 | return iconIds; 301 | } 302 | 303 | /** 304 | * add iconId to repo's iconIds field 305 | * 306 | * @param {Object} ctx request object 307 | * @return {void} 308 | */ 309 | async addIcon2Repo (ctx) { 310 | let params = ctx.request.body || {}; 311 | let userInfo = ctx.userInfo; 312 | 313 | // judge unique from upload icons 314 | let tmpMap = {}; 315 | for (let icon of params.icons) { 316 | if (tmpMap[icon.iconName]) { 317 | throw new Error(`upload repeat icon ${icon.iconName}`); 318 | } 319 | tmpMap[icon.iconName] = true; 320 | } 321 | 322 | // has privilege or not 323 | let userItem = await db.user.findOne({ 324 | userId: userInfo.userId 325 | }); 326 | if (!userItem || !this.hasRepo(userItem.repos, params.repoId)) { 327 | throw new Error('no privilege!'); 328 | } 329 | let repoItem = await db.iconRepo.findOne({ 330 | repoId: params.repoId 331 | }); 332 | // judge unique to avoid repeat between upload and database 333 | for (let icon of params.icons) { 334 | for (let existIcon of repoItem.iconIds) { 335 | if (icon.iconId === existIcon.iconId || icon.iconName === existIcon.iconName) { 336 | throw new Error(`repo ${repoItem.repoName} has contain icon ${icon.iconName}`) 337 | } 338 | } 339 | } 340 | // add many icon one times, need to traverse 341 | for (let icon of params.icons) { 342 | await db.iconRepo.update({ 343 | repoId: params.repoId 344 | }, { 345 | $push: { 346 | 'iconIds': { 347 | iconId: icon.iconId, 348 | iconName: icon.iconName 349 | } 350 | }, 351 | unSync: true 352 | }); 353 | log.debug(`user ${userInfo.userId} add icon ${icon.iconId}-${icon.iconName} to repo ${params.repoId}`); 354 | 355 | await db.iconBelongToRepo.update({ 356 | iconId: icon.iconId 357 | }, { 358 | iconId: icon.iconId, 359 | iconName: icon.iconName, 360 | $push: { 361 | 'repos': { 362 | repoId: repoItem.repoId, 363 | repoName: repoItem.repoName 364 | } 365 | } 366 | }, { 367 | upsert: true 368 | }) 369 | } 370 | 371 | ctx.body = responseFormat.responseFormat(200, 'add success!', true); 372 | } 373 | 374 | /** 375 | * is member or master of repoId 376 | * 377 | * @param {Array} repos repo array 378 | * @param {Number} repoId repo id 379 | * @return {void} 380 | */ 381 | hasRepo (repos = [], repoId) { 382 | for (let repo of repos) { 383 | if (repo.repoId === parseInt(repoId)) { 384 | return true; 385 | } 386 | } 387 | return false; 388 | } 389 | 390 | /** 391 | * delete iconId from repo 392 | * 393 | * @param {Object} ctx request object 394 | * @return {void} 395 | */ 396 | async deleteIconFromRepo (ctx) { 397 | let params = ctx.params || {}; 398 | let userInfo = ctx.userInfo; 399 | let isMember = false; 400 | 401 | // check user has repoId in repos field 402 | let userItem = await db.user.findOne({ 403 | userId: userInfo.userId 404 | }); 405 | // isMember if exist repoId 406 | userItem.repos.forEach((item) => { 407 | if (item.repoId === params.repoId) { 408 | isMember = true; 409 | } 410 | }); 411 | 412 | if (!isMember) { 413 | ctx.body = responseFormat.responseFormat(403, 'no privilege!', false); 414 | } 415 | 416 | await db.iconRepo.update({ 417 | repoId: params.repoId 418 | }, { 419 | $pull: { 420 | 'iconIds': { 421 | iconId: params.iconId 422 | } 423 | }, 424 | unSync: true 425 | }); 426 | // remove relationship for delete operation 427 | await db.iconBelongToRepo.update({ 428 | iconId: params.iconId 429 | }, { 430 | $pull: { 431 | 'repos': { 432 | repoId: params.repoId 433 | } 434 | } 435 | }); 436 | 437 | // remove svg file from resource 438 | let repoItem = await db.iconRepo.findOne({ 439 | repoId: params.repoId 440 | }); 441 | let iconItem = await db.icon.findOne({ 442 | iconId: params.iconId 443 | }); 444 | let iconPath = path.resolve(__dirname, `../../public/resource/repository/${repoItem.ownerId}-${repoItem.iconPrefix}/svg/${iconItem.iconName}.svg`); 445 | await fileUtil.deleteFile(iconPath); 446 | log.debug(`user ${userInfo.userId} delete icon ${params.iconId} from repo ${params.repoId}`); 447 | 448 | ctx.body = responseFormat.responseFormat(200, 'delete success!', true); 449 | } 450 | 451 | /** 452 | * sync database and generate css files 453 | * 454 | * @param {Object} ctx request object 455 | * @return {void} 456 | */ 457 | async syncRepo (ctx) { 458 | let params = ctx.params; 459 | let userInfo = ctx.userInfo; 460 | let isRepoMember = false; 461 | 462 | // judge privilege 463 | let userItem = await db.user.findOne({ 464 | userId: userInfo.userId 465 | }); 466 | userItem.repos.map((item) => { 467 | if (item.userId === params.userId) { 468 | isRepoMember = true; 469 | } 470 | }); 471 | if (!isRepoMember) { 472 | ctx.body = responseFormat.responseFormat(403, 'no privilege!', false); 473 | return; 474 | } 475 | 476 | let repoItem = await db.iconRepo.findOne({ 477 | repoId: params.repoId 478 | }); 479 | // judge update or not 480 | if (repoItem.unSync) { 481 | let repoPath = path.join(config.rootRepoPath, `./${repoItem.ownerId}-${repoItem.iconPrefix}`); 482 | // clean all svg for avoid cache or change iconPrefix, maintain if default 483 | // if (config.productType === 'default') { 484 | // await fileUtil.deleteDirector(repoPath); 485 | // } 486 | let repoSvgPath = path.join(repoPath, './svg/'); 487 | let repoIcons = []; 488 | await fileUtil.createDirector(repoSvgPath); 489 | 490 | // create svg file recursive 491 | for (let k = 0; k < repoItem.iconIds.length; k++) { 492 | let iconItem = (await db.icon.findOne({ 493 | iconId: repoItem.iconIds[k].iconId 494 | }) || {}); 495 | repoIcons.push(iconItem); 496 | await fileUtil.createFile(path.join(repoSvgPath, iconItem.iconName + '.svg'), iconItem.iconContent); 497 | } 498 | let uploadResult = {}; 499 | let svgSpriteResult = ''; 500 | // must has svg 501 | if (repoItem.iconIds.length > 0) { 502 | await icon.compileSvg2Icon(repoPath, repoItem.iconPrefix, repoItem.fontPath); 503 | // output to server and return css url and css content 504 | uploadResult = await uploadService.upload(repoPath); 505 | 506 | // svg sprite 507 | svgSpriteResult = svgSprite(repoItem.iconPrefix, repoIcons); 508 | } else { 509 | uploadResult = {url: '', cssContent: ''}; 510 | svgSpriteResult = ''; 511 | } 512 | // update sync status 513 | await db.iconRepo.update({ 514 | repoId: params.repoId 515 | }, { 516 | unSync: false, 517 | cssUrl: uploadResult.url, 518 | cssContent: uploadResult.cssContent, 519 | svgSpriteContent: svgSpriteResult 520 | }); 521 | log.debug(`user ${userInfo.userId} sync repo ${params.repoId} success`); 522 | 523 | // clean all svg for avoid cache or change iconPrefix, delete if access thirdly upload 524 | // if (config.productType !== 'default') { 525 | // await fileUtil.deleteDirector(repoPath); 526 | // } 527 | ctx.body = responseFormat.responseFormat(200, 'update success!', uploadResult); 528 | return; 529 | } 530 | 531 | ctx.body = responseFormat.responseFormat(200, 'update success!', true); 532 | } 533 | 534 | /** 535 | * judge repo already exist 536 | * 537 | * @param {Object} params post request object 538 | * @param {Object} userInfo userInfo 539 | * @return {void} 540 | */ 541 | async iconRepoExist (params, userInfo) { 542 | let iconLibItem = await db.iconRepo.findOne({ 543 | iconPrefix: params.iconPrefix, 544 | ownerId: userInfo.userId 545 | }); 546 | 547 | if (iconLibItem) { 548 | throw new Error(`${params.iconPrefix} already exist!`) 549 | } 550 | } 551 | 552 | /** 553 | * add member of repo 554 | * 555 | * @param {Object} ctx request object 556 | * @return {void} 557 | */ 558 | async addMember (ctx) { 559 | let params = ctx.params; 560 | let userInfo = ctx.userInfo; 561 | let data = ctx.request.body || {}; 562 | 563 | // only master can add member 564 | let repo = await db.iconRepo.findOne({ 565 | ownerId: userInfo.userId, 566 | repoId: params.repoId 567 | }); 568 | if (!repo) { 569 | ctx.body = responseFormat.responseFormat(403, 'no privilege!', false); 570 | return; 571 | } 572 | // user exist 573 | let user = null; 574 | if (parseInt(data.accountType) === constant.MEMBER_ACCOUNT_TYPE_USER_ID) { 575 | user = await db.user.findOne({ 576 | userId: data.account 577 | }); 578 | } else { 579 | user = await db.user.findOne({ 580 | userName: data.account 581 | }); 582 | } 583 | if (!user) { 584 | ctx.body = responseFormat.responseFormat(200, 'user not exist, make sure has login the site', false); 585 | return; 586 | } 587 | // add member of repo 588 | await db.user.update({ 589 | userId: user.userId 590 | }, { 591 | $push: { 592 | 'repos': { 593 | repoId: repo.repoId, 594 | repoName: repo.repoName 595 | } 596 | } 597 | }); 598 | log.debug(`user ${userInfo.userId} add member ${user.userId} of repo ${repo.repoId}`) 599 | ctx.body = responseFormat.responseFormat(200, '添加成功!', true); 600 | } 601 | 602 | /** 603 | * get recommend repo list 604 | * 605 | * @param {Object} ctx request object 606 | * @return {void} 607 | */ 608 | async getRecommendRepoList (ctx) { 609 | let recommendRepos = await db.repoRecommend.find({}); 610 | let result = []; 611 | let query = ctx.request.query || {}; 612 | // find all recommend repo 613 | for (let repo of recommendRepos) { 614 | let repoItem = await db.iconRepo.findOne({ 615 | repoId: repo.repoId 616 | }, global.globalConfig.iconRepoExportFields, { 617 | lean: true 618 | }); 619 | // traverse repo list for finding icon that belong to repo 620 | repoItem.icons = []; 621 | repoItem.iconCount = (repoItem.iconIds || []).length; 622 | for (let icon of repoItem.iconIds) { 623 | if (repoItem.icons.length >= constant.REPO_LIST_CONTAIN_ICON_COUNT_PER_REPO) { 624 | break; 625 | } 626 | let iconItem = (await db.icon.findOne( 627 | { 628 | iconId: icon.iconId 629 | }, global.globalConfig.iconExportFields) || {}); 630 | repoItem.icons.push(iconItem); 631 | } 632 | result.push(repoItem) 633 | } 634 | 635 | ctx.body = responseFormat.responseFormatList(200, '', result, query); 636 | } 637 | 638 | /** 639 | * add recommend repo 640 | * 641 | * @param {Object} ctx request object 642 | * @return {void} 643 | */ 644 | async addRecommendRepo (ctx) { 645 | let params = ctx.request.body || {}; 646 | if (params.key === config.salt) { 647 | for (let item of params.repos) { 648 | let repo = await db.iconRepo.findOne({ 649 | repoId: item.repoId 650 | }); 651 | if (repo) { 652 | // get increment repoId 653 | let id = await incUtil.getIncId({model: 'repoRecommend', field: 'id'}); 654 | await db.repoRecommend.add({ 655 | id: id, 656 | repoId: repo.repoId 657 | }) 658 | } 659 | } 660 | } 661 | 662 | ctx.body = responseFormat.responseFormat(200, 'save success!', true); 663 | } 664 | 665 | /** 666 | * add recommend repo 667 | * 668 | * @param {Object} ctx request object 669 | * @return {void} 670 | */ 671 | async deleteRecommendRepo (ctx) { 672 | let params = ctx.request.body || {}; 673 | 674 | if (params.key === config.salt) { 675 | for (let item of params.repos) { 676 | await db.repoRecommend.delete({ 677 | repoId: item.repoId 678 | }) 679 | } 680 | } 681 | 682 | ctx.body = responseFormat.responseFormat(200, 'delete success!', true); 683 | } 684 | }; 685 | 686 | module.exports = RepoController; 687 | -------------------------------------------------------------------------------- /public/resource/docs/README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 325 | 326 | 465 | 466 | 467 | 468 | 469 | 470 | 471 |

Nicon

472 | 473 |

Nicon 是一个集图标上传、展示、使用于一身的字体图标管理平台,流程简单,符合日常开发使用习惯,适合企业在内部部署使用。采用 Iconfont 字体图标替换项目中图片图标的使用,以达到缩减体积、风格统一、提高开发效率等目的。若配合设计师使用,设计师可在平台上管理图标,复用图标,减少设计图标耗费的时间,而开发只负责使用设计师维护好的图标库,减少了与设计师的交流成本。

474 | 475 |

优势

476 | 477 |

与其他字体图标管理平台相比,它拥有以下优势:

478 | 479 | 485 | 486 |

结构设计图

487 | 488 |

结构设计图

489 | 490 |

使用流程图

491 | 492 |

使用流程图

493 | 494 |

结构设计图

495 | 496 |

开发使用流程图

497 | 498 |

开发使用流程

499 | 500 |

设计师参与使用流程图

501 | 502 |

开发使用流程

503 | 504 |

如果设计师与开发协同参与使用维护图标库,不仅使设计师可以有一个可视化管理字体图标的平台。还可以减少开发与设计的交流时间。

505 | 506 |

服务安装部署

507 | 508 |

系统要求

509 | 510 | 513 | 514 |

环境要求

515 | 516 | 522 | 523 |

在启动工程之前,必须确保数据库已经启动,且已经把相应的数据库创建好。

524 | 525 |

1、 克隆项目到本地|服务器

526 | 527 |
git clone git@github.com:bolin-L/nicon.git
528 | 529 |

2、 进入到项目工程nicon安装依赖

530 | 531 |
cd nicon && npm install
532 | 533 |

3、运行启动命令

534 | 535 |
npm run publish
536 | 537 |

这时服务基本启动完毕,数据库等其他信息的配置等配置前端静态资源之后再配置。服务默认监听的端口是4843

538 | 539 |

前端静态资源部署

540 | 541 |

图标管理平台采用的是前后端完全分离的开发方式,前端代码放在独立的icon-front。但是前端打包后的代码需要拷贝到服务端工程nicon/public/static中,由服务端提供静态资源服务,如需要修改前端样式,按照以下步骤即可

542 | 543 |

1、克隆前端项目到本地

544 | 545 |
git clone git@github.com:bolin-L/nicon-front.git
546 | 547 |

2、进入到nicon-front工程,安装依赖

548 | 549 |
cd nicon-front && npm install 
550 | 551 |

3、运行打包命令、打包输出到nicon-front/dist文件夹下

552 | 553 |
npm run build
554 | 555 |

4、拷贝dist下的所有资源到服务端工程niconpublic/static

556 | 557 |

Nginx配置

558 | 559 |
server {
560 |     listen 80;
561 |     listen [::]:80;
562 | 
563 |     server_name icon.bolin.site;
564 | 
565 |     access_log   /var/log/nginx/bolin_sites_icon.access.log main;
566 |     error_log    /var/log/nginx/bolin_sites_icon.error.log;
567 | 
568 |     # 配置异步接口请求到服务器
569 |     location / {
570 |         proxy_set_header   X-Real-IP $remote_addr;
571 |         proxy_set_header   Host      $http_host;
572 |         proxy_pass http://127.0.0.1:4843;
573 |     }
574 | }
575 | 576 |

配置到此,平台基本就可以运行起来使用了,浏览器访问icon.bolin.site就可以访问到首页

577 | 578 |

数据库信息配置与三方服务接入

579 | 580 |

该平台在未配置任何数据库信息时就可以启动,但是访问之后会被重定向到安装页面进行数据库信息与三方服务脚本的配置。配置分三部分,根据自己的需求来决定具体要配置哪部分。

581 | 582 |

启动数据配置

583 | 584 |

启动数据配置基本是mongoDBredis数据库信息,且是必须。当然如果接入第三方服务时需要额外的配置也可以通过此页面来配置,添加的所有变量都可以从process.env环境变量对象中拿到。 这些配置最终会输出到 ./bin/start.sh的启动脚本中,如果出现配置错误,先停掉服务然后手动去修改此文件,再执行 sh ./bin/start.sh命令即可重启服务

585 | 586 |

比如我使用github登录与七牛上传服务最终配置如下

587 | 588 |
#!/bin/bash
589 | 
590 | # DB config
591 | 
592 | # mongodb
593 | export MONGODB_NAME=iconRepo;
594 | export MONGODB_HOST=127.0.0.1;
595 | export MONGODB_PORT=27017;
596 | export MONGODB_USERNAME='';
597 | export MONGODB_PASSWORD='';
598 | 
599 | 
600 | # redis
601 | export REDIS_FAMILY=4;
602 | export REDIS_HOST=127.0.0.1;
603 | export REDIS_PORT=6379;
604 | export REDIS_PASSWORD='';
605 | export REDIS_DB=0;
606 | 
607 | 
608 | # config your website host
609 | export productHost='icon.bolin.site';
610 | 
611 | 
612 | # if you want login by github and upload by qiniu, set productType
613 | export productType='github_qiniu';
614 | 
615 | 
616 | # Login config
617 | 
618 | # github openid login
619 | export GITHUB_LOGIN_CLIENT_ID='';
620 | export GITHUB_LOGIN_CLIENT_SECRET='';
621 | export GITHUB_LOGIN_REDIRECT_URI='';
622 | 
623 | 
624 | 
625 | # Upload config
626 | 
627 | # qiniu
628 | export QINIU_UPLOAD_ACCESS_KEY='';
629 | export QINIU_UPLOAD_SECRET_KEY='';
630 | export QINIU_UPLOAD_BUCKET='';
631 | export QINIU_UPLOAD_CDN_HOST='';
632 | 
633 | # start command
634 | npm run restart
635 | 
636 | 637 |

虽然该平台已经可以提供完成的登录、注册,图标库样式文件等静态资源的访问。但是对于企业来说,内部的工具平台最好只接受内部人或只能内网访问,对于静态资源最理想的就是放到自家的cdn服务器上,让平台操作更安全,访问所速度更快等等...

638 | 639 |

基于这样的需求,Nicon支持接入三方登录与字体文件资源上传到三方服务器,但是具体的代码需要自己实现,代码要求暴露出指定的方法且该方法需返回指定的数据,然后通过配置的方式添加到工程中。具体调用什么服务由配置的prodcutType的值决定,productType的值格式为 登录_上传,目前默认服务类型有4种defaultdefault_qiniugithub_defaultgithub_qiniu,当productType的值为这4个时,三方服务脚本是不需要配置的。

640 | 641 |

三方服务配置脚本就在以下的文件夹结构中生成名称为productType值的文件夹,有index.jsconfig.js两个文件。 除了以下的文件夹,用户配置后生成的文件夹都会被ignore掉。

642 | 643 |
├── service
644 | │   ├── login
645 | │   │   ├── default
646 | │   │   │   ├── config.js
647 | │   │   │   └── index.js
648 | │   │   ├── github
649 | │   │   │   ├── config.js
650 | │   │   │   └── index.js
651 | │   │   ├── github_qiniu
652 | │   │   │   ├── config.js
653 | │   │   │   └── index.js
654 | │   │   ├── index.js
655 | │   └── upload
656 | │       ├── default
657 | │       │   ├── config.js
658 | │       │   └── index.js
659 | │       ├── github_qiniu
660 | │       │   ├── config.js
661 | │       │   └── index.js
662 | │       ├── index.js
663 | │       └── qiniu
664 | │           ├── config.js
665 | │           └── index.js
666 | 
667 | 668 |

三方登录

669 | 670 |

比如我需要接入github三方登录与qiniu上传存储服务,那么我的productType就设置为github_qiniu。 那么就会自动在service/login/ 文件夹下创建文件夹 github_qiniu, 然后在该文件夹下创建config.js , 与index.js, 在index.js文件中必须暴露出async login方法, 调用方法后需要返回指定格式的数据

671 | 672 |
// index.js
673 | 
674 | require('request');
675 | let rp = require('request-promise');
676 | let config = require('./config');
677 | 
678 | class GithubOpenIdLogin {
679 |     async login (ctx) {
680 |         return this.getUserBaseInfo(ctx);
681 |     }
682 | 
683 |     async getUserBaseInfo (ctx) {
684 |         // your code
685 |         
686 |         // login 方法返回的数据格式
687 |         return {
688 |             userName: tokenInfo.sub, // 必须且唯一
689 |             password: tokenInfo.sub,
690 |             email: openIdUserInfo.email,
691 |             nickName: openIdUserInfo.nickname,
692 |             fullName: openIdUserInfo.fullname
693 |         }
694 |     }
695 | }
696 | 
697 | let loginIns = new GithubOpenIdLogin();
698 | module.exports = loginIns.login.bind(loginIns);
699 | 
700 | 701 |

三方上传

702 | 703 |

service/upload/ 文件夹下自动创建文件夹 github_qiniu, 然后在该文件夹下创建config.js , 与index.js, 在index.js文件中暴露出async upload方法, 调用方法后需要返回指定格式的数据

704 | 705 |
// index.js
706 | 
707 | let config = require('./config');
708 | let qiniu = require('qiniu');
709 | 
710 | class QiniuUpload {
711 |     async upload (dirPath) {
712 |         let fontMap = await this.uploadFonts(dirPath);
713 |         // 上传font完毕后替换css中的引用
714 |         let cssContent = await this.replaceFontsInCss(dirPath, fontMap);
715 |         let cssUrl = await this.uploadCss(dirPath, cssContent);
716 |         
717 |         // 上传返回数据格式
718 |         return {
719 |             url: cssUrl, // 必须
720 |             cssContent: cssContent // 必须
721 |         };
722 |     }
723 | }
724 | 
725 | let uploadIns = new QiniuUpload();
726 | module.exports = uploadIns.upload.bind(uploadIns);
727 | 
728 | 729 |

至此就已经配置完毕,当保存提交之后工程就会重启,根据配置启动相应的服务。

730 | 731 |

单元测试

732 | 733 |

待补充....

734 | 735 |

License

736 | 737 |

MIT

738 | 739 | 740 | 741 | 744 | 745 | 746 | 747 | 748 | 749 | --------------------------------------------------------------------------------