Migrated auth to Nitro
This commit is contained in:
1 parent
27feb1ce95
commit
eb4e9f0700
16 files changed
+517
-129
No files matched your search
+51
-124
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user