| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239 |
- /*
- * Authentication and Authorization utilities for SaaS VOC Cloud Functions
- */
- /**
- * Safely serialize a value, removing sensitive fields
- */
- function safeValue(value, depth) {
- if (depth > 5 || value === null || value === undefined) return value;
- if (value instanceof Date) return value;
- if (Array.isArray(value)) return value.slice(0, 100).map((item) => safeValue(item, depth + 1));
- if (typeof value !== 'object') return value;
- const output = {};
- Object.keys(value).slice(0, 100).forEach((key) => {
- if (/token|secret|password|credential|authorization|master/i.test(key)) return;
- output[key] = safeValue(value[key], depth + 1);
- });
- return output;
- }
- // Roles that can perform write operations
- const WRITE_ROLES = new Set(['owner', 'admin', 'editor']);
- const ADMIN_ROLES = new Set(['owner', 'admin']);
- let activeRequest = null;
- let activeResponse = null;
- /**
- * Set the current request/response context
- */
- function setContext(request, response) {
- activeRequest = request;
- activeResponse = response;
- }
- /**
- * Clear the current context
- */
- function clearContext() {
- activeRequest = null;
- activeResponse = null;
- }
- /**
- * Get request ID from headers or generate one
- */
- function requestId() {
- return activeRequest?.headers && (
- activeRequest.headers['x-request-id'] ||
- activeRequest.headers['X-Request-Id']
- ) || 'cloud-' + Date.now().toString(36);
- }
- /**
- * Get the current user from the request
- */
- function getUser() {
- return activeRequest?.user || null;
- }
- /**
- * Get the current workspace ID
- */
- function getWorkspaceId() {
- return activeRequest?.workspaceId || '';
- }
- /**
- * Respond with an error
- */
- function fail(status, code, message) {
- activeResponse?.status(status).json({
- success: false,
- code,
- message,
- requestId: requestId(),
- });
- }
- /**
- * Respond with success
- */
- function success(data, statusCode = 200) {
- const response = statusCode === 202
- ? activeResponse?.status(202).json({ success: true, data, requestId: requestId() })
- : activeResponse?.json({ success: true, data, requestId: requestId() });
- return response;
- }
- /**
- * Check if user is authenticated
- */
- function requireAuth() {
- if (!activeRequest?.user) {
- throw { status: 401, code: 'unauthenticated', message: '需要登录' };
- }
- }
- /**
- * Get the active workspace member for the current user
- */
- async function activeMember(workspaceId) {
- requireAuth();
- const user = getUser();
- const query = new Parse.Query('VocWorkspaceMember');
- query.equalTo('workspaceId', workspaceId);
- query.equalTo('userId', user.id);
- query.equalTo('status', 'active');
- return query.first({ useMasterKey: true });
- }
- /**
- * Get all workspaces accessible by the current user
- */
- async function accessibleWorkspaces() {
- requireAuth();
- const user = getUser();
- const memberQuery = new Parse.Query('VocWorkspaceMember');
- memberQuery.equalTo('userId', user.id);
- memberQuery.equalTo('status', 'active');
- memberQuery.limit(100);
- const members = await memberQuery.find({ useMasterKey: true });
- const ids = [...new Set(
- members
- .map((member) => String(member.get('workspaceId') || ''))
- .filter(Boolean)
- )];
- if (!ids.length) return [];
- const workspaceQuery = new Parse.Query('VocWorkspace');
- workspaceQuery.containedIn('publicId', ids);
- workspaceQuery.equalTo('status', 'active');
- workspaceQuery.limit(100);
- const workspaces = await workspaceQuery.find({ useMasterKey: true });
- const roleByWorkspace = new Map(
- members.map((member) => [
- String(member.get('workspaceId') || ''),
- String(member.get('role') || 'viewer'),
- ])
- );
- return workspaces.map((workspace) => ({
- ...safeValue(workspace.toJSON(), 0),
- role: roleByWorkspace.get(String(workspace.get('publicId') || '')) || 'viewer',
- }));
- }
- /**
- * Check if an action is a write action
- */
- function isWriteAction(action) {
- return (
- /\.create$|\.update$|\.upsert$|\.delete$|\.enqueue$|\.retry$|\.cancel$|\.adopt$|\.run$/.test(action) ||
- action === 'ai.chat' ||
- action === 'ai.test' ||
- action === 'competitor.refresh'
- );
- }
- /**
- * Get safe value utility (lazy loaded to avoid circular dependency)
- */
- function safeValue(value, depth) {
- if (depth > 5 || value === null || value === undefined) return value;
- if (value instanceof Date) return value;
- if (Array.isArray(value)) return value.slice(0, 100).map((item) => safeValue(item, depth + 1));
- if (typeof value !== 'object') return value;
- const output = {};
- Object.keys(value).slice(0, 100).forEach((key) => {
- if (/token|secret|password|credential|authorization|master/i.test(key)) return;
- output[key] = safeValue(value[key], depth + 1);
- });
- return output;
- }
- /**
- * Authorize an action for the current user in a workspace
- * @param {string} action - The action being performed
- * @param {string} workspaceId - The workspace ID
- * @returns {Object} Authorization result with member, role, and productIds
- */
- async function authorize(action, workspaceId) {
- requireAuth();
- const member = await activeMember(workspaceId);
- if (!member) {
- throw { status: 403, code: 'workspace_access_denied', message: '无权访问该 workspace' };
- }
- const role = String(member.get('role') || 'viewer');
- // Check admin-only actions
- if (action === 'workspace.members.list' && !ADMIN_ROLES.has(role)) {
- throw { status: 403, code: 'forbidden', message: '权限不足' };
- }
- // Check write permission
- if (isWriteAction(action) && !WRITE_ROLES.has(role)) {
- throw { status: 403, code: 'viewer_write_forbidden', message: 'Viewer 不能执行写入操作' };
- }
- // Get product scope
- const configuredProductIds = member.get('productIds');
- const productIds = Array.isArray(configuredProductIds)
- ? [...new Set(configuredProductIds.filter((value) => typeof value === 'string' && value.length <= 200))]
- : (ADMIN_ROLES.has(role) ? null : []);
- return { member, role, productIds };
- }
- /**
- * Resolve workspace ID, using membership if not provided
- */
- async function resolveWorkspaceId(workspaceId) {
- if (workspaceId && workspaceId.trim()) return workspaceId.trim();
- requireAuth();
- const user = getUser();
- const membershipQuery = new Parse.Query('VocWorkspaceMember');
- membershipQuery.equalTo('userId', user.id);
- membershipQuery.equalTo('status', 'active');
- const membership = await membershipQuery.first({ useMasterKey: true });
- return membership ? String(membership.get('workspaceId') || '') : '';
- }
- /*
- // module.exports = {
- // setContext,
- // clearContext,
- // requestId,
- // getUser,
- // getWorkspaceId,
- // fail,
- // success,
- // requireAuth,
- // activeMember,
- // accessibleWorkspaces,
- // isWriteAction,
- // safeValue,
- // authorize,
- // resolveWorkspaceId,
- // };
- */
|