Skip to content

Commit 8251cf2

Browse files
authored
Merge pull request #132 from makcimerrr/fix/authentik-settings-planning-permission
fix(auth): comptes Authentik — /settings sans redirect Stack + vraie permission planning
2 parents c2cffd8 + 3f3de28 commit 8251cf2

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)