Starter routes (including login and logout)
This commit is contained in:
parent
e988b02f0c
commit
f221c12f40
9 changed files with 246 additions and 0 deletions
9
src/routes/about/+page.svelte
Normal file
9
src/routes/about/+page.svelte
Normal 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>
|
||||
6
src/routes/about/+page.ts
Normal file
6
src/routes/about/+page.ts
Normal 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 }
|
||||
}
|
||||
47
src/routes/auth/login/+server.ts
Normal file
47
src/routes/auth/login/+server.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
8
src/routes/auth/logout/+server.ts
Normal file
8
src/routes/auth/logout/+server.ts
Normal 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 })
|
||||
}
|
||||
73
src/routes/auth/refresh/+server.ts
Normal file
73
src/routes/auth/refresh/+server.ts
Normal 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()
|
||||
})
|
||||
}
|
||||
34
src/routes/login/+page.server.ts
Normal file
34
src/routes/login/+page.server.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
23
src/routes/login/+page.svelte
Normal file
23
src/routes/login/+page.svelte
Normal 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>
|
||||
15
src/routes/me/+page.server.ts
Normal file
15
src/routes/me/+page.server.ts
Normal 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
|
||||
}
|
||||
}
|
||||
31
src/routes/me/+page.svelte
Normal file
31
src/routes/me/+page.svelte
Normal 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>
|
||||
Loading…
Reference in a new issue