diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte
new file mode 100644
index 00000000..ba6ecd61
--- /dev/null
+++ b/src/routes/about/+page.svelte
@@ -0,0 +1,9 @@
+
+
+{@html data.seo}
+
+
{m.hello_world({ name: 'World' })}
+{JSON.stringify(data, null, 2)}
diff --git a/src/routes/about/+page.ts b/src/routes/about/+page.ts
new file mode 100644
index 00000000..61e3d993
--- /dev/null
+++ b/src/routes/about/+page.ts
@@ -0,0 +1,6 @@
+import { getJson } from '$lib/api'
+
+export const load = async ({ fetch }) => {
+ const status = await getJson('/api/v1/version', fetch)
+ return { status }
+}
diff --git a/src/routes/auth/login/+server.ts b/src/routes/auth/login/+server.ts
new file mode 100644
index 00000000..17dc9120
--- /dev/null
+++ b/src/routes/auth/login/+server.ts
@@ -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 })
+ }
+}
diff --git a/src/routes/auth/logout/+server.ts b/src/routes/auth/logout/+server.ts
new file mode 100644
index 00000000..6aca8f1c
--- /dev/null
+++ b/src/routes/auth/logout/+server.ts
@@ -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 })
+}
diff --git a/src/routes/auth/refresh/+server.ts b/src/routes/auth/refresh/+server.ts
new file mode 100644
index 00000000..25ad6433
--- /dev/null
+++ b/src/routes/auth/refresh/+server.ts
@@ -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()
+ })
+}
diff --git a/src/routes/login/+page.server.ts b/src/routes/login/+page.server.ts
new file mode 100644
index 00000000..710ee296
--- /dev/null
+++ b/src/routes/login/+page.server.ts
@@ -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 })
+ }
+}
diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte
new file mode 100644
index 00000000..dc6f30a6
--- /dev/null
+++ b/src/routes/login/+page.svelte
@@ -0,0 +1,23 @@
+
+
+Login
+
+
diff --git a/src/routes/me/+page.server.ts b/src/routes/me/+page.server.ts
new file mode 100644
index 00000000..e9a76175
--- /dev/null
+++ b/src/routes/me/+page.server.ts
@@ -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
+ }
+}
diff --git a/src/routes/me/+page.svelte b/src/routes/me/+page.svelte
new file mode 100644
index 00000000..b1ecd5e7
--- /dev/null
+++ b/src/routes/me/+page.svelte
@@ -0,0 +1,31 @@
+
+
+Welcome, {data.account.username}!
+
+
+ Account
+
+ - User ID: {data.account.userId}
+ - Role: {data.account.role}
+
+
+
+ Preferences
+
+ - Language: {data.user.language}
+ - Theme: {data.user.theme}
+ - Gender: {data.user.gender}
+ - Element: {data.user.element}
+ - Picture: {data.user.picture}
+
+
+
+
+