Harden auth and outbound admin surfaces

This commit is contained in:
2026-05-23 21:12:45 +12:00
parent d9ac54a2ff
commit 1ce01ec348
15 changed files with 495 additions and 110 deletions
+38 -12
View File
@@ -1,27 +1,53 @@
const AUTH_STATE_COOKIE = 'magent_logged_in'
export const getApiBase = () => process.env.NEXT_PUBLIC_API_BASE ?? '/api'
export const getToken = () => {
if (typeof window === 'undefined') return null
return window.localStorage.getItem('magent_token')
const setCookie = (name: string, value: string, maxAgeSeconds: number) => {
if (typeof document === 'undefined') return
document.cookie = `${name}=${value}; Max-Age=${maxAgeSeconds}; Path=/; SameSite=Lax`
}
export const setToken = (token: string) => {
if (typeof window === 'undefined') return
window.localStorage.setItem('magent_token', token)
const clearCookie = (name: string) => {
if (typeof document === 'undefined') return
document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`
}
export const getToken = () => {
if (typeof document === 'undefined') return null
const cookies = document.cookie.split(';').map((entry) => entry.trim())
const marker = cookies.find((entry) => entry.startsWith(`${AUTH_STATE_COOKIE}=`))
if (!marker) return null
const [, value] = marker.split('=', 2)
return value || null
}
export const setToken = (_token: string) => {
setCookie(AUTH_STATE_COOKIE, '1', 60 * 60 * 12)
}
export const clearToken = () => {
clearCookie(AUTH_STATE_COOKIE)
if (typeof window === 'undefined') return
window.localStorage.removeItem('magent_token')
const baseUrl = getApiBase()
void fetch(`${baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
keepalive: true,
}).catch(() => undefined)
}
export const logout = async () => {
const baseUrl = getApiBase()
clearCookie(AUTH_STATE_COOKIE)
await fetch(`${baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
})
}
export const authFetch = (input: RequestInfo | URL, init?: RequestInit) => {
const token = getToken()
const headers = new Headers(init?.headers || {})
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return fetch(input, { ...init, headers })
return fetch(input, { ...init, headers, credentials: 'include' })
}
export const getEventStreamToken = async () => {
+3 -2
View File
@@ -42,13 +42,14 @@ export default function LoginPage() {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
credentials: 'include',
})
if (!response.ok) {
throw new Error('Login failed')
}
const data = await response.json()
if (data?.access_token) {
setToken(data.access_token)
if (data?.authenticated) {
setToken('cookie')
if (typeof window !== 'undefined') {
window.location.href = '/'
return
+4 -3
View File
@@ -106,6 +106,7 @@ function SignupPageContent() {
const response = await fetch(`${baseUrl}/auth/signup`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({
invite_code: inviteCode,
username: username.trim(),
@@ -117,12 +118,12 @@ function SignupPageContent() {
throw new Error(text || 'Sign-up failed')
}
const data = await response.json()
if (data?.access_token) {
setToken(data.access_token)
if (data?.authenticated) {
setToken('cookie')
window.location.href = '/'
return
}
throw new Error('Sign-up did not return a token')
throw new Error('Sign-up did not complete')
} catch (err) {
console.error(err)
setError(err instanceof Error ? err.message : 'Unable to create account.')
+4 -3
View File
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
import { authFetch, clearToken, getApiBase, getToken, logout } from '../lib/auth'
export default function HeaderIdentity() {
const [identity, setIdentity] = useState<{ username: string; role?: string } | null>(null)
@@ -49,7 +49,8 @@ export default function HeaderIdentity() {
const label = `${identity.username}${identity.role ? ` (${identity.role})` : ''}`
const initial = identity.username.slice(0, 1).toUpperCase()
const signOut = () => {
const signOut = async () => {
await logout().catch(() => undefined)
clearToken()
if (typeof window !== 'undefined') {
window.location.href = '/login'
@@ -83,7 +84,7 @@ export default function HeaderIdentity() {
<a href="/changelog" onClick={() => setOpen(false)}>
Changelog
</a>
<button type="button" className="signed-in-signout" onClick={signOut}>
<button type="button" className="signed-in-signout" onClick={() => void signOut()}>
Sign out
</button>
</div>