Skip to content

Commit 084289e

Browse files
author
Lasim
committed
feat: implement logout functionality and enhance session management
1 parent 785fcb0 commit 084289e

File tree

17 files changed

+380
-92
lines changed

17 files changed

+380
-92
lines changed

cookies.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Netscape HTTP Cookie File
2+
# https://curl.se/docs/http-cookies.html
3+
# This file was generated by libcurl! Edit at your own risk.
4+

services/backend/src/db/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,23 @@ export function getDbStatus() {
364364
};
365365
}
366366

367+
// Function to force schema regeneration (useful for development)
368+
export function regenerateSchema(): void {
369+
if (currentDbConfig) {
370+
console.log('[INFO] Forcing schema regeneration...');
371+
dbSchema = generateSchema(currentDbConfig.type);
372+
373+
// Recreate the database instance with new schema
374+
if (dbConnection && currentDbConfig.type === 'sqlite') {
375+
dbInstance = drizzleSqliteAdapter(dbConnection as SqliteDriver.Database, { schema: dbSchema, logger: false });
376+
} else if (dbConnection && currentDbConfig.type === 'postgres') {
377+
dbInstance = drizzlePgAdapter(dbConnection as PgPool, { schema: dbSchema, logger: false });
378+
}
379+
380+
console.log('[INFO] Schema regenerated successfully.');
381+
}
382+
}
383+
367384
// Define a more specific type for DatabaseExtension if possible, or use 'any' for now.
368385
interface DatabaseExtensionWithTables extends DatabaseExtension {
369386
// eslint-disable-next-line @typescript-eslint/no-explicit-any

services/backend/src/db/schema.sqlite.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ export const authUser = sqliteTable('authUser', {
3737
export const authSession = sqliteTable('authSession', {
3838
id: text('id').primaryKey(),
3939
user_id: text('user_id').notNull().references(() => authUser.id, { onDelete: 'cascade' }),
40-
expires_at: integer('expires_at', { mode: 'number' }).notNull(),
40+
expires_at: integer('expires_at').notNull(),
4141
});
4242

4343
export const authKey = sqliteTable('authKey', {
4444
id: text('id').primaryKey(),
4545
user_id: text('user_id').notNull().references(() => authUser.id, { onDelete: 'cascade' }),
4646
primary_key: text('primary_key').notNull(),
4747
hashed_password: text('hashed_password'),
48-
expires: integer('expires', { mode: 'number' }),
48+
expires: integer('expires'),
4949
});
5050

5151
export const teams = sqliteTable('teams', {

services/backend/src/db/schema.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,15 @@ export const authUserTableColumns = {
4949
export const authSessionTableColumns = {
5050
id: (columnBuilder: any) => columnBuilder('id').primaryKey(),
5151
user_id: (columnBuilder: any) => columnBuilder('user_id').notNull(), // Foreign key to authUser.id
52-
expires_at: (columnBuilder: any) => columnBuilder('expires_at', { mode: 'number' }).notNull(), // Lucia v3 uses expires_at
52+
expires_at: (columnBuilder: any) => columnBuilder('expires_at').notNull(), // Lucia v3 uses expires_at as integer timestamp
5353
};
5454

5555
export const authKeyTableColumns = {
5656
id: (columnBuilder: any) => columnBuilder('id').primaryKey(), // e.g., 'email:user@example.com' or 'github:123456'
5757
user_id: (columnBuilder: any) => columnBuilder('user_id').notNull(), // Foreign key to authUser.id
5858
primary_key: (columnBuilder: any) => columnBuilder('primary_key').notNull(),
5959
hashed_password: (columnBuilder: any) => columnBuilder('hashed_password'), // Nullable for OAuth keys
60-
expires: (columnBuilder: any) => columnBuilder('expires', { mode: 'number' }), // Nullable, for things like password reset
60+
expires: (columnBuilder: any) => columnBuilder('expires'), // Nullable, for things like password reset
6161
};
6262

6363
export const teamsTableColumns = {

services/backend/src/fastify/plugins/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import fastifyCors from '@fastify/cors'
55
export const registerFastifyPlugins = async (server: FastifyInstance): Promise<void> => {
66
// Build allowed origins array
77
const defaultOrigins = [
8-
'http://localhost:5174', // Vite dev server (actual dev port)
8+
'http://localhost:5173', // Vite dev server (correct dev port)
9+
'http://localhost:5174', // Alternative Vite dev server port
910
'http://localhost:3000', // Frontend production (if served from same port)
1011
'http://localhost:4173', // Vite preview
1112
];

services/backend/src/hooks/authHook.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { FastifyRequest, FastifyReply, HookHandlerDoneFunction } from 'fastify';
22
import { getLucia } from '../lib/lucia';
3-
import { getDbStatus } from '../db';
3+
import { getDbStatus, getSchema } from '../db';
44
import type { User, Session } from 'lucia';
55

66
// Augment FastifyRequest to include user and session
@@ -28,23 +28,30 @@ export async function authHook(
2828
try {
2929
const lucia = getLucia();
3030
const sessionId = lucia.readSessionCookie(request.headers.cookie ?? '');
31+
3132
if (!sessionId) {
33+
request.log.debug('Auth hook: No session cookie found');
3234
request.user = null;
3335
request.session = null;
3436
return; // Proceed as unauthenticated
3537
}
3638

39+
request.log.debug(`Auth hook: Found session ID: ${sessionId}`);
3740
const { session, user } = await lucia.validateSession(sessionId);
3841

3942
if (session && session.fresh) {
4043
// Session was refreshed, send new cookie
44+
request.log.debug(`Auth hook: Session ${sessionId} is fresh, sending new cookie`);
4145
const sessionCookie = lucia.createSessionCookie(session.id);
4246
reply.setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
4347
}
4448
if (!session) {
4549
// Invalid session, clear cookie
50+
request.log.debug(`Auth hook: Session ${sessionId} is invalid, clearing cookie`);
4651
const sessionCookie = lucia.createBlankSessionCookie();
4752
reply.setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
53+
} else {
54+
request.log.debug(`Auth hook: Session ${sessionId} is valid for user ${user?.id}`);
4855
}
4956

5057
request.user = user;

services/backend/src/lib/lucia.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Lucia } from 'lucia';
22
import { DrizzlePostgreSQLAdapter, DrizzleSQLiteAdapter } from '@lucia-auth/adapter-drizzle';
33
import { GitHub } from 'arctic';
44

5-
import { getDb, getSchema, getDbStatus } from '../db'; // Assuming db/index.ts exports these
5+
import { getDb, getSchema, getDbStatus, regenerateSchema } from '../db'; // Assuming db/index.ts exports these
66
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
77
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
88

@@ -25,6 +25,14 @@ function initializeLucia(): Lucia {
2525
throw new Error('Database dialect not determined. Ensure database is initialized before using Lucia.');
2626
}
2727

28+
// Force schema regeneration to pick up any schema fixes
29+
try {
30+
regenerateSchema();
31+
console.log('[INFO] Schema regenerated for Lucia initialization');
32+
} catch (error) {
33+
console.log('[INFO] Schema regeneration skipped - using existing schema');
34+
}
35+
2836
const db = getDb();
2937
const schema = getSchema();
3038

@@ -43,6 +51,9 @@ function initializeLucia(): Lucia {
4351
authUserTable
4452
);
4553
} else if (dialect === 'sqlite') {
54+
// For SQLite, ensure we're using the correct table structure
55+
// Lucia expects: session table with id, user_id, expires_at
56+
// and user table with id
4657
adapter = new DrizzleSQLiteAdapter(
4758
db as BetterSQLite3Database, // Cast based on dialect
4859
authSessionTable,
@@ -57,10 +68,13 @@ function initializeLucia(): Lucia {
5768
name: 'session', // Important to use a generic name for production
5869
expires: false, // session cookies have very long lifespan (2 years)
5970
attributes: {
60-
// set to `true` when using HTTPS
71+
// For development: use secure: false with sameSite: 'lax' and domain: 'localhost'
72+
// For production: use secure: true with sameSite: 'none' for cross-origin
6173
secure: process.env.NODE_ENV === 'production',
62-
sameSite: 'lax',
63-
// domain: 'yourdomain.com' // set if using a custom domain
74+
sameSite: process.env.NODE_ENV === 'production' ? 'none' : 'lax',
75+
path: '/', // Explicitly set path to root
76+
// Set domain to localhost in development to allow cross-port communication
77+
domain: process.env.NODE_ENV === 'production' ? undefined : 'localhost',
6478
},
6579
},
6680
getUserAttributes: (attributes: DatabaseUserAttributes) => {
@@ -105,6 +119,11 @@ export function resetLucia(): void {
105119
luciaInstance = null;
106120
}
107121

122+
// Force reset on module reload in development
123+
if (process.env.NODE_ENV !== 'production') {
124+
resetLucia();
125+
}
126+
108127
// Getter function for GitHub OAuth instance
109128
export function getGithubAuth(): GitHub {
110129
if (!githubAuthInstance) {

services/backend/src/routes/auth/loginEmail.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export default async function loginEmailRoute(fastify: FastifyInstance) {
1212
async (request, reply: FastifyReply) => {
1313
const { login, password } = request.body;
1414

15+
// Validate required fields
16+
if (!login || !password) {
17+
return reply.status(400).send({ error: 'Email/username and password are required.' });
18+
}
19+
1520
try {
1621
const db = getDb();
1722
const schema = getSchema();

services/backend/src/routes/auth/logout.ts

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,84 @@
11
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
22
import { getLucia } from '../../lib/lucia';
3+
import { getDb, getSchema } from '../../db';
4+
import { eq } from 'drizzle-orm';
35

46
export default async function logoutRoute(fastify: FastifyInstance) {
57
fastify.post(
68
'/logout',
79
async (request: FastifyRequest, reply: FastifyReply) => {
8-
const sessionId = getLucia().readSessionCookie(request.headers.cookie ?? '');
10+
// The global authHook should have already populated request.session if a valid session exists.
11+
// It also handles creating a blank session cookie if the session was invalid.
12+
const lucia = getLucia();
913

10-
if (!sessionId) {
11-
// No session cookie, so user is effectively logged out or was never logged in.
12-
// It's good practice to still clear any potential lingering cookie on the client.
13-
const sessionCookie = getLucia().createBlankSessionCookie();
14-
reply.setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
14+
// Log session information for debugging
15+
fastify.log.info(`Logout attempt - Session exists: ${!!request.session}, Session ID: ${request.session?.id || 'none'}`);
16+
17+
if (!request.session) {
18+
// No active session found by authHook, but let's check if there's a session cookie
19+
// and manually clean it up if Lucia validation failed
20+
const sessionId = lucia.readSessionCookie(request.headers.cookie ?? '');
21+
22+
if (sessionId) {
23+
fastify.log.info(`Found session cookie ${sessionId} but authHook couldn't validate it - attempting manual cleanup`);
24+
25+
try {
26+
// Try to manually delete the session from database
27+
const db = getDb();
28+
const schema = getSchema();
29+
const authSessionTable = schema.authSession;
30+
31+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
32+
const result = await (db as any).delete(authSessionTable).where(eq(authSessionTable.id, sessionId));
33+
fastify.log.info(`Manually deleted session ${sessionId} from database`);
34+
} catch (dbError) {
35+
fastify.log.error(dbError, 'Failed to manually delete session from database');
36+
}
37+
}
38+
39+
// Send a blank cookie to ensure client-side cookie is cleared
40+
const blankCookie = lucia.createBlankSessionCookie();
41+
reply.setCookie(blankCookie.name, blankCookie.value, blankCookie.attributes);
42+
fastify.log.info('No active session to logout - sending blank cookie');
1543
return reply.status(200).send({ message: 'No active session to logout or already logged out.' });
1644
}
1745

1846
try {
19-
const { session } = await getLucia().validateSession(sessionId);
20-
21-
if (session) {
22-
await getLucia().invalidateSession(session.id);
23-
}
47+
const sessionId = request.session.id;
48+
fastify.log.info(`Attempting to invalidate session: ${sessionId}`);
49+
50+
// Invalidate the session identified by authHook.
51+
await lucia.invalidateSession(sessionId);
52+
fastify.log.info(`Session ${sessionId} invalidated successfully`);
2453

25-
// Always send a blank cookie to ensure client-side cookie is cleared
26-
const sessionCookie = getLucia().createBlankSessionCookie();
27-
reply.setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
54+
// Send a blank cookie to ensure client-side cookie is cleared.
55+
const blankCookie = lucia.createBlankSessionCookie();
56+
reply.setCookie(blankCookie.name, blankCookie.value, blankCookie.attributes);
57+
fastify.log.info('Blank cookie sent to clear client session');
2858

2959
return reply.status(200).send({ message: 'Logged out successfully.' });
3060

3161
} catch (error) {
32-
fastify.log.error(error, 'Error during logout:');
33-
// Even if there's an error (e.g., session already invalid), try to clear the cookie.
34-
const sessionCookie = getLucia().createBlankSessionCookie();
35-
reply.setCookie(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
36-
return reply.status(500).send({ error: 'An error occurred during logout.' });
62+
fastify.log.error(error, 'Error during logout (invalidating session from authHook):');
63+
64+
// If Lucia invalidation failed, try manual database cleanup
65+
const sessionId = request.session.id;
66+
try {
67+
const db = getDb();
68+
const schema = getSchema();
69+
const authSessionTable = schema.authSession;
70+
71+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
72+
await (db as any).delete(authSessionTable).where(eq(authSessionTable.id, sessionId));
73+
fastify.log.info(`Manually deleted session ${sessionId} after Lucia invalidation failed`);
74+
} catch (dbError) {
75+
fastify.log.error(dbError, 'Failed to manually delete session after Lucia error');
76+
}
77+
78+
// Even if there's an error, try to clear the cookie.
79+
const blankCookie = lucia.createBlankSessionCookie();
80+
reply.setCookie(blankCookie.name, blankCookie.value, blankCookie.attributes);
81+
return reply.status(200).send({ message: 'Logged out successfully (with fallback cleanup).' });
3782
}
3883
}
3984
);

services/frontend/src/i18n/index.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { createI18n } from 'vue-i18n'
2-
import en from './locales/en'
2+
// Import the merged English messages from the new structure
3+
import enMessages from './locales/en' // This will now import from locales/en/index.ts
34

45
// Create i18n instance with English as the default language
56
const i18n = createI18n({
67
legacy: false,
78
locale: 'en',
89
fallbackLocale: 'en',
910
messages: {
10-
en,
11+
en: enMessages, // Use the merged messages directly
1112
},
1213
})
1314

0 commit comments

Comments
 (0)