feat(auth): auto-login after onboarding, consolidate session cookies
- Extract session cookie issuance into sessionCookies.ts helper; remove duplicated COOKIE_BASE blocks from login, register, oauth callback/authorize, refresh handler, hooks.server.ts, and onboarding. - Derive cookie secure flag from ORIGIN (https://...) instead of NODE_ENV so plain-HTTP production deploys don't silently drop cookies. - Auto-login admin after onboarding completes; UI does a full reload so hooks.server.ts picks up the new session. - Harden onboarding: reject duplicate admin creation, flip onboardingComplete atomically to prevent concurrent completions, error out if no admin found. - Fix Dockerfile CMD operator precedence: node build now always runs after migrate deploy || db push. - Wire ORIGIN env through docker-compose.
This commit is contained in:
+1
-1
@@ -37,4 +37,4 @@ EXPOSE 3000
|
|||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
CMD ["sh", "-c", "npx prisma migrate deploy 2>/dev/null || npx prisma db push && node build"]
|
CMD ["sh", "-c", "(npx prisma migrate deploy 2>/dev/null || npx prisma db push) && ORIGIN=${ORIGIN:-http://localhost:${APP_PORT:-3000}} node build"]
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ services:
|
|||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
- APP_PORT=3000
|
- APP_PORT=3000
|
||||||
- APP_HOST=0.0.0.0
|
- APP_HOST=0.0.0.0
|
||||||
|
- ORIGIN=${ORIGIN:-http://localhost:${APP_PORT:-3000}}
|
||||||
volumes:
|
volumes:
|
||||||
- launcher-data:/app/data
|
- launcher-data:/app/data
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
+6
-19
@@ -7,6 +7,10 @@ import * as apiTokenService from '$lib/server/services/apiTokenService.js';
|
|||||||
import { extractBearerToken } from '$lib/server/middleware/authenticate.js';
|
import { extractBearerToken } from '$lib/server/middleware/authenticate.js';
|
||||||
import { isBoardGuestAccessible } from '$lib/server/middleware/guestAccess.js';
|
import { isBoardGuestAccessible } from '$lib/server/middleware/guestAccess.js';
|
||||||
import { initBackupScheduler } from '$lib/server/jobs/backupScheduler.js';
|
import { initBackupScheduler } from '$lib/server/jobs/backupScheduler.js';
|
||||||
|
import {
|
||||||
|
clearSessionCookies,
|
||||||
|
setRotatedCookies
|
||||||
|
} from '$lib/server/utils/sessionCookies.js';
|
||||||
|
|
||||||
// Initialize backup scheduler on server startup
|
// Initialize backup scheduler on server startup
|
||||||
initBackupScheduler();
|
initBackupScheduler();
|
||||||
@@ -20,13 +24,6 @@ function isPublicPath(pathname: string): boolean {
|
|||||||
const ACCESS_TOKEN_COOKIE = 'access_token';
|
const ACCESS_TOKEN_COOKIE = 'access_token';
|
||||||
const REFRESH_TOKEN_COOKIE = 'refresh_token';
|
const REFRESH_TOKEN_COOKIE = 'refresh_token';
|
||||||
|
|
||||||
const COOKIE_BASE = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax' as const,
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handle: Handle = async ({ event, resolve }) => {
|
export const handle: Handle = async ({ event, resolve }) => {
|
||||||
// Initialize locals
|
// Initialize locals
|
||||||
event.locals.user = null;
|
event.locals.user = null;
|
||||||
@@ -68,15 +65,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
const user = await userService.findById(userIdFromCookie);
|
const user = await userService.findById(userIdFromCookie);
|
||||||
const tokens = await authService.rotateTokens(user.id, user.email, user.role);
|
const tokens = await authService.rotateTokens(user.id, user.email, user.role);
|
||||||
|
|
||||||
// Set new cookies
|
setRotatedCookies(event.cookies, tokens);
|
||||||
event.cookies.set(ACCESS_TOKEN_COOKIE, tokens.accessToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 900 // 15 minutes
|
|
||||||
});
|
|
||||||
event.cookies.set(REFRESH_TOKEN_COOKIE, tokens.refreshToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800 // 7 days
|
|
||||||
});
|
|
||||||
|
|
||||||
event.locals.user = {
|
event.locals.user = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
@@ -92,9 +81,7 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Refresh failed — clear stale cookies
|
// Refresh failed — clear stale cookies
|
||||||
event.cookies.delete(ACCESS_TOKEN_COOKIE, { path: '/' });
|
clearSessionCookies(event.cookies);
|
||||||
event.cookies.delete(REFRESH_TOKEN_COOKIE, { path: '/' });
|
|
||||||
event.cookies.delete('refresh_user_id', { path: '/' });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,8 +157,9 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to login page
|
// Auto-logged in via cookies. Use a full navigation (not goto) so
|
||||||
goto('/login');
|
// hooks.server.ts re-runs and populates locals.user from the new cookies.
|
||||||
|
window.location.href = '/';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type { Cookies } from '@sveltejs/kit';
|
||||||
|
import * as authService from '$lib/server/services/authService.js';
|
||||||
|
|
||||||
|
export const ACCESS_TOKEN_TTL_SEC = 900; // 15 minutes
|
||||||
|
export const REFRESH_TOKEN_TTL_SEC = 604800; // 7 days
|
||||||
|
|
||||||
|
function isHttpsOrigin(): boolean {
|
||||||
|
const origin = process.env.ORIGIN;
|
||||||
|
if (origin) return origin.startsWith('https://');
|
||||||
|
// Fall back to NODE_ENV only when ORIGIN is unset.
|
||||||
|
return process.env.NODE_ENV === 'production';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared cookie attributes (httpOnly, secure, sameSite=lax, path=/).
|
||||||
|
* `secure` is derived from ORIGIN (https://...) rather than NODE_ENV so
|
||||||
|
* plain-HTTP production deployments don't silently drop cookies.
|
||||||
|
*/
|
||||||
|
export function cookieBase() {
|
||||||
|
return {
|
||||||
|
httpOnly: true,
|
||||||
|
secure: isHttpsOrigin(),
|
||||||
|
sameSite: 'lax' as const,
|
||||||
|
path: '/'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SessionUser {
|
||||||
|
readonly id: string;
|
||||||
|
readonly email: string;
|
||||||
|
readonly role: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Issue access + refresh cookies for a freshly-authenticated user.
|
||||||
|
* Persists a new refresh token in the DB.
|
||||||
|
*/
|
||||||
|
export async function issueSessionCookies(cookies: Cookies, user: SessionUser): Promise<void> {
|
||||||
|
const accessToken = authService.signAccessToken({
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role
|
||||||
|
});
|
||||||
|
const refreshToken = authService.generateRefreshToken();
|
||||||
|
await authService.saveRefreshToken(user.id, refreshToken);
|
||||||
|
|
||||||
|
const base = cookieBase();
|
||||||
|
cookies.set('access_token', accessToken, { ...base, maxAge: ACCESS_TOKEN_TTL_SEC });
|
||||||
|
cookies.set('refresh_token', refreshToken, { ...base, maxAge: REFRESH_TOKEN_TTL_SEC });
|
||||||
|
cookies.set('refresh_user_id', user.id, { ...base, maxAge: REFRESH_TOKEN_TTL_SEC });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set access + refresh cookies from a pre-generated token pair (used by refresh rotation).
|
||||||
|
* Does not touch the DB.
|
||||||
|
*/
|
||||||
|
export function setRotatedCookies(
|
||||||
|
cookies: Cookies,
|
||||||
|
tokens: { readonly accessToken: string; readonly refreshToken: string }
|
||||||
|
): void {
|
||||||
|
const base = cookieBase();
|
||||||
|
cookies.set('access_token', tokens.accessToken, { ...base, maxAge: ACCESS_TOKEN_TTL_SEC });
|
||||||
|
cookies.set('refresh_token', tokens.refreshToken, { ...base, maxAge: REFRESH_TOKEN_TTL_SEC });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearSessionCookies(cookies: Cookies): void {
|
||||||
|
cookies.delete('access_token', { path: '/' });
|
||||||
|
cookies.delete('refresh_token', { path: '/' });
|
||||||
|
cookies.delete('refresh_user_id', { path: '/' });
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { success, error } from '$lib/server/utils/response.js';
|
|||||||
import * as onboardingService from '$lib/server/services/onboardingService.js';
|
import * as onboardingService from '$lib/server/services/onboardingService.js';
|
||||||
import * as userService from '$lib/server/services/userService.js';
|
import * as userService from '$lib/server/services/userService.js';
|
||||||
import * as boardService from '$lib/server/services/boardService.js';
|
import * as boardService from '$lib/server/services/boardService.js';
|
||||||
|
import { issueSessionCookies } from '$lib/server/utils/sessionCookies.js';
|
||||||
import { prisma } from '$lib/server/prisma.js';
|
import { prisma } from '$lib/server/prisma.js';
|
||||||
import { DEFAULTS } from '$lib/utils/constants.js';
|
import { DEFAULTS } from '$lib/utils/constants.js';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -31,7 +32,7 @@ export const GET: RequestHandler = async () => {
|
|||||||
* POST /api/onboarding — Complete an onboarding step.
|
* POST /api/onboarding — Complete an onboarding step.
|
||||||
* No auth required (onboarding runs before any user exists).
|
* No auth required (onboarding runs before any user exists).
|
||||||
*/
|
*/
|
||||||
export const POST: RequestHandler = async ({ request }) => {
|
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||||
let body: unknown;
|
let body: unknown;
|
||||||
try {
|
try {
|
||||||
body = await request.json();
|
body = await request.json();
|
||||||
@@ -69,6 +70,12 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
return json(error('Invalid admin account data'), { status: 400 });
|
return json(error('Invalid admin account data'), { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent concurrent admin creation during the onboarding window.
|
||||||
|
const existingAdmins = await prisma.user.count({ where: { role: 'admin' } });
|
||||||
|
if (existingAdmins > 0) {
|
||||||
|
return json(error('Admin account already created'), { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await userService.create({
|
const user = await userService.create({
|
||||||
email: adminData.data.email,
|
email: adminData.data.email,
|
||||||
password: adminData.data.password,
|
password: adminData.data.password,
|
||||||
@@ -166,7 +173,32 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await onboardingService.completeOnboarding();
|
// Atomically flip onboardingComplete false→true so concurrent callers
|
||||||
|
// can't both complete onboarding.
|
||||||
|
const flipped = await prisma.systemSettings.updateMany({
|
||||||
|
where: { id: DEFAULTS.SYSTEM_SETTINGS_ID, onboardingComplete: false },
|
||||||
|
data: { onboardingComplete: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (flipped.count === 0) {
|
||||||
|
// Either no SystemSettings row yet, or another request already completed.
|
||||||
|
await onboardingService.completeOnboarding();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-login the admin user who just completed onboarding.
|
||||||
|
// At this point isOnboardingNeeded() was true, so exactly one admin exists
|
||||||
|
// (enforced by the guard in the 'admin' step).
|
||||||
|
const adminUser = await prisma.user.findFirst({
|
||||||
|
where: { role: 'admin' },
|
||||||
|
orderBy: { createdAt: 'asc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!adminUser) {
|
||||||
|
return json(error('Onboarding complete but no admin user found'), { status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await issueSessionCookies(cookies, adminUser);
|
||||||
|
|
||||||
return json(success({ complete: true }));
|
return json(success({ complete: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { redirect, error } from '@sveltejs/kit';
|
import { redirect, error } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types.js';
|
import type { RequestHandler } from './$types.js';
|
||||||
import * as oauthService from '$lib/server/services/oauthService.js';
|
import * as oauthService from '$lib/server/services/oauthService.js';
|
||||||
|
import { cookieBase } from '$lib/server/utils/sessionCookies.js';
|
||||||
const COOKIE_BASE = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax' as const,
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ cookies, url }) => {
|
export const GET: RequestHandler = async ({ cookies, url }) => {
|
||||||
try {
|
try {
|
||||||
@@ -19,13 +13,14 @@ export const GET: RequestHandler = async ({ cookies, url }) => {
|
|||||||
const codeChallenge = await oauthService.calculateCodeChallenge(codeVerifier);
|
const codeChallenge = await oauthService.calculateCodeChallenge(codeVerifier);
|
||||||
const state = oauthService.generateState();
|
const state = oauthService.generateState();
|
||||||
|
|
||||||
|
const base = cookieBase();
|
||||||
// Store code_verifier and state in HTTP-only cookies for the callback
|
// Store code_verifier and state in HTTP-only cookies for the callback
|
||||||
cookies.set('oauth_code_verifier', codeVerifier, {
|
cookies.set('oauth_code_verifier', codeVerifier, {
|
||||||
...COOKIE_BASE,
|
...base,
|
||||||
maxAge: 600 // 10 minutes — enough for the auth flow
|
maxAge: 600 // 10 minutes — enough for the auth flow
|
||||||
});
|
});
|
||||||
cookies.set('oauth_state', state, {
|
cookies.set('oauth_state', state, {
|
||||||
...COOKIE_BASE,
|
...base,
|
||||||
maxAge: 600
|
maxAge: 600
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,7 @@ import { redirect, error } from '@sveltejs/kit';
|
|||||||
import type { RequestHandler } from './$types.js';
|
import type { RequestHandler } from './$types.js';
|
||||||
import * as oauthService from '$lib/server/services/oauthService.js';
|
import * as oauthService from '$lib/server/services/oauthService.js';
|
||||||
import * as userService from '$lib/server/services/userService.js';
|
import * as userService from '$lib/server/services/userService.js';
|
||||||
import * as authService from '$lib/server/services/authService.js';
|
import { issueSessionCookies } from '$lib/server/utils/sessionCookies.js';
|
||||||
|
|
||||||
const COOKIE_BASE = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax' as const,
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ url, cookies }) => {
|
export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||||
try {
|
try {
|
||||||
@@ -58,28 +51,7 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
|
|||||||
groups: userInfo.groups ? [...userInfo.groups] : undefined
|
groups: userInfo.groups ? [...userInfo.groups] : undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// Issue local JWT tokens (same as local auth flow)
|
await issueSessionCookies(cookies, user);
|
||||||
const accessToken = authService.signAccessToken({
|
|
||||||
userId: user.id,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role
|
|
||||||
});
|
|
||||||
const refreshToken = authService.generateRefreshToken();
|
|
||||||
await authService.saveRefreshToken(user.id, refreshToken);
|
|
||||||
|
|
||||||
// Set session cookies
|
|
||||||
cookies.set('access_token', accessToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 900 // 15 minutes
|
|
||||||
});
|
|
||||||
cookies.set('refresh_token', refreshToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800 // 7 days
|
|
||||||
});
|
|
||||||
cookies.set('refresh_user_id', user.id, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800 // 7 days
|
|
||||||
});
|
|
||||||
|
|
||||||
throw redirect(302, '/');
|
throw redirect(302, '/');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import type { RequestHandler } from './$types.js';
|
|||||||
import * as authService from '$lib/server/services/authService.js';
|
import * as authService from '$lib/server/services/authService.js';
|
||||||
import * as userService from '$lib/server/services/userService.js';
|
import * as userService from '$lib/server/services/userService.js';
|
||||||
import { error as apiError } from '$lib/server/utils/response.js';
|
import { error as apiError } from '$lib/server/utils/response.js';
|
||||||
|
import {
|
||||||
const COOKIE_BASE = {
|
ACCESS_TOKEN_TTL_SEC,
|
||||||
httpOnly: true,
|
clearSessionCookies,
|
||||||
secure: process.env.NODE_ENV === 'production',
|
setRotatedCookies
|
||||||
sameSite: 'lax' as const,
|
} from '$lib/server/utils/sessionCookies.js';
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ cookies }) => {
|
export const POST: RequestHandler = async ({ cookies }) => {
|
||||||
const refreshToken = cookies.get('refresh_token');
|
const refreshToken = cookies.get('refresh_token');
|
||||||
@@ -22,34 +20,22 @@ export const POST: RequestHandler = async ({ cookies }) => {
|
|||||||
try {
|
try {
|
||||||
const isValid = await authService.validateRefreshToken(userId, refreshToken);
|
const isValid = await authService.validateRefreshToken(userId, refreshToken);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
// Clear stale cookies
|
clearSessionCookies(cookies);
|
||||||
cookies.delete('access_token', { path: '/' });
|
|
||||||
cookies.delete('refresh_token', { path: '/' });
|
|
||||||
cookies.delete('refresh_user_id', { path: '/' });
|
|
||||||
return json(apiError('Invalid or expired refresh token'), { status: 401 });
|
return json(apiError('Invalid or expired refresh token'), { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await userService.findById(userId);
|
const user = await userService.findById(userId);
|
||||||
const tokens = await authService.rotateTokens(user.id, user.email, user.role);
|
const tokens = await authService.rotateTokens(user.id, user.email, user.role);
|
||||||
|
|
||||||
cookies.set('access_token', tokens.accessToken, {
|
setRotatedCookies(cookies, tokens);
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 900
|
|
||||||
});
|
|
||||||
cookies.set('refresh_token', tokens.refreshToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800
|
|
||||||
});
|
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { expiresIn: 900 },
|
data: { expiresIn: ACCESS_TOKEN_TTL_SEC },
|
||||||
error: null
|
error: null
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
cookies.delete('access_token', { path: '/' });
|
clearSessionCookies(cookies);
|
||||||
cookies.delete('refresh_token', { path: '/' });
|
|
||||||
cookies.delete('refresh_user_id', { path: '/' });
|
|
||||||
return json(apiError('Token refresh failed'), { status: 401 });
|
return json(apiError('Token refresh failed'), { status: 401 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,17 +5,11 @@ import { fail, redirect } from '@sveltejs/kit';
|
|||||||
import { loginSchema } from '$lib/utils/validators.js';
|
import { loginSchema } from '$lib/utils/validators.js';
|
||||||
import * as userService from '$lib/server/services/userService.js';
|
import * as userService from '$lib/server/services/userService.js';
|
||||||
import * as authService from '$lib/server/services/authService.js';
|
import * as authService from '$lib/server/services/authService.js';
|
||||||
|
import { issueSessionCookies } from '$lib/server/utils/sessionCookies.js';
|
||||||
import { prisma } from '$lib/server/prisma.js';
|
import { prisma } from '$lib/server/prisma.js';
|
||||||
import { DEFAULTS } from '$lib/utils/constants.js';
|
import { DEFAULTS } from '$lib/utils/constants.js';
|
||||||
import type { AuthMode } from '$lib/utils/constants.js';
|
import type { AuthMode } from '$lib/utils/constants.js';
|
||||||
|
|
||||||
const COOKIE_BASE = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax' as const,
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals }) => {
|
export const load: PageServerLoad = async ({ locals }) => {
|
||||||
// If already logged in, redirect to home
|
// If already logged in, redirect to home
|
||||||
if (locals.user) {
|
if (locals.user) {
|
||||||
@@ -43,13 +37,11 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
const { email, password } = form.data;
|
const { email, password } = form.data;
|
||||||
|
|
||||||
// Find user by email
|
|
||||||
const user = await userService.findByEmail(email);
|
const user = await userService.findByEmail(email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return setError(form, 'email', 'Invalid email or password');
|
return setError(form, 'email', 'Invalid email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
|
||||||
if (!user.password) {
|
if (!user.password) {
|
||||||
return setError(form, 'email', 'This account does not use password authentication');
|
return setError(form, 'email', 'This account does not use password authentication');
|
||||||
}
|
}
|
||||||
@@ -59,28 +51,7 @@ export const actions: Actions = {
|
|||||||
return setError(form, 'email', 'Invalid email or password');
|
return setError(form, 'email', 'Invalid email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate tokens
|
await issueSessionCookies(cookies, user);
|
||||||
const accessToken = authService.signAccessToken({
|
|
||||||
userId: user.id,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role
|
|
||||||
});
|
|
||||||
const refreshToken = authService.generateRefreshToken();
|
|
||||||
await authService.saveRefreshToken(user.id, refreshToken);
|
|
||||||
|
|
||||||
// Set cookies
|
|
||||||
cookies.set('access_token', accessToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 900 // 15 minutes
|
|
||||||
});
|
|
||||||
cookies.set('refresh_token', refreshToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800 // 7 days
|
|
||||||
});
|
|
||||||
cookies.set('refresh_user_id', user.id, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800 // 7 days
|
|
||||||
});
|
|
||||||
|
|
||||||
throw redirect(302, '/');
|
throw redirect(302, '/');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,10 @@ import { fail, redirect, error } from '@sveltejs/kit';
|
|||||||
import { registerSchema } from '$lib/utils/validators.js';
|
import { registerSchema } from '$lib/utils/validators.js';
|
||||||
import { prisma } from '$lib/server/prisma.js';
|
import { prisma } from '$lib/server/prisma.js';
|
||||||
import * as userService from '$lib/server/services/userService.js';
|
import * as userService from '$lib/server/services/userService.js';
|
||||||
import * as authService from '$lib/server/services/authService.js';
|
|
||||||
import * as groupService from '$lib/server/services/groupService.js';
|
import * as groupService from '$lib/server/services/groupService.js';
|
||||||
|
import { issueSessionCookies } from '$lib/server/utils/sessionCookies.js';
|
||||||
import { DEFAULTS } from '$lib/utils/constants.js';
|
import { DEFAULTS } from '$lib/utils/constants.js';
|
||||||
|
|
||||||
const COOKIE_BASE = {
|
|
||||||
httpOnly: true,
|
|
||||||
secure: process.env.NODE_ENV === 'production',
|
|
||||||
sameSite: 'lax' as const,
|
|
||||||
path: '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
async function isRegistrationEnabled(): Promise<boolean> {
|
async function isRegistrationEnabled(): Promise<boolean> {
|
||||||
const settings = await prisma.systemSettings.findUnique({
|
const settings = await prisma.systemSettings.findUnique({
|
||||||
where: { id: DEFAULTS.SYSTEM_SETTINGS_ID },
|
where: { id: DEFAULTS.SYSTEM_SETTINGS_ID },
|
||||||
@@ -72,28 +65,7 @@ export const actions: Actions = {
|
|||||||
// Add user to default groups
|
// Add user to default groups
|
||||||
await groupService.addUserToDefaultGroups(user.id);
|
await groupService.addUserToDefaultGroups(user.id);
|
||||||
|
|
||||||
// Auto-login: generate tokens
|
await issueSessionCookies(cookies, user);
|
||||||
const accessToken = authService.signAccessToken({
|
|
||||||
userId: user.id,
|
|
||||||
email: user.email,
|
|
||||||
role: user.role
|
|
||||||
});
|
|
||||||
const refreshToken = authService.generateRefreshToken();
|
|
||||||
await authService.saveRefreshToken(user.id, refreshToken);
|
|
||||||
|
|
||||||
// Set cookies
|
|
||||||
cookies.set('access_token', accessToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 900
|
|
||||||
});
|
|
||||||
cookies.set('refresh_token', refreshToken, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800
|
|
||||||
});
|
|
||||||
cookies.set('refresh_user_id', user.id, {
|
|
||||||
...COOKIE_BASE,
|
|
||||||
maxAge: 604800
|
|
||||||
});
|
|
||||||
|
|
||||||
throw redirect(302, '/');
|
throw redirect(302, '/');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user