Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

53 changes: 25 additions & 28 deletions src/lib/server/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import bcrypt from 'bcryptjs';
import { createHmac, randomBytes, scryptSync, timingSafeEqual } from 'crypto';
import { randomBytes, scryptSync, timingSafeEqual } from 'crypto';
import { logger } from '$lib/server/logger';
import { db } from '$lib/server/db';
import { rateLimits } from '$lib/server/db/schema';
import { rateLimits, operatorSessions } from '$lib/server/db/schema';
import { eq, lt } from 'drizzle-orm';
import { sql } from 'drizzle-orm';
import { env } from '$env/dynamic/private';
import type { Cookies } from '@sveltejs/kit';

const SALT_ROUNDS = 10;
Expand Down Expand Up @@ -82,34 +81,32 @@ export async function clearRateLimit(key: string): Promise<void> {
await db.delete(rateLimits).where(eq(rateLimits.key, key));
}

function getCookieSecret(): string {
const secret = env.COOKIE_SECRET;
if (!secret) throw new Error('COOKIE_SECRET environment variable is required');
return secret;
}
export async function createOperatorSession(operatorId: number): Promise<string> {
// Delete all existing sessions for this operator (transfers session to new login)
await db.delete(operatorSessions).where(eq(operatorSessions.operatorId, operatorId));

export function signCookieValue(value: string): string {
const hmac = createHmac('sha256', getCookieSecret()).update(value).digest('hex');
return `${value}.${hmac}`;
const token = randomBytes(32).toString('hex');
await db.insert(operatorSessions).values({ token, operatorId });
return token;
}

export function verifyCookieValue(signed: string): string | null {
const dot = signed.lastIndexOf('.');
if (dot === -1) return null;
const value = signed.slice(0, dot);
const sig = signed.slice(dot + 1);
const expected = createHmac('sha256', getCookieSecret()).update(value).digest('hex');
if (sig.length !== expected.length) return null;
if (!timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex'))) return null;
return value;
export async function deleteOperatorSessions(operatorId: number): Promise<void> {
await db.delete(operatorSessions).where(eq(operatorSessions.operatorId, operatorId));
}

export function getVerifiedOperatorId(cookies: Cookies): number | null {
const raw = cookies.get('operatorId');
if (!raw) return null;
const value = verifyCookieValue(raw);
if (!value) return null;
const id = parseInt(value);
if (isNaN(id)) return null;
return id;
export async function getVerifiedOperatorId(cookies: Cookies): Promise<number | null> {
const token = cookies.get('operatorSession');
if (!token) return null;

const [session] = await db
.select({ operatorId: operatorSessions.operatorId })
.from(operatorSessions)
.where(eq(operatorSessions.token, token));

if (!session) {
cookies.delete('operatorSession', { path: '/' });
return null;
}

return session.operatorId;
}
9 changes: 9 additions & 0 deletions src/lib/server/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,13 @@ export const adminSessions = pgTable('admin_sessions', {
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow()
});

export const operatorSessions = pgTable('operator_sessions', {
id: serial('id').primaryKey(),
token: text('token').unique().notNull(),
operatorId: integer('operator_id').references(() => operators.id).notNull(),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow()
});


export type Payment = typeof payments.$inferSelect;
export type NewPayment = typeof payments.$inferInsert;
Expand All @@ -261,3 +268,5 @@ export type ClosingChecklistItem = typeof closingChecklistItems.$inferSelect;
export type NewClosingChecklistItem = typeof closingChecklistItems.$inferInsert;
export type AdminSession = typeof adminSessions.$inferSelect;
export type NewAdminSession = typeof adminSessions.$inferInsert;
export type OperatorSession = typeof operatorSessions.$inferSelect;
export type NewOperatorSession = typeof operatorSessions.$inferInsert;
6 changes: 3 additions & 3 deletions src/routes/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getVerifiedOperatorId } from '$lib/server/auth';
import type { LayoutServerLoad } from './$types';

export const load: LayoutServerLoad = async ({ cookies }) => {
const operatorId = getVerifiedOperatorId(cookies);
const operatorId = await getVerifiedOperatorId(cookies);

if (!operatorId) {
return { operator: null, shift: null };
Expand All @@ -17,7 +17,7 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
.where(and(eq(operators.id, operatorId), eq(operators.isActive, true)));

if (!operator) {
cookies.delete('operatorId', { path: '/' });
cookies.delete('operatorSession', { path: '/' });
return { operator: null, shift: null };
}

Expand All @@ -27,7 +27,7 @@ export const load: LayoutServerLoad = async ({ cookies }) => {
.where(and(eq(shifts.operatorId, operatorId), isNull(shifts.endedAt)));

if (!activeShift) {
cookies.delete('operatorId', { path: '/' });
cookies.delete('operatorSession', { path: '/' });
return { operator: null, shift: null };
}

Expand Down
2 changes: 1 addition & 1 deletion src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const load: PageServerLoad = async ({ cookies }) => {
db.select().from(reservations).where(eq(reservations.status, 'active'))
]);

const operatorId = getVerifiedOperatorId(cookies);
const operatorId = await getVerifiedOperatorId(cookies);
let activeRentals: Rental[] = [];
let previousShiftRentals: Rental[] = [];
let shiftStoreSales: Array<{
Expand Down
10 changes: 6 additions & 4 deletions src/routes/api/shifts/close/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from '$lib/server/db';
import { shifts, rentals, operators, storeSales, storeProducts, tourBookings, tourAgencyProducts } from '$lib/server/db/schema';
import { eq, and, isNull } from 'drizzle-orm';
import { logger } from '$lib/server/logger';
import { getVerifiedOperatorId } from '$lib/server/auth';
import { getVerifiedOperatorId, deleteOperatorSessions } from '$lib/server/auth';
import type { RequestHandler } from './$types';
import * as XLSX from 'xlsx';
import { isGoogleConnected, createSpreadsheet } from '$lib/server/google-sheets';
Expand Down Expand Up @@ -52,7 +52,7 @@ interface Pricing {
}

export const POST: RequestHandler = async ({ cookies }) => {
const operatorId = getVerifiedOperatorId(cookies);
const operatorId = await getVerifiedOperatorId(cookies);
if (!operatorId) {
return new Response(JSON.stringify({ error: 'Not logged in' }), { status: 401 });
}
Expand Down Expand Up @@ -317,7 +317,8 @@ export const POST: RequestHandler = async ({ cookies }) => {

const url = await createSpreadsheet(filename, sheets);

cookies.delete('operatorId', { path: '/' });
await deleteOperatorSessions(operatorId);
cookies.delete('operatorSession', { path: '/' });

return new Response(JSON.stringify({ type: 'google_sheets', url }), {
headers: { 'Content-Type': 'application/json' }
Expand Down Expand Up @@ -349,7 +350,8 @@ export const POST: RequestHandler = async ({ cookies }) => {

const buffer = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });

cookies.delete('operatorId', { path: '/' });
await deleteOperatorSessions(operatorId);
cookies.delete('operatorSession', { path: '/' });

return new Response(buffer, {
headers: {
Expand Down
4 changes: 2 additions & 2 deletions src/routes/api/shifts/end/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ import { getVerifiedOperatorId } from '$lib/server/auth';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request, cookies }) => {
const operatorId = getVerifiedOperatorId(cookies);
const operatorId = await getVerifiedOperatorId(cookies);
if (!operatorId) {
return json({ error: 'Not logged in' }, { status: 401 });
}

const [operator] = await db.select({ id: operators.id }).from(operators)
.where(and(eq(operators.id, operatorId), eq(operators.isActive, true)));
if (!operator) {
cookies.delete('operatorId', { path: '/' });
cookies.delete('operatorSession', { path: '/' });
return json({ error: 'Invalid session' }, { status: 401 });
}

Expand Down
7 changes: 6 additions & 1 deletion src/routes/api/shifts/logout/+server.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { json } from '@sveltejs/kit';
import { getVerifiedOperatorId, deleteOperatorSessions } from '$lib/server/auth';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ cookies }) => {
cookies.delete('operatorId', { path: '/' });
const operatorId = await getVerifiedOperatorId(cookies);
if (operatorId) {
await deleteOperatorSessions(operatorId);
}
cookies.delete('operatorSession', { path: '/' });
return json({ success: true });
};
5 changes: 3 additions & 2 deletions src/routes/api/shifts/start/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { json } from '@sveltejs/kit';
import { db } from '$lib/server/db';
import { operators, shifts } from '$lib/server/db/schema';
import { eq, and, isNull } from 'drizzle-orm';
import { verifyPasscode, checkRateLimit, clearRateLimit, logAuthFailure, signCookieValue } from '$lib/server/auth';
import { verifyPasscode, checkRateLimit, clearRateLimit, logAuthFailure, createOperatorSession } from '$lib/server/auth';
import { dev } from '$app/environment';
import type { RequestHandler } from './$types';

Expand Down Expand Up @@ -56,7 +56,8 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress
shift = newShift;
}

cookies.set('operatorId', signCookieValue(String(operatorId)), {
const sessionToken = await createOperatorSession(operatorId);
cookies.set('operatorSession', sessionToken, {
path: '/',
httpOnly: true,
sameSite: 'lax',
Expand Down
8 changes: 4 additions & 4 deletions src/routes/api/store-sales/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ import { getVerifiedOperatorId } from '$lib/server/auth';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request, cookies }) => {
const operatorId = getVerifiedOperatorId(cookies);
const operatorId = await getVerifiedOperatorId(cookies);
if (!operatorId) {
return json({ error: 'Not logged in' }, { status: 401 });
}

const [operator] = await db.select({ id: operators.id }).from(operators)
.where(and(eq(operators.id, operatorId), eq(operators.isActive, true)));
if (!operator) {
cookies.delete('operatorId', { path: '/' });
cookies.delete('operatorSession', { path: '/' });
return json({ error: 'Invalid session' }, { status: 401 });
}

Expand Down Expand Up @@ -87,15 +87,15 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
};

export const GET: RequestHandler = async ({ url, cookies }) => {
const operatorId = getVerifiedOperatorId(cookies);
const operatorId = await getVerifiedOperatorId(cookies);
if (!operatorId) {
return json({ error: 'Not logged in' }, { status: 401 });
}

const [operator] = await db.select({ id: operators.id }).from(operators)
.where(and(eq(operators.id, operatorId), eq(operators.isActive, true)));
if (!operator) {
cookies.delete('operatorId', { path: '/' });
cookies.delete('operatorSession', { path: '/' });
return json({ error: 'Invalid session' }, { status: 401 });
}

Expand Down
Loading