@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,199 @@
|
|
|
1
|
+
export class AuthenticationService {
|
|
2
|
+
auth;
|
|
3
|
+
cryptoService;
|
|
4
|
+
userManagementService;
|
|
5
|
+
constructor(auth, cryptoService) {
|
|
6
|
+
this.auth = auth;
|
|
7
|
+
this.cryptoService = cryptoService;
|
|
8
|
+
}
|
|
9
|
+
setUserManagementService(userManagementService) {
|
|
10
|
+
this.userManagementService = userManagementService;
|
|
11
|
+
}
|
|
12
|
+
async getSession(req) {
|
|
13
|
+
try {
|
|
14
|
+
const session = await this.auth.api.getSession({
|
|
15
|
+
headers: req.headers,
|
|
16
|
+
});
|
|
17
|
+
if (!session || !session.user || !session.session) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
user: {
|
|
22
|
+
id: session.user.id,
|
|
23
|
+
email: session.user.email,
|
|
24
|
+
name: session.user.name,
|
|
25
|
+
},
|
|
26
|
+
session: {
|
|
27
|
+
id: session.session.id,
|
|
28
|
+
userId: session.session.userId,
|
|
29
|
+
token: session.session.token,
|
|
30
|
+
expiresAt: session.session.expiresAt,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
console.error('Failed to get session:', error);
|
|
36
|
+
throw new Error('Failed to get session');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async signIn(email, password, protocol, host) {
|
|
40
|
+
try {
|
|
41
|
+
const url = `${protocol}://${host}/auth/better-auth/sign-in/email`;
|
|
42
|
+
const headers = new Headers();
|
|
43
|
+
headers.set('Content-Type', 'application/json');
|
|
44
|
+
const request = new Request(url, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers,
|
|
47
|
+
body: JSON.stringify({ email, password }),
|
|
48
|
+
});
|
|
49
|
+
const response = await this.auth.handler(request);
|
|
50
|
+
return response;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
console.error('Sign-in failed:', error);
|
|
54
|
+
throw new Error('Sign-in failed');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async signOut(req) {
|
|
58
|
+
try {
|
|
59
|
+
await this.auth.api.signOut({
|
|
60
|
+
headers: req.headers,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
console.error('Sign-out failed:', error);
|
|
65
|
+
throw new Error('Sign-out failed');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async generateAccessToken(req) {
|
|
69
|
+
try {
|
|
70
|
+
const session = await this.getSession(req);
|
|
71
|
+
if (!session) {
|
|
72
|
+
console.error('No session found for access token generation');
|
|
73
|
+
throw new Error('No session found');
|
|
74
|
+
}
|
|
75
|
+
const cookies = req.headers.cookie || '';
|
|
76
|
+
const sessionTokenMatch = cookies.match(/refreshToken=([^;]+)/);
|
|
77
|
+
const sessionToken = sessionTokenMatch && sessionTokenMatch[1] ? decodeURIComponent(sessionTokenMatch[1]) : null;
|
|
78
|
+
if (!sessionToken) {
|
|
79
|
+
console.error('No session token found in cookies');
|
|
80
|
+
throw new Error('No session token found');
|
|
81
|
+
}
|
|
82
|
+
return await this.cryptoService.encrypt(sessionToken);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
console.error('Failed to generate access token:', error);
|
|
86
|
+
throw new Error('Failed to generate access token');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async validateSession(req) {
|
|
90
|
+
try {
|
|
91
|
+
const session = await this.getSession(req);
|
|
92
|
+
if (!session) {
|
|
93
|
+
return {
|
|
94
|
+
isValid: false,
|
|
95
|
+
error: 'No valid session found',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
isValid: true,
|
|
100
|
+
session,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
return {
|
|
105
|
+
isValid: false,
|
|
106
|
+
error: error instanceof Error ? error.message : 'Session validation failed',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async signInMiddleware(req, res, _next) {
|
|
111
|
+
try {
|
|
112
|
+
const { email, password } = req.body;
|
|
113
|
+
if (!email || !password) {
|
|
114
|
+
return res.status(400).json({ error: 'Email and password are required' });
|
|
115
|
+
}
|
|
116
|
+
const response = await this.signIn(email, password, req.protocol, req.get('host') || 'localhost');
|
|
117
|
+
if (response.ok) {
|
|
118
|
+
response.headers.forEach((value, key) => {
|
|
119
|
+
res.set(key, value);
|
|
120
|
+
});
|
|
121
|
+
return res.redirect('/');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Redirect back to sign-in page with error message
|
|
125
|
+
const errorMessage = response.status === 401
|
|
126
|
+
? 'Invalid email or password. Please try again.'
|
|
127
|
+
: 'Sign in failed. Please try again.';
|
|
128
|
+
return res.redirect(`/auth/sign-in?error=${encodeURIComponent(errorMessage)}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
console.error('Sign-in middleware error:', error);
|
|
133
|
+
const errorMessage = 'An error occurred during sign in. Please try again.';
|
|
134
|
+
return res.redirect(`/auth/sign-in?error=${encodeURIComponent(errorMessage)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async signOutMiddleware(req, res, _next) {
|
|
138
|
+
try {
|
|
139
|
+
await this.signOut(req);
|
|
140
|
+
res.clearCookie('refreshToken');
|
|
141
|
+
res.clearCookie('better-auth.csrf_token');
|
|
142
|
+
const redirectPath = req.query.redirect || '/auth/sign-in';
|
|
143
|
+
return res.redirect(redirectPath);
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
console.error('Sign-out middleware error:', error);
|
|
147
|
+
return res.status(500).json({ error: 'Sign-out failed' });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async accessTokenMiddleware(req, res, _next) {
|
|
151
|
+
try {
|
|
152
|
+
const validation = await this.validateSession(req);
|
|
153
|
+
if (!validation.isValid) {
|
|
154
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
155
|
+
}
|
|
156
|
+
const accessToken = await this.generateAccessToken(req);
|
|
157
|
+
return res.json({ accessToken });
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
console.error('Access token middleware error:', error);
|
|
161
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async setPassword(password, req) {
|
|
165
|
+
try {
|
|
166
|
+
return await this.auth.api.setPassword({
|
|
167
|
+
body: {
|
|
168
|
+
newPassword: password,
|
|
169
|
+
},
|
|
170
|
+
headers: req.headers,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
// Don't log error if user already has a password - this is expected behavior
|
|
175
|
+
if (error && typeof error === 'object' && 'body' in error) {
|
|
176
|
+
const apiError = error;
|
|
177
|
+
if (apiError.body?.code === 'USER_ALREADY_HAS_A_PASSWORD') {
|
|
178
|
+
throw new Error('User already has a password');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.error('Failed to set password:', error);
|
|
182
|
+
throw new Error('Failed to set password');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async requireAuthMiddleware(req, res, next) {
|
|
186
|
+
try {
|
|
187
|
+
const session = await this.getSession(req);
|
|
188
|
+
if (!session || !session.user) {
|
|
189
|
+
const currentPath = encodeURIComponent(req.originalUrl || req.url);
|
|
190
|
+
return res.redirect(`/auth/sign-in?redirect=${currentPath}`);
|
|
191
|
+
}
|
|
192
|
+
next();
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('Authentication middleware error:', error);
|
|
196
|
+
return res.redirect('/auth/sign-in');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createBetterAuthConfig } from '../auth/auth-config.js';
|
|
2
|
+
export declare class CryptoServiceError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class CryptoService {
|
|
6
|
+
private readonly secret;
|
|
7
|
+
private readonly algorithm;
|
|
8
|
+
private readonly expiresIn;
|
|
9
|
+
private readonly aesAlgorithm;
|
|
10
|
+
private readonly issuer;
|
|
11
|
+
constructor(auth: Awaited<ReturnType<typeof createBetterAuthConfig>>);
|
|
12
|
+
private deriveKey;
|
|
13
|
+
private encryptData;
|
|
14
|
+
private decryptData;
|
|
15
|
+
encrypt(data: string): Promise<string>;
|
|
16
|
+
decrypt(token: string): Promise<string>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=crypto-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crypto-service.d.ts","sourceRoot":"","sources":["../../src/services/crypto-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAKhE,qBAAa,kBAAmB,SAAQ,KAAK;gBAC/B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;IAC1C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;YAQtD,SAAS;YAKT,WAAW;YAaX,WAAW;IA4BnB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBtC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAiC9C"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from 'crypto';
|
|
3
|
+
import { promisify } from 'util';
|
|
4
|
+
export class CryptoServiceError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = 'CryptoServiceError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class CryptoService {
|
|
11
|
+
secret;
|
|
12
|
+
algorithm;
|
|
13
|
+
expiresIn;
|
|
14
|
+
aesAlgorithm;
|
|
15
|
+
issuer;
|
|
16
|
+
constructor(auth) {
|
|
17
|
+
this.secret = auth.options.secret || 'default-secret';
|
|
18
|
+
this.algorithm = 'HS256';
|
|
19
|
+
this.expiresIn = 3600;
|
|
20
|
+
this.aesAlgorithm = 'aes-256-cbc';
|
|
21
|
+
this.issuer = 'idp-better-auth';
|
|
22
|
+
}
|
|
23
|
+
async deriveKey(salt) {
|
|
24
|
+
const scryptAsync = promisify(scrypt);
|
|
25
|
+
return scryptAsync(this.secret, salt, 32);
|
|
26
|
+
}
|
|
27
|
+
async encryptData(data) {
|
|
28
|
+
const salt = randomBytes(16);
|
|
29
|
+
const iv = randomBytes(16);
|
|
30
|
+
const key = await this.deriveKey(salt);
|
|
31
|
+
const cipher = createCipheriv(this.aesAlgorithm, key, iv);
|
|
32
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
33
|
+
encrypted += cipher.final('hex');
|
|
34
|
+
const result = salt.toString('hex') + ':' + iv.toString('hex') + ':' + encrypted;
|
|
35
|
+
return Buffer.from(result).toString('base64');
|
|
36
|
+
}
|
|
37
|
+
async decryptData(encryptedData) {
|
|
38
|
+
const data = Buffer.from(encryptedData, 'base64').toString('utf8');
|
|
39
|
+
const parts = data.split(':');
|
|
40
|
+
if (parts.length !== 3) {
|
|
41
|
+
throw new CryptoServiceError('Invalid encrypted data format');
|
|
42
|
+
}
|
|
43
|
+
const saltHex = parts[0];
|
|
44
|
+
const ivHex = parts[1];
|
|
45
|
+
const encrypted = parts[2];
|
|
46
|
+
if (!saltHex || !ivHex || !encrypted) {
|
|
47
|
+
throw new CryptoServiceError('Invalid encrypted data format - missing components');
|
|
48
|
+
}
|
|
49
|
+
const salt = Buffer.from(saltHex, 'hex');
|
|
50
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
51
|
+
const key = await this.deriveKey(salt);
|
|
52
|
+
const decipher = createDecipheriv(this.aesAlgorithm, key, iv);
|
|
53
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
54
|
+
decrypted += decipher.final('utf8');
|
|
55
|
+
return decrypted;
|
|
56
|
+
}
|
|
57
|
+
async encrypt(data) {
|
|
58
|
+
try {
|
|
59
|
+
const encryptedData = await this.encryptData(data);
|
|
60
|
+
const payload = { payload: encryptedData };
|
|
61
|
+
const options = {
|
|
62
|
+
algorithm: this.algorithm,
|
|
63
|
+
expiresIn: this.expiresIn,
|
|
64
|
+
issuer: this.issuer,
|
|
65
|
+
};
|
|
66
|
+
const token = jwt.sign(payload, this.secret, options);
|
|
67
|
+
return token;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
throw new CryptoServiceError(`Encryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async decrypt(token) {
|
|
74
|
+
try {
|
|
75
|
+
const decoded = jwt.verify(token, this.secret, {
|
|
76
|
+
algorithms: [this.algorithm],
|
|
77
|
+
});
|
|
78
|
+
if (!decoded || !decoded.exp || decoded.exp < Date.now() / 1000) {
|
|
79
|
+
throw new CryptoServiceError('Token expired');
|
|
80
|
+
}
|
|
81
|
+
if (!decoded.payload || typeof decoded.payload !== 'string') {
|
|
82
|
+
throw new CryptoServiceError('Invalid token payload - missing or invalid encrypted data');
|
|
83
|
+
}
|
|
84
|
+
if (decoded.iss !== this.issuer) {
|
|
85
|
+
throw new CryptoServiceError('Invalid token issuer');
|
|
86
|
+
}
|
|
87
|
+
const decryptedData = await this.decryptData(decoded.payload);
|
|
88
|
+
return decryptedData;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof jwt.JsonWebTokenError) {
|
|
92
|
+
throw new CryptoServiceError(`Decryption failed: Invalid token`);
|
|
93
|
+
}
|
|
94
|
+
if (error instanceof CryptoServiceError) {
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
throw new CryptoServiceError(`Decryption failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createBetterAuthConfig } from '../auth/auth-config.js';
|
|
2
|
+
import { DatabaseUser, DatabaseOperationResult } from '../types/database-models.js';
|
|
3
|
+
export declare class DatabaseService {
|
|
4
|
+
private readonly auth;
|
|
5
|
+
constructor(auth: Awaited<ReturnType<typeof createBetterAuthConfig>>);
|
|
6
|
+
runMigrations(): Promise<void>;
|
|
7
|
+
getUsersDirectly(): Promise<DatabaseUser[]>;
|
|
8
|
+
getUserByIdDirectly(userId: string): Promise<DatabaseUser | null>;
|
|
9
|
+
removeUserDirectly(userId: string): Promise<DatabaseOperationResult>;
|
|
10
|
+
getUserCount(): Promise<number>;
|
|
11
|
+
cleanupExpiredSessions(): Promise<DatabaseOperationResult>;
|
|
12
|
+
isDatabaseHealthy(): Promise<boolean>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=database-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database-service.d.ts","sourceRoot":"","sources":["../../src/services/database-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AAEhE,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,6BAA6B,CAAC;AAEpF,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,sBAAsB,CAAC,CAAC;IAE/E,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAc9B,gBAAgB,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IAuB3C,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IAqBjE,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAmDpE,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAqB/B,sBAAsB,IAAI,OAAO,CAAC,uBAAuB,CAAC;IAqB1D,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC;CAiB5C"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { getMigrations } from 'better-auth/db';
|
|
2
|
+
export class DatabaseService {
|
|
3
|
+
auth;
|
|
4
|
+
constructor(auth) {
|
|
5
|
+
this.auth = auth;
|
|
6
|
+
}
|
|
7
|
+
async runMigrations() {
|
|
8
|
+
try {
|
|
9
|
+
const { runMigrations, compileMigrations } = await getMigrations(this.auth.options);
|
|
10
|
+
await runMigrations();
|
|
11
|
+
await compileMigrations();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.error('Database migration failed:', error);
|
|
15
|
+
throw new Error(`Database migration failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
async getUsersDirectly() {
|
|
19
|
+
try {
|
|
20
|
+
const db = this.auth.options.database;
|
|
21
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
22
|
+
console.error('Database adapter does not support direct operations');
|
|
23
|
+
throw new Error('Database adapter does not support direct operations');
|
|
24
|
+
}
|
|
25
|
+
const stmt = db.prepare('SELECT id, email, name, createdAt FROM user ORDER BY createdAt DESC');
|
|
26
|
+
const users = stmt.all();
|
|
27
|
+
return users;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error('Error getting users directly from database:', error);
|
|
31
|
+
throw new Error(`Failed to get users from database: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async getUserByIdDirectly(userId) {
|
|
35
|
+
try {
|
|
36
|
+
const db = this.auth.options.database;
|
|
37
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
38
|
+
console.error('Database adapter does not support direct operations');
|
|
39
|
+
throw new Error('Database adapter does not support direct operations');
|
|
40
|
+
}
|
|
41
|
+
const stmt = db.prepare('SELECT id, email, name, createdAt FROM user WHERE id = ?');
|
|
42
|
+
const user = stmt.get(userId);
|
|
43
|
+
return user || null;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error('Error getting user by ID from database:', error);
|
|
47
|
+
throw new Error(`Failed to get user from database: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async removeUserDirectly(userId) {
|
|
51
|
+
try {
|
|
52
|
+
const db = this.auth.options.database;
|
|
53
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
54
|
+
console.error('Database adapter does not support direct operations');
|
|
55
|
+
throw new Error('Database adapter does not support direct operations');
|
|
56
|
+
}
|
|
57
|
+
// Clean up user's sessions
|
|
58
|
+
try {
|
|
59
|
+
const deleteSessionsStmt = db.prepare('DELETE FROM session WHERE userId = ?');
|
|
60
|
+
deleteSessionsStmt.run(userId);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Session cleanup may not be needed
|
|
64
|
+
}
|
|
65
|
+
// Clean up user's accounts
|
|
66
|
+
try {
|
|
67
|
+
const deleteAccountsStmt = db.prepare('DELETE FROM account WHERE userId = ?');
|
|
68
|
+
deleteAccountsStmt.run(userId);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Account cleanup may not be needed
|
|
72
|
+
}
|
|
73
|
+
// Clean up organization memberships
|
|
74
|
+
try {
|
|
75
|
+
const deleteMembershipsStmt = db.prepare('DELETE FROM member WHERE userId = ?');
|
|
76
|
+
deleteMembershipsStmt.run(userId);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Organization membership cleanup may not be needed
|
|
80
|
+
}
|
|
81
|
+
// Finally, remove the user
|
|
82
|
+
const deleteUserStmt = db.prepare('DELETE FROM user WHERE id = ?');
|
|
83
|
+
const result = deleteUserStmt.run(userId);
|
|
84
|
+
if (result.changes === 0) {
|
|
85
|
+
console.error(`User ${userId} not found in database`);
|
|
86
|
+
throw new Error(`User ${userId} not found`);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error(`Failed to delete user ${userId} from database:`, error);
|
|
92
|
+
throw new Error(`Failed to delete user ${userId}: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async getUserCount() {
|
|
96
|
+
try {
|
|
97
|
+
const db = this.auth.options.database;
|
|
98
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
99
|
+
console.error('Database adapter does not support direct operations');
|
|
100
|
+
throw new Error('Database adapter does not support direct operations');
|
|
101
|
+
}
|
|
102
|
+
const stmt = db.prepare('SELECT COUNT(*) as count FROM user');
|
|
103
|
+
const result = stmt.get();
|
|
104
|
+
return result.count;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
console.error('Error getting user count from database:', error);
|
|
108
|
+
throw new Error(`Failed to get user count: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async cleanupExpiredSessions() {
|
|
112
|
+
try {
|
|
113
|
+
const db = this.auth.options.database;
|
|
114
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
115
|
+
console.error('Database adapter does not support direct operations');
|
|
116
|
+
throw new Error('Database adapter does not support direct operations');
|
|
117
|
+
}
|
|
118
|
+
const stmt = db.prepare('DELETE FROM session WHERE expiresAt < datetime("now")');
|
|
119
|
+
const result = stmt.run();
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.error('Error cleaning up expired sessions:', error);
|
|
124
|
+
throw new Error(`Failed to cleanup expired sessions: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async isDatabaseHealthy() {
|
|
128
|
+
try {
|
|
129
|
+
const db = this.auth.options.database;
|
|
130
|
+
if (!db || typeof db.prepare !== 'function') {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
// Simple health check - try to query the user table
|
|
134
|
+
const stmt = db.prepare('SELECT 1 FROM user LIMIT 1');
|
|
135
|
+
stmt.get();
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { betterAuth } from 'better-auth';
|
|
2
|
+
import { CryptoService } from './crypto-service.js';
|
|
3
|
+
export declare class MagicLinkService {
|
|
4
|
+
private readonly auth;
|
|
5
|
+
private readonly cryptoService;
|
|
6
|
+
private static readonly DEFAULT_CALLBACK_URL;
|
|
7
|
+
constructor(auth: Awaited<ReturnType<typeof betterAuth>>, cryptoService: CryptoService);
|
|
8
|
+
generateMagicLink(email: string, role?: 'admin' | 'editor' | 'viewer'): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=magic-link-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magic-link-service.d.ts","sourceRoot":"","sources":["../../src/services/magic-link-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEpD,qBAAa,gBAAgB;IAIzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAJhC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAA8B;gBAGvD,IAAI,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,EAC5C,aAAa,EAAE,aAAa;IAGzC,iBAAiB,CACrB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,OAAO,GAAG,QAAQ,GAAG,QAAkB,GAC5C,OAAO,CAAC,MAAM,CAAC;CAyCnB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class MagicLinkService {
|
|
2
|
+
auth;
|
|
3
|
+
cryptoService;
|
|
4
|
+
static DEFAULT_CALLBACK_URL = '/auth/magic-link-success';
|
|
5
|
+
constructor(auth, cryptoService) {
|
|
6
|
+
this.auth = auth;
|
|
7
|
+
this.cryptoService = cryptoService;
|
|
8
|
+
}
|
|
9
|
+
async generateMagicLink(email, role = 'admin') {
|
|
10
|
+
// Clear previous magic link
|
|
11
|
+
delete global.lastMagicLink;
|
|
12
|
+
// Generate magic link directly through Better Auth internals
|
|
13
|
+
const baseURL = this.auth.options.baseURL || 'http://localhost:3000';
|
|
14
|
+
const encodedRole = await this.cryptoService.encrypt(role);
|
|
15
|
+
const callbackURL = `${baseURL}${MagicLinkService.DEFAULT_CALLBACK_URL}?role=${encodedRole}`;
|
|
16
|
+
// Create a mock Request object for Better Auth
|
|
17
|
+
const mockRequest = new Request(`${baseURL}/auth/better-auth/sign-in/magic-link`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
body: JSON.stringify({
|
|
23
|
+
email: email,
|
|
24
|
+
callbackURL: callbackURL,
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
// Call Better Auth handler directly
|
|
28
|
+
const response = await this.auth.handler(mockRequest);
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
const errorText = await response.text();
|
|
31
|
+
throw new Error(`Magic link generation failed: ${response.status} ${errorText}`);
|
|
32
|
+
}
|
|
33
|
+
// Wait a moment for the sendMagicLink callback to be called
|
|
34
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
35
|
+
// Get the generated magic link from global storage
|
|
36
|
+
const magicLink = global.lastMagicLink;
|
|
37
|
+
if (!magicLink) {
|
|
38
|
+
throw new Error('Magic link generation failed - no link received');
|
|
39
|
+
}
|
|
40
|
+
return magicLink;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Request, type Response, type NextFunction } from 'express';
|
|
2
|
+
import { Payload } from '@owox/idp-protocol';
|
|
3
|
+
import { AuthenticationService } from './authentication-service.js';
|
|
4
|
+
import { PageService } from './page-service.js';
|
|
5
|
+
export declare class MiddlewareService {
|
|
6
|
+
private readonly authenticationService;
|
|
7
|
+
private readonly pageService;
|
|
8
|
+
private static readonly DEFAULT_ORGANIZATION_ID;
|
|
9
|
+
constructor(authenticationService: AuthenticationService, pageService: PageService);
|
|
10
|
+
signInMiddleware(req: Request, res: Response, _next: NextFunction): Promise<void | Response>;
|
|
11
|
+
signOutMiddleware(req: Request, res: Response, next: NextFunction): Promise<void | Response>;
|
|
12
|
+
accessTokenMiddleware(req: Request, res: Response, next: NextFunction): Promise<void | Response>;
|
|
13
|
+
userApiMiddleware(req: Request, res: Response, _next: NextFunction): Promise<Response<Payload>>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=middleware-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware-service.d.ts","sourceRoot":"","sources":["../../src/services/middleware-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,YAAY,EAAE,MAAM,SAAS,CAAC;AACzE,OAAO,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAC7C,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,qBAAa,iBAAiB;IAI1B,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IACtC,OAAO,CAAC,QAAQ,CAAC,WAAW;IAJ9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAkB;gBAG9C,qBAAqB,EAAE,qBAAqB,EAC5C,WAAW,EAAE,WAAW;IAGrC,gBAAgB,CACpB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;IAIrB,iBAAiB,CACrB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;IAIrB,qBAAqB,CACzB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,GACjB,OAAO,CAAC,IAAI,GAAG,QAAQ,CAAC;IAIrB,iBAAiB,CACrB,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,QAAQ,EACb,KAAK,EAAE,YAAY,GAClB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;CAsB9B"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export class MiddlewareService {
|
|
2
|
+
authenticationService;
|
|
3
|
+
pageService;
|
|
4
|
+
static DEFAULT_ORGANIZATION_ID = 'owox-default';
|
|
5
|
+
constructor(authenticationService, pageService) {
|
|
6
|
+
this.authenticationService = authenticationService;
|
|
7
|
+
this.pageService = pageService;
|
|
8
|
+
}
|
|
9
|
+
async signInMiddleware(req, res, _next) {
|
|
10
|
+
return this.pageService.signInPage.bind(this.pageService)(req, res);
|
|
11
|
+
}
|
|
12
|
+
async signOutMiddleware(req, res, next) {
|
|
13
|
+
return this.authenticationService.signOutMiddleware(req, res, next);
|
|
14
|
+
}
|
|
15
|
+
async accessTokenMiddleware(req, res, next) {
|
|
16
|
+
return this.authenticationService.accessTokenMiddleware(req, res, next);
|
|
17
|
+
}
|
|
18
|
+
async userApiMiddleware(req, res, _next) {
|
|
19
|
+
try {
|
|
20
|
+
const validation = await this.authenticationService.validateSession(req);
|
|
21
|
+
if (!validation.isValid || !validation.session) {
|
|
22
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
23
|
+
}
|
|
24
|
+
const payload = {
|
|
25
|
+
userId: validation.session.user.id,
|
|
26
|
+
projectId: MiddlewareService.DEFAULT_ORGANIZATION_ID,
|
|
27
|
+
email: validation.session.user.email,
|
|
28
|
+
fullName: validation.session.user.name || validation.session.user.email,
|
|
29
|
+
roles: ['editor'],
|
|
30
|
+
};
|
|
31
|
+
return res.json(payload);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error('User API middleware error:', error);
|
|
35
|
+
return res.status(401).json({ error: 'Unauthorized' });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type Express, type Request as ExpressRequest, type Response as ExpressResponse } from 'express';
|
|
2
|
+
import { AuthenticationService } from './authentication-service.js';
|
|
3
|
+
import { UserManagementService } from './user-management-service.js';
|
|
4
|
+
import { CryptoService } from './crypto-service.js';
|
|
5
|
+
export declare class PageService {
|
|
6
|
+
private readonly authenticationService;
|
|
7
|
+
private readonly userManagementService;
|
|
8
|
+
private readonly cryptoService;
|
|
9
|
+
constructor(authenticationService: AuthenticationService, userManagementService: UserManagementService, cryptoService: CryptoService);
|
|
10
|
+
signInPage(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
11
|
+
setupPasswordPage(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
12
|
+
setPassword(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
13
|
+
magicLinkSuccess(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
14
|
+
registerRoutes(express: Express): void;
|
|
15
|
+
adminDashboard(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
16
|
+
adminUserDetails(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
17
|
+
adminAddUserPage(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
18
|
+
adminAddUser(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
19
|
+
adminDeleteUser(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
20
|
+
adminGenerateMagicLink(req: ExpressRequest, res: ExpressResponse): Promise<void>;
|
|
21
|
+
private generateNameFromEmail;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=page-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-service.d.ts","sourceRoot":"","sources":["../../src/services/page-service.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,OAAO,IAAI,cAAc,EAC9B,KAAK,QAAQ,IAAI,eAAe,EACjC,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAEpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IACtC,OAAO,CAAC,QAAQ,CAAC,qBAAqB;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa;gBAFb,qBAAqB,EAAE,qBAAqB,EAC5C,qBAAqB,EAAE,qBAAqB,EAC5C,aAAa,EAAE,aAAa;IAGzC,UAAU,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAepE,iBAAiB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB3E,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCrE,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAgDhF,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IA6ChC,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAcxE,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B1E,gBAAgB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA6B1E,YAAY,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAqDtE,eAAe,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BzE,sBAAsB,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAsDtF,OAAO,CAAC,qBAAqB;CAoB9B"}
|