archicore 0.3.1 → 0.3.3

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 (42) hide show
  1. package/README.md +48 -4
  2. package/dist/cli/commands/interactive.js +83 -23
  3. package/dist/cli/commands/projects.js +3 -3
  4. package/dist/cli/ui/prompt.d.ts +4 -0
  5. package/dist/cli/ui/prompt.js +22 -0
  6. package/dist/cli/utils/config.js +2 -2
  7. package/dist/cli/utils/upload-utils.js +65 -18
  8. package/dist/code-index/ast-parser.d.ts +4 -0
  9. package/dist/code-index/ast-parser.js +42 -0
  10. package/dist/code-index/index.d.ts +21 -1
  11. package/dist/code-index/index.js +45 -1
  12. package/dist/code-index/source-map-extractor.d.ts +71 -0
  13. package/dist/code-index/source-map-extractor.js +194 -0
  14. package/dist/gitlab/gitlab-service.d.ts +162 -0
  15. package/dist/gitlab/gitlab-service.js +652 -0
  16. package/dist/gitlab/index.d.ts +8 -0
  17. package/dist/gitlab/index.js +8 -0
  18. package/dist/server/config/passport.d.ts +14 -0
  19. package/dist/server/config/passport.js +86 -0
  20. package/dist/server/index.js +52 -10
  21. package/dist/server/middleware/api-auth.d.ts +2 -2
  22. package/dist/server/middleware/api-auth.js +21 -2
  23. package/dist/server/middleware/csrf.d.ts +23 -0
  24. package/dist/server/middleware/csrf.js +96 -0
  25. package/dist/server/routes/auth.d.ts +2 -2
  26. package/dist/server/routes/auth.js +204 -5
  27. package/dist/server/routes/device-auth.js +2 -2
  28. package/dist/server/routes/gitlab.d.ts +12 -0
  29. package/dist/server/routes/gitlab.js +528 -0
  30. package/dist/server/routes/oauth.d.ts +6 -0
  31. package/dist/server/routes/oauth.js +198 -0
  32. package/dist/server/services/audit-service.d.ts +1 -1
  33. package/dist/server/services/auth-service.d.ts +13 -1
  34. package/dist/server/services/auth-service.js +108 -7
  35. package/dist/server/services/email-service.d.ts +63 -0
  36. package/dist/server/services/email-service.js +586 -0
  37. package/dist/server/utils/disposable-email-domains.d.ts +14 -0
  38. package/dist/server/utils/disposable-email-domains.js +192 -0
  39. package/dist/types/api.d.ts +98 -0
  40. package/dist/types/gitlab.d.ts +245 -0
  41. package/dist/types/gitlab.js +11 -0
  42. 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
- name: string;
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: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
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, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
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: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
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.name || profile.email.split('@')[0], profile.avatar, 'free', provider, profile.id]);
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
- await db.query('INSERT INTO sessions (token, user_id, expires_at) VALUES ($1, $2, $3)', [token, userId, new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)]);
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.name || profile.email.split('@')[0],
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: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
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