@checkstack/auth-backend 0.2.2 → 0.4.0

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/src/router.ts CHANGED
@@ -31,6 +31,11 @@ import {
31
31
  PLATFORM_REGISTRATION_CONFIG_ID,
32
32
  } from "./platform-registration-config";
33
33
 
34
+ export const ADMIN_ROLE_ID = "admin";
35
+ export const USERS_ROLE_ID = "users";
36
+ export const ANONYMOUS_ROLE_ID = "anonymous";
37
+ export const APPLICATIONS_ROLE_ID = "applications";
38
+
34
39
  /**
35
40
  * Creates the auth router using contract-based implementation.
36
41
  *
@@ -52,12 +57,12 @@ const os = implement(authContract)
52
57
  */
53
58
  async function getStrategyEnabled(
54
59
  strategyId: string,
55
- configService: ConfigService
60
+ configService: ConfigService,
56
61
  ): Promise<boolean> {
57
62
  const metaConfig = await configService.get(
58
63
  `${strategyId}.meta`,
59
64
  strategyMetaConfigV1,
60
- STRATEGY_META_CONFIG_VERSION
65
+ STRATEGY_META_CONFIG_VERSION,
61
66
  );
62
67
 
63
68
  // Default: credential=true (fresh installs), others=false (require explicit config)
@@ -70,13 +75,13 @@ async function getStrategyEnabled(
70
75
  async function setStrategyEnabled(
71
76
  strategyId: string,
72
77
  enabled: boolean,
73
- configService: ConfigService
78
+ configService: ConfigService,
74
79
  ): Promise<void> {
75
80
  await configService.set(
76
81
  `${strategyId}.meta`,
77
82
  strategyMetaConfigV1,
78
83
  STRATEGY_META_CONFIG_VERSION,
79
- { enabled }
84
+ { enabled },
80
85
  );
81
86
  }
82
87
 
@@ -87,12 +92,12 @@ async function setStrategyEnabled(
87
92
  * @returns true if registration is allowed, false otherwise
88
93
  */
89
94
  async function isRegistrationAllowed(
90
- configService: ConfigService
95
+ configService: ConfigService,
91
96
  ): Promise<boolean> {
92
97
  const config = await configService.get(
93
98
  PLATFORM_REGISTRATION_CONFIG_ID,
94
99
  platformRegistrationConfigV1,
95
- PLATFORM_REGISTRATION_CONFIG_VERSION
100
+ PLATFORM_REGISTRATION_CONFIG_VERSION,
96
101
  );
97
102
  return config?.allowRegistration ?? true;
98
103
  }
@@ -124,7 +129,7 @@ export const createAuthRouter = (
124
129
  isDefault?: boolean;
125
130
  isPublic?: boolean;
126
131
  }[];
127
- }
132
+ },
128
133
  ) => {
129
134
  // Public endpoint for enabled strategies (no authentication required)
130
135
  const getEnabledStrategies = os.getEnabledStrategies.handler(async () => {
@@ -148,7 +153,7 @@ export const createAuthRouter = (
148
153
  icon: strategy.icon,
149
154
  requiresManualRegistration: strategy.requiresManualRegistration,
150
155
  };
151
- })
156
+ }),
152
157
  );
153
158
 
154
159
  // Filter to only return enabled strategies
@@ -173,8 +178,8 @@ export const createAuthRouter = (
173
178
  .where(
174
179
  inArray(
175
180
  schema.userRole.userId,
176
- users.map((u) => u.id)
177
- )
181
+ users.map((u) => u.id),
182
+ ),
178
183
  );
179
184
 
180
185
  return users.map((u) => ({
@@ -186,9 +191,15 @@ export const createAuthRouter = (
186
191
  });
187
192
 
188
193
  const deleteUser = os.deleteUser.handler(async ({ input: id, context }) => {
189
- if (id === "initial-admin-id") {
194
+ // Check if user has admin role - prevent deletion to avoid lockout
195
+ const userRoles = await internalDb
196
+ .select({ roleId: schema.userRole.roleId })
197
+ .from(schema.userRole)
198
+ .where(eq(schema.userRole.userId, id));
199
+
200
+ if (userRoles.some((ur) => ur.roleId === ADMIN_ROLE_ID)) {
190
201
  throw new ORPCError("FORBIDDEN", {
191
- message: "Cannot delete initial admin",
202
+ message: "Cannot delete users with the admin role",
192
203
  });
193
204
  }
194
205
 
@@ -227,7 +238,7 @@ export const createAuthRouter = (
227
238
  .map((rp) => rp.accessRuleId),
228
239
  isSystem: role.isSystem || false,
229
240
  // Anonymous role cannot be assigned to users - it's for unauthenticated access
230
- isAssignable: role.id !== "anonymous",
241
+ isAssignable: role.id !== ANONYMOUS_ROLE_ID,
231
242
  }));
232
243
  });
233
244
 
@@ -244,12 +255,12 @@ export const createAuthRouter = (
244
255
 
245
256
  // Get active access rules to filter input
246
257
  const activeAccessRules = new Set(
247
- accessRuleRegistry.getAccessRules().map((p) => p.id)
258
+ accessRuleRegistry.getAccessRules().map((p) => p.id),
248
259
  );
249
260
 
250
261
  // Filter to only include active access rules
251
262
  const validAccessRules = inputAccessRules.filter((p) =>
252
- activeAccessRules.has(p)
263
+ activeAccessRules.has(p),
253
264
  );
254
265
 
255
266
  await internalDb.transaction(async (tx) => {
@@ -267,7 +278,7 @@ export const createAuthRouter = (
267
278
  validAccessRules.map((accessRuleId) => ({
268
279
  roleId: id,
269
280
  accessRuleId,
270
- }))
281
+ })),
271
282
  );
272
283
  }
273
284
  });
@@ -292,8 +303,8 @@ export const createAuthRouter = (
292
303
  });
293
304
  }
294
305
 
295
- const isUsersRole = id === "users";
296
- const isAdminRole = id === "admin";
306
+ const isUsersRole = id === USERS_ROLE_ID;
307
+ const isAdminRole = id === ADMIN_ROLE_ID;
297
308
 
298
309
  // System roles can have name/description edited, but not deleted
299
310
  // Admin role: access rules cannot be changed (wildcard access)
@@ -302,12 +313,12 @@ export const createAuthRouter = (
302
313
 
303
314
  // Get active access rules to filter input
304
315
  const activeAccessRules = new Set(
305
- accessRuleRegistry.getAccessRules().map((p) => p.id)
316
+ accessRuleRegistry.getAccessRules().map((p) => p.id),
306
317
  );
307
318
 
308
319
  // Filter to only include active access rules
309
320
  const validAccessRules = inputAccessRules.filter((p) =>
310
- activeAccessRules.has(p)
321
+ activeAccessRules.has(p),
311
322
  );
312
323
 
313
324
  // Track disabled authenticated default access rules for "users" role
@@ -319,7 +330,7 @@ export const createAuthRouter = (
319
330
 
320
331
  // Find authenticated default access rules that are being removed
321
332
  const removedDefaults = defaultPermIds.filter(
322
- (defId) => !validAccessRules.includes(defId)
333
+ (defId) => !validAccessRules.includes(defId),
323
334
  );
324
335
 
325
336
  // Insert into disabled_default_access_rule table
@@ -335,7 +346,7 @@ export const createAuthRouter = (
335
346
 
336
347
  // Remove from disabled table if being re-added
337
348
  const readdedDefaults = validAccessRules.filter((p) =>
338
- defaultPermIds.includes(p)
349
+ defaultPermIds.includes(p),
339
350
  );
340
351
  for (const permId of readdedDefaults) {
341
352
  await internalDb
@@ -345,7 +356,7 @@ export const createAuthRouter = (
345
356
  }
346
357
 
347
358
  // Track disabled public default access rules for "anonymous" role
348
- const isAnonymousRole = id === "anonymous";
359
+ const isAnonymousRole = id === ANONYMOUS_ROLE_ID;
349
360
  if (isAnonymousRole) {
350
361
  const allPerms = accessRuleRegistry.getAccessRules();
351
362
  const publicDefaultPermIds = allPerms
@@ -354,7 +365,7 @@ export const createAuthRouter = (
354
365
 
355
366
  // Find public default access rules that are being removed
356
367
  const removedPublicDefaults = publicDefaultPermIds.filter(
357
- (defId) => !validAccessRules.includes(defId)
368
+ (defId) => !validAccessRules.includes(defId),
358
369
  );
359
370
 
360
371
  // Insert into disabled_public_default_access_rule table
@@ -370,13 +381,13 @@ export const createAuthRouter = (
370
381
 
371
382
  // Remove from disabled table if being re-added
372
383
  const readdedPublicDefaults = validAccessRules.filter((p) =>
373
- publicDefaultPermIds.includes(p)
384
+ publicDefaultPermIds.includes(p),
374
385
  );
375
386
  for (const permId of readdedPublicDefaults) {
376
387
  await internalDb
377
388
  .delete(schema.disabledPublicDefaultAccessRule)
378
389
  .where(
379
- eq(schema.disabledPublicDefaultAccessRule.accessRuleId, permId)
390
+ eq(schema.disabledPublicDefaultAccessRule.accessRuleId, permId),
380
391
  );
381
392
  }
382
393
  }
@@ -406,7 +417,7 @@ export const createAuthRouter = (
406
417
  validAccessRules.map((accessRuleId) => ({
407
418
  roleId: id,
408
419
  accessRuleId,
409
- }))
420
+ })),
410
421
  );
411
422
  }
412
423
  });
@@ -468,7 +479,7 @@ export const createAuthRouter = (
468
479
  }
469
480
 
470
481
  // Prevent assignment of the "anonymous" role - it's reserved for unauthenticated users
471
- if (roles.includes("anonymous")) {
482
+ if (roles.includes(ANONYMOUS_ROLE_ID)) {
472
483
  throw new ORPCError("BAD_REQUEST", {
473
484
  message: "The 'anonymous' role cannot be assigned to users",
474
485
  });
@@ -483,11 +494,11 @@ export const createAuthRouter = (
483
494
  roles.map((roleId) => ({
484
495
  userId,
485
496
  roleId,
486
- }))
497
+ })),
487
498
  );
488
499
  }
489
500
  });
490
- }
501
+ },
491
502
  );
492
503
 
493
504
  const getStrategies = os.getStrategies.handler(async () => {
@@ -500,7 +511,7 @@ export const createAuthRouter = (
500
511
  strategy.id,
501
512
  strategy.configSchema,
502
513
  strategy.configVersion,
503
- strategy.migrations
514
+ strategy.migrations,
504
515
  );
505
516
 
506
517
  // Convert Zod schema to JSON Schema with automatic secret metadata
@@ -520,7 +531,7 @@ export const createAuthRouter = (
520
531
  config,
521
532
  adminInstructions: strategy.adminInstructions,
522
533
  };
523
- })
534
+ }),
524
535
  );
525
536
  });
526
537
 
@@ -541,7 +552,7 @@ export const createAuthRouter = (
541
552
  strategy.configSchema,
542
553
  strategy.configVersion,
543
554
  config, // Just the config, no enabled mixed in
544
- strategy.migrations
555
+ strategy.migrations,
545
556
  );
546
557
  }
547
558
 
@@ -574,12 +585,208 @@ export const createAuthRouter = (
574
585
  PLATFORM_REGISTRATION_CONFIG_ID,
575
586
  platformRegistrationConfigV1,
576
587
  PLATFORM_REGISTRATION_CONFIG_VERSION,
577
- { allowRegistration: input.allowRegistration }
588
+ { allowRegistration: input.allowRegistration },
578
589
  );
579
590
  // Trigger auth reload to apply new settings
580
591
  await reloadAuthFn();
581
592
  return { success: true };
582
- }
593
+ },
594
+ );
595
+
596
+ // ==========================================================================
597
+ // ONBOARDING ENDPOINTS
598
+ // ==========================================================================
599
+
600
+ const getOnboardingStatus = os.getOnboardingStatus.handler(async () => {
601
+ // Check if any users exist in the database
602
+ const users = await internalDb
603
+ .select({ id: schema.user.id })
604
+ .from(schema.user)
605
+ .limit(1);
606
+ return { needsOnboarding: users.length === 0 };
607
+ });
608
+
609
+ const completeOnboarding = os.completeOnboarding.handler(
610
+ async ({ input }) => {
611
+ const { name, email, password } = input;
612
+
613
+ // Security check: only allow if no users exist
614
+ const existingUsers = await internalDb
615
+ .select({ id: schema.user.id })
616
+ .from(schema.user)
617
+ .limit(1);
618
+
619
+ if (existingUsers.length > 0) {
620
+ throw new ORPCError("FORBIDDEN", {
621
+ message: "Onboarding has already been completed.",
622
+ });
623
+ }
624
+
625
+ // Validate password against platform's password schema
626
+ const passwordValidation = passwordSchema.safeParse(password);
627
+ if (!passwordValidation.success) {
628
+ throw new ORPCError("BAD_REQUEST", {
629
+ message: passwordValidation.error.issues
630
+ .map((issue) => issue.message)
631
+ .join(", "),
632
+ });
633
+ }
634
+
635
+ // Create the first admin user
636
+ const userId = crypto.randomUUID();
637
+ const accountId = crypto.randomUUID();
638
+ const hashedPassword = await hashPassword(password);
639
+ const now = new Date();
640
+
641
+ await internalDb.transaction(async (tx) => {
642
+ // Create user
643
+ await tx.insert(schema.user).values({
644
+ id: userId,
645
+ email,
646
+ name,
647
+ emailVerified: true,
648
+ createdAt: now,
649
+ updatedAt: now,
650
+ });
651
+
652
+ // Create credential account
653
+ await tx.insert(schema.account).values({
654
+ id: accountId,
655
+ accountId: email,
656
+ providerId: "credential",
657
+ userId,
658
+ password: hashedPassword,
659
+ createdAt: now,
660
+ updatedAt: now,
661
+ });
662
+
663
+ // Assign admin role
664
+ await tx.insert(schema.userRole).values({
665
+ userId,
666
+ roleId: ADMIN_ROLE_ID,
667
+ });
668
+ });
669
+
670
+ return { success: true };
671
+ },
672
+ );
673
+
674
+ // ==========================================================================
675
+ // USER PROFILE ENDPOINTS
676
+ // ==========================================================================
677
+
678
+ const getCurrentUserProfile = os.getCurrentUserProfile.handler(
679
+ async ({ context }) => {
680
+ const user = context.user;
681
+ if (!isRealUser(user)) {
682
+ throw new ORPCError("UNAUTHORIZED", {
683
+ message: "Not authenticated",
684
+ });
685
+ }
686
+
687
+ // Get user data
688
+ const users = await internalDb
689
+ .select()
690
+ .from(schema.user)
691
+ .where(eq(schema.user.id, user.id))
692
+ .limit(1);
693
+
694
+ if (users.length === 0) {
695
+ throw new ORPCError("NOT_FOUND", {
696
+ message: "User not found",
697
+ });
698
+ }
699
+
700
+ // Check if user has a credential account
701
+ const accounts = await internalDb
702
+ .select()
703
+ .from(schema.account)
704
+ .where(
705
+ and(
706
+ eq(schema.account.userId, user.id),
707
+ eq(schema.account.providerId, "credential"),
708
+ ),
709
+ )
710
+ .limit(1);
711
+
712
+ return {
713
+ id: users[0].id,
714
+ name: users[0].name,
715
+ email: users[0].email,
716
+ hasCredentialAccount: accounts.length > 0,
717
+ };
718
+ },
719
+ );
720
+
721
+ const updateCurrentUser = os.updateCurrentUser.handler(
722
+ async ({ input, context }) => {
723
+ const user = context.user;
724
+ if (!isRealUser(user)) {
725
+ throw new ORPCError("UNAUTHORIZED", {
726
+ message: "Not authenticated",
727
+ });
728
+ }
729
+
730
+ const { name, email } = input;
731
+
732
+ // If email is being updated, check if user has a credential account
733
+ if (email !== undefined) {
734
+ const accounts = await internalDb
735
+ .select()
736
+ .from(schema.account)
737
+ .where(
738
+ and(
739
+ eq(schema.account.userId, user.id),
740
+ eq(schema.account.providerId, "credential"),
741
+ ),
742
+ )
743
+ .limit(1);
744
+
745
+ if (accounts.length === 0) {
746
+ throw new ORPCError("FORBIDDEN", {
747
+ message: "Email can only be updated for credential-based accounts.",
748
+ });
749
+ }
750
+
751
+ // Check email uniqueness
752
+ const existingUsers = await internalDb
753
+ .select({ id: schema.user.id })
754
+ .from(schema.user)
755
+ .where(eq(schema.user.email, email))
756
+ .limit(1);
757
+
758
+ if (existingUsers.length > 0 && existingUsers[0].id !== user.id) {
759
+ throw new ORPCError("CONFLICT", {
760
+ message: "A user with this email already exists.",
761
+ });
762
+ }
763
+ }
764
+
765
+ // Build update object
766
+ const updates: { name?: string; email?: string; updatedAt: Date } = {
767
+ updatedAt: new Date(),
768
+ };
769
+ if (name !== undefined) updates.name = name;
770
+ if (email !== undefined) updates.email = email;
771
+
772
+ await internalDb
773
+ .update(schema.user)
774
+ .set(updates)
775
+ .where(eq(schema.user.id, user.id));
776
+
777
+ // If email was updated, also update the credential account's accountId
778
+ if (email !== undefined) {
779
+ await internalDb
780
+ .update(schema.account)
781
+ .set({ accountId: email, updatedAt: new Date() })
782
+ .where(
783
+ and(
784
+ eq(schema.account.userId, user.id),
785
+ eq(schema.account.providerId, "credential"),
786
+ ),
787
+ );
788
+ }
789
+ },
583
790
  );
584
791
 
585
792
  const getAnonymousAccessRules = os.getAnonymousAccessRules.handler(
@@ -587,9 +794,9 @@ export const createAuthRouter = (
587
794
  const rolePerms = await internalDb
588
795
  .select()
589
796
  .from(schema.roleAccessRule)
590
- .where(eq(schema.roleAccessRule.roleId, "anonymous"));
797
+ .where(eq(schema.roleAccessRule.roleId, ANONYMOUS_ROLE_ID));
591
798
  return rolePerms.map((rp) => rp.accessRuleId);
592
- }
799
+ },
593
800
  );
594
801
 
595
802
  const filterUsersByAccessRule = os.filterUsersByAccessRule.handler(
@@ -605,18 +812,18 @@ export const createAuthRouter = (
605
812
  .from(schema.userRole)
606
813
  .innerJoin(
607
814
  schema.roleAccessRule,
608
- eq(schema.userRole.roleId, schema.roleAccessRule.roleId)
815
+ eq(schema.userRole.roleId, schema.roleAccessRule.roleId),
609
816
  )
610
817
  .where(
611
818
  and(
612
819
  inArray(schema.userRole.userId, userIds),
613
- eq(schema.roleAccessRule.accessRuleId, accessRule)
614
- )
820
+ eq(schema.roleAccessRule.accessRuleId, accessRule),
821
+ ),
615
822
  )
616
823
  .groupBy(schema.userRole.userId);
617
824
 
618
825
  return usersWithAccess.map((row) => row.userId);
619
- }
826
+ },
620
827
  );
621
828
 
622
829
  // ==========================================================================
@@ -649,8 +856,16 @@ export const createAuthRouter = (
649
856
 
650
857
  const upsertExternalUser = os.upsertExternalUser.handler(
651
858
  async ({ input, context }) => {
652
- const { email, name, providerId, accountId, password, autoUpdateUser } =
653
- input;
859
+ const {
860
+ email,
861
+ name,
862
+ providerId,
863
+ accountId,
864
+ password,
865
+ autoUpdateUser,
866
+ syncRoles,
867
+ managedRoleIds,
868
+ } = input;
654
869
 
655
870
  // Check if user exists
656
871
  const existingUsers = await internalDb
@@ -659,9 +874,12 @@ export const createAuthRouter = (
659
874
  .where(eq(schema.user.email, email))
660
875
  .limit(1);
661
876
 
877
+ let userId: string;
878
+ let created = false;
879
+
662
880
  if (existingUsers.length > 0) {
663
881
  // User exists - update if autoUpdateUser is enabled
664
- const userId = existingUsers[0].id;
882
+ userId = existingUsers[0].id;
665
883
 
666
884
  if (autoUpdateUser) {
667
885
  await internalDb
@@ -669,50 +887,107 @@ export const createAuthRouter = (
669
887
  .set({ name, updatedAt: new Date() })
670
888
  .where(eq(schema.user.id, userId));
671
889
  }
890
+ } else {
891
+ // Check if registration is allowed before creating new user
892
+ const registrationAllowed = await isRegistrationAllowed(configService);
893
+ if (!registrationAllowed) {
894
+ throw new ORPCError("FORBIDDEN", {
895
+ message:
896
+ "Registration is disabled. Please contact an administrator.",
897
+ });
898
+ }
672
899
 
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.",
900
+ // Create new user and account in a transaction
901
+ userId = crypto.randomUUID();
902
+ const accountEntryId = crypto.randomUUID();
903
+ const now = new Date();
904
+
905
+ await internalDb.transaction(async (tx) => {
906
+ // Create user
907
+ await tx.insert(schema.user).values({
908
+ id: userId,
909
+ email,
910
+ name,
911
+ emailVerified: false,
912
+ createdAt: now,
913
+ updatedAt: now,
914
+ });
915
+
916
+ // Create account
917
+ await tx.insert(schema.account).values({
918
+ id: accountEntryId,
919
+ accountId,
920
+ providerId,
921
+ userId,
922
+ password,
923
+ createdAt: now,
924
+ updatedAt: now,
925
+ });
681
926
  });
682
- }
683
927
 
684
- // Create new user and account in a transaction
685
- const userId = crypto.randomUUID();
686
- const accountEntryId = crypto.randomUUID();
687
- const now = new Date();
928
+ context.logger.info(`Created new user from ${providerId}: ${email}`);
929
+ created = true;
930
+ }
688
931
 
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
- });
932
+ // Handle role sync if syncRoles is provided
933
+ // Uses managedRoleIds to determine which roles are controlled by directory
934
+ if (syncRoles) {
935
+ const syncRoleSet = new Set(syncRoles);
936
+
937
+ // Validate which sync roles actually exist in the database
938
+ const validSyncRoles =
939
+ syncRoles.length > 0
940
+ ? await internalDb
941
+ .select({ id: schema.role.id })
942
+ .from(schema.role)
943
+ .where(inArray(schema.role.id, syncRoles))
944
+ : [];
945
+ const validSyncRoleIds = new Set(validSyncRoles.map((r) => r.id));
946
+
947
+ // Get current user roles
948
+ const currentRoles = await internalDb
949
+ .select({ roleId: schema.userRole.roleId })
950
+ .from(schema.userRole)
951
+ .where(eq(schema.userRole.userId, userId));
952
+ const currentRoleIds = new Set(currentRoles.map((r) => r.roleId));
699
953
 
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
- });
954
+ // Add new roles that user should have
955
+ const rolesToAdd = [...validSyncRoleIds].filter(
956
+ (id) => !currentRoleIds.has(id),
957
+ );
958
+ if (rolesToAdd.length > 0) {
959
+ await internalDb
960
+ .insert(schema.userRole)
961
+ .values(rolesToAdd.map((roleId) => ({ userId, roleId })));
962
+ context.logger.info(
963
+ `Added ${rolesToAdd.length} roles for external user: ${email}`,
964
+ );
965
+ }
711
966
 
712
- context.logger.info(`Created new user from ${providerId}: ${email}`);
967
+ // Remove roles that are managed but user no longer has in directory
968
+ if (managedRoleIds && managedRoleIds.length > 0) {
969
+ // Roles to remove: currently has + is managed + NOT in sync roles
970
+ const rolesToRemove = [...currentRoleIds].filter(
971
+ (id) => managedRoleIds.includes(id) && !syncRoleSet.has(id),
972
+ );
973
+ if (rolesToRemove.length > 0) {
974
+ await internalDb
975
+ .delete(schema.userRole)
976
+ .where(
977
+ and(
978
+ eq(schema.userRole.userId, userId),
979
+ inArray(schema.userRole.roleId, rolesToRemove),
980
+ ),
981
+ );
982
+ context.logger.info(
983
+ `Removed ${rolesToRemove.length} managed roles for external user: ${email}`,
984
+ );
985
+ }
986
+ }
987
+ }
713
988
 
714
- return { userId, created: true };
715
- }
989
+ return { userId, created };
990
+ },
716
991
  );
717
992
 
718
993
  const createSession = os.createSession.handler(async ({ input }) => {
@@ -753,7 +1028,7 @@ export const createAuthRouter = (
753
1028
  // Check if credential strategy is enabled
754
1029
  const credentialEnabled = await getStrategyEnabled(
755
1030
  "credential",
756
- configService
1031
+ configService,
757
1032
  );
758
1033
  if (!credentialEnabled) {
759
1034
  throw new ORPCError("BAD_REQUEST", {
@@ -806,16 +1081,16 @@ export const createAuthRouter = (
806
1081
  // Assign "users" role to new user
807
1082
  await tx.insert(schema.userRole).values({
808
1083
  userId,
809
- roleId: "users",
1084
+ roleId: USERS_ROLE_ID,
810
1085
  });
811
1086
  });
812
1087
 
813
1088
  context.logger.info(
814
- `[auth-backend] Admin created credential user: ${email}`
1089
+ `[auth-backend] Admin created credential user: ${email}`,
815
1090
  );
816
1091
 
817
1092
  return { userId };
818
- }
1093
+ },
819
1094
  );
820
1095
 
821
1096
  // ==========================================================================
@@ -833,8 +1108,8 @@ export const createAuthRouter = (
833
1108
  .where(
834
1109
  inArray(
835
1110
  schema.applicationRole.applicationId,
836
- apps.map((a) => a.id)
837
- )
1111
+ apps.map((a) => a.id),
1112
+ ),
838
1113
  );
839
1114
 
840
1115
  return apps.map((app) => ({
@@ -868,7 +1143,7 @@ export const createAuthRouter = (
868
1143
  const now = new Date();
869
1144
 
870
1145
  // Default role for all applications
871
- const defaultRole = "applications";
1146
+ const defaultRole = APPLICATIONS_ROLE_ID;
872
1147
 
873
1148
  await internalDb.transaction(async (tx) => {
874
1149
  // Create application
@@ -890,7 +1165,7 @@ export const createAuthRouter = (
890
1165
  });
891
1166
 
892
1167
  context.logger.info(
893
- `[auth-backend] Created application: ${name} (${id})`
1168
+ `[auth-backend] Created application: ${name} (${id})`,
894
1169
  );
895
1170
 
896
1171
  return {
@@ -904,7 +1179,7 @@ export const createAuthRouter = (
904
1179
  },
905
1180
  secret: `ck_${id}_${secret}`, // Full secret - only shown once!
906
1181
  };
907
- }
1182
+ },
908
1183
  );
909
1184
 
910
1185
  const updateApplication = os.updateApplication.handler(async ({ input }) => {
@@ -953,7 +1228,7 @@ export const createAuthRouter = (
953
1228
  roles.map((roleId) => ({
954
1229
  applicationId: id,
955
1230
  roleId,
956
- }))
1231
+ })),
957
1232
  );
958
1233
  }
959
1234
  }
@@ -982,7 +1257,7 @@ export const createAuthRouter = (
982
1257
  .where(eq(schema.application.id, id));
983
1258
 
984
1259
  context.logger.info(`[auth-backend] Deleted application: ${id}`);
985
- }
1260
+ },
986
1261
  );
987
1262
 
988
1263
  const regenerateApplicationSecret = os.regenerateApplicationSecret.handler(
@@ -1009,11 +1284,11 @@ export const createAuthRouter = (
1009
1284
  .where(eq(schema.application.id, id));
1010
1285
 
1011
1286
  context.logger.info(
1012
- `[auth-backend] Regenerated secret for application: ${id}`
1287
+ `[auth-backend] Regenerated secret for application: ${id}`,
1013
1288
  );
1014
1289
 
1015
1290
  return { secret: `ck_${id}_${secret}` };
1016
- }
1291
+ },
1017
1292
  );
1018
1293
 
1019
1294
  // ==========================================================================
@@ -1158,10 +1433,10 @@ export const createAuthRouter = (
1158
1433
  .where(
1159
1434
  and(
1160
1435
  eq(schema.userTeam.userId, input.userId),
1161
- eq(schema.userTeam.teamId, input.teamId)
1162
- )
1436
+ eq(schema.userTeam.teamId, input.teamId),
1437
+ ),
1163
1438
  );
1164
- }
1439
+ },
1165
1440
  );
1166
1441
 
1167
1442
  const addTeamManager = os.addTeamManager.handler(async ({ input }) => {
@@ -1177,8 +1452,8 @@ export const createAuthRouter = (
1177
1452
  .where(
1178
1453
  and(
1179
1454
  eq(schema.teamManager.userId, input.userId),
1180
- eq(schema.teamManager.teamId, input.teamId)
1181
- )
1455
+ eq(schema.teamManager.teamId, input.teamId),
1456
+ ),
1182
1457
  );
1183
1458
  });
1184
1459
 
@@ -1189,13 +1464,13 @@ export const createAuthRouter = (
1189
1464
  .from(schema.resourceTeamAccess)
1190
1465
  .innerJoin(
1191
1466
  schema.team,
1192
- eq(schema.resourceTeamAccess.teamId, schema.team.id)
1467
+ eq(schema.resourceTeamAccess.teamId, schema.team.id),
1193
1468
  )
1194
1469
  .where(
1195
1470
  and(
1196
1471
  eq(schema.resourceTeamAccess.resourceType, input.resourceType),
1197
- eq(schema.resourceTeamAccess.resourceId, input.resourceId)
1198
- )
1472
+ eq(schema.resourceTeamAccess.resourceId, input.resourceId),
1473
+ ),
1199
1474
  );
1200
1475
  return rows.map((r) => ({
1201
1476
  teamId: r.resource_team_access.teamId,
@@ -1203,7 +1478,7 @@ export const createAuthRouter = (
1203
1478
  canRead: r.resource_team_access.canRead,
1204
1479
  canManage: r.resource_team_access.canManage,
1205
1480
  }));
1206
- }
1481
+ },
1207
1482
  );
1208
1483
 
1209
1484
  const setResourceTeamAccess = os.setResourceTeamAccess.handler(
@@ -1229,7 +1504,7 @@ export const createAuthRouter = (
1229
1504
  canManage: canManage ?? false,
1230
1505
  },
1231
1506
  });
1232
- }
1507
+ },
1233
1508
  );
1234
1509
 
1235
1510
  const removeResourceTeamAccess = os.removeResourceTeamAccess.handler(
@@ -1240,10 +1515,10 @@ export const createAuthRouter = (
1240
1515
  and(
1241
1516
  eq(schema.resourceTeamAccess.resourceType, input.resourceType),
1242
1517
  eq(schema.resourceTeamAccess.resourceId, input.resourceId),
1243
- eq(schema.resourceTeamAccess.teamId, input.teamId)
1244
- )
1518
+ eq(schema.resourceTeamAccess.teamId, input.teamId),
1519
+ ),
1245
1520
  );
1246
- }
1521
+ },
1247
1522
  );
1248
1523
 
1249
1524
  // Resource-level access settings
@@ -1255,12 +1530,12 @@ export const createAuthRouter = (
1255
1530
  .where(
1256
1531
  and(
1257
1532
  eq(schema.resourceAccessSettings.resourceType, input.resourceType),
1258
- eq(schema.resourceAccessSettings.resourceId, input.resourceId)
1259
- )
1533
+ eq(schema.resourceAccessSettings.resourceId, input.resourceId),
1534
+ ),
1260
1535
  )
1261
1536
  .limit(1);
1262
1537
  return { teamOnly: rows[0]?.teamOnly ?? false };
1263
- }
1538
+ },
1264
1539
  );
1265
1540
 
1266
1541
  const setResourceAccessSettings = os.setResourceAccessSettings.handler(
@@ -1276,7 +1551,7 @@ export const createAuthRouter = (
1276
1551
  ],
1277
1552
  set: { teamOnly },
1278
1553
  });
1279
- }
1554
+ },
1280
1555
  );
1281
1556
 
1282
1557
  // S2S Endpoints for middleware
@@ -1297,8 +1572,8 @@ export const createAuthRouter = (
1297
1572
  .where(
1298
1573
  and(
1299
1574
  eq(schema.resourceTeamAccess.resourceType, resourceType),
1300
- eq(schema.resourceTeamAccess.resourceId, resourceId)
1301
- )
1575
+ eq(schema.resourceTeamAccess.resourceId, resourceId),
1576
+ ),
1302
1577
  );
1303
1578
 
1304
1579
  // No grants = global access applies
@@ -1311,8 +1586,8 @@ export const createAuthRouter = (
1311
1586
  .where(
1312
1587
  and(
1313
1588
  eq(schema.resourceAccessSettings.resourceType, resourceType),
1314
- eq(schema.resourceAccessSettings.resourceId, resourceId)
1315
- )
1589
+ eq(schema.resourceAccessSettings.resourceId, resourceId),
1590
+ ),
1316
1591
  )
1317
1592
  .limit(1);
1318
1593
  const isTeamOnly = settingsRows[0]?.teamOnly ?? false;
@@ -1339,10 +1614,10 @@ export const createAuthRouter = (
1339
1614
 
1340
1615
  const field = action === "manage" ? "canManage" : "canRead";
1341
1616
  const hasAccess = grants.some(
1342
- (g) => userTeamIds.has(g.teamId) && g[field]
1617
+ (g) => userTeamIds.has(g.teamId) && g[field],
1343
1618
  );
1344
1619
  return { hasAccess };
1345
- }
1620
+ },
1346
1621
  );
1347
1622
 
1348
1623
  const getAccessibleResourceIds = os.getAccessibleResourceIds.handler(
@@ -1364,8 +1639,8 @@ export const createAuthRouter = (
1364
1639
  .where(
1365
1640
  and(
1366
1641
  eq(schema.resourceTeamAccess.resourceType, resourceType),
1367
- inArray(schema.resourceTeamAccess.resourceId, resourceIds)
1368
- )
1642
+ inArray(schema.resourceTeamAccess.resourceId, resourceIds),
1643
+ ),
1369
1644
  );
1370
1645
 
1371
1646
  // Get resource-level settings for teamOnly
@@ -1375,11 +1650,11 @@ export const createAuthRouter = (
1375
1650
  .where(
1376
1651
  and(
1377
1652
  eq(schema.resourceAccessSettings.resourceType, resourceType),
1378
- inArray(schema.resourceAccessSettings.resourceId, resourceIds)
1379
- )
1653
+ inArray(schema.resourceAccessSettings.resourceId, resourceIds),
1654
+ ),
1380
1655
  );
1381
1656
  const teamOnlyByResource = new Map(
1382
- settingsRows.map((s) => [s.resourceId, s.teamOnly])
1657
+ settingsRows.map((s) => [s.resourceId, s.teamOnly]),
1383
1658
  );
1384
1659
 
1385
1660
  // Get user's teams
@@ -1414,10 +1689,10 @@ export const createAuthRouter = (
1414
1689
  const isTeamOnly = teamOnlyByResource.get(id) ?? false;
1415
1690
  if (!isTeamOnly && hasGlobalAccess) return true;
1416
1691
  return resourceGrants.some(
1417
- (g) => userTeamIds.has(g.teamId) && g[field]
1692
+ (g) => userTeamIds.has(g.teamId) && g[field],
1418
1693
  );
1419
1694
  });
1420
- }
1695
+ },
1421
1696
  );
1422
1697
 
1423
1698
  const deleteResourceGrants = os.deleteResourceGrants.handler(
@@ -1427,10 +1702,10 @@ export const createAuthRouter = (
1427
1702
  .where(
1428
1703
  and(
1429
1704
  eq(schema.resourceTeamAccess.resourceType, input.resourceType),
1430
- eq(schema.resourceTeamAccess.resourceId, input.resourceId)
1431
- )
1705
+ eq(schema.resourceTeamAccess.resourceId, input.resourceId),
1706
+ ),
1432
1707
  );
1433
- }
1708
+ },
1434
1709
  );
1435
1710
 
1436
1711
  return os.router({
@@ -1450,6 +1725,10 @@ export const createAuthRouter = (
1450
1725
  getRegistrationSchema,
1451
1726
  getRegistrationStatus,
1452
1727
  setRegistrationStatus,
1728
+ getOnboardingStatus,
1729
+ completeOnboarding,
1730
+ getCurrentUserProfile,
1731
+ updateCurrentUser,
1453
1732
  getAnonymousAccessRules,
1454
1733
  getUserById,
1455
1734
  filterUsersByAccessRule,