@checkstack/auth-backend 0.0.2
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.
- package/CHANGELOG.md +142 -0
- package/drizzle/0000_minor_virginia_dare.sql +90 -0
- package/drizzle/0001_certain_madame_hydra.sql +20 -0
- package/drizzle/meta/0000_snapshot.json +580 -0
- package/drizzle/meta/0001_snapshot.json +717 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +7 -0
- package/package.json +34 -0
- package/src/hooks.ts +14 -0
- package/src/index.ts +878 -0
- package/src/meta-config.ts +13 -0
- package/src/platform-registration-config.ts +25 -0
- package/src/router.test.ts +440 -0
- package/src/router.ts +1051 -0
- package/src/schema.ts +173 -0
- package/src/utils/auth-error-redirect.ts +42 -0
- package/src/utils/user.test.ts +99 -0
- package/src/utils/user.ts +62 -0
- package/src/utils/validate-schema.test.ts +85 -0
- package/src/utils/validate-schema.ts +45 -0
- package/tsconfig.json +6 -0
package/src/router.ts
ADDED
|
@@ -0,0 +1,1051 @@
|
|
|
1
|
+
import { implement, ORPCError } from "@orpc/server";
|
|
2
|
+
import {
|
|
3
|
+
autoAuthMiddleware,
|
|
4
|
+
type RpcContext,
|
|
5
|
+
type AuthUser,
|
|
6
|
+
type RealUser,
|
|
7
|
+
type AuthStrategy,
|
|
8
|
+
type ConfigService,
|
|
9
|
+
toJsonSchema,
|
|
10
|
+
} from "@checkstack/backend-api";
|
|
11
|
+
import { authContract, passwordSchema } from "@checkstack/auth-common";
|
|
12
|
+
import { hashPassword } from "better-auth/crypto";
|
|
13
|
+
import * as schema from "./schema";
|
|
14
|
+
import { eq, inArray, and } from "drizzle-orm";
|
|
15
|
+
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
|
|
16
|
+
import { authHooks } from "./hooks";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Type guard to check if user is a RealUser (not a service).
|
|
20
|
+
*/
|
|
21
|
+
function isRealUser(user: AuthUser | undefined): user is RealUser {
|
|
22
|
+
return user?.type === "user";
|
|
23
|
+
}
|
|
24
|
+
import {
|
|
25
|
+
strategyMetaConfigV1,
|
|
26
|
+
STRATEGY_META_CONFIG_VERSION,
|
|
27
|
+
} from "./meta-config";
|
|
28
|
+
import {
|
|
29
|
+
platformRegistrationConfigV1,
|
|
30
|
+
PLATFORM_REGISTRATION_CONFIG_VERSION,
|
|
31
|
+
PLATFORM_REGISTRATION_CONFIG_ID,
|
|
32
|
+
} from "./platform-registration-config";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates the auth router using contract-based implementation.
|
|
36
|
+
*
|
|
37
|
+
* Auth and permissions are automatically enforced via autoAuthMiddleware
|
|
38
|
+
* based on the contract's meta.userType and meta.permissions.
|
|
39
|
+
*/
|
|
40
|
+
const os = implement(authContract)
|
|
41
|
+
.$context<RpcContext>()
|
|
42
|
+
.use(autoAuthMiddleware);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the enabled state for an authentication strategy from its meta config.
|
|
46
|
+
*
|
|
47
|
+
* @param strategyId - The ID of the strategy
|
|
48
|
+
* @param configService - The ConfigService instance
|
|
49
|
+
* @returns The enabled state:
|
|
50
|
+
* - If meta config exists: returns the stored enabled value
|
|
51
|
+
* - If no meta config (fresh install): defaults to true for credential, false for others
|
|
52
|
+
*/
|
|
53
|
+
async function getStrategyEnabled(
|
|
54
|
+
strategyId: string,
|
|
55
|
+
configService: ConfigService
|
|
56
|
+
): Promise<boolean> {
|
|
57
|
+
const metaConfig = await configService.get(
|
|
58
|
+
`${strategyId}.meta`,
|
|
59
|
+
strategyMetaConfigV1,
|
|
60
|
+
STRATEGY_META_CONFIG_VERSION
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// Default: credential=true (fresh installs), others=false (require explicit config)
|
|
64
|
+
return metaConfig?.enabled ?? strategyId === "credential";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Set the enabled state for an authentication strategy in its meta config.
|
|
69
|
+
*/
|
|
70
|
+
async function setStrategyEnabled(
|
|
71
|
+
strategyId: string,
|
|
72
|
+
enabled: boolean,
|
|
73
|
+
configService: ConfigService
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
await configService.set(
|
|
76
|
+
`${strategyId}.meta`,
|
|
77
|
+
strategyMetaConfigV1,
|
|
78
|
+
STRATEGY_META_CONFIG_VERSION,
|
|
79
|
+
{ enabled }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if platform-wide registration is currently allowed.
|
|
85
|
+
*
|
|
86
|
+
* @param configService - The ConfigService instance
|
|
87
|
+
* @returns true if registration is allowed, false otherwise
|
|
88
|
+
*/
|
|
89
|
+
async function isRegistrationAllowed(
|
|
90
|
+
configService: ConfigService
|
|
91
|
+
): Promise<boolean> {
|
|
92
|
+
const config = await configService.get(
|
|
93
|
+
PLATFORM_REGISTRATION_CONFIG_ID,
|
|
94
|
+
platformRegistrationConfigV1,
|
|
95
|
+
PLATFORM_REGISTRATION_CONFIG_VERSION
|
|
96
|
+
);
|
|
97
|
+
return config?.allowRegistration ?? true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface AuthStrategyInfo {
|
|
101
|
+
id: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate a cryptographically secure 32-character secret for API applications.
|
|
106
|
+
*/
|
|
107
|
+
function generateSecret(): string {
|
|
108
|
+
const chars =
|
|
109
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
110
|
+
const array = new Uint8Array(32);
|
|
111
|
+
crypto.getRandomValues(array);
|
|
112
|
+
return Array.from(array, (byte) => chars[byte % chars.length]).join("");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const createAuthRouter = (
|
|
116
|
+
internalDb: NodePgDatabase<typeof schema>,
|
|
117
|
+
strategyRegistry: { getStrategies: () => AuthStrategy<unknown>[] },
|
|
118
|
+
reloadAuthFn: () => Promise<void>,
|
|
119
|
+
configService: ConfigService,
|
|
120
|
+
permissionRegistry: {
|
|
121
|
+
getPermissions: () => {
|
|
122
|
+
id: string;
|
|
123
|
+
description?: string;
|
|
124
|
+
isAuthenticatedDefault?: boolean;
|
|
125
|
+
isPublicDefault?: boolean;
|
|
126
|
+
}[];
|
|
127
|
+
}
|
|
128
|
+
) => {
|
|
129
|
+
// Public endpoint for enabled strategies (no authentication required)
|
|
130
|
+
const getEnabledStrategies = os.getEnabledStrategies.handler(async () => {
|
|
131
|
+
const registeredStrategies = strategyRegistry.getStrategies();
|
|
132
|
+
|
|
133
|
+
const enabledStrategies = await Promise.all(
|
|
134
|
+
registeredStrategies.map(async (strategy) => {
|
|
135
|
+
// Get enabled state from meta config
|
|
136
|
+
const enabled = await getStrategyEnabled(strategy.id, configService);
|
|
137
|
+
|
|
138
|
+
// Determine strategy type
|
|
139
|
+
const type: "credential" | "social" =
|
|
140
|
+
strategy.id === "credential" ? "credential" : "social";
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
id: strategy.id,
|
|
144
|
+
displayName: strategy.displayName,
|
|
145
|
+
description: strategy.description,
|
|
146
|
+
type,
|
|
147
|
+
enabled,
|
|
148
|
+
icon: strategy.icon,
|
|
149
|
+
requiresManualRegistration: strategy.requiresManualRegistration,
|
|
150
|
+
};
|
|
151
|
+
})
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Filter to only return enabled strategies
|
|
155
|
+
return enabledStrategies.filter((s) => s.enabled);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const permissions = os.permissions.handler(async ({ context }) => {
|
|
159
|
+
const user = context.user;
|
|
160
|
+
if (!isRealUser(user)) {
|
|
161
|
+
return { permissions: [] };
|
|
162
|
+
}
|
|
163
|
+
return { permissions: user.permissions || [] };
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const getUsers = os.getUsers.handler(async () => {
|
|
167
|
+
const users = await internalDb.select().from(schema.user);
|
|
168
|
+
if (users.length === 0) return [];
|
|
169
|
+
|
|
170
|
+
const userRoles = await internalDb
|
|
171
|
+
.select()
|
|
172
|
+
.from(schema.userRole)
|
|
173
|
+
.where(
|
|
174
|
+
inArray(
|
|
175
|
+
schema.userRole.userId,
|
|
176
|
+
users.map((u) => u.id)
|
|
177
|
+
)
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
return users.map((u) => ({
|
|
181
|
+
...u,
|
|
182
|
+
roles: userRoles
|
|
183
|
+
.filter((ur) => ur.userId === u.id)
|
|
184
|
+
.map((ur) => ur.roleId),
|
|
185
|
+
}));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const deleteUser = os.deleteUser.handler(async ({ input: id, context }) => {
|
|
189
|
+
if (id === "initial-admin-id") {
|
|
190
|
+
throw new ORPCError("FORBIDDEN", {
|
|
191
|
+
message: "Cannot delete initial admin",
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Delete user and all related records in a transaction
|
|
196
|
+
// Foreign keys are set to "ON DELETE no action", so we must manually delete related records
|
|
197
|
+
await internalDb.transaction(async (tx) => {
|
|
198
|
+
// Delete user roles
|
|
199
|
+
await tx.delete(schema.userRole).where(eq(schema.userRole.userId, id));
|
|
200
|
+
|
|
201
|
+
// Delete sessions
|
|
202
|
+
await tx.delete(schema.session).where(eq(schema.session.userId, id));
|
|
203
|
+
|
|
204
|
+
// Delete accounts
|
|
205
|
+
await tx.delete(schema.account).where(eq(schema.account.userId, id));
|
|
206
|
+
|
|
207
|
+
// Finally, delete the user
|
|
208
|
+
await tx.delete(schema.user).where(eq(schema.user.id, id));
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Emit hook for cross-plugin cleanup (notifications, theme preferences, etc.)
|
|
212
|
+
await context.emitHook(authHooks.userDeleted, { userId: id });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const getRoles = os.getRoles.handler(async () => {
|
|
216
|
+
const roles = await internalDb.select().from(schema.role);
|
|
217
|
+
const rolePermissions = await internalDb
|
|
218
|
+
.select()
|
|
219
|
+
.from(schema.rolePermission);
|
|
220
|
+
|
|
221
|
+
return roles.map((role) => ({
|
|
222
|
+
id: role.id,
|
|
223
|
+
name: role.name,
|
|
224
|
+
description: role.description,
|
|
225
|
+
permissions: rolePermissions
|
|
226
|
+
.filter((rp) => rp.roleId === role.id)
|
|
227
|
+
.map((rp) => rp.permissionId),
|
|
228
|
+
isSystem: role.isSystem || false,
|
|
229
|
+
// Anonymous role cannot be assigned to users - it's for unauthenticated access
|
|
230
|
+
isAssignable: role.id !== "anonymous",
|
|
231
|
+
}));
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const getPermissions = os.getPermissions.handler(async () => {
|
|
235
|
+
// Return only currently active permissions (registered by loaded plugins)
|
|
236
|
+
return permissionRegistry.getPermissions();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const createRole = os.createRole.handler(async ({ input }) => {
|
|
240
|
+
const { name, description, permissions: inputPermissions } = input;
|
|
241
|
+
|
|
242
|
+
// Generate UUID for new role
|
|
243
|
+
const id = crypto.randomUUID();
|
|
244
|
+
|
|
245
|
+
// Get active permissions to filter input
|
|
246
|
+
const activePermissions = new Set(
|
|
247
|
+
permissionRegistry.getPermissions().map((p) => p.id)
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
// Filter to only include active permissions
|
|
251
|
+
const validPermissions = inputPermissions.filter((p) =>
|
|
252
|
+
activePermissions.has(p)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
await internalDb.transaction(async (tx) => {
|
|
256
|
+
// Create role
|
|
257
|
+
await tx.insert(schema.role).values({
|
|
258
|
+
id,
|
|
259
|
+
name,
|
|
260
|
+
description: description || undefined,
|
|
261
|
+
isSystem: false,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Create role-permission mappings
|
|
265
|
+
if (validPermissions.length > 0) {
|
|
266
|
+
await tx.insert(schema.rolePermission).values(
|
|
267
|
+
validPermissions.map((permissionId) => ({
|
|
268
|
+
roleId: id,
|
|
269
|
+
permissionId,
|
|
270
|
+
}))
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const updateRole = os.updateRole.handler(async ({ input, context }) => {
|
|
277
|
+
const { id, name, description, permissions: inputPermissions } = input;
|
|
278
|
+
|
|
279
|
+
// Track if user has this role (for permission elevation prevention)
|
|
280
|
+
const userRoles = isRealUser(context.user) ? context.user.roles || [] : [];
|
|
281
|
+
const isUserOwnRole = userRoles.includes(id);
|
|
282
|
+
|
|
283
|
+
// Check if role exists
|
|
284
|
+
const existingRole = await internalDb
|
|
285
|
+
.select()
|
|
286
|
+
.from(schema.role)
|
|
287
|
+
.where(eq(schema.role.id, id));
|
|
288
|
+
|
|
289
|
+
if (existingRole.length === 0) {
|
|
290
|
+
throw new ORPCError("NOT_FOUND", {
|
|
291
|
+
message: `Role ${id} not found`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const isUsersRole = id === "users";
|
|
296
|
+
const isAdminRole = id === "admin";
|
|
297
|
+
|
|
298
|
+
// System roles can have name/description edited, but not deleted
|
|
299
|
+
// Admin role: permissions cannot be changed (wildcard permission)
|
|
300
|
+
// Users role: permissions can be changed with default tracking
|
|
301
|
+
// User's own role: permissions cannot be changed (prevent self-elevation)
|
|
302
|
+
|
|
303
|
+
// Get active permissions to filter input
|
|
304
|
+
const activePermissions = new Set(
|
|
305
|
+
permissionRegistry.getPermissions().map((p) => p.id)
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
// Filter to only include active permissions
|
|
309
|
+
const validPermissions = inputPermissions.filter((p) =>
|
|
310
|
+
activePermissions.has(p)
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
// Track disabled authenticated default permissions for "users" role
|
|
314
|
+
if (isUsersRole && !isUserOwnRole) {
|
|
315
|
+
const allPerms = permissionRegistry.getPermissions();
|
|
316
|
+
const defaultPermIds = allPerms
|
|
317
|
+
.filter((p) => p.isAuthenticatedDefault)
|
|
318
|
+
.map((p) => p.id);
|
|
319
|
+
|
|
320
|
+
// Find authenticated default permissions that are being removed
|
|
321
|
+
const removedDefaults = defaultPermIds.filter(
|
|
322
|
+
(defId) => !validPermissions.includes(defId)
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
// Insert into disabled_default_permission table
|
|
326
|
+
for (const permId of removedDefaults) {
|
|
327
|
+
await internalDb
|
|
328
|
+
.insert(schema.disabledDefaultPermission)
|
|
329
|
+
.values({
|
|
330
|
+
permissionId: permId,
|
|
331
|
+
disabledAt: new Date(),
|
|
332
|
+
})
|
|
333
|
+
.onConflictDoNothing();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Remove from disabled table if being re-added
|
|
337
|
+
const readdedDefaults = validPermissions.filter((p) =>
|
|
338
|
+
defaultPermIds.includes(p)
|
|
339
|
+
);
|
|
340
|
+
for (const permId of readdedDefaults) {
|
|
341
|
+
await internalDb
|
|
342
|
+
.delete(schema.disabledDefaultPermission)
|
|
343
|
+
.where(eq(schema.disabledDefaultPermission.permissionId, permId));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Track disabled public default permissions for "anonymous" role
|
|
348
|
+
const isAnonymousRole = id === "anonymous";
|
|
349
|
+
if (isAnonymousRole) {
|
|
350
|
+
const allPerms = permissionRegistry.getPermissions();
|
|
351
|
+
const publicDefaultPermIds = allPerms
|
|
352
|
+
.filter((p) => p.isPublicDefault)
|
|
353
|
+
.map((p) => p.id);
|
|
354
|
+
|
|
355
|
+
// Find public default permissions that are being removed
|
|
356
|
+
const removedPublicDefaults = publicDefaultPermIds.filter(
|
|
357
|
+
(defId) => !validPermissions.includes(defId)
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
// Insert into disabled_public_default_permission table
|
|
361
|
+
for (const permId of removedPublicDefaults) {
|
|
362
|
+
await internalDb
|
|
363
|
+
.insert(schema.disabledPublicDefaultPermission)
|
|
364
|
+
.values({
|
|
365
|
+
permissionId: permId,
|
|
366
|
+
disabledAt: new Date(),
|
|
367
|
+
})
|
|
368
|
+
.onConflictDoNothing();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Remove from disabled table if being re-added
|
|
372
|
+
const readdedPublicDefaults = validPermissions.filter((p) =>
|
|
373
|
+
publicDefaultPermIds.includes(p)
|
|
374
|
+
);
|
|
375
|
+
for (const permId of readdedPublicDefaults) {
|
|
376
|
+
await internalDb
|
|
377
|
+
.delete(schema.disabledPublicDefaultPermission)
|
|
378
|
+
.where(
|
|
379
|
+
eq(schema.disabledPublicDefaultPermission.permissionId, permId)
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
await internalDb.transaction(async (tx) => {
|
|
385
|
+
// Update role name/description if provided (allowed for ALL roles including system and own roles)
|
|
386
|
+
if (name !== undefined || description !== undefined) {
|
|
387
|
+
const updates: { name?: string; description?: string | null } = {};
|
|
388
|
+
if (name !== undefined) updates.name = name;
|
|
389
|
+
if (description !== undefined) updates.description = description;
|
|
390
|
+
|
|
391
|
+
await tx.update(schema.role).set(updates).where(eq(schema.role.id, id));
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Skip permission changes for admin role (wildcard) or user's own role (prevent self-elevation)
|
|
395
|
+
if (isAdminRole || isUserOwnRole) {
|
|
396
|
+
return; // Don't modify permissions
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Replace permission mappings for non-admin roles
|
|
400
|
+
await tx
|
|
401
|
+
.delete(schema.rolePermission)
|
|
402
|
+
.where(eq(schema.rolePermission.roleId, id));
|
|
403
|
+
|
|
404
|
+
if (validPermissions.length > 0) {
|
|
405
|
+
await tx.insert(schema.rolePermission).values(
|
|
406
|
+
validPermissions.map((permissionId) => ({
|
|
407
|
+
roleId: id,
|
|
408
|
+
permissionId,
|
|
409
|
+
}))
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const deleteRole = os.deleteRole.handler(async ({ input: id, context }) => {
|
|
416
|
+
// Security check: prevent users from deleting their own roles
|
|
417
|
+
const userRoles = isRealUser(context.user) ? context.user.roles || [] : [];
|
|
418
|
+
if (userRoles.includes(id)) {
|
|
419
|
+
throw new ORPCError("FORBIDDEN", {
|
|
420
|
+
message: "Cannot delete a role that you currently have",
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Check if role is a system role
|
|
425
|
+
const existingRole = await internalDb
|
|
426
|
+
.select()
|
|
427
|
+
.from(schema.role)
|
|
428
|
+
.where(eq(schema.role.id, id));
|
|
429
|
+
|
|
430
|
+
if (existingRole.length === 0) {
|
|
431
|
+
throw new ORPCError("NOT_FOUND", {
|
|
432
|
+
message: `Role ${id} not found`,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (existingRole[0].isSystem) {
|
|
437
|
+
throw new ORPCError("FORBIDDEN", {
|
|
438
|
+
message: "Cannot delete system role",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Delete role and related records in transaction
|
|
443
|
+
await internalDb.transaction(async (tx) => {
|
|
444
|
+
// Delete role-permission mappings
|
|
445
|
+
await tx
|
|
446
|
+
.delete(schema.rolePermission)
|
|
447
|
+
.where(eq(schema.rolePermission.roleId, id));
|
|
448
|
+
|
|
449
|
+
// Delete user-role mappings
|
|
450
|
+
await tx.delete(schema.userRole).where(eq(schema.userRole.roleId, id));
|
|
451
|
+
|
|
452
|
+
// Delete the role itself
|
|
453
|
+
await tx.delete(schema.role).where(eq(schema.role.id, id));
|
|
454
|
+
});
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const updateUserRoles = os.updateUserRoles.handler(
|
|
458
|
+
async ({ input, context }) => {
|
|
459
|
+
const { userId, roles } = input;
|
|
460
|
+
|
|
461
|
+
const currentUserId = isRealUser(context.user)
|
|
462
|
+
? context.user.id
|
|
463
|
+
: undefined;
|
|
464
|
+
if (userId === currentUserId) {
|
|
465
|
+
throw new ORPCError("FORBIDDEN", {
|
|
466
|
+
message: "Cannot update your own roles",
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Prevent assignment of the "anonymous" role - it's reserved for unauthenticated users
|
|
471
|
+
if (roles.includes("anonymous")) {
|
|
472
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
473
|
+
message: "The 'anonymous' role cannot be assigned to users",
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await internalDb.transaction(async (tx) => {
|
|
478
|
+
await tx
|
|
479
|
+
.delete(schema.userRole)
|
|
480
|
+
.where(eq(schema.userRole.userId, userId));
|
|
481
|
+
if (roles.length > 0) {
|
|
482
|
+
await tx.insert(schema.userRole).values(
|
|
483
|
+
roles.map((roleId) => ({
|
|
484
|
+
userId,
|
|
485
|
+
roleId,
|
|
486
|
+
}))
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
);
|
|
492
|
+
|
|
493
|
+
const getStrategies = os.getStrategies.handler(async () => {
|
|
494
|
+
const registeredStrategies = strategyRegistry.getStrategies();
|
|
495
|
+
|
|
496
|
+
return Promise.all(
|
|
497
|
+
registeredStrategies.map(async (strategy) => {
|
|
498
|
+
// Get redacted config from ConfigService
|
|
499
|
+
const config = await configService.getRedacted(
|
|
500
|
+
strategy.id,
|
|
501
|
+
strategy.configSchema,
|
|
502
|
+
strategy.configVersion,
|
|
503
|
+
strategy.migrations
|
|
504
|
+
);
|
|
505
|
+
|
|
506
|
+
// Convert Zod schema to JSON Schema with automatic secret metadata
|
|
507
|
+
const jsonSchema = toJsonSchema(strategy.configSchema);
|
|
508
|
+
|
|
509
|
+
// Get enabled state from meta config
|
|
510
|
+
const enabled = await getStrategyEnabled(strategy.id, configService);
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
id: strategy.id,
|
|
514
|
+
displayName: strategy.displayName,
|
|
515
|
+
description: strategy.description,
|
|
516
|
+
icon: strategy.icon,
|
|
517
|
+
enabled,
|
|
518
|
+
configVersion: strategy.configVersion,
|
|
519
|
+
configSchema: jsonSchema,
|
|
520
|
+
config,
|
|
521
|
+
adminInstructions: strategy.adminInstructions,
|
|
522
|
+
};
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
const updateStrategy = os.updateStrategy.handler(async ({ input }) => {
|
|
528
|
+
const { id, enabled, config } = input;
|
|
529
|
+
const strategy = strategyRegistry.getStrategies().find((s) => s.id === id);
|
|
530
|
+
|
|
531
|
+
if (!strategy) {
|
|
532
|
+
throw new ORPCError("NOT_FOUND", {
|
|
533
|
+
message: `Strategy ${id} not found`,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Save strategy configuration (if provided)
|
|
538
|
+
if (config) {
|
|
539
|
+
await configService.set(
|
|
540
|
+
id,
|
|
541
|
+
strategy.configSchema,
|
|
542
|
+
strategy.configVersion,
|
|
543
|
+
config, // Just the config, no enabled mixed in
|
|
544
|
+
strategy.migrations
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// Save enabled state separately in meta config
|
|
549
|
+
await setStrategyEnabled(id, enabled, configService);
|
|
550
|
+
|
|
551
|
+
// Trigger auth reload
|
|
552
|
+
await reloadAuthFn();
|
|
553
|
+
|
|
554
|
+
return { success: true };
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const reloadAuth = os.reloadAuth.handler(async () => {
|
|
558
|
+
await reloadAuthFn();
|
|
559
|
+
return { success: true };
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const getRegistrationSchema = os.getRegistrationSchema.handler(() => {
|
|
563
|
+
return toJsonSchema(platformRegistrationConfigV1);
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const getRegistrationStatus = os.getRegistrationStatus.handler(async () => {
|
|
567
|
+
const allowRegistration = await isRegistrationAllowed(configService);
|
|
568
|
+
return { allowRegistration };
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const setRegistrationStatus = os.setRegistrationStatus.handler(
|
|
572
|
+
async ({ input }) => {
|
|
573
|
+
await configService.set(
|
|
574
|
+
PLATFORM_REGISTRATION_CONFIG_ID,
|
|
575
|
+
platformRegistrationConfigV1,
|
|
576
|
+
PLATFORM_REGISTRATION_CONFIG_VERSION,
|
|
577
|
+
{ allowRegistration: input.allowRegistration }
|
|
578
|
+
);
|
|
579
|
+
// Trigger auth reload to apply new settings
|
|
580
|
+
await reloadAuthFn();
|
|
581
|
+
return { success: true };
|
|
582
|
+
}
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
const getAnonymousPermissions = os.getAnonymousPermissions.handler(
|
|
586
|
+
async () => {
|
|
587
|
+
const rolePerms = await internalDb
|
|
588
|
+
.select()
|
|
589
|
+
.from(schema.rolePermission)
|
|
590
|
+
.where(eq(schema.rolePermission.roleId, "anonymous"));
|
|
591
|
+
return rolePerms.map((rp) => rp.permissionId);
|
|
592
|
+
}
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
const filterUsersByPermission = os.filterUsersByPermission.handler(
|
|
596
|
+
async ({ input }) => {
|
|
597
|
+
const { userIds, permission } = input;
|
|
598
|
+
|
|
599
|
+
if (userIds.length === 0) return [];
|
|
600
|
+
|
|
601
|
+
// Single efficient query: join user_role with role_permission
|
|
602
|
+
// and filter by both userIds AND the specific permission
|
|
603
|
+
const usersWithPermission = await internalDb
|
|
604
|
+
.select({ userId: schema.userRole.userId })
|
|
605
|
+
.from(schema.userRole)
|
|
606
|
+
.innerJoin(
|
|
607
|
+
schema.rolePermission,
|
|
608
|
+
eq(schema.userRole.roleId, schema.rolePermission.roleId)
|
|
609
|
+
)
|
|
610
|
+
.where(
|
|
611
|
+
and(
|
|
612
|
+
inArray(schema.userRole.userId, userIds),
|
|
613
|
+
eq(schema.rolePermission.permissionId, permission)
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
.groupBy(schema.userRole.userId);
|
|
617
|
+
|
|
618
|
+
return usersWithPermission.map((row) => row.userId);
|
|
619
|
+
}
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
// ==========================================================================
|
|
623
|
+
// SERVICE-TO-SERVICE ENDPOINTS (for external auth providers like LDAP)
|
|
624
|
+
// ==========================================================================
|
|
625
|
+
|
|
626
|
+
const getUserById = os.getUserById.handler(async ({ input }) => {
|
|
627
|
+
const users = await internalDb
|
|
628
|
+
.select({
|
|
629
|
+
id: schema.user.id,
|
|
630
|
+
email: schema.user.email,
|
|
631
|
+
name: schema.user.name,
|
|
632
|
+
})
|
|
633
|
+
.from(schema.user)
|
|
634
|
+
.where(eq(schema.user.id, input.userId))
|
|
635
|
+
.limit(1);
|
|
636
|
+
|
|
637
|
+
return users.length > 0 ? users[0] : undefined;
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
const findUserByEmail = os.findUserByEmail.handler(async ({ input }) => {
|
|
641
|
+
const users = await internalDb
|
|
642
|
+
.select({ id: schema.user.id })
|
|
643
|
+
.from(schema.user)
|
|
644
|
+
.where(eq(schema.user.email, input.email))
|
|
645
|
+
.limit(1);
|
|
646
|
+
|
|
647
|
+
return users.length > 0 ? { id: users[0].id } : undefined;
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const upsertExternalUser = os.upsertExternalUser.handler(
|
|
651
|
+
async ({ input, context }) => {
|
|
652
|
+
const { email, name, providerId, accountId, password, autoUpdateUser } =
|
|
653
|
+
input;
|
|
654
|
+
|
|
655
|
+
// Check if user exists
|
|
656
|
+
const existingUsers = await internalDb
|
|
657
|
+
.select({ id: schema.user.id })
|
|
658
|
+
.from(schema.user)
|
|
659
|
+
.where(eq(schema.user.email, email))
|
|
660
|
+
.limit(1);
|
|
661
|
+
|
|
662
|
+
if (existingUsers.length > 0) {
|
|
663
|
+
// User exists - update if autoUpdateUser is enabled
|
|
664
|
+
const userId = existingUsers[0].id;
|
|
665
|
+
|
|
666
|
+
if (autoUpdateUser) {
|
|
667
|
+
await internalDb
|
|
668
|
+
.update(schema.user)
|
|
669
|
+
.set({ name, updatedAt: new Date() })
|
|
670
|
+
.where(eq(schema.user.id, userId));
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return { userId, created: false };
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Check if registration is allowed before creating new user
|
|
677
|
+
const registrationAllowed = await isRegistrationAllowed(configService);
|
|
678
|
+
if (!registrationAllowed) {
|
|
679
|
+
throw new ORPCError("FORBIDDEN", {
|
|
680
|
+
message: "Registration is disabled. Please contact an administrator.",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Create new user and account in a transaction
|
|
685
|
+
const userId = crypto.randomUUID();
|
|
686
|
+
const accountEntryId = crypto.randomUUID();
|
|
687
|
+
const now = new Date();
|
|
688
|
+
|
|
689
|
+
await internalDb.transaction(async (tx) => {
|
|
690
|
+
// Create user
|
|
691
|
+
await tx.insert(schema.user).values({
|
|
692
|
+
id: userId,
|
|
693
|
+
email,
|
|
694
|
+
name,
|
|
695
|
+
emailVerified: false,
|
|
696
|
+
createdAt: now,
|
|
697
|
+
updatedAt: now,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// Create account
|
|
701
|
+
await tx.insert(schema.account).values({
|
|
702
|
+
id: accountEntryId,
|
|
703
|
+
accountId,
|
|
704
|
+
providerId,
|
|
705
|
+
userId,
|
|
706
|
+
password,
|
|
707
|
+
createdAt: now,
|
|
708
|
+
updatedAt: now,
|
|
709
|
+
});
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
context.logger.info(`Created new user from ${providerId}: ${email}`);
|
|
713
|
+
|
|
714
|
+
return { userId, created: true };
|
|
715
|
+
}
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
const createSession = os.createSession.handler(async ({ input }) => {
|
|
719
|
+
const { userId, token, expiresAt } = input;
|
|
720
|
+
const sessionId = crypto.randomUUID();
|
|
721
|
+
const now = new Date();
|
|
722
|
+
|
|
723
|
+
await internalDb.insert(schema.session).values({
|
|
724
|
+
id: sessionId,
|
|
725
|
+
userId,
|
|
726
|
+
token,
|
|
727
|
+
expiresAt,
|
|
728
|
+
createdAt: now,
|
|
729
|
+
updatedAt: now,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
return { sessionId };
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// ==========================================================================
|
|
736
|
+
// ADMIN USER CREATION (bypasses registration check)
|
|
737
|
+
// ==========================================================================
|
|
738
|
+
|
|
739
|
+
const createCredentialUser = os.createCredentialUser.handler(
|
|
740
|
+
async ({ input, context }) => {
|
|
741
|
+
const { email, name, password } = input;
|
|
742
|
+
|
|
743
|
+
// Validate password against platform's password schema
|
|
744
|
+
const passwordValidation = passwordSchema.safeParse(password);
|
|
745
|
+
if (!passwordValidation.success) {
|
|
746
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
747
|
+
message: passwordValidation.error.issues
|
|
748
|
+
.map((issue) => issue.message)
|
|
749
|
+
.join(", "),
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Check if credential strategy is enabled
|
|
754
|
+
const credentialEnabled = await getStrategyEnabled(
|
|
755
|
+
"credential",
|
|
756
|
+
configService
|
|
757
|
+
);
|
|
758
|
+
if (!credentialEnabled) {
|
|
759
|
+
throw new ORPCError("BAD_REQUEST", {
|
|
760
|
+
message:
|
|
761
|
+
"Credential strategy is not enabled. Enable it in Authentication Settings first.",
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Check if user already exists
|
|
766
|
+
const existingUsers = await internalDb
|
|
767
|
+
.select({ id: schema.user.id })
|
|
768
|
+
.from(schema.user)
|
|
769
|
+
.where(eq(schema.user.email, email))
|
|
770
|
+
.limit(1);
|
|
771
|
+
|
|
772
|
+
if (existingUsers.length > 0) {
|
|
773
|
+
throw new ORPCError("CONFLICT", {
|
|
774
|
+
message: "A user with this email already exists.",
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Create user directly in database (bypasses registration check)
|
|
779
|
+
const userId = crypto.randomUUID();
|
|
780
|
+
const accountId = crypto.randomUUID();
|
|
781
|
+
const hashedPassword = await hashPassword(password);
|
|
782
|
+
const now = new Date();
|
|
783
|
+
|
|
784
|
+
await internalDb.transaction(async (tx) => {
|
|
785
|
+
// Create user
|
|
786
|
+
await tx.insert(schema.user).values({
|
|
787
|
+
id: userId,
|
|
788
|
+
email,
|
|
789
|
+
name,
|
|
790
|
+
emailVerified: true, // Admin-created users are pre-verified
|
|
791
|
+
createdAt: now,
|
|
792
|
+
updatedAt: now,
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
// Create credential account
|
|
796
|
+
await tx.insert(schema.account).values({
|
|
797
|
+
id: accountId,
|
|
798
|
+
accountId: email,
|
|
799
|
+
providerId: "credential",
|
|
800
|
+
userId,
|
|
801
|
+
password: hashedPassword,
|
|
802
|
+
createdAt: now,
|
|
803
|
+
updatedAt: now,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
// Assign "users" role to new user
|
|
807
|
+
await tx.insert(schema.userRole).values({
|
|
808
|
+
userId,
|
|
809
|
+
roleId: "users",
|
|
810
|
+
});
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
context.logger.info(
|
|
814
|
+
`[auth-backend] Admin created credential user: ${email}`
|
|
815
|
+
);
|
|
816
|
+
|
|
817
|
+
return { userId };
|
|
818
|
+
}
|
|
819
|
+
);
|
|
820
|
+
|
|
821
|
+
// ==========================================================================
|
|
822
|
+
// APPLICATION MANAGEMENT
|
|
823
|
+
// External applications (API keys) with RBAC integration
|
|
824
|
+
// ==========================================================================
|
|
825
|
+
|
|
826
|
+
const getApplications = os.getApplications.handler(async () => {
|
|
827
|
+
const apps = await internalDb.select().from(schema.application);
|
|
828
|
+
if (apps.length === 0) return [];
|
|
829
|
+
|
|
830
|
+
const appRoles = await internalDb
|
|
831
|
+
.select()
|
|
832
|
+
.from(schema.applicationRole)
|
|
833
|
+
.where(
|
|
834
|
+
inArray(
|
|
835
|
+
schema.applicationRole.applicationId,
|
|
836
|
+
apps.map((a) => a.id)
|
|
837
|
+
)
|
|
838
|
+
);
|
|
839
|
+
|
|
840
|
+
return apps.map((app) => ({
|
|
841
|
+
id: app.id,
|
|
842
|
+
name: app.name,
|
|
843
|
+
description: app.description,
|
|
844
|
+
roles: appRoles
|
|
845
|
+
.filter((ar) => ar.applicationId === app.id)
|
|
846
|
+
.map((ar) => ar.roleId),
|
|
847
|
+
createdById: app.createdById,
|
|
848
|
+
createdAt: app.createdAt,
|
|
849
|
+
lastUsedAt: app.lastUsedAt,
|
|
850
|
+
}));
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const createApplication = os.createApplication.handler(
|
|
854
|
+
async ({ input, context }) => {
|
|
855
|
+
const { name, description } = input;
|
|
856
|
+
|
|
857
|
+
const userId = isRealUser(context.user) ? context.user.id : undefined;
|
|
858
|
+
if (!userId) {
|
|
859
|
+
throw new ORPCError("UNAUTHORIZED", {
|
|
860
|
+
message: "User ID required to create application",
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const id = crypto.randomUUID();
|
|
865
|
+
const secret = generateSecret();
|
|
866
|
+
// Hash with bcrypt via better-auth's hashPassword
|
|
867
|
+
const secretHash = await hashPassword(secret);
|
|
868
|
+
const now = new Date();
|
|
869
|
+
|
|
870
|
+
// Default role for all applications
|
|
871
|
+
const defaultRole = "applications";
|
|
872
|
+
|
|
873
|
+
await internalDb.transaction(async (tx) => {
|
|
874
|
+
// Create application
|
|
875
|
+
await tx.insert(schema.application).values({
|
|
876
|
+
id,
|
|
877
|
+
name,
|
|
878
|
+
description: description ?? undefined,
|
|
879
|
+
secretHash,
|
|
880
|
+
createdById: userId,
|
|
881
|
+
createdAt: now,
|
|
882
|
+
updatedAt: now,
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
// Assign default "applications" role
|
|
886
|
+
await tx.insert(schema.applicationRole).values({
|
|
887
|
+
applicationId: id,
|
|
888
|
+
roleId: defaultRole,
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
context.logger.info(
|
|
893
|
+
`[auth-backend] Created application: ${name} (${id})`
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
application: {
|
|
898
|
+
id,
|
|
899
|
+
name,
|
|
900
|
+
description: description ?? undefined,
|
|
901
|
+
roles: [defaultRole],
|
|
902
|
+
createdById: userId,
|
|
903
|
+
createdAt: now,
|
|
904
|
+
},
|
|
905
|
+
secret: `ck_${id}_${secret}`, // Full secret - only shown once!
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
const updateApplication = os.updateApplication.handler(async ({ input }) => {
|
|
911
|
+
const { id, name, description, roles } = input;
|
|
912
|
+
|
|
913
|
+
// Check if application exists
|
|
914
|
+
const existing = await internalDb
|
|
915
|
+
.select()
|
|
916
|
+
.from(schema.application)
|
|
917
|
+
.where(eq(schema.application.id, id))
|
|
918
|
+
.limit(1);
|
|
919
|
+
|
|
920
|
+
if (existing.length === 0) {
|
|
921
|
+
throw new ORPCError("NOT_FOUND", {
|
|
922
|
+
message: `Application ${id} not found`,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
await internalDb.transaction(async (tx) => {
|
|
927
|
+
// Update application fields
|
|
928
|
+
const updates: {
|
|
929
|
+
name?: string;
|
|
930
|
+
description?: string | null;
|
|
931
|
+
updatedAt: Date;
|
|
932
|
+
} = {
|
|
933
|
+
updatedAt: new Date(),
|
|
934
|
+
};
|
|
935
|
+
if (name !== undefined) updates.name = name;
|
|
936
|
+
if (description !== undefined) updates.description = description;
|
|
937
|
+
|
|
938
|
+
await tx
|
|
939
|
+
.update(schema.application)
|
|
940
|
+
.set(updates)
|
|
941
|
+
.where(eq(schema.application.id, id));
|
|
942
|
+
|
|
943
|
+
// Update roles if provided
|
|
944
|
+
if (roles !== undefined) {
|
|
945
|
+
// Delete existing role mappings
|
|
946
|
+
await tx
|
|
947
|
+
.delete(schema.applicationRole)
|
|
948
|
+
.where(eq(schema.applicationRole.applicationId, id));
|
|
949
|
+
|
|
950
|
+
// Insert new role mappings
|
|
951
|
+
if (roles.length > 0) {
|
|
952
|
+
await tx.insert(schema.applicationRole).values(
|
|
953
|
+
roles.map((roleId) => ({
|
|
954
|
+
applicationId: id,
|
|
955
|
+
roleId,
|
|
956
|
+
}))
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
const deleteApplication = os.deleteApplication.handler(
|
|
964
|
+
async ({ input: id, context }) => {
|
|
965
|
+
// Check if application exists
|
|
966
|
+
const existing = await internalDb
|
|
967
|
+
.select()
|
|
968
|
+
.from(schema.application)
|
|
969
|
+
.where(eq(schema.application.id, id))
|
|
970
|
+
.limit(1);
|
|
971
|
+
|
|
972
|
+
if (existing.length === 0) {
|
|
973
|
+
throw new ORPCError("NOT_FOUND", {
|
|
974
|
+
message: `Application ${id} not found`,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Cascade delete is handled by FK constraint on applicationRole
|
|
979
|
+
// Just delete the application
|
|
980
|
+
await internalDb
|
|
981
|
+
.delete(schema.application)
|
|
982
|
+
.where(eq(schema.application.id, id));
|
|
983
|
+
|
|
984
|
+
context.logger.info(`[auth-backend] Deleted application: ${id}`);
|
|
985
|
+
}
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
const regenerateApplicationSecret = os.regenerateApplicationSecret.handler(
|
|
989
|
+
async ({ input: id, context }) => {
|
|
990
|
+
// Check if application exists
|
|
991
|
+
const existing = await internalDb
|
|
992
|
+
.select()
|
|
993
|
+
.from(schema.application)
|
|
994
|
+
.where(eq(schema.application.id, id))
|
|
995
|
+
.limit(1);
|
|
996
|
+
|
|
997
|
+
if (existing.length === 0) {
|
|
998
|
+
throw new ORPCError("NOT_FOUND", {
|
|
999
|
+
message: `Application ${id} not found`,
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
const secret = generateSecret();
|
|
1004
|
+
const secretHash = await hashPassword(secret);
|
|
1005
|
+
|
|
1006
|
+
await internalDb
|
|
1007
|
+
.update(schema.application)
|
|
1008
|
+
.set({ secretHash, updatedAt: new Date() })
|
|
1009
|
+
.where(eq(schema.application.id, id));
|
|
1010
|
+
|
|
1011
|
+
context.logger.info(
|
|
1012
|
+
`[auth-backend] Regenerated secret for application: ${id}`
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
return { secret: `ck_${id}_${secret}` };
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
|
|
1019
|
+
return os.router({
|
|
1020
|
+
getEnabledStrategies,
|
|
1021
|
+
permissions,
|
|
1022
|
+
getUsers,
|
|
1023
|
+
deleteUser,
|
|
1024
|
+
getRoles,
|
|
1025
|
+
getPermissions,
|
|
1026
|
+
createRole,
|
|
1027
|
+
updateRole,
|
|
1028
|
+
deleteRole,
|
|
1029
|
+
updateUserRoles,
|
|
1030
|
+
getStrategies,
|
|
1031
|
+
updateStrategy,
|
|
1032
|
+
reloadAuth,
|
|
1033
|
+
getRegistrationSchema,
|
|
1034
|
+
getRegistrationStatus,
|
|
1035
|
+
setRegistrationStatus,
|
|
1036
|
+
getAnonymousPermissions,
|
|
1037
|
+
getUserById,
|
|
1038
|
+
filterUsersByPermission,
|
|
1039
|
+
findUserByEmail,
|
|
1040
|
+
upsertExternalUser,
|
|
1041
|
+
createSession,
|
|
1042
|
+
createCredentialUser,
|
|
1043
|
+
getApplications,
|
|
1044
|
+
createApplication,
|
|
1045
|
+
updateApplication,
|
|
1046
|
+
deleteApplication,
|
|
1047
|
+
regenerateApplicationSecret,
|
|
1048
|
+
});
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
export type AuthRouter = ReturnType<typeof createAuthRouter>;
|