plugin-ai-api 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,111 +1,142 @@
1
- /**
2
- * This file is part of the NocoBase (R) project.
3
- * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
- * Authors: NocoBase Team.
5
- *
6
- * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
- * For more information, please refer to: https://www.nocobase.com/agreement.
8
- */
9
-
10
- import { Context, Next } from '@nocobase/actions';
11
- import { toOpenAIError } from '../utils/openai-format';
12
-
13
- /**
14
- * Authentication middleware for /api/ai-llm/v1/* routes.
15
- *
16
- * Validates Bearer token against NocoBase's apiKeys collection.
17
- * Sets ctx.state.currentUser for downstream handlers.
18
- */
19
- export async function authenticateBearer(ctx: Context): Promise<boolean> {
20
- const authHeader = ctx.get('Authorization') || '';
21
- if (!authHeader.startsWith('Bearer ')) {
22
- ctx.status = 401;
23
- ctx.body = toOpenAIError(
24
- 401,
25
- 'Missing or invalid Authorization header. Expected: Bearer <api-key>',
26
- 'invalid_request_error',
27
- 'invalid_api_key',
28
- );
29
- return false;
30
- }
31
-
32
- const token = authHeader.substring(7).trim();
33
- if (!token) {
34
- ctx.status = 401;
35
- ctx.body = toOpenAIError(401, 'API key is empty', 'invalid_request_error', 'invalid_api_key');
36
- return false;
37
- }
38
-
39
- try {
40
- // Use NocoBase's auth system directly to validate the token.
41
- // NocoBase API keys are JWT tokens signed with the app secret.
42
- // We decode them directly via authManager.jwt rather than going through
43
- // the full NocoBase auth middleware stack (which requires being inside
44
- // the resourcer pipeline, but we run before it).
45
- const auth = ctx.app['authManager'];
46
- if (!auth) {
47
- ctx.status = 500;
48
- ctx.body = toOpenAIError(500, 'Auth system not available', 'server_error');
49
- return false;
50
- }
51
-
52
- // Try to authenticate using the token as a NocoBase JWT/API token
53
- // NocoBase API keys are essentially JWT tokens signed with the app secret
54
- const jwt = ctx.app['authManager']?.jwt;
55
- if (!jwt) {
56
- ctx.status = 500;
57
- ctx.body = toOpenAIError(500, 'JWT system not available', 'server_error');
58
- return false;
59
- }
60
-
61
- let decoded: any;
62
- try {
63
- decoded = await jwt.decode(token);
64
- } catch (e) {
65
- ctx.status = 401;
66
- ctx.body = toOpenAIError(401, 'Incorrect API key provided', 'invalid_request_error', 'invalid_api_key');
67
- return false;
68
- }
69
-
70
- if (!decoded || !decoded.userId) {
71
- ctx.status = 401;
72
- ctx.body = toOpenAIError(401, 'Invalid or expired API key', 'invalid_request_error', 'invalid_api_key');
73
- return false;
74
- }
75
-
76
- // Load user
77
- const user = await ctx.db.getRepository('users').findOne({
78
- filterByTk: decoded.userId,
79
- });
80
-
81
- if (!user) {
82
- ctx.status = 401;
83
- ctx.body = toOpenAIError(401, 'User not found for this API key', 'invalid_request_error', 'invalid_api_key');
84
- return false;
85
- }
86
-
87
- // Set user context for downstream handlers
88
- ctx.state.currentUser = user;
89
- ctx.state.currentRoles = decoded.roleName ? [decoded.roleName] : ['member'];
90
-
91
- // Also set ctx.auth for AIEmployee compatibility.
92
- // Only expose non-sensitive fields — exclude password, token, 2FA secrets, etc.
93
- if (!ctx.auth) {
94
- (ctx as any).auth = {};
95
- }
96
- const SAFE_USER_FIELDS = new Set(['id', 'username', 'nickname', 'email', 'createdAt', 'updatedAt']);
97
- const rawUser = user.toJSON?.() ?? {};
98
- const safeUser: Record<string, any> = { id: user.id };
99
- for (const field of SAFE_USER_FIELDS) {
100
- if (rawUser[field] !== undefined) safeUser[field] = rawUser[field];
101
- }
102
- (ctx as any).auth.user = safeUser;
103
-
104
- return true;
105
- } catch (err) {
106
- ctx.log.error('AI API auth error:', err);
107
- ctx.status = 401;
108
- ctx.body = toOpenAIError(401, 'Authentication failed', 'invalid_request_error', 'invalid_api_key');
109
- return false;
110
- }
111
- }
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { Context } from '@nocobase/actions';
11
+ import { toOpenAIError } from '../utils/openai-format';
12
+
13
+ /**
14
+ * Authentication middleware for /api/ai-llm/v1/* routes.
15
+ *
16
+ * Resolve the principal prepared by NocoBase auth/ACL middleware. OIDC resource
17
+ * authentication rewrites the external access token to an internal session token
18
+ * and sets currentUser before this gateway runs. API keys are the one case where
19
+ * we still need to decode the token locally because they are signed directly by
20
+ * NocoBase and carry a fixed roleName.
21
+ */
22
+ export async function authenticateBearer(ctx: Context): Promise<boolean> {
23
+ const authHeader = ctx.get('Authorization') || '';
24
+ if (!authHeader.startsWith('Bearer ')) {
25
+ ctx.status = 401;
26
+ ctx.body = toOpenAIError(
27
+ 401,
28
+ 'Missing or invalid Authorization header. Expected: Bearer <access-token>',
29
+ 'invalid_request_error',
30
+ 'invalid_api_key',
31
+ );
32
+ return false;
33
+ }
34
+
35
+ const token = authHeader.substring(7).trim();
36
+ if (!token) {
37
+ ctx.status = 401;
38
+ ctx.body = toOpenAIError(401, 'API key is empty', 'invalid_request_error', 'invalid_api_key');
39
+ return false;
40
+ }
41
+
42
+ try {
43
+ if (ctx.state.currentUser) {
44
+ if (!ctx.state.currentRole) {
45
+ const requestedRole = ctx.get('X-Role');
46
+ const rolesRepository = ctx.db.getRepository('users.roles', ctx.state.currentUser.id);
47
+ const roles = await rolesRepository.find({ fields: ['name'] });
48
+ const roleNames = roles.map((role: { name: string }) => role.name);
49
+ ctx.state.currentRole = roleNames.includes(requestedRole) ? requestedRole : roleNames[0];
50
+ ctx.state.currentRoles = ctx.state.currentRole ? [ctx.state.currentRole] : roleNames;
51
+ }
52
+ return true;
53
+ }
54
+
55
+ // Try to authenticate using the token as a NocoBase JWT/API token
56
+ // NocoBase API keys are essentially JWT tokens signed with the app secret
57
+ const jwt = ctx.app['authManager']?.jwt;
58
+ if (!jwt) {
59
+ ctx.status = 500;
60
+ ctx.body = toOpenAIError(500, 'JWT system not available', 'server_error');
61
+ return false;
62
+ }
63
+
64
+ let decoded: { userId?: string | number; roleName?: string; temp?: boolean };
65
+ try {
66
+ decoded = await jwt.decode(token);
67
+ } catch (e) {
68
+ ctx.status = 401;
69
+ ctx.body = toOpenAIError(401, 'Incorrect API key provided', 'invalid_request_error', 'invalid_api_key');
70
+ return false;
71
+ }
72
+
73
+ if (!decoded || !decoded.userId) {
74
+ ctx.status = 401;
75
+ ctx.body = toOpenAIError(401, 'Invalid or expired API key', 'invalid_request_error', 'invalid_api_key');
76
+ return false;
77
+ }
78
+
79
+ // This fallback is for API keys only. A normal login/OIDC token should have
80
+ // been resolved by NocoBase auth middleware and must not be treated as the
81
+ // member role merely because it has no roleName claim.
82
+ if (!decoded.roleName) {
83
+ ctx.status = 401;
84
+ ctx.body = toOpenAIError(
85
+ 401,
86
+ 'Token was not resolved by the NocoBase auth middleware',
87
+ 'invalid_request_error',
88
+ 'invalid_api_key',
89
+ );
90
+ return false;
91
+ }
92
+
93
+ const user = await ctx.db.getRepository('users').findOne({
94
+ filterByTk: decoded.userId,
95
+ });
96
+
97
+ if (!user) {
98
+ ctx.status = 401;
99
+ ctx.body = toOpenAIError(401, 'User not found for this API key', 'invalid_request_error', 'invalid_api_key');
100
+ return false;
101
+ }
102
+
103
+ // Set user context for downstream handlers
104
+ ctx.state.currentUser = user;
105
+ const rolesRepository = ctx.db.getRepository('users.roles', user.id);
106
+ const roles = await rolesRepository.find({ fields: ['name'] });
107
+ const roleNames = roles.map((role: { name: string }) => role.name);
108
+ if (!roleNames.includes(decoded.roleName)) {
109
+ ctx.status = 403;
110
+ ctx.body = toOpenAIError(
111
+ 403,
112
+ 'The API key role is no longer assigned to this user',
113
+ 'permission_denied',
114
+ 'role_not_permitted',
115
+ );
116
+ return false;
117
+ }
118
+ ctx.state.currentRole = decoded.roleName;
119
+ ctx.state.currentRoles = [decoded.roleName];
120
+ ctx.state.aiApiAuthType = 'apiKey';
121
+
122
+ // Also set ctx.auth for AIEmployee compatibility.
123
+ // Only expose non-sensitive fields — exclude password, token, 2FA secrets, etc.
124
+ if (!ctx.auth) {
125
+ (ctx as any).auth = {};
126
+ }
127
+ const SAFE_USER_FIELDS = new Set(['id', 'username', 'nickname', 'email', 'createdAt', 'updatedAt']);
128
+ const rawUser = user.toJSON?.() ?? {};
129
+ const safeUser: Record<string, any> = { id: user.id };
130
+ for (const field of SAFE_USER_FIELDS) {
131
+ if (rawUser[field] !== undefined) safeUser[field] = rawUser[field];
132
+ }
133
+ (ctx as any).auth.user = safeUser;
134
+
135
+ return true;
136
+ } catch (err) {
137
+ ctx.log.error('AI API auth error:', err);
138
+ ctx.status = 401;
139
+ ctx.body = toOpenAIError(401, 'Authentication failed', 'invalid_request_error', 'invalid_api_key');
140
+ return false;
141
+ }
142
+ }