guoziyun 1 år sedan
förälder
incheckning
880a19a7be
6 ändrade filer med 376 tillägg och 0 borttagningar
  1. 59 0
      libs/auth/base.js
  2. 79 0
      libs/auth/can.js
  3. 93 0
      libs/auth/checker.js
  4. 7 0
      libs/auth/index.js
  5. 30 0
      libs/auth/menu-config.js
  6. 108 0
      libs/auth/menu.js

+ 59 - 0
libs/auth/base.js

@@ -0,0 +1,59 @@
+const resources = [
+  { value: 'art', label: '作品' },
+  { value: 'art-daily', label: '每日一图' },
+  { value: 'art-album', label: 'Art Album' },
+  { value: 'category', label: '分类管理' },
+  { value: 'translate', label: '多语翻译' },
+  { value: 'user', label: '用户' },
+  { value: 'role', label: '角色' },
+];
+
+const actions = [
+  { value: 'read', label: '查看' },
+  { value: 'create', label: '创建' },
+  { value: 'update', label: '更新' },
+  { value: 'delete', label: '删除' },
+];
+
+const possessions = [
+  { value: 'own', label: '自己的' },
+  { value: 'any', label: '全部' },
+];
+
+
+const resourcesHash = resources.reduce((h, cur) => { h[cur.value] = cur.label; return h; }, {});
+const actionsHash = actions.reduce((h, cur) => { h[cur.value] = cur.label; return h; }, {});
+const possessionsHash = possessions.reduce((h, cur) => { h[cur.value] = cur.label; return h; }, {});
+
+
+/**
+ * 将grant信息翻译成人可读的
+ * @param {*} grants 
+ */
+function translate(grant) {
+  let { resource, action, possession, attributes } = grant;
+  resource = resourcesHash[resource] || resource;
+  action = actionsHash[action] || action;
+  possession = possessionsHash[possession] || possession;
+  return { resource, action, possession, attributes };
+}
+
+
+function fullGrants() {
+  let grants = [];
+  resources.forEach(res => {
+    actions.forEach(act => {
+      grants.push({
+        resource: res.value,
+        action: act.value,
+        possession: 'any',
+        attributes: '*',
+      })
+    });
+  })
+  return grants;
+}
+
+
+
+module.exports = { resources, actions, possessions, translate, fullGrants }

+ 79 - 0
libs/auth/can.js

@@ -0,0 +1,79 @@
+const model = require('../../models/');
+const AccessControl = require('accesscontrol').AccessControl;
+const createError = require('http-errors');
+const base = require('./base');
+
+
+/**
+ * Check is user has permission.
+ * @param {*} action 
+ * @param {*} resource 
+ * @returns 
+ */
+function need(action, resource) {
+  return (req, res, next) => {
+    (async () => {
+      let ac = await getAc(req);
+      let role = 'user';
+      let perm = ac.permission({ role, action, resource });
+      if (!perm.granted) throw createError(403, '权限不够');
+      next();
+    })().catch(next)
+  };
+}
+
+
+async function readAny(req, resource) {
+  if(!req.ac) throw 'ac no build';
+  /**@type {AccessControl} */
+  let ac = req.ac;
+  return ac.can('user').readAny(resource).granted;
+}
+
+/**
+ * 
+ * @param {string} userId 
+ * @returns {AccessControl}
+ */
+async function buildAc(userId) {
+  let user = await model.User.findById(userId).populate('roles');
+
+  let grants = [];
+  if (['chengen', 'guoziyun'].includes(user.username)) {
+    grants = base.fullGrants();
+  } else {
+    user.roles.forEach(role => {
+      grants = grants.concat(role.grants);
+    })
+  }
+  // console.log('grants', grants);
+  grants = grantsTransform(grants);
+  // console.log('grants2ac', grants);
+  return new AccessControl(grants);
+}
+
+
+/**
+ * 
+ * @param {import('express').Request} req 
+ * @returns {AccessControl}
+ */
+async function getAc(req) {
+  if(req.ac) return req.ac;
+  req.ac = await buildAc(req.session.user._id);
+  return req.ac;
+}
+
+
+function grantsTransform(grants) {
+  return grants.map(item => {
+    let { resource, action, possession, attributes } = item;
+    return {
+      role: 'user',
+      resource, attributes,
+      action: `${action}:${possession}`,
+    }
+  })
+}
+
+module.exports = { need, buildAc, getAc,  readAny }

+ 93 - 0
libs/auth/checker.js

@@ -0,0 +1,93 @@
+const createError = require('http-errors');
+const models = require('../../models');
+
+
+function checkLogin(req, res, next) {
+  if (!req.session) {
+    next(createError(500, 'session服务异常'));
+  } else if (!req.session.user) {
+    next(createError(401, '请先登录'));
+  } else {
+    next();
+  }
+}
+
+
+async function checkPermission(permission, req) {
+  if (!req.session)
+    throw createError(500, 'session服务异常');
+  if (!req.session.user)
+    throw createError(401, '请先登录');
+
+  await checkAndUpdateAuth(req);
+
+  let auth = req.session.auth;
+  if (!auth)
+    throw createError(401, '权限不够');
+
+  if (permission && !auth.isSuper) {
+    let p = auth.permissions.find(x => {
+      return x == permission;
+    })
+    if (!p) throw createError(401, '权限不够')
+  }
+}
+
+/**
+ * 检测用户是否有某项
+ * @param {String} permission 
+ */
+function checkAdmin(permission) {
+  return function (req, res, next) {
+    (async function () {
+      await checkPermission(permission, req);
+      next();
+    })().catch(next);
+  }
+}
+
+
+/**
+ * Update session auth.
+ * 有效期1分钟
+ * @param {Request} req 
+ */
+async function checkAndUpdateAuth(req) {
+  let user = req.session.user;
+  let auth = req.session.auth;
+  auth = await buildAuth(user);
+  req.session.auth = auth;
+}
+
+/**
+ * Build auth from database.
+ * @param {*} user 
+ * @param {*} dealerId 
+ */
+async function buildAuth(_user) {
+  let auth = {};
+  if (!_user) return auth;
+  let user = await models.User.findById(_user._id) 
+    .populate('roles');
+
+  let permissions = user.roles.reduce((ps, role) => {
+    ps = ps.concat(role.permissions);
+    return ps;
+  }, []);
+
+  let roles = user.roles; 
+
+  let isSuper = permissions.includes('*');
+
+  auth = { permissions, roles, isSuper };
+  auth.ts = new Date().getTime();
+  return auth;
+}
+
+
+module.exports = {
+  checkLogin,
+  checkAdmin,
+  checkPermission,
+  checkAndUpdateAuth,
+}

+ 7 - 0
libs/auth/index.js

@@ -0,0 +1,7 @@
+module.exports = {
+  base: require('./base'), //基础信息
+  can: require('./can'),
+  need: require('./can').need,
+  getAc: require('./can').getAc,
+  Menu : require('./menu'),
+}

+ 30 - 0
libs/auth/menu-config.js

@@ -0,0 +1,30 @@
+
+/**
+ * menu config
+ */
+
+module.exports = [
+
+  {
+    name: '作品管理',
+    icon: 'fas fa-images',
+    children: [
+      { name: '我的作品', url: '/pages/my', permission: { resource: 'art', action: 'read:own' } },
+      { name: '所有作品', url: '/pages/all', permission: { resource: 'art', action: 'read:any' } },
+      { name: '专辑管理', url: '/coloring/album', permission: { resource: 'art-album', action: 'read:any' } },
+      { name: '回收站', url: '/pages/dropped', permission: { resource: 'art', action: 'read:own' } },
+      { name: '定时发布', url: '/pages/publish-schedules', permission: { resource: 'art-publish', action: 'read:any' } },
+    ]
+  },
+
+  {
+    name: '系统管理',
+    icon: 'fas fa-users',
+    url: '/system',
+    children: [
+      { name: '用户管理', url: '/system/users', permission: { resource: 'user', action: 'read:own' } },
+      { url: '/system/roles', name: '角色管理', permission: { resource: 'role', action: 'read:own' } },
+    ],
+  },
+
+];

+ 108 - 0
libs/auth/menu.js

@@ -0,0 +1,108 @@
+const { AccessControl } = require('accesscontrol');
+const { buildAc } = require('./can');
+
+class Menu {
+  ac;
+  menuList;
+
+  /**
+   * 
+   * @param {AccessControl} ac 
+   * @param {*} menuList 
+   */
+  constructor(menuList, ac) {
+    this.ac = ac;
+    this.menu = {
+      name: 'ROOT',
+      children: menuList,
+    }
+  }
+
+  /*
+   * 深度优先
+   */
+  travelDFS(menu, callback, depth) {
+    depth = depth || 0;
+    if (menu.children) {
+      menu.children.forEach(c => this.travelDFS(c, callback, depth + 1))
+    }
+    callback(menu, depth);
+  }
+
+  /*
+   * 广度优先
+   */
+  travelBFS(menu, callback, depth) {
+    depth = depth || 0;
+    callback(menu, depth);
+    if (menu.children) {
+      menu.children.forEach(c => this.travelBFS(c, callback, depth + 1))
+    }
+  }
+
+  checkPermission(role, permission) {
+    permission.role = role;
+    return this.ac.permission(permission).granted;
+  }
+
+  /*
+   * 根据角色获取菜单
+   */
+  getByRole(role) {
+    let menu = this.dcopy(this.menu);
+    this.travelDFS(menu, (item) => {
+      let keep = item.permission ? this.checkPermission(role, item.permission) : true;
+      item.keep = item.children ? item.children.some(c => c.keep) : keep;
+    });
+    this.travelBFS(menu, (item) => {
+      if (item.children) item.children = item.children.filter(c => c.keep);
+    });
+
+    //移除无用信息
+    this.travelBFS(menu, (item) => {
+      delete item.permission;
+      delete item.keep;
+    });
+
+    //this.print(menu);
+    //console.log(JSON.stringify(menu, null, 2));
+    return menu.children;
+  }
+
+
+  print(menu) {
+    menu = menu || this.menu;
+    this.travelBFS(menu, (item, depth) => {
+      console.log(
+        '\t\t\t\t\t\t\t\t\t'.substr(0, depth), depth, item.name, item.url);
+    })
+  }
+
+  dcopy(obj) {
+    return JSON.parse(JSON.stringify(obj));
+  }
+
+}
+
+
+
+
+
+if (require.main === module) {
+
+  (async () => {
+    let username = 'chengen.test';
+    let user = await require('../../models').User.findOne({ username });
+    if (!user) throw new Error(`user ${username}`);
+    let ac = await buildAc(user._id);
+    const menu = new Menu(require('./menu-config'), ac);
+    menu.print();
+    let roleMenu = menu.getByRole('user');
+    //let roleMenu = menu.getByRole('调解员');
+    console.log(JSON.stringify(roleMenu, null, 2));
+
+  })().catch(console.error)
+
+}
+
+module.exports = Menu;