Starter routes (including login and logout)

This commit is contained in:
Justin Edmund 2025-09-09 03:19:12 -07:00
parent e988b02f0c
commit f221c12f40
9 changed files with 246 additions and 0 deletions

View file

@ -0,0 +1,9 @@
<script lang="ts">
import { m } from '$lib/paraglide/messages'
export let data: { status: unknown; seo: string }
</script>
<svelte:head>{@html data.seo}</svelte:head>
<h1>{m.hello_world({ name: 'World' })}</h1>
<pre>{JSON.stringify(data, null, 2)}</pre>

View file

@ -0,0 +1,6 @@
import { getJson } from '$lib/api'
export const load = async ({ fetch }) => {
const status = await getJson<any>('/api/v1/version', fetch)
return { status }
}

View file

@ -0,0 +1,47 @@
import type { RequestHandler } from '@sveltejs/kit'
import { json } from '@sveltejs/kit'
import { z } from 'zod'
import { passwordGrantLogin } from '$lib/auth/oauth'
import { users } from '$lib/api/resources/users'
import { buildCookies } from '$lib/auth/map'
import { setAccountCookie, setUserCookie, setRefreshCookie } from '$lib/auth/cookies'
const LoginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
grant_type: z.literal('password')
})
export const POST: RequestHandler = async ({ request, cookies, url, fetch }) => {
const raw = await request.json().catch(() => ({}))
const parsed = LoginSchema.safeParse(raw)
if (!parsed.success) {
const details = parsed.error.flatten((i) => i.message)
return json({ error: 'Validation error', details }, { status: 400 })
}
try {
const oauth = await passwordGrantLogin(fetch, parsed.data)
const info = await users.info(fetch, oauth.user.username, {
headers: {
Authorization: `Bearer ${oauth.access_token}`
}
})
const { account, user, accessTokenExpiresAt, refresh } = buildCookies(oauth, info)
const secure = url.protocol === 'https:'
setAccountCookie(cookies, account, { secure, expires: accessTokenExpiresAt })
setUserCookie(cookies, user, { secure, expires: accessTokenExpiresAt })
setRefreshCookie(cookies, refresh, { secure, expires: accessTokenExpiresAt })
return json({ success: true, user: { username: info.username, avatar: info.avatar } })
} catch (e: any) {
if (String(e?.message) === 'unauthorized') {
return json({ error: 'Invalid email or password' }, { status: 401 })
}
return json({ error: 'Failed to login' }, { status: 502 })
}
}

View file

@ -0,0 +1,8 @@
import type { RequestHandler } from '@sveltejs/kit'
import { json } from '@sveltejs/kit'
export const POST: RequestHandler = async ({ cookies }) => {
cookies.delete('account', { path: '/' })
cookies.delete('user', { path: '/' })
return json({ success: true })
}

View file

@ -0,0 +1,73 @@
import type { RequestHandler } from '@sveltejs/kit'
import { json } from '@sveltejs/kit'
import { PUBLIC_SIERO_OAUTH_URL } from '$env/static/public'
import {
getRefreshFromCookies,
setAccountCookie,
setRefreshCookie,
clearAuthCookies
} from '$lib/auth/cookies'
type OAuthRefreshResponse = {
access_token: string
token_type: 'Bearer'
expires_in: number
refresh_token: string
created_at: number
user: {
id: string
username: string
role: number
}
}
export const POST: RequestHandler = async ({ cookies, fetch, url }) => {
const refresh = getRefreshFromCookies(cookies)
if (!refresh) {
return json({ error: 'no_refresh_token' }, { status: 401 })
}
const res = await fetch(`${PUBLIC_SIERO_OAUTH_URL}/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
refresh_token: refresh,
grant_type: 'refresh_token'
})
})
if (res.status === 401) {
clearAuthCookies(cookies)
return json({ error: 'refresh_unauthorized' }, { status: 401 })
}
if (!res.ok) {
return json({ error: 'refresh_failed' }, { status: 502 })
}
const data = (await res.json()) as OAuthRefreshResponse
const secure = url.protocol === 'https:'
const accessTokenExpiresAt = new Date((data.created_at + data.expires_in) * 1000)
setAccountCookie(
cookies,
{
userId: data.user.id,
username: data.user.username,
token: data.access_token,
role: data.user.role
},
{
secure,
expires: accessTokenExpiresAt
}
)
setRefreshCookie(cookies, data.refresh_token, { secure })
return json({
success: true,
username: data.user.username,
expires_at: accessTokenExpiresAt.toISOString()
})
}

View file

@ -0,0 +1,34 @@
import type { Actions, PageServerLoad } from './$types'
import { fail, redirect } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ locals, url }) => {
if (locals.session.isAuthenticated) {
throw redirect(302, url.searchParams.get('next') ?? '/me')
}
return {}
}
export const actions: Actions = {
default: async ({ request, fetch, url }) => {
const form = await request.formData()
const email = String(form.get('email') ?? '')
const password = String(form.get('password') ?? '')
if (!email || !password) {
return fail(400, { error: 'Email and password are required', email })
}
const res = await fetch('/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, grant_type: 'password' })
})
if (res.ok) {
throw redirect(303, url.searchParams.get('next') ?? '/me')
}
const j = await res.json().catch(() => ({}))
return fail(res.status, { error: j.error ?? 'Login failed', email })
}
}

View file

@ -0,0 +1,23 @@
<script lang="ts">
export let form: { error: string; email: string } | undefined
</script>
<h1>Login</h1>
<form method="post">
<label>
Email address
<input type="email" name="email" value={form?.email ?? ''} autocomplete="email" required />
</label>
<label>
Password
<input type="password" name="password" minlength="8" autocomplete="current-password" required />
</label>
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<button type="submit">Sign in</button>
</form>

View file

@ -0,0 +1,15 @@
import type { PageServerLoad } from './$types'
import { redirect } from '@sveltejs/kit'
export const load: PageServerLoad = async ({ parent }) => {
const { isAuthenticated, account, currentUser } = await parent()
if (!isAuthenticated) {
throw redirect(302, '/login?next=/me')
}
return {
account,
user: currentUser
}
}

View file

@ -0,0 +1,31 @@
<script lang="ts">
import type { AccountCookie } from '$lib/types/AccountCookie'
import type { UserCookie } from '$lib/types/UserCookie'
export let data: { account: AccountCookie; user: UserCookie }
</script>
<h1>Welcome, {data.account.username}!</h1>
<section>
<h2>Account</h2>
<ul>
<li><strong>User ID:</strong> {data.account.userId}</li>
<li><strong>Role:</strong> {data.account.role}</li>
</ul>
<section>
<h2>Preferences</h2>
<ul>
<li><strong>Language:</strong> {data.user.language}</li>
<li><strong>Theme:</strong> {data.user.theme}</li>
<li><strong>Gender:</strong> {data.user.gender}</li>
<li><strong>Element:</strong> {data.user.element}</li>
<li><strong>Picture:</strong> {data.user.picture}</li>
</ul>
</section>
<form method="post" action="/auth/logout">
<button>Log out</button>
</form>
</section>