Skip to content

Commit 3f3de28

Browse files
makcimerrrclaude
andcommitted
fix(auth): comptes Authentik — /settings sans redirect Stack + vraie permission planning
Deux bugs prod pour les utilisateurs connectés via Authentik (sans session Stack) : 1. /settings redirigeait vers /handler/sign-in : le composant <AccountSettings> de Stack redirige quand il n'y a pas de session Stack. Il n'est plus rendu que pour les comptes Stack ; les comptes Authentik voient une carte info (nom/email + renvoi vers Authentik). 2. planningPermission toujours 'reader' : les pages planning lisaient la permission via useUser() de Stack (null pour Authentik) et resolveUser()/le layout codaient 'reader' en dur pour la branche Authentik, alors que la page /members écrit la permission dans la table locale `users` (ex. Vivien = editor). resolveUser() lit désormais role + planningPermission dans la table locale pour les comptes Authentik, et un contexte UserAccess (alimenté par le layout serveur) remplace useUser() dans les pages client (planning, absences, extraction, employees, history, settings, onglet Nova). Au passage : le groupe Authentik 'Developers' est reconnu admin dans le middleware (aligné sur auth-options et le layout), et le layout dashboard délègue à resolveUser() au lieu de dupliquer la logique. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c2cffd8 commit 3f3de28

12 files changed

Lines changed: 160 additions & 129 deletions

File tree

app/(dashboard)/employees/page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
1111
import { useToast } from "@/components/hooks/use-toast";
1212
import { Users, Plus, Trash2, Edit, Mail, Phone } from "lucide-react";
1313
import type { Employee } from "@/lib/db/schema/employees";
14-
import { useUser } from "@stackframe/stack";
14+
import { useUserAccess } from "@/contexts/user-access-context";
1515
import { PageHeader } from '@/components/page-header';
1616
import { EmptyState } from '@/components/ui/empty-state';
1717
import { LoadingCard } from '@/components/ui/loading-card';
@@ -36,12 +36,8 @@ export default function EmployeesPage() {
3636
const [showAddDialog, setShowAddDialog] = useState(false);
3737
const [showEditDialog, setShowEditDialog] = useState(false);
3838
const { toast } = useToast();
39-
const stackUser = useUser();
40-
const planningPermission = stackUser
41-
? ((stackUser.clientReadOnlyMetadata?.planningPermission ||
42-
stackUser.clientMetadata?.planningPermission ||
43-
'reader') as string)
44-
: 'reader';
39+
const access = useUserAccess();
40+
const planningPermission = access?.planningPermission ?? 'reader';
4541

4642
useEffect(() => {
4743
if (employeesError) {

app/(dashboard)/history/_components/history-client.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useEffect, useRef, useState } from "react";
4-
import { useUser } from "@stackframe/stack";
4+
import { useUserAccess } from "@/contexts/user-access-context";
55
import { Button } from "@/components/ui/button";
66
import { Input } from "@/components/ui/input";
77
import { Badge } from "@/components/ui/badge";
@@ -30,7 +30,7 @@ interface HistoryEntry {
3030
}
3131

3232
export default function HistoryClient({ initialHistory }: { initialHistory: HistoryEntry[] }) {
33-
const stackUser = useUser();
33+
const access = useUserAccess();
3434
// Données initiales fournies par le serveur (limit=100, sans filtre) →
3535
// pas de fetch au montage, pas de flash de loader.
3636
const [history, setHistory] = useState<HistoryEntry[]>(initialHistory);
@@ -43,11 +43,7 @@ export default function HistoryClient({ initialHistory }: { initialHistory: Hist
4343
// filtre change réellement.
4444
const skipInitialFetch = useRef(true);
4545

46-
const planningPermission = stackUser
47-
? ((stackUser.clientReadOnlyMetadata?.planningPermission ||
48-
stackUser.clientMetadata?.planningPermission ||
49-
'reader') as string)
50-
: 'reader';
46+
const planningPermission = access?.planningPermission ?? 'reader';
5147

5248
useEffect(() => {
5349
if (skipInitialFetch.current) {

app/(dashboard)/layout.tsx

Lines changed: 37 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,10 @@ import { SiteHeader } from '@/components/site-header';
33
import { AppTabs } from '@/components/app-tabs';
44
import { BottomNav } from '@/components/bottom-nav';
55
import { AssistantBubble } from '@/components/assistant/assistant-bubble';
6-
import { stackServerApp } from '@/lib/stack-server';
76
import { redirect } from 'next/navigation';
8-
import { getServerSession } from 'next-auth/next';
9-
import { authOptions } from '@/lib/auth-options'; // ton NextAuth config
7+
import { resolveUser } from '@/lib/api/with-auth';
108
import { isAdminRole } from '@/lib/nav-apps';
9+
import { UserAccessProvider } from '@/contexts/user-access-context';
1110
import type React from 'react';
1211

1312
// Dashboard authentifié : rendu à la demande (jamais prérendu au build).
@@ -20,79 +19,15 @@ export default async function DashboardLayout({
2019
children: React.ReactNode;
2120
}) {
2221
// ===============================
23-
// 1. Essayer Stack Auth
22+
// 1. Résolution unifiée Stack / Authentik (rôle + permission planning ;
23+
// les comptes Authentik lisent leurs accès dans la table locale `users`).
2424
// ===============================
25-
const stackUser = await stackServerApp.getUser();
26-
27-
let user: {
28-
id: string;
29-
email: string;
30-
name: string;
31-
image?: string;
32-
role: string;
33-
planningPermission: string;
34-
provider: 'stack' | 'authentik';
35-
} | null = null;
36-
37-
if (stackUser) {
38-
// Utilisateur Stack Auth trouvé
39-
user = {
40-
id: stackUser.id,
41-
email: stackUser.primaryEmail ?? '',
42-
name: stackUser.displayName ?? stackUser.primaryEmail ?? '',
43-
image: stackUser.profileImageUrl ?? undefined,
44-
role:
45-
stackUser.serverMetadata?.role ||
46-
stackUser.clientReadOnlyMetadata?.role ||
47-
stackUser.clientMetadata?.role ||
48-
'user',
49-
planningPermission:
50-
stackUser.serverMetadata?.planningPermission ||
51-
stackUser.clientReadOnlyMetadata?.planningPermission ||
52-
stackUser.clientMetadata?.planningPermission ||
53-
'reader',
54-
provider: 'stack',
55-
};
56-
57-
/*console.log('✅ Dashboard - Stack Auth utilisateur:', user.email, '- Rôle:', user.role);*/
58-
} else {
59-
// ===============================
60-
// 2. Essayer NextAuth / Authentik
61-
// ===============================
62-
const session = await getServerSession(authOptions);
63-
64-
if (session?.user?.email) {
65-
// Déterminer le rôle à partir des groupes Authentik
66-
const groups: string[] = (session.user.groups || []) as string[];
67-
// Admin = groupe Developers (ou authentik Admins). Sans groupe = étudiant.
68-
const isAdmin = groups.includes('Developers') || groups.includes('authentik Admins');
69-
70-
user = {
71-
id: session.user.id ?? '',
72-
email: session.user.email ?? '',
73-
name: session.user.name ?? session.user.email ?? '',
74-
image: session.user.image ?? undefined,
75-
role: isAdmin ? 'Admin' : 'user',
76-
planningPermission: 'reader', // par défaut pour Authentik
77-
provider: 'authentik',
78-
};
79-
80-
console.log(
81-
'✅ Dashboard - NextAuth/Authentik utilisateur connecté:',
82-
user.email,
83-
'Role:',
84-
user.role
85-
);
86-
}
87-
}
25+
const user = await resolveUser();
8826

8927
// ===============================
9028
// 3. Pas de session détectée → rediriger vers login
9129
// ===============================
9230
if (!user) {
93-
console.log(
94-
'⛔ Dashboard - Aucun utilisateur connecté, redirection vers /login'
95-
);
9631
redirect('/login');
9732
}
9833

@@ -111,36 +46,51 @@ export default async function DashboardLayout({
11146
// ===============================
11247
const isStudent = !isAdminRole(user.role);
11348

49+
// Accès unifié exposé aux Client Components (pages planning/settings…) :
50+
// remplace useUser() de Stack, qui renvoie null pour les comptes Authentik.
51+
const access = {
52+
email: user.email,
53+
name: user.name,
54+
image: user.image ?? null,
55+
role: user.role,
56+
planningPermission: user.planningPermission,
57+
provider: user.provider,
58+
};
59+
11460
// Étudiant : shell minimal (juste le header pour le menu/déconnexion). La
11561
// navigation est la navbar PARTAGÉE z01-student-nav (sidebar desktop / bottom
11662
// mobile, rendue dans la landing) — identique sur hub/émargement/01deck. On
11763
// réserve l'espace : padding gauche (sidebar desktop) / bas (barre mobile).
11864
if (isStudent) {
11965
return (
120-
<div className="fixed inset-0 flex flex-col overflow-hidden">
121-
<SiteHeader />
122-
<div className="flex flex-1 flex-col min-h-0 overflow-auto">{children}</div>
123-
</div>
66+
<UserAccessProvider value={access}>
67+
<div className="fixed inset-0 flex flex-col overflow-hidden">
68+
<SiteHeader />
69+
<div className="flex flex-1 flex-col min-h-0 overflow-auto">{children}</div>
70+
</div>
71+
</UserAccessProvider>
12472
);
12573
}
12674

12775
// Admin : shell complet (sidebar + onglets + bottom-nav).
12876
return (
129-
<div className="fixed inset-0 flex overflow-hidden">
130-
<AppSidebar user={user} />
131-
<div className="flex flex-1 flex-col min-w-0 overflow-hidden">
132-
<SiteHeader />
133-
<AppTabs />
134-
<div className="flex flex-1 flex-col min-h-0 overflow-auto pb-14 md:pb-0">
135-
<div className="@container/main flex flex-1 flex-col gap-2">
136-
{children}
77+
<UserAccessProvider value={access}>
78+
<div className="fixed inset-0 flex overflow-hidden">
79+
<AppSidebar user={user} />
80+
<div className="flex flex-1 flex-col min-w-0 overflow-hidden">
81+
<SiteHeader />
82+
<AppTabs />
83+
<div className="flex flex-1 flex-col min-h-0 overflow-auto pb-14 md:pb-0">
84+
<div className="@container/main flex flex-1 flex-col gap-2">
85+
{children}
86+
</div>
13787
</div>
13888
</div>
89+
<BottomNav user={user} />
90+
{(user.role === 'Admin' || user.role === 'Super Admin') && (
91+
<AssistantBubble userId={user.email} />
92+
)}
13993
</div>
140-
<BottomNav user={user} />
141-
{(user.role === 'Admin' || user.role === 'Super Admin') && (
142-
<AssistantBubble userId={user.email} />
143-
)}
144-
</div>
94+
</UserAccessProvider>
14595
);
14696
}

app/(dashboard)/planning/absences/page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import { getWeekNumber } from '@/lib/db/utils';
3434
import { Badge } from '@/components/ui/badge';
3535
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
3636
import { toast as sonnerToast } from 'sonner';
37-
import { useUser } from "@stackframe/stack";
37+
import { useUserAccess } from "@/contexts/user-access-context";
3838
import { PageHeader } from '@/components/page-header';
3939
import { FilterToolbar } from '@/components/planning/filter-toolbar';
4040
import { EmployeeColorDot } from '@/components/planning/employee-color-dot';
@@ -170,12 +170,8 @@ export default function AbsencesPage() {
170170
search: string;
171171
}>({ employeeId: 'all', type: 'all', start: null, end: null, search: '' });
172172
const { toast } = useToast();
173-
const stackUser = useUser();
174-
const planningPermission = stackUser
175-
? ((stackUser.clientReadOnlyMetadata?.planningPermission ||
176-
stackUser.clientMetadata?.planningPermission ||
177-
'reader') as string)
178-
: 'reader';
173+
const access = useUserAccess();
174+
const planningPermission = access?.planningPermission ?? 'reader';
179175

180176
const [addDialogOpen, setAddDialogOpen] = useState(false);
181177
const [addEmployeeId, setAddEmployeeId] = useState('');

app/(dashboard)/planning/extraction/page.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
66
import { Badge } from '@/components/ui/badge';
77
import { addDays, isAfter, parseISO } from 'date-fns';
88
import { FileBarChart, Loader2 } from 'lucide-react';
9-
import { useUser } from "@stackframe/stack";
9+
import { useUserAccess } from "@/contexts/user-access-context";
1010
import { PageHeader } from '@/components/page-header';
1111
import { FilterToolbar } from '@/components/planning/filter-toolbar';
1212
import { EmployeeColorDot } from '@/components/planning/employee-color-dot';
@@ -75,12 +75,8 @@ export default function ExtractionPage() {
7575
const [rows, setRows] = useState<ExtractionRow[]>([]);
7676
const [loading, setLoading] = useState(false);
7777
const [employees, setEmployees] = useState<Employee[]>([]);
78-
const stackUser = useUser();
79-
const planningPermission = stackUser
80-
? ((stackUser.clientReadOnlyMetadata?.planningPermission ||
81-
stackUser.clientMetadata?.planningPermission ||
82-
'reader') as string)
83-
: 'reader';
78+
const access = useUserAccess();
79+
const planningPermission = access?.planningPermission ?? 'reader';
8480

8581
useEffect(() => {
8682
fetch('/api/employees')

app/(dashboard)/planning/page.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Badge } from '@/components/ui/badge';
55
import { Button } from '@/components/ui/button';
66
import { Loader2, LayoutTemplate, Users, Clock, List, Grid } from 'lucide-react';
77
import { useToast } from '@/components/hooks/use-toast';
8-
import { useUser } from '@stackframe/stack';
8+
import { useUserAccess } from '@/contexts/user-access-context';
99
import { useIsMobile } from '@/components/hooks/use-mobile';
1010
import { PageHeader } from '@/components/page-header';
1111
import { WeekSelector } from '@/components/planning/week-selector';
@@ -37,10 +37,8 @@ const slotTypeConfig: Record<string, { label: string; bgColor: string; borderCol
3737
};
3838

3939
export default function PlanningPage() {
40-
const stackUser = useUser();
41-
const planningPermission = stackUser
42-
? ((stackUser.clientReadOnlyMetadata?.planningPermission || stackUser.clientMetadata?.planningPermission || 'reader') as string)
43-
: 'reader';
40+
const access = useUserAccess();
41+
const planningPermission = access?.planningPermission ?? 'reader';
4442
const isEditor = planningPermission === 'editor';
4543
const isMobile = useIsMobile();
4644

app/(dashboard)/settings/page.tsx

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useSearchParams } from 'next/navigation';
55
import { useTheme } from 'next-themes';
66
import { useUser } from '@stackframe/stack';
77
import { AccountSettings } from '@stackframe/stack';
8+
import { useUserAccess } from '@/contexts/user-access-context';
89
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
910
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
1011
import { Switch } from '@/components/ui/switch';
@@ -49,7 +50,11 @@ export default function SettingsPage() {
4950
// anywhere in the app switches the active tab (not just on first mount).
5051
const tabFromUrl = searchParams.get('tab') ?? 'profile';
5152
const { theme, setTheme } = useTheme();
53+
// Session Stack (null pour un compte Authentik) — sert uniquement aux
54+
// features Stack (AccountSettings, prefs stockées en clientMetadata).
5255
const user = useUser();
56+
// Accès unifié Stack/Authentik résolu côté serveur (layout dashboard).
57+
const access = useUserAccess();
5358
const { density, setDensity } = useUIPreferences();
5459
const [mounted, setMounted] = useState(false);
5560
const [emailNotifications, setEmailNotifications] = useState(true);
@@ -104,8 +109,8 @@ export default function SettingsPage() {
104109
updateNotificationPref('browserNotifications', checked);
105110
};
106111

107-
const userRole = (user?.clientReadOnlyMetadata as Record<string, unknown>)?.role as string || 'user';
108-
const planningPermission = (user?.clientReadOnlyMetadata as Record<string, unknown>)?.planningPermission as string || 'reader';
112+
const userRole = access?.role ?? 'user';
113+
const planningPermission = access?.planningPermission ?? 'reader';
109114

110115
return (
111116
<div className="page-container flex flex-col gap-4 md:gap-6 p-4 md:p-6">
@@ -180,7 +185,27 @@ export default function SettingsPage() {
180185
</CardDescription>
181186
</CardHeader>
182187
<CardContent>
183-
<AccountSettings fullPage={false} />
188+
{user ? (
189+
// Compte Stack : panneau de gestion Stack (email, mdp, MFA…).
190+
// NE PAS le rendre sans session Stack : il redirige vers
191+
// /handler/sign-in (cas des comptes Authentik).
192+
<AccountSettings fullPage={false} />
193+
) : (
194+
<div className="space-y-3">
195+
<div className="flex items-center justify-between p-3 border rounded-lg">
196+
<span className="text-sm font-medium">Nom</span>
197+
<span className="text-sm text-muted-foreground">{access?.name ?? '—'}</span>
198+
</div>
199+
<div className="flex items-center justify-between p-3 border rounded-lg">
200+
<span className="text-sm font-medium">Email</span>
201+
<span className="text-sm text-muted-foreground">{access?.email ?? '—'}</span>
202+
</div>
203+
<p className="text-xs text-muted-foreground">
204+
Votre compte est géré par le SSO Zone01 (Authentik). Les informations
205+
personnelles et le mot de passe se modifient depuis Authentik.
206+
</p>
207+
</div>
208+
)}
184209
</CardContent>
185210
</Card>
186211
</TabsContent>

components/settings/nova-settings-tab.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { useEffect, useState } from 'react';
44
import { useUser } from '@stackframe/stack';
5+
import { useUserAccess } from '@/contexts/user-access-context';
56
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
67
import { Button } from '@/components/ui/button';
78
import { Badge } from '@/components/ui/badge';
@@ -33,7 +34,10 @@ type ConversationStats = {
3334
*/
3435
export function NovaSettingsTab() {
3536
const user = useUser();
36-
const userId = user?.primaryEmail || 'anonymous';
37+
const access = useUserAccess();
38+
// Même identifiant que la bulle Nova (layout : userId = email unifié) —
39+
// sans le fallback access, un compte Authentik retombait sur 'anonymous'.
40+
const userId = user?.primaryEmail || access?.email || 'anonymous';
3741
const [stats, setStats] = useState<ConversationStats | null>(null);
3842
const [deleting, setDeleting] = useState(false);
3943

0 commit comments

Comments
 (0)