Some checks failed
Deploy / Deploy Production (push) Has been skipped
Deploy / Cleanup Preview (push) Has been skipped
Deploy / Deploy Preview (push) Failing after 0s
CI / Scraper / Test (pull_request) Successful in 11s
CI / UI / Build (pull_request) Failing after 14s
CI / Scraper / Lint (pull_request) Successful in 23s
CI / Scraper / Build (pull_request) Successful in 24s
iOS CI / Build (push) Has been cancelled
iOS CI / Test (push) Has been cancelled
iOS CI / Build (pull_request) Failing after 2s
iOS CI / Test (pull_request) Has been skipped
- iOS SwiftUI app (ios/LibNovel/) targeting iOS 17+, generated via xcodegen - Full feature set: auth, home, library, book detail, chapter reader, browse, audio player, profile - Kingfisher for image loading, swift-markdown-ui for chapter rendering - Base URL: https://v2.libnovel.kalekber.cc - SvelteKit JSON API routes (ui/src/routes/api/) for iOS consumption: auth/login, auth/register, auth/me, auth/logout, auth/change-password, home, library, book/[slug], chapter/[slug]/[n], search, ranking, progress/[slug], presign/audio (updated) - Gitea Actions CI: .gitea/workflows/ios.yaml (build + test on macos-latest) - justfile: ios-gen, ios-build, ios-test recipes
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
import { json, error } from '@sveltejs/kit';
|
|
import type { RequestHandler } from './$types';
|
|
import { createUser, mergeSessionProgress, createUserSession } from '$lib/server/pocketbase';
|
|
import { createAuthToken } from '../../../../hooks.server';
|
|
import { log } from '$lib/server/logger';
|
|
import { randomBytes } from 'node:crypto';
|
|
|
|
const AUTH_COOKIE = 'libnovel_auth';
|
|
const ONE_YEAR = 60 * 60 * 24 * 365;
|
|
|
|
/**
|
|
* POST /api/auth/register
|
|
* Body: { username: string, password: string }
|
|
* Returns: { token: string, user: { id, username, role } }
|
|
*
|
|
* Sets the libnovel_auth cookie and returns the raw token value so the
|
|
* iOS app can persist it for subsequent requests.
|
|
*/
|
|
export const POST: RequestHandler = async ({ request, cookies, locals }) => {
|
|
let body: { username?: string; password?: string };
|
|
try {
|
|
body = await request.json();
|
|
} catch {
|
|
error(400, 'Invalid JSON body');
|
|
}
|
|
|
|
const username = (body.username ?? '').trim();
|
|
const password = body.password ?? '';
|
|
|
|
if (!username || !password) {
|
|
error(400, 'Username and password are required');
|
|
}
|
|
if (username.length < 3 || username.length > 32) {
|
|
error(400, 'Username must be between 3 and 32 characters');
|
|
}
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
|
error(400, 'Username may only contain letters, numbers, underscores and hyphens');
|
|
}
|
|
if (password.length < 8) {
|
|
error(400, 'Password must be at least 8 characters');
|
|
}
|
|
|
|
let user;
|
|
try {
|
|
user = await createUser(username, password);
|
|
} catch (e: unknown) {
|
|
const msg = e instanceof Error ? e.message : 'Registration failed.';
|
|
if (msg.includes('Username already taken')) {
|
|
error(409, 'That username is already taken');
|
|
}
|
|
log.error('api/auth/register', 'unexpected error', { username, err: String(e) });
|
|
error(500, 'An error occurred. Please try again.');
|
|
}
|
|
|
|
// Merge anonymous session progress (non-fatal)
|
|
mergeSessionProgress(locals.sessionId, user.id).catch((e) =>
|
|
log.warn('api/auth/register', 'mergeSessionProgress failed (non-fatal)', { err: String(e) })
|
|
);
|
|
|
|
const authSessionId = randomBytes(16).toString('hex');
|
|
|
|
const userAgent = request.headers.get('user-agent') ?? '';
|
|
const ip =
|
|
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??
|
|
request.headers.get('x-real-ip') ??
|
|
'';
|
|
createUserSession(user.id, authSessionId, userAgent, ip).catch((e) =>
|
|
log.warn('api/auth/register', 'createUserSession failed (non-fatal)', { err: String(e) })
|
|
);
|
|
|
|
const token = createAuthToken(user.id, user.username, user.role ?? 'user', authSessionId);
|
|
|
|
cookies.set(AUTH_COOKIE, token, {
|
|
path: '/',
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
maxAge: ONE_YEAR
|
|
});
|
|
|
|
return json({
|
|
token,
|
|
user: { id: user.id, username: user.username, role: user.role ?? 'user' }
|
|
});
|
|
};
|