Skip to content

Commit 0316790

Browse files
Merge pull request #17 from NYCU-SDC/feat/CORE-188-adminNav-api-integration
[CORE-188] AdminNav Api Integration
2 parents 759209c + 6dc38a3 commit 0316790

File tree

7 files changed

+107
-28
lines changed

7 files changed

+107
-28
lines changed

src/features/auth/hooks/useAuth.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { authService, type AuthUser } from "@/features/auth/services/authService";
12
import { useQuery } from "@tanstack/react-query";
2-
import { authService, type AuthUser } from "../services/authService";
3+
4+
const canAccessAdmin = (user: AuthUser | null) => !!user;
35

46
export const useAuth = () => {
57
const { data: user, isLoading } = useQuery<AuthUser | null>({
@@ -12,6 +14,7 @@ export const useAuth = () => {
1214
user: user ?? null,
1315
isLoading,
1416
isAuthenticated: !!user,
15-
isOnboarded: user?.is_onboarded ?? false
17+
isOnboarded: user?.is_onboarded ?? false,
18+
isAdmin: canAccessAdmin(user ?? null)
1619
};
1720
};
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { useOrgMembers } from "@/features/dashboard/hooks/useOrgSettings";
2+
import { useMemo } from "react";
3+
import { useAuth } from "./useAuth";
4+
5+
const ORG_SLUG = "SDC";
6+
7+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
8+
const getString = (value: unknown): string | null => (typeof value === "string" ? value : null);
9+
const getStringArray = (value: unknown): string[] => (Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : []);
10+
const normalize = (value: string) => value.trim().toLowerCase();
11+
12+
export const useOrgAdminAccess = () => {
13+
const { user, isLoading: isAuthLoading } = useAuth();
14+
const membersQuery = useOrgMembers(ORG_SLUG, !!user);
15+
16+
const isOrgMember = useMemo(() => {
17+
if (!user) return false;
18+
const rawMembers = membersQuery.data;
19+
if (!Array.isArray(rawMembers)) return false;
20+
21+
const byId = normalize(user.id ?? "");
22+
const byUsername = normalize(user.username ?? "");
23+
const byEmails = new Set((user.emails ?? []).map(normalize));
24+
25+
return rawMembers.some(item => {
26+
if (!isRecord(item)) return false;
27+
const member = isRecord(item.member) ? item.member : item;
28+
29+
const memberId = normalize(getString(member.id) ?? "");
30+
const memberUsername = normalize(getString(member.username) ?? "");
31+
const memberEmails = getStringArray(member.emails).map(normalize);
32+
33+
if (byId && memberId && byId === memberId) return true;
34+
if (byUsername && memberUsername && byUsername === memberUsername) return true;
35+
36+
return memberEmails.some(email => byEmails.has(email));
37+
});
38+
}, [membersQuery.data, user]);
39+
40+
const isLoading = isAuthLoading || membersQuery.isLoading;
41+
const canAccessOrgAdmin = isOrgMember;
42+
43+
return {
44+
user,
45+
canAccessOrgAdmin,
46+
isLoading
47+
};
48+
};

src/features/auth/services/authService.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type AuthUser = {
88
roles?: Array<"USER">;
99
is_onboarded?: boolean;
1010
allow_onboarding?: boolean;
11+
isMember?: boolean;
12+
isFirstLogin?: boolean;
1113
};
1214

1315
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
@@ -67,7 +69,7 @@ export const authService = {
6769

6870
return readJsonSafely<unknown>(response);
6971
},
70-
72+
7173
async getCurrentUser<TUser extends AuthUser = AuthUser>(): Promise<TUser | null> {
7274
const response = await fetch("/api/users/me", {
7375
credentials: "include"
@@ -81,7 +83,7 @@ export const authService = {
8183
}
8284

8385
return readJsonSafely<TUser>(response);
84-
},
86+
},
8587

8688
async updateOnboarding(payload: { username: string; name: string }) {
8789
const response = await fetch("/api/users/onboarding", {

src/features/dashboard/hooks/useOrgSettings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ export const useOrg = (slug: string) =>
1111
queryFn: () => api.getOrg(slug)
1212
});
1313

14-
export const useOrgMembers = (slug: string) =>
14+
export const useOrgMembers = (slug: string, enabled = true) =>
1515
useQuery({
1616
queryKey: orgKeys.members(slug),
17-
queryFn: () => api.listOrgMembers(slug)
17+
queryFn: () => api.listOrgMembers(slug),
18+
enabled
1819
});
1920

2021
/* ---------- Mutations ---------- */

src/layouts/AdminNav.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useOrgAdminAccess } from "@/features/auth/hooks/useOrgAdminAccess";
12
import { ClipboardList, FileText, LogOut, Menu, Settings, X } from "lucide-react";
23
import { useState } from "react";
34
import { Link, matchPath, useLocation, useParams } from "react-router-dom";
@@ -16,15 +17,15 @@ export const AdminNav = () => {
1617
const isFormDetail = !!matchPath({ path: "/orgs/sdc/forms/:formid/*", end: false }, pathname);
1718
const isSettings = pathname.startsWith("/orgs/sdc/settings");
1819

19-
const user = {
20-
name: "Alice King",
21-
username: "alice",
22-
avatarUrl: ""
23-
};
20+
const { user, canAccessOrgAdmin, isLoading } = useOrgAdminAccess();
2421

25-
const displayName = user.name || user.username || "??";
26-
const initials = displayName.slice(0, 2).toUpperCase();
27-
const hasAvatar = !!user.avatarUrl;
22+
if (isLoading || !canAccessOrgAdmin) {
23+
return null;
24+
}
25+
26+
const displayName = user?.name || user?.username || user?.emails?.[0] || "ˊ_>ˋ";
27+
const initials = user ? displayName.slice(0, 2).toUpperCase() : displayName;
28+
const hasAvatar = !!user?.avatarUrl;
2829

2930
return (
3031
<>
@@ -60,11 +61,11 @@ export const AdminNav = () => {
6061
</Link>
6162

6263
{/* Avatar */}
63-
<div className={styles.avatarContainer}>
64-
{hasAvatar ? <img src={user.avatarUrl} alt={displayName} className={styles.avatarImg} /> : <div className={styles.avatarFallback}>{initials}</div>}
64+
<div className={styles.avatarContainer} title={user?.username || displayName}>
65+
{hasAvatar ? <img src={user?.avatarUrl} alt={displayName} className={styles.avatarImg} /> : <div className={styles.avatarFallback}>{initials}</div>}
6566
</div>
6667

67-
<Link to="/" className={styles.link}>
68+
<Link to="/logout" className={styles.link} aria-label="Logout" title="Logout">
6869
<div className={`${styles.navItem} ${styles.logoutItem}`}>
6970
<LogOut size={22} />
7071
</div>

src/routes/AppRouter.tsx

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ComponentsDemo } from "@/features/dashboard/components/ComponentsDemo";
88
import { AdminFormDetailPage, AdminFormsPage, FormDetailPage, FormsListPage } from "@/features/form/components";
99
import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
1010
import OrgRewriteToSdc from "./OrgRewriteToSdc";
11+
import RequireOrgAdminAccess from "./RequireOrgAdminAccess";
1112

1213
export const AppRouter = () => {
1314
return (
@@ -26,19 +27,23 @@ export const AppRouter = () => {
2627
<Route path="/forms" element={<FormsListPage />} />
2728
<Route path="/forms/:id" element={<FormDetailPage />} />
2829

29-
{/* Organization redirects */}
30-
<Route path="/orgs/:orgId/*" element={<OrgRewriteToSdc />} />
31-
<Route path="/orgs" element={<Navigate to="/orgs/sdc/forms" replace />} />
32-
<Route path="/orgs/sdc" element={<Navigate to="/orgs/sdc/forms" replace />} />
30+
{/* Organization redirects (org member only) */}
31+
<Route element={<RequireOrgAdminAccess />}>
32+
<Route path="/orgs/:orgId/*" element={<OrgRewriteToSdc />} />
33+
<Route path="/orgs" element={<Navigate to="/orgs/sdc/forms" replace />} />
34+
<Route path="/orgs/sdc" element={<Navigate to="/orgs/sdc/forms" replace />} />
35+
</Route>
3336

3437
{/* Admin routes */}
35-
<Route path="/orgs/sdc/forms" element={<AdminFormsPage />} />
36-
<Route path="/orgs/sdc/forms/:formid/info" element={<AdminFormDetailPage />} />
37-
<Route path="/orgs/sdc/forms/:formid/edit" element={<AdminFormDetailPage />} />
38-
<Route path="/orgs/sdc/forms/:formid/section/:sectionId/edit" element={<AdminFormDetailPage />} />
39-
<Route path="/orgs/sdc/forms/:formid/reply" element={<AdminFormDetailPage />} />
40-
<Route path="/orgs/sdc/forms/:formid/design" element={<AdminFormDetailPage />} />
41-
<Route path="/orgs/sdc/settings" element={<AdminSettingsPage />} />
38+
<Route element={<RequireOrgAdminAccess />}>
39+
<Route path="/orgs/sdc/forms" element={<AdminFormsPage />} />
40+
<Route path="/orgs/sdc/forms/:formid/info" element={<AdminFormDetailPage />} />
41+
<Route path="/orgs/sdc/forms/:formid/edit" element={<AdminFormDetailPage />} />
42+
<Route path="/orgs/sdc/forms/:formid/section/:sectionId/edit" element={<AdminFormDetailPage />} />
43+
<Route path="/orgs/sdc/forms/:formid/reply" element={<AdminFormDetailPage />} />
44+
<Route path="/orgs/sdc/forms/:formid/design" element={<AdminFormDetailPage />} />
45+
<Route path="/orgs/sdc/settings" element={<AdminSettingsPage />} />
46+
</Route>
4247

4348
{/* 404 */}
4449
<Route path="*" element={<NotFoundPage />} />
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { useOrgAdminAccess } from "@/features/auth/hooks/useOrgAdminAccess";
2+
import { Navigate, Outlet, useLocation } from "react-router-dom";
3+
4+
const RequireOrgAdminAccess = () => {
5+
const { canAccessOrgAdmin, isLoading } = useOrgAdminAccess();
6+
const location = useLocation();
7+
8+
if (isLoading) {
9+
return null;
10+
}
11+
12+
if (!canAccessOrgAdmin) {
13+
return <Navigate to="/forms" replace state={{ from: location.pathname }} />;
14+
}
15+
16+
return <Outlet />;
17+
};
18+
19+
export default RequireOrgAdminAccess;

0 commit comments

Comments
 (0)