@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.
- package/README.md +183 -0
- package/dist/adapters/database.d.ts +29 -0
- package/dist/adapters/database.d.ts.map +1 -0
- package/dist/adapters/database.js +85 -0
- package/dist/auth/auth-config.d.ts +4 -0
- package/dist/auth/auth-config.d.ts.map +1 -0
- package/dist/auth/auth-config.js +72 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/providers/better-auth-provider.d.ts +34 -0
- package/dist/providers/better-auth-provider.d.ts.map +1 -0
- package/dist/providers/better-auth-provider.js +99 -0
- package/dist/services/authentication-service.d.ts +23 -0
- package/dist/services/authentication-service.d.ts.map +1 -0
- package/dist/services/authentication-service.js +199 -0
- package/dist/services/crypto-service.d.ts +18 -0
- package/dist/services/crypto-service.d.ts.map +1 -0
- package/dist/services/crypto-service.js +100 -0
- package/dist/services/database-service.d.ts +14 -0
- package/dist/services/database-service.d.ts.map +1 -0
- package/dist/services/database-service.js +142 -0
- package/dist/services/magic-link-service.d.ts +10 -0
- package/dist/services/magic-link-service.d.ts.map +1 -0
- package/dist/services/magic-link-service.js +42 -0
- package/dist/services/middleware-service.d.ts +15 -0
- package/dist/services/middleware-service.d.ts.map +1 -0
- package/dist/services/middleware-service.js +38 -0
- package/dist/services/page-service.d.ts +23 -0
- package/dist/services/page-service.d.ts.map +1 -0
- package/dist/services/page-service.js +331 -0
- package/dist/services/request-handler-service.d.ts +10 -0
- package/dist/services/request-handler-service.d.ts.map +1 -0
- package/dist/services/request-handler-service.js +50 -0
- package/dist/services/template-service.d.ts +28 -0
- package/dist/services/template-service.d.ts.map +1 -0
- package/dist/services/template-service.js +198 -0
- package/dist/services/token-service.d.ts +15 -0
- package/dist/services/token-service.d.ts.map +1 -0
- package/dist/services/token-service.js +108 -0
- package/dist/services/user-management-service.d.ts +74 -0
- package/dist/services/user-management-service.d.ts.map +1 -0
- package/dist/services/user-management-service.js +396 -0
- package/dist/templates/admin-add-user.html +298 -0
- package/dist/templates/admin-dashboard.html +214 -0
- package/dist/templates/admin-user-details.html +278 -0
- package/dist/templates/password-setup.html +267 -0
- package/dist/templates/password-success.html +197 -0
- package/dist/templates/sign-in.html +201 -0
- package/dist/types/auth-session.d.ts +25 -0
- package/dist/types/auth-session.d.ts.map +1 -0
- package/dist/types/auth-session.js +1 -0
- package/dist/types/database-models.d.ts +54 -0
- package/dist/types/database-models.d.ts.map +1 -0
- package/dist/types/database-models.js +1 -0
- package/dist/types/index.d.ts +45 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- 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
|
+
}
|