112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
import { error, isHttpError, json, type Cookies } from '@sveltejs/kit';
|
|
import { AuthError, SESSION_COOKIE, type AuthResult } from './index.js';
|
|
import { PasswordServiceBusyError } from './passwords.js';
|
|
import type { AuthUser } from '../../auth.js';
|
|
|
|
export function sessionCookie(url: URL): string {
|
|
return url.protocol === 'https:' ? SESSION_COOKIE : 'ec_session_dev';
|
|
}
|
|
export function requireUserTransport(url: URL): void {
|
|
if (url.protocol === 'https:') return;
|
|
if (
|
|
process.env.EC_AUTH_ALLOW_HTTP === 'true' &&
|
|
['localhost', '127.0.0.1', '[::1]', '::1'].includes(url.hostname)
|
|
)
|
|
return;
|
|
error(
|
|
426,
|
|
'Operator login requires HTTPS. For local development only, set EC_AUTH_ALLOW_HTTP=true and use localhost.'
|
|
);
|
|
}
|
|
export function requireSameOrigin(request: Request, url: URL): void {
|
|
if (
|
|
request.headers.get('origin') !== url.origin ||
|
|
request.headers.get('sec-fetch-site') === 'cross-site'
|
|
) {
|
|
error(403, 'This action requires a same-origin request.');
|
|
}
|
|
}
|
|
export function sessionResponse(result: AuthResult, cookies: Cookies, url: URL) {
|
|
cookies.set(sessionCookie(url), result.session.token, {
|
|
path: '/',
|
|
httpOnly: true,
|
|
secure: url.protocol === 'https:',
|
|
sameSite: 'lax',
|
|
expires: new Date(result.session.expiresAt)
|
|
});
|
|
return json({ user: result.user }, { headers: { 'Cache-Control': 'no-store' } });
|
|
}
|
|
export function requireUser(user: AuthUser | null): AuthUser {
|
|
if (!user) throw new AuthError('Sign in to continue.', 401);
|
|
return user;
|
|
}
|
|
export async function authBody(
|
|
request: Request,
|
|
maxBytes = 8192
|
|
): Promise<Record<string, unknown>> {
|
|
if (!request.headers.get('content-type')?.toLowerCase().startsWith('application/json'))
|
|
throw new AuthError('Send a JSON request.', 415);
|
|
if (Number(request.headers.get('content-length')) > maxBytes)
|
|
throw new AuthError('Request is too large.', 413);
|
|
const reader = request.body?.getReader();
|
|
if (!reader) throw new AuthError('A JSON object is required.');
|
|
const chunks: Uint8Array[] = [];
|
|
let length = 0;
|
|
try {
|
|
for (;;) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
length += value.byteLength;
|
|
if (length > maxBytes) {
|
|
void reader.cancel().catch(() => {});
|
|
throw new AuthError('Request is too large.', 413);
|
|
}
|
|
chunks.push(value);
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
let body: unknown;
|
|
try {
|
|
body = JSON.parse(Buffer.concat(chunks, length).toString('utf8'));
|
|
} catch {
|
|
throw new AuthError('Invalid JSON.');
|
|
}
|
|
if (!body || typeof body !== 'object' || Array.isArray(body))
|
|
throw new AuthError('A JSON object is required.');
|
|
return body as Record<string, unknown>;
|
|
}
|
|
export function authError(cause: unknown): Response {
|
|
if (isHttpError(cause))
|
|
return json(
|
|
{ error: cause.body.message },
|
|
{ status: cause.status, headers: { 'Cache-Control': 'no-store' } }
|
|
);
|
|
const message = cause instanceof Error ? cause.message : 'Authentication request failed.';
|
|
const status =
|
|
cause instanceof AuthError
|
|
? cause.status
|
|
: cause instanceof PasswordServiceBusyError
|
|
? 429
|
|
: message.startsWith('Use a password')
|
|
? 400
|
|
: 500;
|
|
return json(
|
|
{ error: status === 500 ? 'The authentication request could not be completed.' : message },
|
|
{
|
|
status,
|
|
headers: {
|
|
'Cache-Control': 'no-store',
|
|
...(status === 429
|
|
? {
|
|
'Retry-After':
|
|
cause instanceof PasswordServiceBusyError
|
|
? String(cause.retryAfterSeconds)
|
|
: '900'
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
);
|
|
}
|