|
@@ -0,0 +1,429 @@
|
|
|
|
|
+// FmodeAgent 用户管理云函数(2026-11-10 新建)
|
|
|
|
|
+//
|
|
|
|
|
+// 目的:给公司管理员提供「组织成员」的增删改查。
|
|
|
|
|
+//
|
|
|
|
|
+// op: list { keyword?, page?, pageSize? } → { items, total, page, pageSize }
|
|
|
|
|
+// op: add { userId } → 把用户拉进管理员所在组织
|
|
|
|
|
+// op: update { userId, nickname?, mobile?, password? } → 改资料 / 重置密码
|
|
|
|
|
+// op: remove { userId } → **软删**(isDeleted=true)+ 移出组织
|
|
|
|
|
+// op: get { userId } → 单条详情(编辑弹窗回填用)
|
|
|
|
|
+//
|
|
|
|
|
+// 🔴 安全边界(用户 2026-11-10 明确要求):
|
|
|
|
|
+// ① **每个 op 的第一件事都是服务端鉴权** —— 前端 `canManageCompany` 只决定
|
|
|
|
|
+// 「入口显不显示」,改了前端也越不了权;
|
|
|
|
|
+// ② 作用域**强制锁在管理员自己的 companyId**,前端传任何 company 参数一律忽略;
|
|
|
|
|
+// ③ 改密码、软删必须 `useMasterKey: true`(前端直改 `_User` 是被禁止的)。
|
|
|
|
|
+//
|
|
|
|
|
+// 平台约定(与 fmodeagent-chatthreads 一致,实测):
|
|
|
|
|
+// · 入口 async function handler(request, response);入参在全局 params
|
|
|
|
|
+// · 身份:优先 request.user(平台按 sessionToken 注入),否则按 token 查 _Session
|
|
|
|
|
+// · 🔴 输出必须 response.status(200).json(...) 显式写出;直接 return 对象平台不回包
|
|
|
|
|
+//
|
|
|
|
|
+// 表:
|
|
|
|
|
+// _User objectId / username / mobile / nickname / company Pointer<Company> / isDeleted
|
|
|
|
|
+// _Role <companyId>_Admin / <companyId>_Public / FMODE_AGENT_SUPERADMIN
|
|
|
|
|
+
|
|
|
|
|
+var CLASS_USER = '_User';
|
|
|
|
|
+var SUPER_ROLE = 'FMODE_AGENT_SUPERADMIN';
|
|
|
|
|
+var DEFAULT_PAGE_SIZE = 20;
|
|
|
|
|
+var MAX_PAGE_SIZE = 100;
|
|
|
|
|
+
|
|
|
|
|
+function _ok(data) {
|
|
|
|
|
+ response.status(200).json(Object.assign({ code: 200 }, data || {}));
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+function _fail(code, mess) {
|
|
|
|
|
+ response.status(200).json({ code: code, mess: mess });
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function _str(v, max) {
|
|
|
|
|
+ var s = v == null ? '' : String(v);
|
|
|
|
|
+ return max && s.length > max ? s.slice(0, max) : s;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function _sessionToken() {
|
|
|
|
|
+ var h = request && request.headers ? request.headers : {};
|
|
|
|
|
+ if (h['x-parse-session-token']) return h['x-parse-session-token'];
|
|
|
|
|
+ if (params && params.sessionToken) return params.sessionToken;
|
|
|
|
|
+ if (request && request.body && request.body.token) return request.body.token;
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function _getUser(t) {
|
|
|
|
|
+ if (!t) return null;
|
|
|
|
|
+ if (request.user && request.user.id && !(params && params.sessionToken)) return request.user;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var q = new Parse.Query(Parse.Session);
|
|
|
|
|
+ q.equalTo('sessionToken', t);
|
|
|
|
|
+ var s = await q.first({ useMasterKey: true });
|
|
|
|
|
+ return s ? s.get('user') : null;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function _roleUserIds(name) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var q = new Parse.Query(Parse.Role);
|
|
|
|
|
+ q.equalTo('name', name);
|
|
|
|
|
+ var role = await q.first({ useMasterKey: true });
|
|
|
|
|
+ if (!role) return [];
|
|
|
|
|
+ var us = await role.getUsers().query().find({ useMasterKey: true });
|
|
|
|
|
+ return us.map(function (u) {
|
|
|
|
|
+ return u.id;
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 当前用户的公司指针(无组织返回 null) */
|
|
|
|
|
+function _userCompanyPtr(u) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var p = u.get('company');
|
|
|
|
|
+ return p && p.id ? p : null;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 当前用户是否「只开了个人试用组织」而非真正加入了组织(2026-11-10)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 为什么要判
|
|
|
|
|
+ *
|
|
|
|
|
+ * 实测(2026-11-10):新用户登录时系统会**自动创建一个以他命名的组织**
|
|
|
|
|
+ * (如 liujun 的组织就叫「liujun」),并写入 `enterpriseTrialClaimedAt`。
|
|
|
|
|
+ * 所以 `companyId` 非空**并不能**说明他已被某个组织接纳 ——
|
|
|
|
|
+ * 只判 `companyId` 会让等待页形同虚设。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ## 判据(两项都满足才算个人试用)
|
|
|
|
|
+ *
|
|
|
|
|
+ * 1. 该组织的 `enterpriseTrialClaimedAt` 有值(实测:100 个组织里 86 个个人组织全带此标记,
|
|
|
|
|
+ * 康师傅/路觅教育这类正式组织都没有);
|
|
|
|
|
+ * 2. 该组织里**只有他一个人**(被管理员拉进来的组织至少有 2 人)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 第 2 条兜住「试用组织后来被别人加入」→ 自动转为正式组织的情况。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @returns true = 个人试用组织,应视为「无组织」
|
|
|
|
|
+ */
|
|
|
|
|
+async function _isPersonalTrialOrg(companyPtr, userId) {
|
|
|
|
|
+ if (!companyPtr) return false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var cq = new Parse.Query('Company');
|
|
|
|
|
+ var company = await cq.get(companyPtr.id, { useMasterKey: true });
|
|
|
|
|
+ if (!company || !company.get('enterpriseTrialClaimedAt')) return false;
|
|
|
|
|
+
|
|
|
|
|
+ var uq = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ uq.equalTo('company', companyPtr);
|
|
|
|
|
+ uq.notEqualTo('isDeleted', true);
|
|
|
|
|
+ var count = await uq.count({ useMasterKey: true });
|
|
|
|
|
+ return count <= 1;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ /* 查不到就按「正式组织」处理,避免把正常用户挡在外面 */
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 判断某个用户是否有「真正归属的组织」 */
|
|
|
|
|
+async function _hasRealOrg(user) {
|
|
|
|
|
+ var ptr = _userCompanyPtr(user);
|
|
|
|
|
+ if (!ptr) return false;
|
|
|
|
|
+ return !(await _isPersonalTrialOrg(ptr, user.id));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 重新从库中取一份完整用户对象。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 平台注入的 `request.user` 是**代理对象**:只有 `id`,
|
|
|
|
|
+ * 调 `get('company')` 拿不到字段(实测:明明有组织却判成「未归属」)。
|
|
|
|
|
+ * 所以鉴权后必须按 objectId 重新查一次。
|
|
|
|
|
+ */
|
|
|
|
|
+async function _reloadUser(u) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var q = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ var fresh = await q.get(u.id, { useMasterKey: true });
|
|
|
|
|
+ return fresh || u;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 服务端鉴权 —— **每个 op 必须先过这里**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * @returns { user, companyId, isSuperAdmin } 或 null(已被 _fail 写出)
|
|
|
|
|
+ */
|
|
|
|
|
+async function _authorize() {
|
|
|
|
|
+ var user = await _getUser(_sessionToken());
|
|
|
|
|
+ if (!user) {
|
|
|
|
|
+ _fail(401, 'SessionToken 无效或已过期');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 平台注入的对象只有 id,字段要重新取(见 _reloadUser 注释) */
|
|
|
|
|
+ user = await _reloadUser(user);
|
|
|
|
|
+
|
|
|
|
|
+ var superIds = await _roleUserIds(SUPER_ROLE);
|
|
|
|
|
+ var isSuperAdmin = superIds.indexOf(user.id) >= 0;
|
|
|
|
|
+ var companyPtr = _userCompanyPtr(user);
|
|
|
|
|
+ var companyId = companyPtr ? companyPtr.id : null;
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 超管:与公司管理员一样,**只操作自己所属的组织**。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 不接受前端传的 `companyId` —— 那等于给超管开了一个「查任意租户」的入口,
|
|
|
|
|
+ * 前端一改就能越权。跨租户管理如果需要,应另做受审计的运维接口。
|
|
|
|
|
+ */
|
|
|
|
|
+ if (isSuperAdmin) {
|
|
|
|
|
+ if (!companyId) {
|
|
|
|
|
+ _fail(403, '当前超管账号未归属任何组织,无法使用成员管理');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return { user: user, companyId: companyId, isSuperAdmin: true };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 公司管理员:roles 里必须有 `<companyId>_Admin`
|
|
|
|
|
+ if (!companyId) {
|
|
|
|
|
+ _fail(403, '当前账号不属于任何组织');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ var roleNames = [];
|
|
|
|
|
+ try {
|
|
|
|
|
+ var rq = new Parse.Query(Parse.Role);
|
|
|
|
|
+ rq.equalTo('name', companyId + '_Admin');
|
|
|
|
|
+ var role = await rq.first({ useMasterKey: true });
|
|
|
|
|
+ if (role) {
|
|
|
|
|
+ var us = await role.getUsers().query().find({ useMasterKey: true });
|
|
|
|
|
+ roleNames = us.map(function (u) {
|
|
|
|
|
+ return u.id;
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ /* 查不到角色 → 视为非管理员 */
|
|
|
|
|
+ }
|
|
|
|
|
+ if (roleNames.indexOf(user.id) < 0) {
|
|
|
|
|
+ _fail(403, '仅公司管理员可管理成员');
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return { user: user, companyId: companyId, isSuperAdmin: false };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 用户 → 返回给前端的精简结构 */
|
|
|
|
|
+function _toItem(u) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: u.id,
|
|
|
|
|
+ username: _str(u.get('username'), 80),
|
|
|
|
|
+ nickname: _str(u.get('nickname'), 60),
|
|
|
|
|
+ mobile: _str(u.get('mobile'), 20),
|
|
|
|
|
+ isDeleted: !!u.get('isDeleted'),
|
|
|
|
|
+ createdAt: u.createdAt ? u.createdAt.getTime() : 0,
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** 该用户是否在本组织内 */
|
|
|
|
|
+async function _userInCompany(userId, companyId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var q = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ var u = await q.get(userId, { useMasterKey: true });
|
|
|
|
|
+ var p = _userCompanyPtr(u);
|
|
|
|
|
+ return { user: u, inCompany: !!p && p.id === companyId };
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return { user: null, inCompany: false };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function opList(ctx) {
|
|
|
|
|
+ var page = Math.max(1, parseInt(params.page, 10) || 1);
|
|
|
|
|
+ var pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, parseInt(params.pageSize, 10) || DEFAULT_PAGE_SIZE));
|
|
|
|
|
+ var keyword = _str(params.keyword, 60).trim();
|
|
|
|
|
+ var companyPtr = { __type: 'Pointer', className: 'Company', objectId: ctx.companyId };
|
|
|
|
|
+
|
|
|
|
|
+ function buildQuery() {
|
|
|
|
|
+ var q = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ q.equalTo('company', companyPtr);
|
|
|
|
|
+ q.notEqualTo('isDeleted', true);
|
|
|
|
|
+ if (keyword) {
|
|
|
|
|
+ /* 昵称 / 手机号 / objectId 三选一命中 */
|
|
|
|
|
+ var byName = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ byName.contains('nickname', keyword);
|
|
|
|
|
+ var byMobile = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ byMobile.contains('mobile', keyword);
|
|
|
|
|
+ var byUsername = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ byUsername.contains('username', keyword);
|
|
|
|
|
+ if (keyword.length <= 12) {
|
|
|
|
|
+ var byId = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ byId.equalTo('objectId', keyword);
|
|
|
|
|
+ q = Parse.Query.or(byName, byMobile, byUsername, byId);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ q = Parse.Query.or(byName, byMobile, byUsername);
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 关键词查询也要受组织约束 —— 否则能搜到别的组织的人 */
|
|
|
|
|
+ q.equalTo('company', companyPtr);
|
|
|
|
|
+ q.notEqualTo('isDeleted', true);
|
|
|
|
|
+ }
|
|
|
|
|
+ q.descending('createdAt');
|
|
|
|
|
+ return q;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var total = await buildQuery().count({ useMasterKey: true });
|
|
|
|
|
+ var q = buildQuery();
|
|
|
|
|
+ q.skip((page - 1) * pageSize);
|
|
|
|
|
+ q.limit(pageSize);
|
|
|
|
|
+ var rows = await q.find({ useMasterKey: true });
|
|
|
|
|
+
|
|
|
|
|
+ return _ok({
|
|
|
|
|
+ items: rows.map(_toItem),
|
|
|
|
|
+ total: total,
|
|
|
|
|
+ page: page,
|
|
|
|
|
+ pageSize: pageSize,
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function opAdd(ctx) {
|
|
|
|
|
+ var userId = _str(params.userId, 120).trim();
|
|
|
|
|
+ if (!userId) return _fail(400, '请填写用户 ID');
|
|
|
|
|
+
|
|
|
|
|
+ var found;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var q = new Parse.Query(CLASS_USER);
|
|
|
|
|
+ found = await q.get(userId, { useMasterKey: true });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return _fail(404, '用户不存在:' + userId);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (found.get('isDeleted')) return _fail(400, '该用户已被删除,不能加入组织');
|
|
|
|
|
+
|
|
|
|
|
+ var cur = _userCompanyPtr(found);
|
|
|
|
|
+ if (cur && cur.id !== ctx.companyId) {
|
|
|
|
|
+ return _fail(409, '该用户已属于其他组织,请先联系其原组织处理');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* 改 `_User.company` 指针(运维口径:换组织改指针,objectId 不变) */
|
|
|
|
|
+ var companyPtr = { __type: 'Pointer', className: 'Company', objectId: ctx.companyId };
|
|
|
|
|
+ found.set('company', Parse.Object.fromJSON(companyPtr));
|
|
|
|
|
+ await found.save(null, { useMasterKey: true });
|
|
|
|
|
+
|
|
|
|
|
+ /* 补上普通成员角色(人群聊/可见性等 ACL 依赖它) */
|
|
|
|
|
+ try {
|
|
|
|
|
+ var rq = new Parse.Query(Parse.Role);
|
|
|
|
|
+ rq.equalTo('name', ctx.companyId + '_Public');
|
|
|
|
|
+ var role = await rq.first({ useMasterKey: true });
|
|
|
|
|
+ if (role) await role.getUsers().add(found).save(null, { useMasterKey: true });
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ /* 角色补不上不影响主流程 */
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return _ok({ item: _toItem(found) });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function opGet(ctx) {
|
|
|
|
|
+ var userId = _str(params.userId, 120).trim();
|
|
|
|
|
+ if (!userId) return _fail(400, '用户 ID 不能为空');
|
|
|
|
|
+ var r = await _userInCompany(userId, ctx.companyId);
|
|
|
|
|
+ if (!r.user) return _fail(404, '用户不存在');
|
|
|
|
|
+ if (!r.inCompany) return _fail(403, '该用户不在本组织');
|
|
|
|
|
+ return _ok({ item: _toItem(r.user) });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function opUpdate(ctx) {
|
|
|
|
|
+ var userId = _str(params.userId, 120).trim();
|
|
|
|
|
+ if (!userId) return _fail(400, '用户 ID 不能为空');
|
|
|
|
|
+ var r = await _userInCompany(userId, ctx.companyId);
|
|
|
|
|
+ if (!r.user) return _fail(404, '用户不存在');
|
|
|
|
|
+ if (!r.inCompany) return _fail(403, '该用户不在本组织');
|
|
|
|
|
+
|
|
|
|
|
+ var u = r.user;
|
|
|
|
|
+ if (params.nickname != null) u.set('nickname', _str(params.nickname, 60));
|
|
|
|
|
+ if (params.mobile != null) {
|
|
|
|
|
+ var mobile = _str(params.mobile, 20).trim();
|
|
|
|
|
+ if (mobile && !/^1\d{10}$/.test(mobile)) return _fail(400, '手机号格式不正确');
|
|
|
|
|
+ u.set('mobile', mobile);
|
|
|
|
|
+ }
|
|
|
|
|
+ /* 🔴 改密码必须 useMasterKey;空串表示「不修改」 */
|
|
|
|
|
+ if (params.password != null && String(params.password).length > 0) {
|
|
|
|
|
+ var pwd = String(params.password);
|
|
|
|
|
+ if (pwd.length < 6) return _fail(400, '密码至少 6 位');
|
|
|
|
|
+ u.set('password', pwd);
|
|
|
|
|
+ }
|
|
|
|
|
+ await u.save(null, { useMasterKey: true });
|
|
|
|
|
+ return _ok({ item: _toItem(u) });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function opRemove(ctx) {
|
|
|
|
|
+ var userId = _str(params.userId, 120).trim();
|
|
|
|
|
+ if (!userId) return _fail(400, '用户 ID 不能为空');
|
|
|
|
|
+ if (userId === ctx.user.id) return _fail(400, '不能删除自己');
|
|
|
|
|
+
|
|
|
|
|
+ var r = await _userInCompany(userId, ctx.companyId);
|
|
|
|
|
+ if (!r.user) return _fail(404, '用户不存在');
|
|
|
|
|
+ if (!r.inCompany) return _fail(403, '该用户不在本组织');
|
|
|
|
|
+
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 软删(平台铁律:删除用软删 isDeleted=true)+ 移出组织。
|
|
|
|
|
+ * 不物理删除:Matrix 绑定、会话归属等仍引用该 objectId,物理删会留下悬空指针。
|
|
|
|
|
+ */
|
|
|
|
|
+ var u = r.user;
|
|
|
|
|
+ u.set('isDeleted', true);
|
|
|
|
|
+ u.unset('company');
|
|
|
|
|
+ await u.save(null, { useMasterKey: true });
|
|
|
|
|
+ return _ok({ removed: true });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function opMe(ctx) {
|
|
|
|
|
+ /*
|
|
|
|
|
+ * 「我有没有真正的组织」—— 供前端 /pending 页与 orgGuard 判定。
|
|
|
|
|
+ *
|
|
|
|
|
+ * 🔴 不走管理员鉴权(`_authorize` 会 403),每个登录用户都要能调,
|
|
|
|
|
+ * 所以这里单独处理:只回自己的身份与归属,不泄露任何他人数据。
|
|
|
|
|
+ */
|
|
|
|
|
+ var user = await _getUser(_sessionToken());
|
|
|
|
|
+ if (!user) return _fail(401, 'SessionToken 无效或已过期');
|
|
|
|
|
+ user = await _reloadUser(user);
|
|
|
|
|
+
|
|
|
|
|
+ var ptr = _userCompanyPtr(user);
|
|
|
|
|
+ var real = ptr ? !(await _isPersonalTrialOrg(ptr, user.id)) : false;
|
|
|
|
|
+ var companyName = '';
|
|
|
|
|
+ if (ptr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var c = await new Parse.Query('Company').get(ptr.id, { useMasterKey: true });
|
|
|
|
|
+ companyName = c.get('name') || '';
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ /* 忽略 */
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return _ok({
|
|
|
|
|
+ userId: user.id,
|
|
|
|
|
+ hasOrg: real,
|
|
|
|
|
+ companyId: ptr ? ptr.id : '',
|
|
|
|
|
+ companyName: companyName,
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handler(request, response) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ params = params && typeof params === 'object' ? params : (request && request.body && request.body.params) || {};
|
|
|
|
|
+
|
|
|
|
|
+ /* `/me` 只需登录态,不需要管理员身份 —— 必须放在 _authorize 之前 */
|
|
|
|
|
+ if (params.op === 'me') return await opMe();
|
|
|
|
|
+
|
|
|
|
|
+ var ctx = await _authorize();
|
|
|
|
|
+ if (!ctx) return; // _authorize 已写出错误响应
|
|
|
|
|
+
|
|
|
|
|
+ switch (params.op) {
|
|
|
|
|
+ case 'list':
|
|
|
|
|
+ return await opList(ctx);
|
|
|
|
|
+ case 'add':
|
|
|
|
|
+ return await opAdd(ctx);
|
|
|
|
|
+ case 'get':
|
|
|
|
|
+ return await opGet(ctx);
|
|
|
|
|
+ case 'update':
|
|
|
|
|
+ return await opUpdate(ctx);
|
|
|
|
|
+ case 'remove':
|
|
|
|
|
+ return await opRemove(ctx);
|
|
|
|
|
+ default:
|
|
|
|
|
+ return _fail(400, '未知 op: ' + params.op);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ return _fail(500, '用户管理服务异常: ' + ((e && e.message) || String(e)));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|