refactor(auth): remove GET /users/me endpoint and related exports

This commit is contained in:
2026-04-23 18:16:46 -04:00
parent e7aad33682
commit 1aac03c2dc
10 changed files with 1 additions and 815 deletions
-3
View File
@@ -63,9 +63,6 @@
"./features/auth/client": { "./features/auth/client": {
"import": "./dist/features/auth/AuthPage.client.js" "import": "./dist/features/auth/AuthPage.client.js"
}, },
"./features/auth/components": {
"import": "./dist/features/auth/components/index.js"
},
"./features/admin": { "./features/admin": {
"import": "./dist/features/admin/index.js" "import": "./dist/features/admin/index.js"
}, },
-274
View File
@@ -1,274 +0,0 @@
# Client dashboard and user features
This guide explains how to build a **client dashboard** in your Next.js app using Zen auth: protect routes, show the current user (name, avatar), add an account section to edit profile and avatar, and redirect to login when the user is not connected.
## What is available
| Need | Solution |
|------|----------|
| Require login on a page | `protect()` in a **server component** redirects to login if not authenticated |
| Get current user on server | `getSession()` from `@zen/core/auth/actions` |
| Check auth without redirect | `checkAuth()` from `@zen/core/auth` |
| Require a role | `requireRole(['admin', 'manager'])` from `@zen/core/auth` |
| Show user in client (header/nav) | `UserMenu` or `UserAvatar` + `useCurrentUser` from `@zen/core/auth/components` |
| Edit account (name + avatar) | `AccountSection` from `@zen/core/auth/components` |
| Call user API from client | `GET /zen/api/users/me`, `PUT /zen/api/users/profile`, `POST/DELETE /zen/api/users/profile/picture` (with `credentials: 'include'`) |
All user APIs are **session-based**: the session cookie is read on the server. No token in client code. Avatar and profile updates are scoped to the current user; the API validates the session on every request.
---
## 1. Protect a dashboard page (redirect if not logged in)
Use `protect()` in a **server component**. If there is no valid session, the user is redirected to the login page.
```js
// app/dashboard/page.js (Server Component)
import { protect } from '@zen/core/auth';
import { DashboardClient } from './DashboardClient';
export default async function DashboardPage() {
const session = await protect({ redirectTo: '/auth/login' });
return (
<div>
<h1>Dashboard</h1>
<DashboardClient initialUser={session.user} />
</div>
);
}
```
- `redirectTo`: where to send the user if not authenticated (default: `'/auth/login'`).
- `protect()` returns the **session** (with `session.user`: `id`, `email`, `name`, `role`, `image`, etc.).
---
## 2. Display the current user in the layout (name, avatar)
**Option A Server: pass user into a client component**
In your layout or header (server component), get the session and pass `user` to a client component that shows avatar and name:
```js
// app/layout.js or app/dashboard/layout.js
import { getSession } from '@zen/core/auth/actions';
import { UserMenu } from '@zen/core/auth/components';
export default async function Layout({ children }) {
const session = await getSession();
return (
<div>
<header>
{session?.user ? (
<UserMenu user={session.user} accountHref="/dashboard/account" logoutHref="/auth/logout" />
) : (
<a href="/auth/login">Log in</a>
)}
</header>
{children}
</div>
);
}
```
**Option B Client only: fetch user with `useCurrentUser`**
If you prefer not to pass user from the server, use the hook in a client component. It calls `GET /zen/api/users/me` with the session cookie:
```js
'use client';
import { UserMenu } from '@zen/core/auth/components';
export function Header() {
return (
<UserMenu
accountHref="/dashboard/account"
logoutHref="/auth/logout"
/>
);
}
```
`UserMenu` with no `user` prop will call `useCurrentUser()` itself and show a loading state until the request finishes. If the user is not logged in, it renders nothing (you can show a “Log in” link elsewhere).
**Components:**
- **`UserMenu`** Avatar + name + dropdown with “My account” and “Log out”. Props: `user` (optional), `accountHref`, `logoutHref`, `className`.
- **`UserAvatar`** Only the avatar (image or initials). Props: `user`, `size` (`'sm' | 'md' | 'lg'`), `className`.
- **`useCurrentUser()`** Returns `{ user, loading, error, refetch }`. Use when you need the current user in a client component without receiving it from the server.
---
## 3. Account page (edit profile and avatar)
Use **`AccountSection`** on a page that is already protected (e.g. `/dashboard/account`). It shows:
- Profile picture (upload / remove)
- Full name (editable)
- Email (read-only)
- Optional “Account created” date
**Server page:**
```js
// app/dashboard/account/page.js
import { protect } from '@zen/core/auth';
import { AccountSection } from '@zen/core/auth/components';
export default async function AccountPage() {
const session = await protect({ redirectTo: '/auth/login' });
return (
<div>
<h1>My account</h1>
<AccountSection initialUser={session.user} />
</div>
);
}
```
- **`initialUser`** Optional. If you pass `session.user`, the section uses it immediately and does not need an extra API call on load.
- **`onUpdate`** Optional callback after profile or avatar update; you can use it to refresh parent state or revalidate.
`AccountSection` uses:
- `PUT /zen/api/users/profile` for name
- `POST /zen/api/users/profile/picture` for upload
- `DELETE /zen/api/users/profile/picture` for remove
All with `credentials: 'include'` (session cookie). Ensure your app uses **ToastProvider** (from `@zen/core/toast`) if you want toasts.
---
## 4. Check if the user is connected (without redirect)
Use **`checkAuth()`** in a server component when you only need to know whether someone is logged in:
```js
import { checkAuth } from '@zen/core/auth';
export default async function Page() {
const session = await checkAuth();
return session ? <div>Hello, {session.user.name}</div> : <div>Please log in</div>;
}
```
Use **`requireRole()`** when a page is only for certain roles:
```js
import { requireRole } from '@zen/core/auth';
export default async function ManagerPage() {
const session = await requireRole(['admin', 'manager'], {
redirectTo: '/auth/login',
forbiddenRedirect: '/dashboard',
});
return <div>Manager content</div>;
}
```
---
## 5. Security summary
- **Session cookie**: HttpOnly, validated on the server for every protected API call.
- **User APIs**:
- `GET /zen/api/users/me` current user only.
- `PUT /zen/api/users/profile` update only the authenticated users name.
- Profile picture upload/delete scoped to the current user; storage path includes `users/{userId}/...`.
- **Storage**: User files under `users/{userId}/...` are only served if the request session matches that `userId` (or admin).
- **Protection**: Use `protect()` or `requireRole()` in server components so unauthenticated or unauthorized users never see sensitive dashboard content.
---
## 6. Minimal dashboard example
```text
app/
layout.js # Root layout with ToastProvider if you use it
auth/
[...auth]/page.js # Zen default auth page (login, register, logout, etc.)
dashboard/
layout.js # Optional: layout that shows UserMenu and requires login
page.js # Protected dashboard home
account/
page.js # Protected account page with AccountSection
```
**dashboard/layout.js:**
```js
import { protect } from '@zen/core/auth';
import { UserMenu } from '@zen/core/auth/components';
import Link from 'next/link';
export default async function DashboardLayout({ children }) {
const session = await protect({ redirectTo: '/auth/login' });
return (
<div>
<header className="flex justify-between items-center p-4 border-b">
<Link href="/dashboard">Dashboard</Link>
<UserMenu
user={session.user}
accountHref="/dashboard/account"
logoutHref="/auth/logout"
/>
</header>
<main className="p-4">{children}</main>
</div>
);
}
```
**dashboard/page.js:**
```js
import { protect } from '@zen/core/auth';
export default async function DashboardPage() {
const session = await protect({ redirectTo: '/auth/login' });
return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p><a href="/dashboard/account">Edit my account</a></p>
</div>
);
}
```
**dashboard/account/page.js:**
```js
import { protect } from '@zen/core/auth';
import { AccountSection } from '@zen/core/auth/components';
export default async function AccountPage() {
const session = await protect({ redirectTo: '/auth/login' });
return (
<div>
<h1>My account</h1>
<AccountSection initialUser={session.user} />
</div>
);
}
```
This gives you a protected dashboard, user display in the header, and a dedicated account page to modify profile and avatar, with redirect to login when the user is not connected.
---
## 7. Facturation (invoices) section
If you use the **Invoice** module and want logged-in users to see their own invoices in the dashboard:
- **Userclient link**: In the admin, link a user to a client (User edit → Client). Only invoices for that client are shown.
- **API**: `GET /zen/api/invoices/me` (session required) returns the current users linked client and that clients invoices.
- **Component**: Use `ClientInvoicesSection` from `@zen/core/invoice/dashboard` on a protected page (e.g. `/dashboard/invoices`).
See the [Invoice module dashboard guide](../../modules/invoice/README-dashboard.md) for the full setup (API details, page example, linking users to clients, and security).
+1 -20
View File
@@ -37,24 +37,6 @@ function logAndObscureError(error, fallback) {
return fallback; return fallback;
} }
// ---------------------------------------------------------------------------
// GET /zen/api/users/me
// ---------------------------------------------------------------------------
async function handleGetCurrentUser(_request, _params, { session }) {
return apiSuccess({
user: {
id: session.user.id,
email: session.user.email,
name: session.user.name,
role: session.user.role,
image: session.user.image,
emailVerified: session.user.email_verified,
createdAt: session.user.created_at
}
});
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// GET /zen/api/users/:id (admin only) // GET /zen/api/users/:id (admin only)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -481,12 +463,11 @@ async function handleDeleteRole(_request, { id: roleId }) {
// Route definitions // Route definitions
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// //
// Order matters: specific paths (/users/me, /users/profile) must come before // Order matters: specific paths (/users/profile) must come before
// parameterised paths (/users/:id) so they match first. // parameterised paths (/users/:id) so they match first.
export const routes = defineApiRoutes([ export const routes = defineApiRoutes([
{ path: '/users', method: 'GET', handler: handleListUsers, auth: 'admin' }, { path: '/users', method: 'GET', handler: handleListUsers, auth: 'admin' },
{ path: '/users/me', method: 'GET', handler: handleGetCurrentUser, auth: 'user' },
{ path: '/users/profile', method: 'PUT', handler: handleUpdateProfile, auth: 'user' }, { path: '/users/profile', method: 'PUT', handler: handleUpdateProfile, auth: 'user' },
{ path: '/users/profile/picture', method: 'POST', handler: handleUploadProfilePicture, auth: 'user' }, { path: '/users/profile/picture', method: 'POST', handler: handleUploadProfilePicture, auth: 'user' },
{ path: '/users/profile/picture', method: 'DELETE', handler: handleDeleteProfilePicture, auth: 'user' }, { path: '/users/profile/picture', method: 'DELETE', handler: handleDeleteProfilePicture, auth: 'user' },
@@ -1,279 +0,0 @@
'use client';
/**
* Reusable "Edit account" section: display name, email (read-only), avatar upload/remove.
* Use on a protected dashboard page. Requires session cookie (user must be logged in).
*
* @param {Object} props
* @param {Object} [props.initialUser] - Initial user from server (e.g. getSession().user). If omitted, fetches from API.
* @param {function} [props.onUpdate] - Called after profile or avatar update with the new user object (e.g. to refresh layout)
*/
import React, { useState, useEffect, useRef } from 'react';
import { Card, Input, Button } from '@zen/core/shared/components';
import { useToast } from '@zen/core/toast';
import { useCurrentUser } from './useCurrentUser.js';
import UserAvatar from './UserAvatar.js';
const API_BASE = '/zen/api';
function getImageUrl(imageKey) {
if (!imageKey) return null;
return `/zen/api/storage/${imageKey}`;
}
export default function AccountSection({ initialUser, onUpdate }) {
const toast = useToast();
const { user: fetchedUser, loading: fetchLoading, refetch } = useCurrentUser();
const user = initialUser ?? fetchedUser;
const [formData, setFormData] = useState({ name: user?.name ?? '' });
const [saving, setSaving] = useState(false);
const [uploadingImage, setUploadingImage] = useState(false);
const [imagePreview, setImagePreview] = useState(null);
const fileInputRef = useRef(null);
useEffect(() => {
if (user) {
setFormData((prev) => ({ ...prev, name: user.name ?? '' }));
setImagePreview(user.image ? getImageUrl(user.image) : null);
}
}, [user]);
const handleNameChange = (value) => {
setFormData((prev) => ({ ...prev, name: value }));
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!formData.name?.trim()) {
toast.error('Le nom est requis');
return;
}
setSaving(true);
try {
const res = await fetch(`${API_BASE}/users/profile`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ name: formData.name.trim() }),
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.message || data.error || 'Échec de la mise à jour du profil');
}
toast.success('Profil mis à jour avec succès');
onUpdate?.(data.user);
refetch();
} catch (err) {
toast.error(err.message || 'Échec de la mise à jour du profil');
} finally {
setSaving(false);
}
};
const handleReset = () => {
setFormData({ name: user?.name ?? '' });
};
const handleImageSelect = async (e) => {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.error('Veuillez sélectionner un fichier image');
return;
}
if (file.size > 5 * 1024 * 1024) {
toast.error("L'image doit faire moins de 5MB");
return;
}
const reader = new FileReader();
reader.onloadend = () => setImagePreview(reader.result);
reader.readAsDataURL(file);
setUploadingImage(true);
try {
const fd = new FormData();
fd.append('file', file);
const res = await fetch(`${API_BASE}/users/profile/picture`, {
method: 'POST',
credentials: 'include',
body: fd,
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.message || data.error || 'Upload failed');
}
setImagePreview(getImageUrl(data.user?.image));
toast.success('Photo de profil mise à jour avec succès');
onUpdate?.(data.user);
refetch();
} catch (err) {
toast.error(err.message || 'Upload failed');
setImagePreview(user?.image ? getImageUrl(user.image) : null);
} finally {
setUploadingImage(false);
}
};
const handleRemoveImage = async () => {
if (!user?.image) return;
setUploadingImage(true);
try {
const res = await fetch(`${API_BASE}/users/profile/picture`, {
method: 'DELETE',
credentials: 'include',
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.message || data.error || 'Remove failed');
}
setImagePreview(null);
toast.success('Photo de profil supprimée avec succès');
onUpdate?.(data.user);
refetch();
} catch (err) {
toast.error(err.message || 'Remove failed');
} finally {
setUploadingImage(false);
}
};
const created_at = user?.created_at ?? user?.createdAt;
const hasChanges = formData.name?.trim() !== (user?.name ?? '');
if (fetchLoading && !initialUser) {
return (
<Card variant="lightDark">
<div className="animate-pulse space-y-4">
<div className="h-24 rounded-xl bg-neutral-200 dark:bg-neutral-700" />
<div className="h-32 rounded-xl bg-neutral-200 dark:bg-neutral-700" />
</div>
</Card>
);
}
if (!user) {
return null;
}
return (
<div className="space-y-6">
<Card variant="lightDark">
<div className="space-y-4">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-white">
Photo de profil
</h2>
<div className="flex flex-wrap items-center gap-6">
<div className="relative">
{imagePreview ? (
<img
src={imagePreview}
alt="Profile"
className="w-24 h-24 rounded-full object-cover border-2 border-neutral-200 dark:border-neutral-700"
/>
) : (
<UserAvatar user={user} size="lg" className="w-24 h-24" />
)}
{uploadingImage && (
<div className="absolute inset-0 rounded-full bg-black/50 flex items-center justify-center">
<div className="w-8 h-8 border-2 border-white border-t-transparent rounded-full animate-spin" />
</div>
)}
</div>
<div className="flex flex-col gap-2">
<p className="text-sm text-neutral-500 dark:text-neutral-400">
Téléchargez une nouvelle photo de profil. Taille max 5MB.
</p>
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleImageSelect}
className="hidden"
/>
<Button
type="button"
variant="secondary"
onClick={() => fileInputRef.current?.click()}
disabled={uploadingImage}
>
{uploadingImage ? 'Téléchargement...' : 'Télécharger une image'}
</Button>
{imagePreview && (
<Button
type="button"
variant="secondary"
onClick={handleRemoveImage}
disabled={uploadingImage}
>
Supprimer
</Button>
)}
</div>
</div>
</div>
</div>
</Card>
<Card variant="lightDark">
<form onSubmit={handleSubmit} className="space-y-6">
<h2 className="text-lg font-semibold text-neutral-900 dark:text-white">
Informations personnelles
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
label="Nom complet"
type="text"
value={formData.name}
onChange={handleNameChange}
placeholder="Entrez votre nom complet"
required
disabled={saving}
/>
<Input
label="Courriel"
type="email"
value={user.email ?? ''}
disabled
readOnly
description="L'email ne peut pas être modifié"
/>
</div>
{created_at && (
<Input
label="Compte créé"
type="text"
value={new Date(created_at).toLocaleDateString('fr-FR', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
disabled
readOnly
/>
)}
<div className="flex justify-end gap-3 pt-4 border-t border-neutral-200 dark:border-neutral-700">
<Button
type="button"
variant="secondary"
onClick={handleReset}
disabled={saving || !hasChanges}
>
Réinitialiser
</Button>
<Button
type="submit"
variant="primary"
disabled={saving || !hasChanges}
loading={saving}
>
{saving ? 'Enregistrement...' : 'Enregistrer les modifications'}
</Button>
</div>
</form>
</Card>
</div>
);
}
@@ -1,55 +0,0 @@
'use client';
/**
* Displays the current user's avatar (image or initials fallback).
* Image is loaded from /zen/api/storage/{key}; session cookie is sent automatically.
*
* @param {Object} props
* @param {Object} props.user - User object with optional image (storage key) and name
* @param {'sm'|'md'|'lg'} [props.size='md'] - Size of the avatar
* @param {string} [props.className] - Additional CSS classes for the wrapper
*/
function getImageUrl(imageKey) {
if (!imageKey) return null;
return `/zen/api/storage/${imageKey}`;
}
function getInitials(name) {
if (!name || !name.trim()) return '?';
return name
.trim()
.split(/\s+/)
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
const sizeClasses = {
sm: 'w-8 h-8 text-xs',
md: 'w-10 h-10 text-sm',
lg: 'w-12 h-12 text-base',
};
export default function UserAvatar({ user, size = 'md', className = '' }) {
const sizeClass = sizeClasses[size] || sizeClasses.md;
const imageUrl = user?.image ? getImageUrl(user.image) : null;
return (
<div
className={`rounded-full overflow-hidden flex items-center justify-center bg-neutral-700 text-white font-medium shrink-0 ${sizeClass} ${className}`}
aria-hidden
>
{imageUrl ? (
<img
src={imageUrl}
alt={user?.name ? `${user.name} avatar` : 'Avatar'}
className="w-full h-full object-cover"
/>
) : (
<span>{getInitials(user?.name)}</span>
)}
</div>
);
}
-90
View File
@@ -1,90 +0,0 @@
'use client';
/**
* User menu: avatar + name with optional dropdown (account link, logout).
* Can receive user from server (e.g. from getSession()) or use useCurrentUser() on client.
*
* @param {Object} props
* @param {Object} [props.user] - User from server (getSession().user). If not provided, useCurrentUser() is used.
* @param {string} [props.accountHref='/dashboard/account'] - Link for "My account"
* @param {string} [props.logoutHref='/auth/logout'] - Link for logout
* @param {string} [props.className] - Extra classes for the menu wrapper
*/
import { Fragment } from 'react';
import { Menu, Transition } from '@headlessui/react';
import UserAvatar from './UserAvatar.js';
import { useCurrentUser } from './useCurrentUser.js';
export default function UserMenu({
user: userProp,
accountHref = '/dashboard/account',
logoutHref = '/auth/logout',
className = '',
}) {
const { user: userFromHook, loading } = useCurrentUser();
const user = userProp ?? userFromHook;
if (loading && !userProp) {
return (
<div className={`flex items-center gap-2 ${className}`}>
<div className="w-10 h-10 rounded-full bg-neutral-700 animate-pulse" />
<div className="h-4 w-24 bg-neutral-700 rounded animate-pulse" />
</div>
);
}
if (!user) {
return null;
}
return (
<Menu as="div" className={`relative ${className}`}>
<Menu.Button className="flex items-center gap-2 sm:gap-3 px-2 py-1.5 rounded-lg hover:bg-black/10 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-neutral-400 transition-colors">
<UserAvatar user={user} size="md" />
<span className="text-sm font-medium text-inherit truncate max-w-[120px] sm:max-w-[160px]">
{user.name || user.email || 'Account'}
</span>
<svg className="w-4 h-4 text-neutral-500 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</Menu.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 mt-2 w-56 origin-top-right rounded-lg bg-white dark:bg-neutral-900 shadow-lg border border-neutral-200 dark:border-neutral-700 py-1 focus:outline-none z-50">
<div className="px-4 py-3 border-b border-neutral-200 dark:border-neutral-700">
<p className="text-sm font-medium text-neutral-900 dark:text-white truncate">{user.name || 'User'}</p>
<p className="text-xs text-neutral-500 dark:text-neutral-400 truncate">{user.email}</p>
</div>
<Menu.Item>
{({ active }) => (
<a
href={accountHref}
className={`block px-4 py-2 text-sm ${active ? 'bg-neutral-100 dark:bg-neutral-800' : ''} text-neutral-700 dark:text-neutral-300`}
>
My account
</a>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
href={logoutHref}
className={`block px-4 py-2 text-sm ${active ? 'bg-neutral-100 dark:bg-neutral-800' : ''} text-neutral-700 dark:text-neutral-300`}
>
Log out
</a>
)}
</Menu.Item>
</Menu.Items>
</Transition>
</Menu>
);
}
-6
View File
@@ -1,6 +0,0 @@
'use client';
export { default as UserAvatar } from './UserAvatar.js';
export { default as UserMenu } from './UserMenu.js';
export { default as AccountSection } from './AccountSection.js';
export { useCurrentUser } from './useCurrentUser.js';
@@ -1,66 +0,0 @@
'use client';
/**
* Client hook to fetch the current user from the API.
* Uses session cookie (credentials: 'include'); safe to use in client components.
*
* @returns {{ user: Object|null, loading: boolean, error: string|null, refetch: function }}
*
* @example
* const { user, loading, error, refetch } = useCurrentUser();
* if (loading) return <Spinner />;
* if (error) return <div>Error: {error}</div>;
* if (!user) return <Link href="/auth/login">Log in</Link>;
* return <span>Hello, {user.name}</span>;
*/
import { useState, useEffect, useCallback } from 'react';
const API_BASE = '/zen/api';
export function useCurrentUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const fetchUser = useCallback(async () => {
setLoading(true);
setError(null);
try {
const res = await fetch(`${API_BASE}/users/me`, {
method: 'GET',
credentials: 'include',
headers: { Accept: 'application/json' },
});
const data = await res.json();
if (!res.ok) {
if (res.status === 401) {
setUser(null);
return;
}
setError(data.message || data.error || 'Failed to load user');
setUser(null);
return;
}
if (data.user) {
setUser(data.user);
} else {
setUser(null);
}
} catch (err) {
console.error('[useCurrentUser]', err);
setError(err.message || 'Failed to load user');
setUser(null);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
fetchUser();
}, [fetchUser]);
return { user, loading, error, refetch: fetchUser };
}
-3
View File
@@ -1,6 +1,5 @@
/** /**
* Zen Authentication — server barrel. * Zen Authentication — server barrel.
* Client components live in @zen/core/features/auth/components.
* Server actions live in @zen/core/features/auth/actions. * Server actions live in @zen/core/features/auth/actions.
*/ */
@@ -39,8 +38,6 @@ export {
generateId generateId
} from './password.js'; } from './password.js';
export { protect, checkAuth, requireRole } from './protect.js';
export { export {
registerAction, registerAction,
loginAction, loginAction,
-19
View File
@@ -1,19 +0,0 @@
import { getSession } from './actions.js';
import { redirect } from 'next/navigation';
export async function protect({ redirectTo = '/auth/login' } = {}) {
const session = await getSession();
if (!session) redirect(redirectTo);
return session;
}
export async function checkAuth() {
return getSession();
}
export async function requireRole(allowedRoles = [], { redirectTo = '/auth/login', forbiddenRedirect = '/' } = {}) {
const session = await getSession();
if (!session) redirect(redirectTo);
if (!allowedRoles.includes(session.user.role)) redirect(forbiddenRedirect);
return session;
}