auth.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. /*
  2. * Authentication and Authorization utilities for SaaS VOC Cloud Functions
  3. */
  4. /**
  5. * Safely serialize a value, removing sensitive fields
  6. */
  7. function safeValue(value, depth) {
  8. if (depth > 5 || value === null || value === undefined) return value;
  9. if (value instanceof Date) return value;
  10. if (Array.isArray(value)) return value.slice(0, 100).map((item) => safeValue(item, depth + 1));
  11. if (typeof value !== 'object') return value;
  12. const output = {};
  13. Object.keys(value).slice(0, 100).forEach((key) => {
  14. if (/token|secret|password|credential|authorization|master/i.test(key)) return;
  15. output[key] = safeValue(value[key], depth + 1);
  16. });
  17. return output;
  18. }
  19. // Roles that can perform write operations
  20. const WRITE_ROLES = new Set(['owner', 'admin', 'editor']);
  21. const ADMIN_ROLES = new Set(['owner', 'admin']);
  22. let activeRequest = null;
  23. let activeResponse = null;
  24. /**
  25. * Set the current request/response context
  26. */
  27. function setContext(request, response) {
  28. activeRequest = request;
  29. activeResponse = response;
  30. }
  31. /**
  32. * Clear the current context
  33. */
  34. function clearContext() {
  35. activeRequest = null;
  36. activeResponse = null;
  37. }
  38. /**
  39. * Get request ID from headers or generate one
  40. */
  41. function requestId() {
  42. return activeRequest?.headers && (
  43. activeRequest.headers['x-request-id'] ||
  44. activeRequest.headers['X-Request-Id']
  45. ) || 'cloud-' + Date.now().toString(36);
  46. }
  47. /**
  48. * Get the current user from the request
  49. */
  50. function getUser() {
  51. return activeRequest?.user || null;
  52. }
  53. /**
  54. * Get the current workspace ID
  55. */
  56. function getWorkspaceId() {
  57. return activeRequest?.workspaceId || '';
  58. }
  59. /**
  60. * Respond with an error
  61. */
  62. function fail(status, code, message) {
  63. activeResponse?.status(status).json({
  64. success: false,
  65. code,
  66. message,
  67. requestId: requestId(),
  68. });
  69. }
  70. /**
  71. * Respond with success
  72. */
  73. function success(data, statusCode = 200) {
  74. const response = statusCode === 202
  75. ? activeResponse?.status(202).json({ success: true, data, requestId: requestId() })
  76. : activeResponse?.json({ success: true, data, requestId: requestId() });
  77. return response;
  78. }
  79. /**
  80. * Check if user is authenticated
  81. */
  82. function requireAuth() {
  83. if (!activeRequest?.user) {
  84. throw { status: 401, code: 'unauthenticated', message: '需要登录' };
  85. }
  86. }
  87. /**
  88. * Get the active workspace member for the current user
  89. */
  90. async function activeMember(workspaceId) {
  91. requireAuth();
  92. const user = getUser();
  93. const query = new Parse.Query('VocWorkspaceMember');
  94. query.equalTo('workspaceId', workspaceId);
  95. query.equalTo('userId', user.id);
  96. query.equalTo('status', 'active');
  97. return query.first({ useMasterKey: true });
  98. }
  99. /**
  100. * Get all workspaces accessible by the current user
  101. */
  102. async function accessibleWorkspaces() {
  103. requireAuth();
  104. const user = getUser();
  105. const memberQuery = new Parse.Query('VocWorkspaceMember');
  106. memberQuery.equalTo('userId', user.id);
  107. memberQuery.equalTo('status', 'active');
  108. memberQuery.limit(100);
  109. const members = await memberQuery.find({ useMasterKey: true });
  110. const ids = [...new Set(
  111. members
  112. .map((member) => String(member.get('workspaceId') || ''))
  113. .filter(Boolean)
  114. )];
  115. if (!ids.length) return [];
  116. const workspaceQuery = new Parse.Query('VocWorkspace');
  117. workspaceQuery.containedIn('publicId', ids);
  118. workspaceQuery.equalTo('status', 'active');
  119. workspaceQuery.limit(100);
  120. const workspaces = await workspaceQuery.find({ useMasterKey: true });
  121. const roleByWorkspace = new Map(
  122. members.map((member) => [
  123. String(member.get('workspaceId') || ''),
  124. String(member.get('role') || 'viewer'),
  125. ])
  126. );
  127. return workspaces.map((workspace) => ({
  128. ...safeValue(workspace.toJSON(), 0),
  129. role: roleByWorkspace.get(String(workspace.get('publicId') || '')) || 'viewer',
  130. }));
  131. }
  132. /**
  133. * Check if an action is a write action
  134. */
  135. function isWriteAction(action) {
  136. return (
  137. /\.create$|\.update$|\.upsert$|\.delete$|\.enqueue$|\.retry$|\.cancel$|\.adopt$|\.run$/.test(action) ||
  138. action === 'ai.chat' ||
  139. action === 'ai.test' ||
  140. action === 'competitor.refresh'
  141. );
  142. }
  143. /**
  144. * Get safe value utility (lazy loaded to avoid circular dependency)
  145. */
  146. function safeValue(value, depth) {
  147. if (depth > 5 || value === null || value === undefined) return value;
  148. if (value instanceof Date) return value;
  149. if (Array.isArray(value)) return value.slice(0, 100).map((item) => safeValue(item, depth + 1));
  150. if (typeof value !== 'object') return value;
  151. const output = {};
  152. Object.keys(value).slice(0, 100).forEach((key) => {
  153. if (/token|secret|password|credential|authorization|master/i.test(key)) return;
  154. output[key] = safeValue(value[key], depth + 1);
  155. });
  156. return output;
  157. }
  158. /**
  159. * Authorize an action for the current user in a workspace
  160. * @param {string} action - The action being performed
  161. * @param {string} workspaceId - The workspace ID
  162. * @returns {Object} Authorization result with member, role, and productIds
  163. */
  164. async function authorize(action, workspaceId) {
  165. requireAuth();
  166. const member = await activeMember(workspaceId);
  167. if (!member) {
  168. throw { status: 403, code: 'workspace_access_denied', message: '无权访问该 workspace' };
  169. }
  170. const role = String(member.get('role') || 'viewer');
  171. // Check admin-only actions
  172. if (action === 'workspace.members.list' && !ADMIN_ROLES.has(role)) {
  173. throw { status: 403, code: 'forbidden', message: '权限不足' };
  174. }
  175. // Check write permission
  176. if (isWriteAction(action) && !WRITE_ROLES.has(role)) {
  177. throw { status: 403, code: 'viewer_write_forbidden', message: 'Viewer 不能执行写入操作' };
  178. }
  179. // Get product scope
  180. const configuredProductIds = member.get('productIds');
  181. const productIds = Array.isArray(configuredProductIds)
  182. ? [...new Set(configuredProductIds.filter((value) => typeof value === 'string' && value.length <= 200))]
  183. : (ADMIN_ROLES.has(role) ? null : []);
  184. return { member, role, productIds };
  185. }
  186. /**
  187. * Resolve workspace ID, using membership if not provided
  188. */
  189. async function resolveWorkspaceId(workspaceId) {
  190. if (workspaceId && workspaceId.trim()) return workspaceId.trim();
  191. requireAuth();
  192. const user = getUser();
  193. const membershipQuery = new Parse.Query('VocWorkspaceMember');
  194. membershipQuery.equalTo('userId', user.id);
  195. membershipQuery.equalTo('status', 'active');
  196. const membership = await membershipQuery.first({ useMasterKey: true });
  197. return membership ? String(membership.get('workspaceId') || '') : '';
  198. }
  199. /*
  200. // module.exports = {
  201. // setContext,
  202. // clearContext,
  203. // requestId,
  204. // getUser,
  205. // getWorkspaceId,
  206. // fail,
  207. // success,
  208. // requireAuth,
  209. // activeMember,
  210. // accessibleWorkspaces,
  211. // isWriteAction,
  212. // safeValue,
  213. // authorize,
  214. // resolveWorkspaceId,
  215. // };
  216. */