Add account settings page and polish avatar/SSO button edge cases
- New /account route: edit name/email, change password, delete account (detaches the user's orders instead of leaving a dangling reference, since orders.customerId has no DB-level FK to better-auth's user table) - Better Auth: allow email changes without a verification email (no email sending is wired up anywhere in this app) and enable account deletion - Header avatar: fall back to the initials placeholder if the profile image fails to load, not just when there's no image URL - Fix low-contrast "Continue with Google" button in dark mode (it used the page background color and a hardcoded dark-gray label, so it nearly disappeared against a dark background) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -21,8 +21,8 @@
|
||||
}
|
||||
|
||||
.google {
|
||||
background: $color-background;
|
||||
color: #3c4043;
|
||||
background: $color-surface;
|
||||
color: $color-text;
|
||||
border: 1px solid $color-border;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -983,6 +983,21 @@ export const getOrdersForCustomer = async (
|
||||
return summarizeOrders(db, orders)
|
||||
}
|
||||
|
||||
// Called when an account is deleted. orders.customerId is a soft reference
|
||||
// to better-auth's user table (no DB-level FK), so deleting the user row
|
||||
// would otherwise just leave those orders pointing at an id that no longer
|
||||
// exists. Detaching them instead keeps the Order history intact for
|
||||
// kitchen/admin purposes, just no longer attributed to an account.
|
||||
export const detachCustomerOrders = async (
|
||||
db: Db,
|
||||
customerId: string,
|
||||
): Promise<void> => {
|
||||
await db
|
||||
.update(schema.orders)
|
||||
.set({ customerId: null })
|
||||
.where(eq(schema.orders.customerId, customerId))
|
||||
}
|
||||
|
||||
export type AdminOrderSummary = CustomerOrderSummary & {
|
||||
customer:
|
||||
| { type: 'account'; customerId: string }
|
||||
|
||||
@@ -14,6 +14,22 @@ const AccountIcon = () => (
|
||||
</svg>
|
||||
)
|
||||
|
||||
const SettingsIcon = () => (
|
||||
<svg
|
||||
className={styles.menuIcon}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.75"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const OrdersIcon = () => (
|
||||
<svg
|
||||
className={styles.menuIcon}
|
||||
@@ -68,6 +84,7 @@ const BetterAuthHeader = () => {
|
||||
const { clear: clearBasket } = useBasket()
|
||||
const router = useRouter()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [imageFailed, setImageFailed] = useState(false)
|
||||
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||
const pathname = useRouterState({
|
||||
select: (state) => state.location.pathname,
|
||||
@@ -77,6 +94,10 @@ const BetterAuthHeader = () => {
|
||||
setOpen(false)
|
||||
}, [pathname])
|
||||
|
||||
useEffect(() => {
|
||||
setImageFailed(false)
|
||||
}, [session?.user?.image])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
|
||||
@@ -112,8 +133,13 @@ const BetterAuthHeader = () => {
|
||||
}
|
||||
>
|
||||
{session?.user ? (
|
||||
session.user.image ? (
|
||||
<img src={session.user.image} alt="" className={styles.avatarImage} />
|
||||
session.user.image && !imageFailed ? (
|
||||
<img
|
||||
src={session.user.image}
|
||||
alt=""
|
||||
className={styles.avatarImage}
|
||||
onError={() => setImageFailed(true)}
|
||||
/>
|
||||
) : (
|
||||
<span className={styles.avatarInitial}>
|
||||
{session.user.name.charAt(0).toUpperCase() || 'U'}
|
||||
@@ -133,6 +159,10 @@ const BetterAuthHeader = () => {
|
||||
<OrdersIcon />
|
||||
Orders
|
||||
</Link>
|
||||
<Link to="/account" role="menuitem" className={styles.menuButton}>
|
||||
<SettingsIcon />
|
||||
Account
|
||||
</Link>
|
||||
{session.user.role === 'admin' && (
|
||||
<Link
|
||||
to="/admin"
|
||||
|
||||
@@ -2,6 +2,9 @@ import { betterAuth } from 'better-auth'
|
||||
import { tanstackStartCookies } from 'better-auth/tanstack-start'
|
||||
import { Pool } from 'pg'
|
||||
|
||||
import { db } from '#/db/index.ts'
|
||||
import { detachCustomerOrders } from '#/domain/ordering.ts'
|
||||
|
||||
// Each provider is only registered once its credentials are configured, so
|
||||
// local dev without OAuth apps set up still works via email/password.
|
||||
export const auth = betterAuth({
|
||||
@@ -20,6 +23,20 @@ export const auth = betterAuth({
|
||||
input: false,
|
||||
},
|
||||
},
|
||||
// No email-sending set up anywhere in this app (no verification flow,
|
||||
// no password-reset email either), so a changed address is accepted
|
||||
// immediately rather than left pending on a confirmation link that
|
||||
// could never be sent.
|
||||
changeEmail: {
|
||||
enabled: true,
|
||||
updateEmailWithoutVerification: true,
|
||||
},
|
||||
deleteUser: {
|
||||
enabled: true,
|
||||
beforeDelete: async (user) => {
|
||||
await detachCustomerOrders(db, user.id)
|
||||
},
|
||||
},
|
||||
},
|
||||
socialProviders: {
|
||||
...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
import { Route as rootRouteImport } from './routes/__root'
|
||||
import { Route as IndexRouteImport } from './routes/index'
|
||||
import { Route as AccountRouteImport } from './routes/account'
|
||||
import { Route as AdminRouteImport } from './routes/admin'
|
||||
import { Route as BasketRouteImport } from './routes/basket'
|
||||
import { Route as HelpRouteImport } from './routes/help'
|
||||
@@ -32,6 +33,11 @@ const IndexRoute = IndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AccountRoute = AccountRouteImport.update({
|
||||
id: '/account',
|
||||
path: '/account',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
const AdminRoute = AdminRouteImport.update({
|
||||
id: '/admin',
|
||||
path: '/admin',
|
||||
@@ -115,6 +121,7 @@ const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
'/account': typeof AccountRoute
|
||||
'/admin': typeof AdminRouteWithChildren
|
||||
'/basket': typeof BasketRoute
|
||||
'/help': typeof HelpRoute
|
||||
@@ -134,6 +141,7 @@ export interface FileRoutesByFullPath {
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
'/': typeof IndexRoute
|
||||
'/account': typeof AccountRoute
|
||||
'/basket': typeof BasketRoute
|
||||
'/help': typeof HelpRoute
|
||||
'/login': typeof LoginRoute
|
||||
@@ -153,6 +161,7 @@ export interface FileRoutesByTo {
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport
|
||||
'/': typeof IndexRoute
|
||||
'/account': typeof AccountRoute
|
||||
'/admin': typeof AdminRouteWithChildren
|
||||
'/basket': typeof BasketRoute
|
||||
'/help': typeof HelpRoute
|
||||
@@ -174,6 +183,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/account'
|
||||
| '/admin'
|
||||
| '/basket'
|
||||
| '/help'
|
||||
@@ -193,6 +203,7 @@ export interface FileRouteTypes {
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
| '/'
|
||||
| '/account'
|
||||
| '/basket'
|
||||
| '/help'
|
||||
| '/login'
|
||||
@@ -211,6 +222,7 @@ export interface FileRouteTypes {
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
| '/account'
|
||||
| '/admin'
|
||||
| '/basket'
|
||||
| '/help'
|
||||
@@ -231,6 +243,7 @@ export interface FileRouteTypes {
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute
|
||||
AccountRoute: typeof AccountRoute
|
||||
AdminRoute: typeof AdminRouteWithChildren
|
||||
BasketRoute: typeof BasketRoute
|
||||
HelpRoute: typeof HelpRoute
|
||||
@@ -251,6 +264,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof IndexRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/account': {
|
||||
id: '/account'
|
||||
path: '/account'
|
||||
fullPath: '/account'
|
||||
preLoaderRoute: typeof AccountRouteImport
|
||||
parentRoute: typeof rootRouteImport
|
||||
}
|
||||
'/admin': {
|
||||
id: '/admin'
|
||||
path: '/admin'
|
||||
@@ -390,6 +410,7 @@ const AdminRouteWithChildren = AdminRoute._addFileChildren(AdminRouteChildren)
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
AccountRoute: AccountRoute,
|
||||
AdminRoute: AdminRouteWithChildren,
|
||||
BasketRoute: BasketRoute,
|
||||
HelpRoute: HelpRoute,
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
@use '../styles/variables' as *;
|
||||
|
||||
.page {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
padding: $space-5 $space-3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-5;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-3;
|
||||
padding: $space-4;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius;
|
||||
background: $color-surface;
|
||||
}
|
||||
|
||||
.sectionHeading {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-2;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $space-1;
|
||||
font-size: 0.85rem;
|
||||
color: $color-text-muted;
|
||||
|
||||
input {
|
||||
padding: $space-2;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius;
|
||||
font-size: 1rem;
|
||||
color: $color-text;
|
||||
background: $color-background;
|
||||
}
|
||||
}
|
||||
|
||||
.submitButton {
|
||||
align-self: flex-start;
|
||||
padding: $space-2 $space-4;
|
||||
border: none;
|
||||
border-radius: $radius;
|
||||
background: $color-primary;
|
||||
color: $color-primary-contrast;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: opacity 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $color-danger;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: $color-success;
|
||||
font-size: 0.85rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dangerZone {
|
||||
border-color: $color-danger;
|
||||
}
|
||||
|
||||
.dangerText {
|
||||
margin: 0;
|
||||
color: $color-text-muted;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dangerActions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $space-2;
|
||||
}
|
||||
|
||||
.deleteButton {
|
||||
align-self: flex-start;
|
||||
padding: $space-2 $space-4;
|
||||
border: 1px solid $color-danger;
|
||||
border-radius: $radius;
|
||||
background: $color-background;
|
||||
color: $color-danger;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: $color-danger;
|
||||
color: $color-primary-contrast;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.cancelButton {
|
||||
padding: $space-2 $space-4;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $radius;
|
||||
background: $color-background;
|
||||
color: $color-text;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: $color-text;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
|
||||
import { authClient } from '#/lib/auth-client.ts'
|
||||
import { getCurrentUser } from '#/server/auth.ts'
|
||||
|
||||
import styles from './account.module.scss'
|
||||
|
||||
const AccountPage = () => {
|
||||
const user = Route.useLoaderData()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [name, setName] = useState(user.name)
|
||||
const [email, setEmail] = useState(user.email)
|
||||
const [savedName, setSavedName] = useState(user.name)
|
||||
const [savedEmail, setSavedEmail] = useState(user.email)
|
||||
const [profileSubmitting, setProfileSubmitting] = useState(false)
|
||||
const [profileError, setProfileError] = useState<string | null>(null)
|
||||
const [profileSaved, setProfileSaved] = useState(false)
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [passwordSubmitting, setPasswordSubmitting] = useState(false)
|
||||
const [passwordError, setPasswordError] = useState<string | null>(null)
|
||||
const [passwordSaved, setPasswordSaved] = useState(false)
|
||||
|
||||
const [confirmingDelete, setConfirmingDelete] = useState(false)
|
||||
const [deletePassword, setDeletePassword] = useState('')
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [deleteError, setDeleteError] = useState<string | null>(null)
|
||||
|
||||
const submitProfile = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
setProfileSubmitting(true)
|
||||
setProfileError(null)
|
||||
setProfileSaved(false)
|
||||
|
||||
try {
|
||||
if (name !== savedName) {
|
||||
const { error } = await authClient.updateUser({ name })
|
||||
if (error) throw new Error(error.message ?? 'Could not update your name.')
|
||||
setSavedName(name)
|
||||
}
|
||||
if (email !== savedEmail) {
|
||||
const { error } = await authClient.changeEmail({ newEmail: email })
|
||||
if (error) throw new Error(error.message ?? 'Could not update your email.')
|
||||
setSavedEmail(email)
|
||||
}
|
||||
setProfileSaved(true)
|
||||
} catch (err) {
|
||||
setProfileError(
|
||||
err instanceof Error ? err.message : 'Something went wrong.',
|
||||
)
|
||||
} finally {
|
||||
setProfileSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const submitPassword = async (event: FormEvent) => {
|
||||
event.preventDefault()
|
||||
setPasswordSubmitting(true)
|
||||
setPasswordError(null)
|
||||
setPasswordSaved(false)
|
||||
|
||||
try {
|
||||
const { error } = await authClient.changePassword({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
})
|
||||
if (error) throw new Error(error.message ?? 'Could not change your password.')
|
||||
setCurrentPassword('')
|
||||
setNewPassword('')
|
||||
setPasswordSaved(true)
|
||||
} catch (err) {
|
||||
setPasswordError(
|
||||
err instanceof Error ? err.message : 'Something went wrong.',
|
||||
)
|
||||
} finally {
|
||||
setPasswordSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteAccount = async () => {
|
||||
setDeleting(true)
|
||||
setDeleteError(null)
|
||||
|
||||
try {
|
||||
const { error } = await authClient.deleteUser(
|
||||
deletePassword ? { password: deletePassword } : {},
|
||||
)
|
||||
if (error) throw new Error(error.message ?? 'Could not delete your account.')
|
||||
await navigate({ to: '/' })
|
||||
} catch (err) {
|
||||
setDeleteError(
|
||||
err instanceof Error ? err.message : 'Something went wrong.',
|
||||
)
|
||||
setDeleting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.page}>
|
||||
<h1 className={styles.heading}>Account</h1>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionHeading}>Profile</h2>
|
||||
<form className={styles.form} onSubmit={submitProfile}>
|
||||
<label className={styles.field}>
|
||||
Name
|
||||
<input
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className={styles.field}>
|
||||
Email
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.submitButton}
|
||||
disabled={profileSubmitting}
|
||||
>
|
||||
{profileSubmitting ? 'Saving…' : 'Save changes'}
|
||||
</button>
|
||||
{profileError && <p className={styles.error}>{profileError}</p>}
|
||||
{profileSaved && <p className={styles.success}>Saved.</p>}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className={styles.section}>
|
||||
<h2 className={styles.sectionHeading}>Password</h2>
|
||||
<form className={styles.form} onSubmit={submitPassword}>
|
||||
<label className={styles.field}>
|
||||
Current password
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(event) => setCurrentPassword(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label className={styles.field}>
|
||||
New password
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(event) => setNewPassword(event.target.value)}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
className={styles.submitButton}
|
||||
disabled={passwordSubmitting}
|
||||
>
|
||||
{passwordSubmitting ? 'Changing…' : 'Change password'}
|
||||
</button>
|
||||
{passwordError && <p className={styles.error}>{passwordError}</p>}
|
||||
{passwordSaved && <p className={styles.success}>Password changed.</p>}
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section className={`${styles.section} ${styles.dangerZone}`}>
|
||||
<h2 className={styles.sectionHeading}>Delete account</h2>
|
||||
<p className={styles.dangerText}>
|
||||
This permanently deletes your account. Your past orders stay on
|
||||
record but are no longer linked to you. This can’t be undone.
|
||||
</p>
|
||||
{confirmingDelete ? (
|
||||
<div className={styles.form}>
|
||||
<label className={styles.field}>
|
||||
Confirm your password
|
||||
<input
|
||||
type="password"
|
||||
value={deletePassword}
|
||||
onChange={(event) => setDeletePassword(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<div className={styles.dangerActions}>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.deleteButton}
|
||||
disabled={deleting}
|
||||
onClick={() => void deleteAccount()}
|
||||
>
|
||||
{deleting ? 'Deleting…' : 'Permanently delete my account'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.cancelButton}
|
||||
disabled={deleting}
|
||||
onClick={() => {
|
||||
setConfirmingDelete(false)
|
||||
setDeletePassword('')
|
||||
setDeleteError(null)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
{deleteError && <p className={styles.error}>{deleteError}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.deleteButton}
|
||||
onClick={() => setConfirmingDelete(true)}
|
||||
>
|
||||
Delete account
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/account')({
|
||||
loader: async () => {
|
||||
try {
|
||||
return await getCurrentUser()
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'Not signed in') {
|
||||
throw redirect({ to: '/login' })
|
||||
}
|
||||
throw error
|
||||
}
|
||||
},
|
||||
component: AccountPage,
|
||||
})
|
||||
@@ -33,3 +33,14 @@ export const requireAdminSession = createServerFn({ method: 'GET' }).handler(
|
||||
await requireAdmin()
|
||||
},
|
||||
)
|
||||
|
||||
// The account settings route's loader calls this to both fetch the
|
||||
// signed-in user and gate the page — thrown message is matched against by
|
||||
// the route the same way withAdminGuard matches "Admin access required".
|
||||
export const getCurrentUser = createServerFn({ method: 'GET' }).handler(
|
||||
async () => {
|
||||
const user = await getSessionUser()
|
||||
if (!user) throw new Error('Not signed in')
|
||||
return user
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user