archicore 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -4
- package/dist/cli/commands/interactive.js +83 -23
- package/dist/cli/commands/projects.js +3 -3
- package/dist/cli/ui/prompt.d.ts +4 -0
- package/dist/cli/ui/prompt.js +22 -0
- package/dist/cli/utils/config.js +2 -2
- package/dist/cli/utils/upload-utils.js +65 -18
- package/dist/code-index/ast-parser.d.ts +4 -0
- package/dist/code-index/ast-parser.js +42 -0
- package/dist/code-index/index.d.ts +21 -1
- package/dist/code-index/index.js +45 -1
- package/dist/code-index/source-map-extractor.d.ts +71 -0
- package/dist/code-index/source-map-extractor.js +194 -0
- package/dist/gitlab/gitlab-service.d.ts +162 -0
- package/dist/gitlab/gitlab-service.js +652 -0
- package/dist/gitlab/index.d.ts +8 -0
- package/dist/gitlab/index.js +8 -0
- package/dist/server/config/passport.d.ts +14 -0
- package/dist/server/config/passport.js +86 -0
- package/dist/server/index.js +52 -10
- package/dist/server/middleware/api-auth.d.ts +2 -2
- package/dist/server/middleware/api-auth.js +21 -2
- package/dist/server/middleware/csrf.d.ts +23 -0
- package/dist/server/middleware/csrf.js +96 -0
- package/dist/server/routes/auth.d.ts +2 -2
- package/dist/server/routes/auth.js +204 -5
- package/dist/server/routes/device-auth.js +2 -2
- package/dist/server/routes/gitlab.d.ts +12 -0
- package/dist/server/routes/gitlab.js +528 -0
- package/dist/server/routes/oauth.d.ts +6 -0
- package/dist/server/routes/oauth.js +198 -0
- package/dist/server/services/audit-service.d.ts +1 -1
- package/dist/server/services/auth-service.d.ts +13 -1
- package/dist/server/services/auth-service.js +108 -7
- package/dist/server/services/email-service.d.ts +63 -0
- package/dist/server/services/email-service.js +586 -0
- package/dist/server/utils/disposable-email-domains.d.ts +14 -0
- package/dist/server/utils/disposable-email-domains.js +192 -0
- package/dist/types/api.d.ts +98 -0
- package/dist/types/gitlab.d.ts +245 -0
- package/dist/types/gitlab.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth Authentication Routes
|
|
3
|
+
* Google and GitHub OAuth login flows
|
|
4
|
+
*/
|
|
5
|
+
import { Router } from 'express';
|
|
6
|
+
import passport from '../config/passport.js';
|
|
7
|
+
import { AuthService } from '../services/auth-service.js';
|
|
8
|
+
import { auditService } from '../services/audit-service.js';
|
|
9
|
+
import { Logger } from '../../utils/logger.js';
|
|
10
|
+
import { isDisposableEmail, getDisposableEmailError } from '../utils/disposable-email-domains.js';
|
|
11
|
+
export const oauthRouter = Router();
|
|
12
|
+
const authService = AuthService.getInstance();
|
|
13
|
+
// Cookie configuration for auth tokens
|
|
14
|
+
const COOKIE_OPTIONS = {
|
|
15
|
+
httpOnly: true,
|
|
16
|
+
secure: process.env.NODE_ENV === 'production',
|
|
17
|
+
sameSite: 'lax', // lax for OAuth redirects
|
|
18
|
+
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
19
|
+
path: '/'
|
|
20
|
+
};
|
|
21
|
+
// Helper to set auth cookie
|
|
22
|
+
function setAuthCookie(res, token) {
|
|
23
|
+
res.cookie('archicore_token', token, COOKIE_OPTIONS);
|
|
24
|
+
}
|
|
25
|
+
// Validate redirect URL to prevent open redirect attacks
|
|
26
|
+
function isValidRedirect(url) {
|
|
27
|
+
if (!url)
|
|
28
|
+
return false;
|
|
29
|
+
// Must start with / and not with // (protocol-relative URL)
|
|
30
|
+
if (!url.startsWith('/') || url.startsWith('//'))
|
|
31
|
+
return false;
|
|
32
|
+
// Block encoded characters that could be used for bypass
|
|
33
|
+
if (url.includes('%') || url.includes('\\'))
|
|
34
|
+
return false;
|
|
35
|
+
// Block URLs that could redirect externally
|
|
36
|
+
if (url.includes('@') || url.includes('://'))
|
|
37
|
+
return false;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
// Get redirect URL from query or default to dashboard
|
|
41
|
+
function getRedirectUrl(req) {
|
|
42
|
+
const redirect = req.query.redirect;
|
|
43
|
+
if (isValidRedirect(redirect)) {
|
|
44
|
+
return redirect;
|
|
45
|
+
}
|
|
46
|
+
return '/dashboard';
|
|
47
|
+
}
|
|
48
|
+
// Generate error redirect URL
|
|
49
|
+
function getErrorRedirectUrl(message) {
|
|
50
|
+
return `/auth?error=${encodeURIComponent(message)}`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* GET /api/auth/oauth/google
|
|
54
|
+
* Initiate Google OAuth flow
|
|
55
|
+
*/
|
|
56
|
+
oauthRouter.get('/google', (req, res, next) => {
|
|
57
|
+
// Store redirect URL in session/cookie for after OAuth
|
|
58
|
+
const redirectUrl = getRedirectUrl(req);
|
|
59
|
+
res.cookie('oauth_redirect', redirectUrl, {
|
|
60
|
+
httpOnly: true,
|
|
61
|
+
secure: process.env.NODE_ENV === 'production',
|
|
62
|
+
maxAge: 10 * 60 * 1000, // 10 minutes
|
|
63
|
+
});
|
|
64
|
+
passport.authenticate('google', {
|
|
65
|
+
session: false,
|
|
66
|
+
scope: ['profile', 'email'],
|
|
67
|
+
})(req, res, next);
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* GET /api/auth/oauth/google/callback
|
|
71
|
+
* Google OAuth callback
|
|
72
|
+
*/
|
|
73
|
+
oauthRouter.get('/google/callback', passport.authenticate('google', { session: false, failureRedirect: '/auth?error=google_auth_failed' }), async (req, res) => {
|
|
74
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
75
|
+
const userAgent = req.headers['user-agent'];
|
|
76
|
+
try {
|
|
77
|
+
const profile = req.user;
|
|
78
|
+
if (!profile || !profile.email) {
|
|
79
|
+
Logger.error('[OAuth] Google callback - no profile or email');
|
|
80
|
+
return res.redirect(getErrorRedirectUrl('No email provided by Google'));
|
|
81
|
+
}
|
|
82
|
+
// Check for disposable email
|
|
83
|
+
if (isDisposableEmail(profile.email)) {
|
|
84
|
+
Logger.warn(`[OAuth] Google callback - disposable email blocked: ${profile.email}`);
|
|
85
|
+
return res.redirect(getErrorRedirectUrl(getDisposableEmailError()));
|
|
86
|
+
}
|
|
87
|
+
// Login or register user via OAuth
|
|
88
|
+
const result = await authService.oauthLogin('google', profile);
|
|
89
|
+
if (!result.success || !result.token) {
|
|
90
|
+
Logger.error('[OAuth] Google callback - login failed:', result.error);
|
|
91
|
+
return res.redirect(getErrorRedirectUrl(result.error || 'Authentication failed'));
|
|
92
|
+
}
|
|
93
|
+
// Audit log
|
|
94
|
+
await auditService.log({
|
|
95
|
+
userId: result.user?.id,
|
|
96
|
+
username: result.user?.username,
|
|
97
|
+
action: 'auth.oauth.google',
|
|
98
|
+
ip,
|
|
99
|
+
userAgent,
|
|
100
|
+
details: { email: profile.email },
|
|
101
|
+
success: true,
|
|
102
|
+
});
|
|
103
|
+
// Get redirect URL from cookie with validation
|
|
104
|
+
const savedRedirect = req.cookies.oauth_redirect;
|
|
105
|
+
const redirectUrl = isValidRedirect(savedRedirect) ? savedRedirect : '/dashboard';
|
|
106
|
+
res.clearCookie('oauth_redirect');
|
|
107
|
+
// Set httpOnly cookie with auth token (secure, not exposed to JS)
|
|
108
|
+
setAuthCookie(res, result.token);
|
|
109
|
+
// Redirect to frontend without token in URL (token is in httpOnly cookie)
|
|
110
|
+
Logger.info(`[OAuth] Google login successful for ${profile.email}, redirecting to ${redirectUrl}`);
|
|
111
|
+
res.redirect(redirectUrl);
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
Logger.error('[OAuth] Google callback error:', error);
|
|
115
|
+
await auditService.log({
|
|
116
|
+
action: 'auth.oauth.google',
|
|
117
|
+
ip,
|
|
118
|
+
userAgent,
|
|
119
|
+
success: false,
|
|
120
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
121
|
+
});
|
|
122
|
+
res.redirect(getErrorRedirectUrl('Authentication failed'));
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
/**
|
|
126
|
+
* GET /api/auth/oauth/github
|
|
127
|
+
* Initiate GitHub OAuth flow
|
|
128
|
+
*/
|
|
129
|
+
oauthRouter.get('/github', (req, res, next) => {
|
|
130
|
+
// Store redirect URL in cookie for after OAuth
|
|
131
|
+
const redirectUrl = getRedirectUrl(req);
|
|
132
|
+
res.cookie('oauth_redirect', redirectUrl, {
|
|
133
|
+
httpOnly: true,
|
|
134
|
+
secure: process.env.NODE_ENV === 'production',
|
|
135
|
+
maxAge: 10 * 60 * 1000, // 10 minutes
|
|
136
|
+
});
|
|
137
|
+
passport.authenticate('github', {
|
|
138
|
+
session: false,
|
|
139
|
+
scope: ['user:email'],
|
|
140
|
+
})(req, res, next);
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* GET /api/auth/oauth/github/callback
|
|
144
|
+
* GitHub OAuth callback
|
|
145
|
+
*/
|
|
146
|
+
oauthRouter.get('/github/callback', passport.authenticate('github', { session: false, failureRedirect: '/auth?error=github_auth_failed' }), async (req, res) => {
|
|
147
|
+
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
148
|
+
const userAgent = req.headers['user-agent'];
|
|
149
|
+
try {
|
|
150
|
+
const profile = req.user;
|
|
151
|
+
if (!profile || !profile.email) {
|
|
152
|
+
Logger.error('[OAuth] GitHub callback - no profile or email');
|
|
153
|
+
return res.redirect(getErrorRedirectUrl('No email provided by GitHub. Please make your email public in GitHub settings.'));
|
|
154
|
+
}
|
|
155
|
+
// Check for disposable email
|
|
156
|
+
if (isDisposableEmail(profile.email)) {
|
|
157
|
+
Logger.warn(`[OAuth] GitHub callback - disposable email blocked: ${profile.email}`);
|
|
158
|
+
return res.redirect(getErrorRedirectUrl(getDisposableEmailError()));
|
|
159
|
+
}
|
|
160
|
+
// Login or register user via OAuth
|
|
161
|
+
const result = await authService.oauthLogin('github', profile);
|
|
162
|
+
if (!result.success || !result.token) {
|
|
163
|
+
Logger.error('[OAuth] GitHub callback - login failed:', result.error);
|
|
164
|
+
return res.redirect(getErrorRedirectUrl(result.error || 'Authentication failed'));
|
|
165
|
+
}
|
|
166
|
+
// Audit log
|
|
167
|
+
await auditService.log({
|
|
168
|
+
userId: result.user?.id,
|
|
169
|
+
username: result.user?.username,
|
|
170
|
+
action: 'auth.oauth.github',
|
|
171
|
+
ip,
|
|
172
|
+
userAgent,
|
|
173
|
+
details: { email: profile.email },
|
|
174
|
+
success: true,
|
|
175
|
+
});
|
|
176
|
+
// Get redirect URL from cookie with validation
|
|
177
|
+
const savedRedirect = req.cookies.oauth_redirect;
|
|
178
|
+
const redirectUrl = isValidRedirect(savedRedirect) ? savedRedirect : '/dashboard';
|
|
179
|
+
res.clearCookie('oauth_redirect');
|
|
180
|
+
// Set httpOnly cookie with auth token (secure, not exposed to JS)
|
|
181
|
+
setAuthCookie(res, result.token);
|
|
182
|
+
// Redirect to frontend without token in URL (token is in httpOnly cookie)
|
|
183
|
+
Logger.info(`[OAuth] GitHub login successful for ${profile.email}, redirecting to ${redirectUrl}`);
|
|
184
|
+
res.redirect(redirectUrl);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
Logger.error('[OAuth] GitHub callback error:', error);
|
|
188
|
+
await auditService.log({
|
|
189
|
+
action: 'auth.oauth.github',
|
|
190
|
+
ip,
|
|
191
|
+
userAgent,
|
|
192
|
+
success: false,
|
|
193
|
+
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
|
194
|
+
});
|
|
195
|
+
res.redirect(getErrorRedirectUrl('Authentication failed'));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
//# sourceMappingURL=oauth.js.map
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Logs user actions for security and compliance purposes
|
|
5
5
|
* Supports PostgreSQL (primary) with JSON file fallback
|
|
6
6
|
*/
|
|
7
|
-
export type AuditAction = 'auth.login' | 'auth.logout' | 'auth.register' | 'auth.password_change' | 'auth.device_login' | 'user.update' | 'user.tier_change' | 'user.delete' | 'project.create' | 'project.delete' | 'project.analyze' | 'project.simulate' | 'project.ask' | 'project.docs' | 'github.connect' | 'github.disconnect' | 'github.repo_connect' | 'github.repo_disconnect' | 'github.webhook_received' | 'api.key_create' | 'api.key_revoke' | 'api.request' | 'admin.login' | 'admin.user_update' | 'admin.user_delete' | 'admin.tier_change' | 'admin.view_logs';
|
|
7
|
+
export type AuditAction = 'auth.login' | 'auth.logout' | 'auth.register' | 'auth.password_change' | 'auth.device_login' | 'auth.oauth.google' | 'auth.oauth.github' | 'user.update' | 'user.tier_change' | 'user.delete' | 'project.create' | 'project.delete' | 'project.analyze' | 'project.simulate' | 'project.ask' | 'project.docs' | 'github.connect' | 'github.disconnect' | 'github.repo_connect' | 'github.repo_disconnect' | 'github.webhook_received' | 'api.key_create' | 'api.key_revoke' | 'api.request' | 'admin.login' | 'admin.user_update' | 'admin.user_delete' | 'admin.tier_change' | 'admin.view_logs';
|
|
8
8
|
export type AuditSeverity = 'info' | 'warning' | 'critical';
|
|
9
9
|
export interface AuditLogEntry {
|
|
10
10
|
id: string;
|
|
@@ -17,6 +17,18 @@ export declare class AuthService {
|
|
|
17
17
|
private createDefaultAdmin;
|
|
18
18
|
private saveUsers;
|
|
19
19
|
private saveSessions;
|
|
20
|
+
/**
|
|
21
|
+
* Store session in Redis for fast lookup
|
|
22
|
+
*/
|
|
23
|
+
private cacheSession;
|
|
24
|
+
/**
|
|
25
|
+
* Get session from Redis cache
|
|
26
|
+
*/
|
|
27
|
+
private getCachedSession;
|
|
28
|
+
/**
|
|
29
|
+
* Remove session from Redis cache
|
|
30
|
+
*/
|
|
31
|
+
private removeCachedSession;
|
|
20
32
|
private readonly BCRYPT_ROUNDS;
|
|
21
33
|
/**
|
|
22
34
|
* Hash password with bcrypt (async)
|
|
@@ -49,7 +61,7 @@ export declare class AuthService {
|
|
|
49
61
|
oauthLogin(provider: 'github' | 'google', profile: {
|
|
50
62
|
id: string;
|
|
51
63
|
email: string;
|
|
52
|
-
|
|
64
|
+
displayName: string;
|
|
53
65
|
avatar?: string;
|
|
54
66
|
}): Promise<AuthResponse>;
|
|
55
67
|
validateToken(token: string): Promise<User | null>;
|
|
@@ -11,6 +11,11 @@ import { join } from 'path';
|
|
|
11
11
|
import { TIER_LIMITS } from '../../types/user.js';
|
|
12
12
|
import { db } from './database.js';
|
|
13
13
|
import { Logger } from '../../utils/logger.js';
|
|
14
|
+
import { emailService } from './email-service.js';
|
|
15
|
+
import { cache } from './cache.js';
|
|
16
|
+
// Redis session configuration
|
|
17
|
+
const SESSION_PREFIX = 'session:';
|
|
18
|
+
const SESSION_TTL = 7 * 24 * 60 * 60; // 7 days in seconds
|
|
14
19
|
const DATA_DIR = '.archicore';
|
|
15
20
|
const USERS_FILE = 'users.json';
|
|
16
21
|
const SESSIONS_FILE = 'sessions.json';
|
|
@@ -85,6 +90,53 @@ export class AuthService {
|
|
|
85
90
|
const sessionsPath = join(this.dataDir, SESSIONS_FILE);
|
|
86
91
|
await writeFile(sessionsPath, JSON.stringify({ sessions: this.sessions }, null, 2));
|
|
87
92
|
}
|
|
93
|
+
// ========== REDIS SESSION METHODS ==========
|
|
94
|
+
/**
|
|
95
|
+
* Store session in Redis for fast lookup
|
|
96
|
+
*/
|
|
97
|
+
async cacheSession(token, userId, expiresAt) {
|
|
98
|
+
try {
|
|
99
|
+
const sessionData = {
|
|
100
|
+
userId,
|
|
101
|
+
expiresAt: expiresAt.toISOString()
|
|
102
|
+
};
|
|
103
|
+
await cache.set(`${SESSION_PREFIX}${token}`, sessionData, {
|
|
104
|
+
ttl: SESSION_TTL
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
Logger.debug('Session cache set failed (non-critical):', error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get session from Redis cache
|
|
113
|
+
*/
|
|
114
|
+
async getCachedSession(token) {
|
|
115
|
+
try {
|
|
116
|
+
const data = await cache.get(`${SESSION_PREFIX}${token}`);
|
|
117
|
+
if (data) {
|
|
118
|
+
return {
|
|
119
|
+
userId: data.userId,
|
|
120
|
+
expiresAt: new Date(data.expiresAt)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
Logger.debug('Session cache get failed (non-critical):', error);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Remove session from Redis cache
|
|
131
|
+
*/
|
|
132
|
+
async removeCachedSession(token) {
|
|
133
|
+
try {
|
|
134
|
+
await cache.del(`${SESSION_PREFIX}${token}`);
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
Logger.debug('Session cache delete failed (non-critical):', error);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
88
140
|
// ========== HELPER METHODS ==========
|
|
89
141
|
BCRYPT_ROUNDS = 12;
|
|
90
142
|
/**
|
|
@@ -195,6 +247,11 @@ export class AuthService {
|
|
|
195
247
|
});
|
|
196
248
|
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
197
249
|
const user = this.rowToUser(userResult.rows[0]);
|
|
250
|
+
// Cache session in Redis
|
|
251
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
252
|
+
await this.cacheSession(token, userId, expiresAt);
|
|
253
|
+
// Send welcome email (non-blocking)
|
|
254
|
+
emailService.sendWelcome(email, username).catch(err => Logger.warn('[Auth] Failed to send welcome email:', err));
|
|
198
255
|
return { success: true, token, user: this.sanitizeUser(user) };
|
|
199
256
|
}
|
|
200
257
|
catch (error) {
|
|
@@ -224,13 +281,18 @@ export class AuthService {
|
|
|
224
281
|
this.users.push(user);
|
|
225
282
|
await this.saveUsers();
|
|
226
283
|
const token = this.generateToken();
|
|
284
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
227
285
|
const session = {
|
|
228
286
|
token,
|
|
229
287
|
userId: user.id,
|
|
230
|
-
expiresAt
|
|
288
|
+
expiresAt
|
|
231
289
|
};
|
|
232
290
|
this.sessions.push(session);
|
|
233
291
|
await this.saveSessions();
|
|
292
|
+
// Cache session in Redis
|
|
293
|
+
await this.cacheSession(token, user.id, expiresAt);
|
|
294
|
+
// Send welcome email (non-blocking)
|
|
295
|
+
emailService.sendWelcome(email, username).catch(err => Logger.warn('[Auth] Failed to send welcome email:', err));
|
|
234
296
|
return { success: true, token, user: this.sanitizeUser(user) };
|
|
235
297
|
}
|
|
236
298
|
async login(email, password) {
|
|
@@ -253,8 +315,11 @@ export class AuthService {
|
|
|
253
315
|
await db.query('UPDATE users SET password_hash = $1 WHERE id = $2', [newHash, row.id]);
|
|
254
316
|
Logger.info(`Upgraded password hash for user ${row.id}`);
|
|
255
317
|
}
|
|
318
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
256
319
|
await db.query('UPDATE users SET last_login_at = NOW() WHERE id = $1', [row.id]);
|
|
257
|
-
await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, row.id,
|
|
320
|
+
await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, row.id, expiresAt]);
|
|
321
|
+
// Cache session in Redis
|
|
322
|
+
await this.cacheSession(token, row.id, expiresAt);
|
|
258
323
|
const user = this.rowToUser(row);
|
|
259
324
|
return { success: true, token, user: this.sanitizeUser(user) };
|
|
260
325
|
}
|
|
@@ -275,13 +340,16 @@ export class AuthService {
|
|
|
275
340
|
user.lastLoginAt = new Date();
|
|
276
341
|
await this.saveUsers();
|
|
277
342
|
const token = this.generateToken();
|
|
343
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
278
344
|
const session = {
|
|
279
345
|
token,
|
|
280
346
|
userId: user.id,
|
|
281
|
-
expiresAt
|
|
347
|
+
expiresAt
|
|
282
348
|
};
|
|
283
349
|
this.sessions.push(session);
|
|
284
350
|
await this.saveSessions();
|
|
351
|
+
// Cache session in Redis
|
|
352
|
+
await this.cacheSession(token, user.id, expiresAt);
|
|
285
353
|
return { success: true, token, user: this.sanitizeUser(user) };
|
|
286
354
|
}
|
|
287
355
|
async oauthLogin(provider, profile) {
|
|
@@ -294,7 +362,7 @@ export class AuthService {
|
|
|
294
362
|
// Create new user
|
|
295
363
|
userId = 'user-' + randomUUID();
|
|
296
364
|
await db.query(`INSERT INTO users (id, email, username, avatar, tier, provider, provider_id)
|
|
297
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [userId, profile.email.toLowerCase(), profile.
|
|
365
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`, [userId, profile.email.toLowerCase(), profile.displayName || profile.email.split('@')[0], profile.avatar, 'free', provider, profile.id]);
|
|
298
366
|
}
|
|
299
367
|
else {
|
|
300
368
|
// Update existing
|
|
@@ -305,7 +373,10 @@ export class AuthService {
|
|
|
305
373
|
WHERE id = $1`, [userId, profile.avatar, provider, profile.id]);
|
|
306
374
|
}
|
|
307
375
|
const token = this.generateToken();
|
|
308
|
-
|
|
376
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
377
|
+
await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, userId, expiresAt]);
|
|
378
|
+
// Cache session in Redis
|
|
379
|
+
await this.cacheSession(token, userId, expiresAt);
|
|
309
380
|
result = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
|
|
310
381
|
const user = this.rowToUser(result.rows[0]);
|
|
311
382
|
return { success: true, token, user: this.sanitizeUser(user) };
|
|
@@ -323,7 +394,7 @@ export class AuthService {
|
|
|
323
394
|
user = {
|
|
324
395
|
id: 'user-' + randomUUID(),
|
|
325
396
|
email: profile.email.toLowerCase(),
|
|
326
|
-
username: profile.
|
|
397
|
+
username: profile.displayName || profile.email.split('@')[0],
|
|
327
398
|
avatar: profile.avatar,
|
|
328
399
|
tier: 'free',
|
|
329
400
|
provider,
|
|
@@ -345,16 +416,38 @@ export class AuthService {
|
|
|
345
416
|
}
|
|
346
417
|
await this.saveUsers();
|
|
347
418
|
const token = this.generateToken();
|
|
419
|
+
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
|
|
348
420
|
const session = {
|
|
349
421
|
token,
|
|
350
422
|
userId: user.id,
|
|
351
|
-
expiresAt
|
|
423
|
+
expiresAt
|
|
352
424
|
};
|
|
353
425
|
this.sessions.push(session);
|
|
354
426
|
await this.saveSessions();
|
|
427
|
+
// Cache session in Redis
|
|
428
|
+
await this.cacheSession(token, user.id, expiresAt);
|
|
355
429
|
return { success: true, token, user: this.sanitizeUser(user) };
|
|
356
430
|
}
|
|
357
431
|
async validateToken(token) {
|
|
432
|
+
// Try Redis cache first for fast lookup
|
|
433
|
+
const cachedSession = await this.getCachedSession(token);
|
|
434
|
+
if (cachedSession) {
|
|
435
|
+
// Check expiration
|
|
436
|
+
if (cachedSession.expiresAt < new Date()) {
|
|
437
|
+
await this.removeCachedSession(token);
|
|
438
|
+
// Also remove from primary storage
|
|
439
|
+
if (this.useDatabase()) {
|
|
440
|
+
await db.query('DELETE FROM sessions WHERE token = $1', [token]).catch(() => { });
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
// Get user from cache or database
|
|
445
|
+
const user = await this.getUser(cachedSession.userId);
|
|
446
|
+
if (user) {
|
|
447
|
+
return user;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// Fall back to database/JSON storage
|
|
358
451
|
if (this.useDatabase()) {
|
|
359
452
|
try {
|
|
360
453
|
const result = await db.query('SELECT * FROM sessions WHERE token = $1', [token]);
|
|
@@ -363,11 +456,14 @@ export class AuthService {
|
|
|
363
456
|
const session = result.rows[0];
|
|
364
457
|
if (new Date(session.expires_at) < new Date()) {
|
|
365
458
|
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
|
|
459
|
+
await this.removeCachedSession(token);
|
|
366
460
|
return null;
|
|
367
461
|
}
|
|
368
462
|
const userResult = await db.query('SELECT * FROM users WHERE id = $1', [session.user_id]);
|
|
369
463
|
if (userResult.rows.length === 0)
|
|
370
464
|
return null;
|
|
465
|
+
// Cache the session for next time
|
|
466
|
+
await this.cacheSession(token, session.user_id, new Date(session.expires_at));
|
|
371
467
|
return this.rowToUser(userResult.rows[0]);
|
|
372
468
|
}
|
|
373
469
|
catch (error) {
|
|
@@ -383,11 +479,16 @@ export class AuthService {
|
|
|
383
479
|
if (new Date(session.expiresAt) < new Date()) {
|
|
384
480
|
this.sessions = this.sessions.filter(s => s.token !== token);
|
|
385
481
|
await this.saveSessions();
|
|
482
|
+
await this.removeCachedSession(token);
|
|
386
483
|
return null;
|
|
387
484
|
}
|
|
485
|
+
// Cache the session for next time
|
|
486
|
+
await this.cacheSession(token, session.userId, new Date(session.expiresAt));
|
|
388
487
|
return this.users.find(u => u.id === session.userId) || null;
|
|
389
488
|
}
|
|
390
489
|
async logout(token) {
|
|
490
|
+
// Always remove from Redis cache
|
|
491
|
+
await this.removeCachedSession(token);
|
|
391
492
|
if (this.useDatabase()) {
|
|
392
493
|
await db.query('DELETE FROM sessions WHERE token = $1', [token]);
|
|
393
494
|
return;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
interface EmailOptions {
|
|
2
|
+
to: string;
|
|
3
|
+
subject: string;
|
|
4
|
+
html: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
}
|
|
7
|
+
declare class EmailService {
|
|
8
|
+
private transporter;
|
|
9
|
+
private config;
|
|
10
|
+
private enabled;
|
|
11
|
+
constructor();
|
|
12
|
+
/**
|
|
13
|
+
* Initialize email service from environment variables
|
|
14
|
+
*/
|
|
15
|
+
private initializeFromEnv;
|
|
16
|
+
/**
|
|
17
|
+
* Create nodemailer transporter
|
|
18
|
+
*/
|
|
19
|
+
private createTransporter;
|
|
20
|
+
/**
|
|
21
|
+
* Verify SMTP connection
|
|
22
|
+
*/
|
|
23
|
+
verify(): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Send email
|
|
26
|
+
*/
|
|
27
|
+
send(options: EmailOptions): Promise<boolean>;
|
|
28
|
+
/**
|
|
29
|
+
* Send device verification code email
|
|
30
|
+
*/
|
|
31
|
+
sendDeviceCode(email: string, code: string, verificationUrl: string): Promise<boolean>;
|
|
32
|
+
/**
|
|
33
|
+
* Send email verification code
|
|
34
|
+
*/
|
|
35
|
+
sendVerificationCode(email: string, code: string): Promise<boolean>;
|
|
36
|
+
/**
|
|
37
|
+
* Send welcome email after registration
|
|
38
|
+
*/
|
|
39
|
+
sendWelcome(email: string, username: string): Promise<boolean>;
|
|
40
|
+
/**
|
|
41
|
+
* Create HTML template for device verification code
|
|
42
|
+
*/
|
|
43
|
+
private createDeviceCodeTemplate;
|
|
44
|
+
/**
|
|
45
|
+
* Create HTML template for welcome email
|
|
46
|
+
*/
|
|
47
|
+
private createWelcomeTemplate;
|
|
48
|
+
/**
|
|
49
|
+
* Create HTML template for email verification code
|
|
50
|
+
*/
|
|
51
|
+
private createVerificationCodeTemplate;
|
|
52
|
+
/**
|
|
53
|
+
* Strip HTML tags for plain text version
|
|
54
|
+
*/
|
|
55
|
+
private stripHtml;
|
|
56
|
+
/**
|
|
57
|
+
* Check if email service is enabled
|
|
58
|
+
*/
|
|
59
|
+
isEnabled(): boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare const emailService: EmailService;
|
|
62
|
+
export {};
|
|
63
|
+
//# sourceMappingURL=email-service.d.ts.map
|