Migrated auth to Nitro

This commit is contained in:
lucast committed 2026-09-29 00:23:30 +02:00
1 parent 27feb1ce95
commit eb4e9f0700
16 files changed
+517 -129

No files matched your search

+51 -124
View File
@@ -1,147 +1,74 @@
import axios, { AxiosError } from "axios"
let refreshPromise: Promise<any> | null = null
let waitForPromise: boolean = false
const setCookie = (
name: string,
value: string,
options: {
maxAge?: number
path?: string
sameSite?: "lax" | "strict" | "none"
secure?: boolean
} = {},
) => {
let cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
if (options.maxAge !== undefined) {
cookie += `; Max-Age=${options.maxAge}`
}
cookie += `; Path=${options.path ?? "/"}`
cookie += `; SameSite=${options.sameSite ?? "Lax"}`
if (options.secure) cookie += "; Secure"
document.cookie = cookie
type SessionResponse = {
sessionToken: string
expireAt: string
}
export const useAuth = () => {
const refreshToken = useCookie<string | null>("refreshToken", {
sameSite: "lax",
})
const sessionToken = useCookie<string | null>("sessionToken", {
sameSite: "lax",
maxAge: 60 * 60, // 60 minutes
maxAge: 60 * 60,
})
const signin = async (
email: string,
password: string,
remember: boolean = false,
) => {
let refreshPromise: Promise<string | null> | null = null
const signin = async (email: string, password: string, remember = false) => {
try {
const res = await cfetch(
"/auth/signin",
{
method: "POST",
data: {
email,
password,
},
},
false,
)
// NOTE: Quite hacky... this should be changed.
// TODO: Add httpOnly and update the backend to support that.
if (remember) {
setCookie("refreshToken", res.data.refreshToken, {
maxAge: 60 * 60 * 24 * 30, // 30 days
})
} else {
setCookie("refreshToken", res.data.refreshToken)
}
//refreshToken.value = res.data.refreshToken
await refreshSessionToken()
await navigateTo("/")
} catch (e) {
if (axios.isAxiosError(e) && e.response?.data?.message) {
throw new Error(e.response.data.message)
}
throw new Error("Failed to sign in")
}
}
const signup = async (name: string, email: string, password: string) => {}
const signout = () => {
refreshToken.value = null
sessionToken.value = null
}
const refreshSessionToken = async () => {
if (waitForPromise) {
// Force it to wait up to 50 ms for it to set refreshPromise, hopefully less
for (let i = 0; i < 10; i++) {
if (refreshPromise != null) {
break
}
await new Promise((r) => setTimeout(r, 5))
}
}
waitForPromise = true
if (refreshPromise != null) {
waitForPromise = false
return refreshPromise
}
refreshPromise = cfetch(
"/auth/session",
{
data: {
refreshToken: refreshToken.value,
},
await $fetch("/api/auth/signin", {
method: "POST",
},
false,
)
.then((res) => {
const token = res.data.sessionToken
sessionToken.value = token
refreshPromise = null
return token
body: { email, password, remember: Boolean(remember) },
})
.catch((e) => {
refreshPromise = null
sessionToken.value = null
if (!(await refreshSessionToken())) throw new Error("Unable to create session")
await navigateTo("/")
} catch (error: any) {
throw new Error(error?.data?.message || error?.statusMessage || "Failed to sign in")
}
}
const signup = async (name: string, email: string, password: string) => {
try {
await $fetch("/api/auth/register", {
method: "POST",
body: { name, email, password },
})
if (!(await refreshSessionToken())) throw new Error("Unable to create session")
await navigateTo("/")
} catch (error: any) {
throw new Error(error?.data?.message || error?.statusMessage || "Failed to sign up")
}
}
const signout = async () => {
try {
await $fetch("/api/auth/signout", { method: "POST" })
} finally {
sessionToken.value = null
}
}
const refreshSessionToken = async (): Promise<string | null> => {
if (refreshPromise) return refreshPromise
refreshPromise = $fetch<SessionResponse>("/api/auth/session", {
method: "POST",
})
.then((response) => {
sessionToken.value = response.sessionToken
return response.sessionToken
})
.catch(() => {
sessionToken.value = null
return null
})
.finally(() => {
waitForPromise = false
refreshPromise = null
})
return refreshPromise
}
if (getCurrentInstance()) {
onMounted(() => {
if (refreshToken.value && !sessionToken.value) refreshSessionToken()
})
}
return {
refreshToken,
sessionToken,
signin,
signup,
+35 -4
View File
@@ -1,6 +1,37 @@
export default defineNuxtRouteMiddleware((to, from) => {
const auth = useAuth()
import { appendResponseHeader } from "h3"
if (!auth.refreshToken.value && !to.fullPath.includes("/auth"))
return navigateTo("/auth/signin")
export default defineNuxtRouteMiddleware(async (to) => {
if (to.path.startsWith("/auth")) return
const auth = useAuth()
try {
// Forward the incoming SSR credentials to this endpoint; the browser sends
// same-origin cookies automatically during client navigation.
const requestHeaders = import.meta.server
? useRequestHeaders(["cookie", "authorization"])
: undefined
const response = await $fetch.raw<{
authenticated: boolean
sessionToken?: string
}>("/api/auth/session", { headers: requestHeaders })
const session = response._data
// Nuxt forwards incoming request cookies automatically, but internal SSR
// fetch responses do not forward Set-Cookie to the browser automatically.
if (import.meta.server) {
const event = useRequestEvent()
if (event) {
for (const cookie of response.headers.getSetCookie()) {
appendResponseHeader(event, "set-cookie", cookie)
}
}
}
if (session?.sessionToken) auth.sessionToken.value = session.sessionToken
if (session?.authenticated) return
} catch {
// Treat unavailable auth state as unauthenticated.
}
return navigateTo("/auth/signin")
})
+1 -1
View File
@@ -10,7 +10,7 @@ const submit = async (e: any) => {
} catch {}
try {
await auth.signin(data.email, data.password, data.remember)
await auth.signin(data.email, data.password, data.remember === "on")
} catch (e: string | any) {
toast.show(e, "error")
}