feat(mvp): phase 3 - authentication system

Implement local auth flow: login, registration, logout, JWT access/refresh
tokens in HTTP-only cookies, hooks.server.ts middleware, guest mode support,
Superforms + Zod validation, and reusable auth/authorize middleware.
This commit is contained in:
2026-03-24 20:45:14 +03:00
parent f1b1aa5975
commit 2c001df322
19 changed files with 751 additions and 28 deletions
+22
View File
@@ -0,0 +1,22 @@
import { redirect } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
/**
* Reusable authentication check helper.
* Throws a redirect to /login if the user is not authenticated.
* Returns the authenticated user from event.locals.
*/
export function requireAuth(event: RequestEvent) {
const user = event.locals.user;
if (!user) {
throw redirect(302, '/login');
}
return user;
}
/**
* Check if the current request has an authenticated user without redirecting.
*/
export function isAuthenticated(event: RequestEvent): boolean {
return event.locals.user !== null;
}
+25
View File
@@ -0,0 +1,25 @@
import { error } from '@sveltejs/kit';
import type { RequestEvent } from '@sveltejs/kit';
import { requireAuth } from './authenticate.js';
import { UserRole } from '$lib/utils/constants.js';
/**
* Role-based access check. Ensures the user is authenticated and has one of the required roles.
* Throws a 403 error if the user's role is not in the allowed list.
*/
export function requireRole(event: RequestEvent, ...allowedRoles: string[]) {
const user = requireAuth(event);
if (!allowedRoles.includes(user.role)) {
throw error(403, { message: 'Insufficient permissions' });
}
return user;
}
/**
* Shorthand: require admin role.
*/
export function requireAdmin(event: RequestEvent) {
return requireRole(event, UserRole.ADMIN);
}
+49
View File
@@ -0,0 +1,49 @@
import { prisma } from '../prisma.js';
/**
* Check if a board is guest-accessible (visible to unauthenticated users).
*/
export async function isBoardGuestAccessible(boardId: string): Promise<boolean> {
const board = await prisma.board.findUnique({
where: { id: boardId },
select: { isGuestAccessible: true }
});
return board?.isGuestAccessible ?? false;
}
/**
* Get all guest-accessible boards.
*/
export async function getGuestAccessibleBoards() {
return prisma.board.findMany({
where: { isGuestAccessible: true },
orderBy: { name: 'asc' },
select: {
id: true,
name: true,
icon: true,
description: true,
isDefault: true
}
});
}
/**
* Get the default guest-accessible board (if any).
* Returns the first board that is both default and guest-accessible,
* or the first guest-accessible board if none is default.
*/
export async function getDefaultGuestBoard() {
const defaultBoard = await prisma.board.findFirst({
where: { isGuestAccessible: true, isDefault: true },
select: { id: true, name: true }
});
if (defaultBoard) return defaultBoard;
return prisma.board.findFirst({
where: { isGuestAccessible: true },
orderBy: { name: 'asc' },
select: { id: true, name: true }
});
}
+10
View File
@@ -0,0 +1,10 @@
/**
* JWT utilities — thin re-exports from authService.
* authService already handles sign, verify, and refresh token generation.
*/
export {
signAccessToken,
verifyAccessToken,
generateRefreshToken,
getRefreshTokenExpiry
} from '../services/authService.js';
+5
View File
@@ -0,0 +1,5 @@
/**
* Password utilities — thin re-exports from authService.
* authService already handles bcrypt hash and compare.
*/
export { hashPassword, verifyPassword } from '../services/authService.js';