Add auth helper libraries
This commit is contained in:
parent
76513f1fd5
commit
e988b02f0c
4 changed files with 153 additions and 0 deletions
82
src/lib/auth/cookies.ts
Normal file
82
src/lib/auth/cookies.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import type { Cookies } from '@sveltejs/kit'
|
||||||
|
import type { AccountCookie } from '$lib/types/AccountCookie'
|
||||||
|
import type { UserCookie } from '$lib/types/UserCookie'
|
||||||
|
|
||||||
|
export const ACCOUNT_COOKIE = 'account'
|
||||||
|
export const USER_COOKIE = 'user'
|
||||||
|
export const REFRESH_COOKIE = 'refresh'
|
||||||
|
const SIXTY_DAYS = 60 * 60 * 24 * 60
|
||||||
|
|
||||||
|
export function setAccountCookie(
|
||||||
|
cookies: Cookies,
|
||||||
|
data: AccountCookie,
|
||||||
|
{ secure, expires }: { secure: boolean; expires: Date }
|
||||||
|
) {
|
||||||
|
cookies.set(ACCOUNT_COOKIE, JSON.stringify(data), {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure,
|
||||||
|
expires,
|
||||||
|
maxAge: SIXTY_DAYS
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUserCookie(
|
||||||
|
cookies: Cookies,
|
||||||
|
data: UserCookie,
|
||||||
|
{ secure, expires }: { secure: boolean; expires: Date }
|
||||||
|
) {
|
||||||
|
cookies.set(USER_COOKIE, JSON.stringify(data), {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: false,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure,
|
||||||
|
expires,
|
||||||
|
maxAge: SIXTY_DAYS
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setRefreshCookie(
|
||||||
|
cookies: Cookies,
|
||||||
|
data: string,
|
||||||
|
{ secure, expires }: { secure: boolean; expires?: Date }
|
||||||
|
) {
|
||||||
|
cookies.set(REFRESH_COOKIE, data, {
|
||||||
|
path: '/',
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'lax',
|
||||||
|
secure,
|
||||||
|
...(expires ? { expires } : {})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAccountFromCookies(cookies: Cookies): AccountCookie | null {
|
||||||
|
const raw = cookies.get(ACCOUNT_COOKIE)
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as AccountCookie
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserFromCookies(cookies: Cookies): UserCookie | null {
|
||||||
|
const raw = cookies.get(USER_COOKIE)
|
||||||
|
if (!raw) return null
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw) as UserCookie
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRefreshFromCookies(cookies: Cookies): string | null {
|
||||||
|
return cookies.get(REFRESH_COOKIE) ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAuthCookies(cookies: Cookies) {
|
||||||
|
cookies.delete(ACCOUNT_COOKIE, { path: '/' })
|
||||||
|
cookies.delete(USER_COOKIE, { path: '/' })
|
||||||
|
cookies.delete(REFRESH_COOKIE, { path: '/' })
|
||||||
|
}
|
||||||
25
src/lib/auth/map.ts
Normal file
25
src/lib/auth/map.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { OAuthLoginResponse } from './oauth'
|
||||||
|
import type { UserInfoResponse } from '$lib/api/resources/users'
|
||||||
|
import type { AccountCookie } from '$lib/types/AccountCookie'
|
||||||
|
import type { UserCookie } from '$lib/types/UserCookie'
|
||||||
|
|
||||||
|
export function buildCookies(oauth: OAuthLoginResponse, info: UserInfoResponse) {
|
||||||
|
const accessTokenExpiresAt = new Date((oauth.created_at + oauth.expires_in) * 1000)
|
||||||
|
|
||||||
|
const account: AccountCookie = {
|
||||||
|
userId: info.id,
|
||||||
|
username: info.username,
|
||||||
|
token: oauth.access_token,
|
||||||
|
role: info.role
|
||||||
|
}
|
||||||
|
|
||||||
|
const user: UserCookie = {
|
||||||
|
picture: info.avatar.picture ?? '',
|
||||||
|
element: info.avatar.element ?? '',
|
||||||
|
language: info.language ?? 'en',
|
||||||
|
gender: info.gender ?? 0,
|
||||||
|
theme: info.theme ?? 'system'
|
||||||
|
}
|
||||||
|
|
||||||
|
return { account, user, accessTokenExpiresAt, refresh: oauth.refresh_token }
|
||||||
|
}
|
||||||
16
src/lib/auth/oauth.schema.ts
Normal file
16
src/lib/auth/oauth.schema.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const OAuthLoginResponseSchema = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
token_type: z.literal('Bearer'),
|
||||||
|
expires_in: z.number().int().positive(),
|
||||||
|
refresh_token: z.string(),
|
||||||
|
created_at: z.number().int().nonnegative(),
|
||||||
|
user: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
username: z.string(),
|
||||||
|
role: z.number().int()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
export type OAuthLoginResponse = z.infer<typeof OAuthLoginResponseSchema>
|
||||||
30
src/lib/auth/oauth.ts
Normal file
30
src/lib/auth/oauth.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type { FetchLike } from '$lib/api/core'
|
||||||
|
import { OAUTH_BASE } from '$lib/config'
|
||||||
|
|
||||||
|
export interface OAuthLoginResponse {
|
||||||
|
access_token: string
|
||||||
|
token_type: 'Bearer'
|
||||||
|
expires_in: number
|
||||||
|
refresh_token: string
|
||||||
|
created_at: number
|
||||||
|
user: {
|
||||||
|
id: string
|
||||||
|
username: string
|
||||||
|
role: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function passwordGrantLogin(
|
||||||
|
fetchFn: FetchLike,
|
||||||
|
body: { email: string; password: string; grant_type: 'password' }
|
||||||
|
): Promise<OAuthLoginResponse> {
|
||||||
|
const url = `${OAUTH_BASE}/token`
|
||||||
|
const res = await fetchFn(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(body)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.status === 401) throw new Error('unauthorized')
|
||||||
|
if (!res.ok) throw new Error(`oauth_error_${res.status}`)
|
||||||
|
return res.json() as Promise<OAuthLoginResponse>
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue