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