|
|
@@ -18,7 +18,7 @@ const ALLOWED_CLASSES = new Set([
|
|
|
'PageTemplate','PaperQuestions','PayPlat','Permission','PlatComp','App','DesignAnswer','DesignAsk','Attachment','DesignPage','DesignQuestion','DesignRes','Feedback','DesignScence','StudentAchieve','ContentArticle','DesignSiteInfo','Profile','AdInfo','AdZone','ARoleAuth','Baike','ExamSysQuestions','ContactInfo','VocabularyWord','AssessmentProfile','Agency','MemoryPracticeRecord','DeliveryCenter','CourseBinding','PracticeRecord','CourseAppointment','Account','SurveyItem','SurveyLog','LessonRecord','DailyStudyRecord','CommonModel','ContentPublish','Company','_Role','_User','CRMSAttr','Currency','Datadic','Datadiccategory','DesignTlp','DocModel','DocPermission','ExamClass','ExamSysPapers','ExamType','ExamPoint','ExTeacher','FontPicShape','FontPicShapeType','Grade','GradeCate','Group','GroupModel','GuestBar','Guestbook','Guestcate','MailTemp','Manager','MisProcedure','MisProLevel','MisSign','MisType','PageStyle','Model','ModelField','Node','NodeAuth','NodeModelTemplate','Product','PlatUserRole','Pub','PubTw','PubWTHD','PubZXDC','PublishNode','QuestionsKnowledge','Role','StoreProduct','SafeMobile','Search','SenTask','ServiceSeat','ShopFareTlp','ShopMoneyRegular','Special','StoreApplication','StoreStyle','SysCSSManage','SysHoliday','SysLog','Temp','ThirdPlatInfo','UserCredit','UserDummyPoint','UserFriendGroup','UserLevel','UserSIcon','UserUserPoint','UserExpDomP','UserExpHis'
|
|
|
]);
|
|
|
const READ_ONLY_CLASSES = new Set(['_Role','Permission','PayPlat','ThirdPlatInfo']);
|
|
|
-const SYSTEM_FIELDS = new Set(['objectId','createdAt','updatedAt','ACL','company','password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','adminRoleKey','isAdmin','roles','role']);
|
|
|
+const SYSTEM_FIELDS = new Set(['objectId','createdAt','updatedAt','ACL','company','password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','adminRoleKey','isAdmin','roles','role','isDisabled','legacyUserId','legacyGroupId','legacyUserData']);
|
|
|
const HIDDEN_FIELDS = new Set(['password','authData','sessionToken','legacyPasswordHash','legacyPasswordHashType','appPassword','newapiToken','fmodeApiToken','useMasterKey','adminPassword','randNumber','apiKey','appSecret','secret','token']);
|
|
|
const GENERIC_WRITE_TYPES = new Set(['String','Number','Boolean','Date','Pointer','Object','Array']);
|
|
|
|
|
|
@@ -49,6 +49,7 @@ async function requireAdmin(request) {
|
|
|
const current = request.user || (typeof user !== 'undefined' ? user : null);
|
|
|
if (!current) fail(401, '管理员会话已失效');
|
|
|
await current.fetch({ useMasterKey: true });
|
|
|
+ if (current.get('isDisabled') === true || current.get('isDeleted') === true) fail(403, '管理员账号已停用');
|
|
|
const roles = Array.isArray(current.get('roles')) ? current.get('roles').map(String) : [];
|
|
|
const role = String(current.get('role') || '');
|
|
|
const roleKey = String(current.get('adminRoleKey') || '');
|
|
|
@@ -59,6 +60,25 @@ async function requireAdmin(request) {
|
|
|
if (!company && !isSuperAdmin) fail(403, '管理员账号尚未分配帐套');
|
|
|
return { current, roles, isSuperAdmin, company };
|
|
|
}
|
|
|
+function isAdminAccount(target) {
|
|
|
+ const roles = Array.isArray(target.get('roles')) ? target.get('roles').map(String) : [];
|
|
|
+ return target.get('isAdmin') === true || target.get('role') === 'admin' || roles.includes('admin') || target.get('adminRoleKey') === 'super-admin' || roles.includes('super-admin');
|
|
|
+}
|
|
|
+function assertCanManageUser(context, target, action) {
|
|
|
+ if (action === 'lock' && target.id === context.current.id) fail(400, '不能停用当前登录账号');
|
|
|
+ if (isAdminAccount(target) && target.id !== context.current.id && !context.isSuperAdmin) fail(403, '只有超级管理员可以管理其他管理员账号');
|
|
|
+}
|
|
|
+async function revokeSessions(targets) {
|
|
|
+ let revoked = 0;
|
|
|
+ while (targets.length) {
|
|
|
+ const query = new Parse.Query('_Session'); query.containedIn('user', targets); query.limit(1000);
|
|
|
+ const sessions = await query.find({ useMasterKey: true });
|
|
|
+ if (!sessions.length) break;
|
|
|
+ await Parse.Object.destroyAll(sessions, { useMasterKey: true }); revoked += sessions.length;
|
|
|
+ if (sessions.length < 1000) break;
|
|
|
+ }
|
|
|
+ return revoked;
|
|
|
+}
|
|
|
function assertClass(className) {
|
|
|
if (!ALLOWED_CLASSES.has(className)) fail(400, '不允许访问该数据类');
|
|
|
}
|
|
|
@@ -197,6 +217,47 @@ async function handler(request, response) {
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
+ if (operation === 'userBatch') {
|
|
|
+ const action = String(input.action || '');
|
|
|
+ if (!['lock','unlock','move'].includes(action)) fail(400, '不支持的用户批量操作');
|
|
|
+ const requestedIds = Array.isArray(input.objectIds) ? input.objectIds : [input.objectId];
|
|
|
+ const objectIds = [...new Set(requestedIds.map((value) => String(value || '').trim()).filter(Boolean))];
|
|
|
+ if (!objectIds.length || objectIds.length > 100) fail(400, '每次请选择 1 至 100 个用户');
|
|
|
+ const userFields = await schemaFor('_User');
|
|
|
+ const query = new Parse.Query('_User'); applyTenant(query, userFields, context, input.companyId); query.containedIn('objectId', objectIds); query.limit(objectIds.length);
|
|
|
+ const targets = await query.find({ useMasterKey: true });
|
|
|
+ if (targets.length !== objectIds.length) fail(404, '部分用户不存在或不属于当前帐套');
|
|
|
+ for (const target of targets) assertCanManageUser(context, target, action);
|
|
|
+ let group = null;
|
|
|
+ let groupId = 0;
|
|
|
+ if (action === 'move') {
|
|
|
+ groupId = Number(input.groupId);
|
|
|
+ if (!Number.isInteger(groupId) || groupId < 1) fail(400, '请选择有效用户组');
|
|
|
+ const companyIds = [...new Set(targets.map((target) => pointerId(target.get('company'))).filter(Boolean))];
|
|
|
+ if (companyIds.length !== 1) fail(400, '批量移动的用户必须属于同一帐套');
|
|
|
+ const groupRow = await Psql.oneOrNone('SELECT "objectId" FROM "Group" WHERE "company"=$1 AND "groupId"=$2 LIMIT 1', [companyIds[0], groupId]);
|
|
|
+ if (!groupRow) fail(404, '目标用户组不存在或不属于当前帐套');
|
|
|
+ group = await new Parse.Query('Group').get(String(groupRow.objectId), { useMasterKey: true });
|
|
|
+ }
|
|
|
+ const now = new Date();
|
|
|
+ for (const target of targets) {
|
|
|
+ const legacyData = { ...(target.get('legacyUserData') || {}) };
|
|
|
+ if (action === 'move') {
|
|
|
+ target.set('legacyGroupId', groupId); legacyData.GroupID = groupId;
|
|
|
+ } else {
|
|
|
+ const disabled = action === 'lock'; target.set('isDisabled', disabled); legacyData.State = disabled ? 0 : 1;
|
|
|
+ if (userFields.statusAction) target.set('statusAction', disabled ? 'admin-lock' : 'admin-unlock');
|
|
|
+ if (userFields.statusReason) target.set('statusReason', String(input.reason || '').trim().slice(0, 200));
|
|
|
+ if (userFields.statusUpdatedAt) target.set('statusUpdatedAt', now);
|
|
|
+ if (userFields.statusUpdatedBy) target.set('statusUpdatedBy', context.current.id);
|
|
|
+ }
|
|
|
+ target.set('legacyUserData', legacyData);
|
|
|
+ }
|
|
|
+ await Parse.Object.saveAll(targets, { useMasterKey: true });
|
|
|
+ const revokedSessions = action === 'lock' ? await revokeSessions(targets) : 0;
|
|
|
+ for (const target of targets) await audit({ ...context, company: target.get('company') || context.company }, 'user-' + action, '_User', target.id);
|
|
|
+ return response.json({ success: true, data: { action, updated: targets.length, revokedSessions, group: group ? serializeObject(group) : null, results: targets.map(serializeObject) } });
|
|
|
+ }
|
|
|
const className = String(input.className || '');
|
|
|
assertClass(className);
|
|
|
const fields = await schemaFor(className);
|
|
|
@@ -233,12 +294,14 @@ async function handler(request, response) {
|
|
|
}
|
|
|
if (operation === 'delete') {
|
|
|
if (!classWritable || !objectId) fail(400, '该记录不允许删除'); const query = new Parse.Query(className); applyTenant(query, fields, context, input.companyId); const object = await query.get(objectId, { useMasterKey: true });
|
|
|
+ if (className === '_User') assertCanManageUser(context, object, 'lock');
|
|
|
if (fields.isDeleted) { object.set('isDeleted', true); await object.save(null, { useMasterKey: true }); } else await object.destroy({ useMasterKey: true });
|
|
|
+ if (className === '_User') await revokeSessions([object]);
|
|
|
await audit(context, 'delete', className, objectId); return response.json({ success: true, data: { objectId, softDeleted: Boolean(fields.isDeleted) } });
|
|
|
}
|
|
|
if (operation === 'resetPassword') {
|
|
|
if (className !== '_User' || !objectId || typeof input.newPassword !== 'string' || input.newPassword.length < 8) fail(400, '密码至少 8 位');
|
|
|
- const query = new Parse.Query('_User'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); target.setPassword(input.newPassword); if (fields.passwordResetRequired) target.set('passwordResetRequired', false); await target.save(null, { useMasterKey: true }); await audit(context, 'reset-password', '_User', objectId); return response.json({ success: true, data: { objectId } });
|
|
|
+ const query = new Parse.Query('_User'); applyTenant(query, fields, context, input.companyId); const target = await query.get(objectId, { useMasterKey: true }); assertCanManageUser(context, target, 'reset-password'); target.setPassword(input.newPassword); if (fields.passwordResetRequired) target.set('passwordResetRequired', false); await target.save(null, { useMasterKey: true }); const revokedSessions = await revokeSessions([target]); await audit(context, 'reset-password', '_User', objectId); return response.json({ success: true, data: { objectId, revokedSessions } });
|
|
|
}
|
|
|
fail(400, '不支持的后台操作');
|
|
|
} catch (error) {
|
|
|
@@ -328,6 +391,7 @@ function legacyAliases(value, className) {
|
|
|
}; const map = maps[className] || {}; for (const [legacy,source] of Object.entries(map)) if (row[legacy] === undefined && row[source] !== undefined) row[legacy] = row[source]; return row;
|
|
|
}
|
|
|
async function currentUser(request, required = true) { const current = request.user || (typeof user !== 'undefined' ? user : null); if (!current) { if (required) fail(401, '登录状态已失效'); return null; } await current.fetch({ useMasterKey: true }); const company = current.get('company'); if (company && company.id !== DEFAULT_COMPANY_ID) fail(403, '用户不属于小树英语帐套'); return current; }
|
|
|
+async function revokeUserSessions(target) { let count = 0; while (true) { const query = new Parse.Query('_Session'); query.equalTo('user', target); query.limit(1000); const sessions = await query.find({ useMasterKey:true }); if (!sessions.length) break; await Parse.Object.destroyAll(sessions,{useMasterKey:true}); count += sessions.length; if (sessions.length < 1000) break; } return count; }
|
|
|
function objectValue(source, key) { return source && typeof source.get === 'function' ? source.get(key) : source && source[key]; }
|
|
|
function legacyData(source) { const value = objectValue(source, 'legacyUserData'); return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; }
|
|
|
function firstValue(...values) { return values.find((value) => value !== undefined && value !== null && value !== ''); }
|
|
|
@@ -581,7 +645,7 @@ async function handler(request, response) {
|
|
|
if (BLOCKED[action]) return response.status(501).json(reject('migration_blocked: ' + BLOCKED[action]));
|
|
|
if (action === 'user_login_passwd') {
|
|
|
const username = String(input.name || input.username || '').trim(); const password = String(input.passwd || input.password || ''); if (!username || !password) return response.status(400).json(reject('请输入账号和密码'));
|
|
|
- try { const loggedIn = await Parse.User.logIn(username, password); if (loggedIn.get('passwordResetRequired') === true) return response.status(403).json(reject('旧系统账号必须先重置 Parse 密码')); return response.json(envelope(legacyUser(loggedIn, true), { State: loggedIn.get('isDisabled') === true ? 0 : 1 })); } catch (_) { return response.status(401).json(reject('账号或密码错误')); }
|
|
|
+ try { const loggedIn = await Parse.User.logIn(username, password); if (loggedIn.get('isDisabled') === true || loggedIn.get('isDeleted') === true) { await revokeUserSessions(loggedIn).catch(() => undefined); return response.status(403).json(reject('账号已停用')); } if (loggedIn.get('passwordResetRequired') === true) return response.status(403).json(reject('旧系统账号必须先重置 Parse 密码')); return response.json(envelope(legacyUser(loggedIn, true), { State: 1 })); } catch (_) { return response.status(401).json(reject('账号或密码错误')); }
|
|
|
}
|
|
|
if (action === 'user_register') { const registered = await registerUser(input); return response.json(envelope(registered.result,registered.addon)); }
|
|
|
if (action === 'user_info_name') { const username = String(input.uname || input.name || '').trim(); if (!username) return response.status(400).json(reject('缺少用户名')); const rows = await Psql.query('SELECT "objectId" FROM "_User" WHERE "company" = $1 AND "username" = $2 LIMIT 1', [DEFAULT_COMPANY_ID, username]); const found = rows[0]; return found ? response.json(envelope({ objectId: found.objectId })) : response.status(404).json(reject('用户不存在')); }
|