@owox/idp-better-auth 0.5.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.
Files changed (59) hide show
  1. package/README.md +183 -0
  2. package/dist/adapters/database.d.ts +29 -0
  3. package/dist/adapters/database.d.ts.map +1 -0
  4. package/dist/adapters/database.js +85 -0
  5. package/dist/auth/auth-config.d.ts +4 -0
  6. package/dist/auth/auth-config.d.ts.map +1 -0
  7. package/dist/auth/auth-config.js +72 -0
  8. package/dist/index.d.ts +15 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +16 -0
  11. package/dist/providers/better-auth-provider.d.ts +34 -0
  12. package/dist/providers/better-auth-provider.d.ts.map +1 -0
  13. package/dist/providers/better-auth-provider.js +99 -0
  14. package/dist/services/authentication-service.d.ts +23 -0
  15. package/dist/services/authentication-service.d.ts.map +1 -0
  16. package/dist/services/authentication-service.js +199 -0
  17. package/dist/services/crypto-service.d.ts +18 -0
  18. package/dist/services/crypto-service.d.ts.map +1 -0
  19. package/dist/services/crypto-service.js +100 -0
  20. package/dist/services/database-service.d.ts +14 -0
  21. package/dist/services/database-service.d.ts.map +1 -0
  22. package/dist/services/database-service.js +142 -0
  23. package/dist/services/magic-link-service.d.ts +10 -0
  24. package/dist/services/magic-link-service.d.ts.map +1 -0
  25. package/dist/services/magic-link-service.js +42 -0
  26. package/dist/services/middleware-service.d.ts +15 -0
  27. package/dist/services/middleware-service.d.ts.map +1 -0
  28. package/dist/services/middleware-service.js +38 -0
  29. package/dist/services/page-service.d.ts +23 -0
  30. package/dist/services/page-service.d.ts.map +1 -0
  31. package/dist/services/page-service.js +331 -0
  32. package/dist/services/request-handler-service.d.ts +10 -0
  33. package/dist/services/request-handler-service.d.ts.map +1 -0
  34. package/dist/services/request-handler-service.js +50 -0
  35. package/dist/services/template-service.d.ts +28 -0
  36. package/dist/services/template-service.d.ts.map +1 -0
  37. package/dist/services/template-service.js +198 -0
  38. package/dist/services/token-service.d.ts +15 -0
  39. package/dist/services/token-service.d.ts.map +1 -0
  40. package/dist/services/token-service.js +108 -0
  41. package/dist/services/user-management-service.d.ts +74 -0
  42. package/dist/services/user-management-service.d.ts.map +1 -0
  43. package/dist/services/user-management-service.js +396 -0
  44. package/dist/templates/admin-add-user.html +298 -0
  45. package/dist/templates/admin-dashboard.html +214 -0
  46. package/dist/templates/admin-user-details.html +278 -0
  47. package/dist/templates/password-setup.html +267 -0
  48. package/dist/templates/password-success.html +197 -0
  49. package/dist/templates/sign-in.html +201 -0
  50. package/dist/types/auth-session.d.ts +25 -0
  51. package/dist/types/auth-session.d.ts.map +1 -0
  52. package/dist/types/auth-session.js +1 -0
  53. package/dist/types/database-models.d.ts +54 -0
  54. package/dist/types/database-models.d.ts.map +1 -0
  55. package/dist/types/database-models.js +1 -0
  56. package/dist/types/index.d.ts +45 -0
  57. package/dist/types/index.d.ts.map +1 -0
  58. package/dist/types/index.js +2 -0
  59. package/package.json +69 -0
@@ -0,0 +1,108 @@
1
+ export class TokenService {
2
+ auth;
3
+ cryptoService;
4
+ static DEFAULT_ORGANIZATION_ID = 'owox-default';
5
+ constructor(auth, cryptoService) {
6
+ this.auth = auth;
7
+ this.cryptoService = cryptoService;
8
+ }
9
+ async introspectToken(token) {
10
+ try {
11
+ const cleanToken = token.replace('Bearer ', '');
12
+ const decryptedToken = await this.cryptoService.decrypt(cleanToken);
13
+ const encodedToken = encodeURIComponent(decryptedToken);
14
+ const session = await this.auth.api.getSession({
15
+ headers: new Headers({
16
+ Cookie: `refreshToken=${encodedToken}`,
17
+ }),
18
+ });
19
+ if (!session || !session.user) {
20
+ return null;
21
+ }
22
+ return {
23
+ userId: session.user.id,
24
+ projectId: TokenService.DEFAULT_ORGANIZATION_ID,
25
+ email: session.user.email,
26
+ fullName: session.user.name || session.user.email,
27
+ roles: ['admin'],
28
+ };
29
+ }
30
+ catch (error) {
31
+ console.error('Token introspection failed:', error);
32
+ throw new Error('Token introspection failed');
33
+ }
34
+ }
35
+ async parseToken(token) {
36
+ return this.introspectToken(token);
37
+ }
38
+ async verifyToken(token) {
39
+ try {
40
+ const cleanToken = token.replace('Bearer ', '');
41
+ const decryptedToken = await this.cryptoService.decrypt(cleanToken);
42
+ const session = await this.auth.api.getSession({
43
+ headers: new Headers({
44
+ Authorization: `Bearer ${decryptedToken}`,
45
+ Cookie: `refreshToken=${decryptedToken}`,
46
+ }),
47
+ });
48
+ if (!session || !session.user) {
49
+ return null;
50
+ }
51
+ return {
52
+ userId: session.user.id,
53
+ projectId: TokenService.DEFAULT_ORGANIZATION_ID,
54
+ email: session.user.email,
55
+ fullName: session.user.name || session.user.email,
56
+ roles: ['admin'],
57
+ };
58
+ }
59
+ catch (error) {
60
+ console.error('Token verification failed:', error);
61
+ throw new Error('Token verification failed');
62
+ }
63
+ }
64
+ async refreshToken(refreshToken) {
65
+ try {
66
+ // TODO: decode refresh token
67
+ const session = await this.auth.api.getSession({
68
+ headers: new Headers({
69
+ Cookie: `refreshToken=${refreshToken}`,
70
+ }),
71
+ });
72
+ if (!session) {
73
+ throw new Error('Invalid refresh token');
74
+ }
75
+ const encryptedToken = await this.cryptoService.encrypt(session.session.token);
76
+ return {
77
+ accessToken: encryptedToken,
78
+ };
79
+ }
80
+ catch (error) {
81
+ console.error('Token refresh failed:', error);
82
+ throw new Error('Token refresh failed');
83
+ }
84
+ }
85
+ async revokeToken(token) {
86
+ try {
87
+ const cleanToken = token.replace('Bearer ', '');
88
+ const decryptedToken = await this.cryptoService.decrypt(cleanToken);
89
+ const session = await this.auth.api.getSession({
90
+ headers: new Headers({
91
+ Authorization: `Bearer ${decryptedToken}`,
92
+ Cookie: `refreshToken=${decryptedToken}`,
93
+ }),
94
+ });
95
+ if (session) {
96
+ await this.auth.api.signOut({
97
+ headers: new Headers({
98
+ Cookie: `refreshToken=${decryptedToken}`,
99
+ }),
100
+ });
101
+ }
102
+ }
103
+ catch (error) {
104
+ console.error('Failed to revoke token:', error);
105
+ throw new Error('Failed to revoke token');
106
+ }
107
+ }
108
+ }
@@ -0,0 +1,74 @@
1
+ import { createBetterAuthConfig } from '../auth/auth-config.js';
2
+ import { MagicLinkService } from './magic-link-service.js';
3
+ import { CryptoService } from './crypto-service.js';
4
+ import { Payload, AddUserCommandResponse } from '@owox/idp-protocol';
5
+ import { type Role } from '../types/index.js';
6
+ import { type Request as ExpressRequest } from 'express';
7
+ export declare class UserManagementService {
8
+ private readonly auth;
9
+ private readonly magicLinkService;
10
+ private readonly cryptoService?;
11
+ private static readonly DEFAULT_ORGANIZATION_ID;
12
+ private static readonly DEFAULT_ORGANIZATION_NAME;
13
+ private static readonly DEFAULT_ORGANIZATION_SLUG;
14
+ private readonly baseURL;
15
+ /**
16
+ * Role hierarchy permissions
17
+ * admin can invite: admin, editor, viewer
18
+ * editor can invite: editor, viewer
19
+ * viewer can invite: viewer
20
+ */
21
+ private static readonly roleHierarchy;
22
+ constructor(auth: Awaited<ReturnType<typeof createBetterAuthConfig>>, magicLinkService: MagicLinkService, cryptoService?: CryptoService | undefined);
23
+ addUserViaMagicLink(username: string): Promise<AddUserCommandResponse>;
24
+ listUsers(): Promise<Payload[]>;
25
+ private listUsersDirectly;
26
+ removeUser(userId: string): Promise<void>;
27
+ private removeUserDirectly;
28
+ addMemberToOrganization(req: ExpressRequest, role: Role): Promise<void>;
29
+ private ensureDefaultOrganization;
30
+ private createDefaultOrganizationForUser;
31
+ private addUserToOrganization;
32
+ getUsersForAdmin(): Promise<Array<{
33
+ id: string;
34
+ email: string;
35
+ name: string | null;
36
+ role: string;
37
+ createdAt: string;
38
+ updatedAt: string | null;
39
+ }>>;
40
+ getUserDetails(userId: string): Promise<{
41
+ id: string;
42
+ email: string;
43
+ name: string | null;
44
+ role: string;
45
+ createdAt: string;
46
+ updatedAt: string | null;
47
+ organizationId: string | null;
48
+ } | null>;
49
+ generateMagicLinkForUser(email: string, role: Role): Promise<string>;
50
+ updateUserName(userId: string, name: string): Promise<void>;
51
+ getUserRole(userId: string): Promise<string | null>;
52
+ private getDbInstance;
53
+ /**
54
+ * Check if current user role can invite target role
55
+ */
56
+ canInviteRole(currentUserRole: Role, targetRole: Role): boolean;
57
+ /**
58
+ * Get roles that current user can invite
59
+ */
60
+ getAllowedRolesForInvite(currentUserRole: Role): Role[];
61
+ /**
62
+ * Validate if role exists
63
+ */
64
+ isValidRole(role: string): role is Role;
65
+ /**
66
+ * Get role priority (higher number = more permissions)
67
+ */
68
+ getRolePriority(role: Role): number;
69
+ /**
70
+ * Check if current role has higher or equal priority than target role
71
+ */
72
+ hasHigherOrEqualPriority(currentRole: Role, targetRole: Role): boolean;
73
+ }
74
+ //# sourceMappingURL=user-management-service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"user-management-service.d.ts","sourceRoot":"","sources":["../../src/services/user-management-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,396 @@
1
+ export class UserManagementService {
2
+ auth;
3
+ magicLinkService;
4
+ cryptoService;
5
+ static DEFAULT_ORGANIZATION_ID = 'owox_data_marts_organization';
6
+ static DEFAULT_ORGANIZATION_NAME = 'OWOX Data Marts';
7
+ static DEFAULT_ORGANIZATION_SLUG = 'owox-data-marts';
8
+ baseURL;
9
+ /**
10
+ * Role hierarchy permissions
11
+ * admin can invite: admin, editor, viewer
12
+ * editor can invite: editor, viewer
13
+ * viewer can invite: viewer
14
+ */
15
+ static roleHierarchy = {
16
+ admin: ['admin', 'editor', 'viewer'],
17
+ editor: ['editor', 'viewer'],
18
+ viewer: ['viewer'],
19
+ };
20
+ constructor(auth, magicLinkService, cryptoService) {
21
+ this.auth = auth;
22
+ this.magicLinkService = magicLinkService;
23
+ this.cryptoService = cryptoService;
24
+ this.baseURL = this.auth.options.baseURL || 'http://localhost:3000';
25
+ }
26
+ async addUserViaMagicLink(username) {
27
+ try {
28
+ const magicLink = await this.magicLinkService.generateMagicLink(username);
29
+ return {
30
+ username,
31
+ magicLink,
32
+ };
33
+ }
34
+ catch (error) {
35
+ console.error('Error adding user:', error);
36
+ throw new Error(`Failed to add user: ${error instanceof Error ? error.message : 'Unknown error'}`);
37
+ }
38
+ }
39
+ async listUsers() {
40
+ try {
41
+ const listMembersRequest = new Request(`${this.baseURL}/auth/better-auth/organization/list-members`, {
42
+ method: 'POST',
43
+ headers: {
44
+ 'Content-Type': 'application/json',
45
+ },
46
+ body: JSON.stringify({
47
+ organizationId: UserManagementService.DEFAULT_ORGANIZATION_ID,
48
+ }),
49
+ });
50
+ try {
51
+ const response = await this.auth.handler(listMembersRequest);
52
+ if (response.ok) {
53
+ const result = await response.json();
54
+ const members = result.members || [];
55
+ return members.map((member) => ({
56
+ userId: member.user?.id || member.userId,
57
+ projectId: UserManagementService.DEFAULT_ORGANIZATION_ID,
58
+ email: member.user?.email || 'unknown@email.com',
59
+ fullName: member.user?.name || member.user?.email || 'Unknown User',
60
+ roles: ['editor'],
61
+ }));
62
+ }
63
+ else {
64
+ return await this.listUsersDirectly();
65
+ }
66
+ }
67
+ catch {
68
+ return await this.listUsersDirectly();
69
+ }
70
+ }
71
+ catch (error) {
72
+ console.error('Error listing users:', error);
73
+ throw new Error('Failed to list users');
74
+ }
75
+ }
76
+ async listUsersDirectly() {
77
+ try {
78
+ const db = this.auth.options.database;
79
+ if (!db || typeof db.prepare !== 'function') {
80
+ return [];
81
+ }
82
+ const stmt = db.prepare('SELECT id, email, name FROM user ORDER BY createdAt DESC');
83
+ const users = stmt.all();
84
+ return users.map((user) => ({
85
+ userId: user.id,
86
+ projectId: UserManagementService.DEFAULT_ORGANIZATION_ID,
87
+ email: user.email,
88
+ fullName: user.name || user.email,
89
+ roles: ['editor'],
90
+ }));
91
+ }
92
+ catch (error) {
93
+ console.error('Error listing users directly from database:', error);
94
+ return [];
95
+ }
96
+ }
97
+ async removeUser(userId) {
98
+ try {
99
+ // Step 1: Remove user from organization first
100
+ const removeMemberRequest = new Request(`${this.baseURL}/auth/better-auth/organization/remove-member`, {
101
+ method: 'POST',
102
+ headers: {
103
+ 'Content-Type': 'application/json',
104
+ },
105
+ body: JSON.stringify({
106
+ userId: userId,
107
+ organizationId: UserManagementService.DEFAULT_ORGANIZATION_ID,
108
+ }),
109
+ });
110
+ try {
111
+ await this.auth.handler(removeMemberRequest);
112
+ }
113
+ catch {
114
+ // Continue with user deletion even if organization removal fails
115
+ }
116
+ // Step 2: Remove user from Better Auth system
117
+ await this.removeUserDirectly(userId);
118
+ }
119
+ catch (error) {
120
+ console.error('Error removing user:', error);
121
+ throw new Error(`Failed to remove user: ${error instanceof Error ? error.message : 'Unknown error'}`);
122
+ }
123
+ }
124
+ async removeUserDirectly(userId) {
125
+ try {
126
+ const db = this.auth.options.database;
127
+ if (!db || typeof db.prepare !== 'function') {
128
+ console.error('Database adapter does not support direct operations');
129
+ throw new Error('Database adapter does not support direct operations');
130
+ }
131
+ // Clean up user's sessions
132
+ try {
133
+ const deleteSessionsStmt = db.prepare('DELETE FROM session WHERE userId = ?');
134
+ deleteSessionsStmt.run(userId);
135
+ }
136
+ catch {
137
+ // Session cleanup may not be needed
138
+ }
139
+ // Clean up user's accounts
140
+ try {
141
+ const deleteAccountsStmt = db.prepare('DELETE FROM account WHERE userId = ?');
142
+ deleteAccountsStmt.run(userId);
143
+ }
144
+ catch {
145
+ // Account cleanup may not be needed
146
+ }
147
+ // Clean up organization memberships
148
+ try {
149
+ const deleteMembershipsStmt = db.prepare('DELETE FROM member WHERE userId = ?');
150
+ deleteMembershipsStmt.run(userId);
151
+ }
152
+ catch {
153
+ // Organization membership cleanup may not be needed
154
+ }
155
+ // Finally, remove the user
156
+ const deleteUserStmt = db.prepare('DELETE FROM user WHERE id = ?');
157
+ const result = deleteUserStmt.run(userId);
158
+ if (result.changes === 0) {
159
+ console.error(`User ${userId} not found`);
160
+ throw new Error(`User ${userId} not found`);
161
+ }
162
+ }
163
+ catch (error) {
164
+ console.error(`Failed to delete user ${userId}:`, error);
165
+ throw new Error(`Failed to delete user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
166
+ }
167
+ }
168
+ async addMemberToOrganization(req, role) {
169
+ const session = await this.auth.api.getSession({
170
+ headers: req.headers,
171
+ });
172
+ if (!session) {
173
+ throw new Error('No session found');
174
+ }
175
+ if (!(await this.ensureDefaultOrganization())) {
176
+ await this.createDefaultOrganizationForUser(session.user.id, role);
177
+ }
178
+ else {
179
+ await this.addUserToOrganization(session.user.id, role);
180
+ }
181
+ }
182
+ async ensureDefaultOrganization() {
183
+ try {
184
+ const db = this.auth.options.database;
185
+ if (!db || typeof db.prepare !== 'function') {
186
+ console.error('Database adapter does not support direct operations for organization check');
187
+ throw new Error('Database adapter does not support direct operations for organization check');
188
+ }
189
+ const stmt = db.prepare('SELECT id FROM organization WHERE slug = ? LIMIT 1');
190
+ const existingOrg = stmt.get(UserManagementService.DEFAULT_ORGANIZATION_SLUG);
191
+ if (existingOrg) {
192
+ return true;
193
+ }
194
+ }
195
+ catch (error) {
196
+ console.error('Error checking default organization:', error);
197
+ throw new Error('Error checking default organization');
198
+ }
199
+ return false;
200
+ }
201
+ async createDefaultOrganizationForUser(userId, role) {
202
+ try {
203
+ const db = this.getDbInstance();
204
+ const orgId = UserManagementService.DEFAULT_ORGANIZATION_ID;
205
+ try {
206
+ const insertOrgStmt = db.prepare(`
207
+ INSERT INTO organization (id, name, slug, metadata, createdAt)
208
+ VALUES (?, ?, ?, ?, ?)
209
+ `);
210
+ const now = new Date().toISOString();
211
+ insertOrgStmt.run(orgId, UserManagementService.DEFAULT_ORGANIZATION_NAME, UserManagementService.DEFAULT_ORGANIZATION_SLUG, JSON.stringify({ isDefault: true, createdBy: userId }), now);
212
+ const insertMemberStmt = db.prepare(`
213
+ INSERT INTO member (id, organizationId, userId, role, createdAt)
214
+ VALUES (?, ?, ?, ?, ?)
215
+ `);
216
+ const memberId = `${orgId}_${userId}`;
217
+ insertMemberStmt.run(memberId, orgId, userId, role, now);
218
+ }
219
+ catch (dbError) {
220
+ if (String(dbError).includes('UNIQUE constraint failed') ||
221
+ String(dbError).includes('already exists')) {
222
+ await this.addUserToOrganization(userId, role);
223
+ }
224
+ else {
225
+ throw dbError;
226
+ }
227
+ }
228
+ }
229
+ catch (error) {
230
+ console.error('Error creating default organization for user:', error);
231
+ throw new Error('Error creating default organization for user');
232
+ }
233
+ }
234
+ async addUserToOrganization(userId, role) {
235
+ const db = this.getDbInstance();
236
+ try {
237
+ // First check if user already exists in organization
238
+ const checkStmt = db.prepare('SELECT role FROM member WHERE userId = ? AND organizationId = ?');
239
+ const existingMember = checkStmt.get(userId, UserManagementService.DEFAULT_ORGANIZATION_ID);
240
+ if (existingMember) {
241
+ // Update existing member role
242
+ const updateStmt = db.prepare('UPDATE member SET role = ? WHERE userId = ? AND organizationId = ?');
243
+ updateStmt.run(role, userId, UserManagementService.DEFAULT_ORGANIZATION_ID);
244
+ }
245
+ else {
246
+ // Insert new member
247
+ const insertMemberStmt = db.prepare(`
248
+ INSERT INTO member (id, organizationId, userId, role, createdAt)
249
+ VALUES (?, ?, ?, ?, ?)
250
+ `);
251
+ const memberId = `${UserManagementService.DEFAULT_ORGANIZATION_ID}_${userId}`;
252
+ const now = new Date().toISOString();
253
+ insertMemberStmt.run(memberId, UserManagementService.DEFAULT_ORGANIZATION_ID, userId, role, now);
254
+ }
255
+ }
256
+ catch (error) {
257
+ console.error('Error in addUserToOrganization:', error);
258
+ if (!String(error).includes('UNIQUE constraint failed')) {
259
+ throw error;
260
+ }
261
+ }
262
+ }
263
+ async getUsersForAdmin() {
264
+ try {
265
+ const db = this.getDbInstance();
266
+ const stmt = db.prepare(`
267
+ SELECT
268
+ u.id,
269
+ u.email,
270
+ u.name,
271
+ u.createdAt,
272
+ u.updatedAt,
273
+ COALESCE(m.role, 'viewer') as role
274
+ FROM user u
275
+ LEFT JOIN member m ON u.id = m.userId
276
+ ORDER BY u.createdAt DESC
277
+ `);
278
+ const users = stmt.all();
279
+ return users;
280
+ }
281
+ catch (error) {
282
+ console.error('Error getting users for admin:', error);
283
+ throw new Error('Failed to get users for admin');
284
+ }
285
+ }
286
+ async getUserDetails(userId) {
287
+ try {
288
+ const db = this.getDbInstance();
289
+ const stmt = db.prepare(`
290
+ SELECT
291
+ u.id,
292
+ u.email,
293
+ u.name,
294
+ u.createdAt,
295
+ u.updatedAt,
296
+ COALESCE(m.role, 'viewer') as role,
297
+ m.organizationId
298
+ FROM user u
299
+ LEFT JOIN member m ON u.id = m.userId
300
+ WHERE u.id = ?
301
+ `);
302
+ const user = stmt.get(userId);
303
+ return user || null;
304
+ }
305
+ catch (error) {
306
+ console.error('Error getting user details:', error);
307
+ throw new Error('Failed to get user details');
308
+ }
309
+ }
310
+ async generateMagicLinkForUser(email, role) {
311
+ try {
312
+ if (!this.cryptoService) {
313
+ throw new Error('CryptoService not available for magic link generation');
314
+ }
315
+ const magicLink = await this.magicLinkService.generateMagicLink(email, role);
316
+ return magicLink;
317
+ }
318
+ catch (error) {
319
+ console.error('Error generating magic link for user:', error);
320
+ throw new Error('Failed to generate magic link');
321
+ }
322
+ }
323
+ async updateUserName(userId, name) {
324
+ try {
325
+ const db = this.getDbInstance();
326
+ const updateStmt = db.prepare('UPDATE user SET name = ? WHERE id = ?');
327
+ const result = updateStmt.run(name, userId);
328
+ if (result.changes === 0) {
329
+ throw new Error(`User ${userId} not found or name not updated`);
330
+ }
331
+ }
332
+ catch (error) {
333
+ console.error('Error updating user name:', error);
334
+ throw new Error('Failed to update user name');
335
+ }
336
+ }
337
+ async getUserRole(userId) {
338
+ try {
339
+ const db = this.getDbInstance();
340
+ const stmt = db.prepare(`
341
+ SELECT role FROM member
342
+ WHERE userId = ? AND organizationId = ?
343
+ `);
344
+ const member = stmt.get(userId, UserManagementService.DEFAULT_ORGANIZATION_ID);
345
+ return member ? member.role : null;
346
+ }
347
+ catch (error) {
348
+ console.error('Failed to get user role:', error);
349
+ throw new Error('Failed to get user role');
350
+ }
351
+ }
352
+ getDbInstance() {
353
+ const db = this.auth.options.database;
354
+ if (!db || typeof db.prepare !== 'function') {
355
+ throw new Error('Database adapter does not support direct operations');
356
+ }
357
+ return db;
358
+ }
359
+ // ========== Role Permission Methods ==========
360
+ /**
361
+ * Check if current user role can invite target role
362
+ */
363
+ canInviteRole(currentUserRole, targetRole) {
364
+ const allowedRoles = UserManagementService.roleHierarchy[currentUserRole];
365
+ return allowedRoles.includes(targetRole);
366
+ }
367
+ /**
368
+ * Get roles that current user can invite
369
+ */
370
+ getAllowedRolesForInvite(currentUserRole) {
371
+ return UserManagementService.roleHierarchy[currentUserRole];
372
+ }
373
+ /**
374
+ * Validate if role exists
375
+ */
376
+ isValidRole(role) {
377
+ return ['admin', 'editor', 'viewer'].includes(role);
378
+ }
379
+ /**
380
+ * Get role priority (higher number = more permissions)
381
+ */
382
+ getRolePriority(role) {
383
+ const priorities = {
384
+ admin: 3,
385
+ editor: 2,
386
+ viewer: 1,
387
+ };
388
+ return priorities[role];
389
+ }
390
+ /**
391
+ * Check if current role has higher or equal priority than target role
392
+ */
393
+ hasHigherOrEqualPriority(currentRole, targetRole) {
394
+ return this.getRolePriority(currentRole) >= this.getRolePriority(targetRole);
395
+ }
396
+ }