Skip to content

Commit c983c37

Browse files
committed
AuthGuard
1 parent 8c02424 commit c983c37

File tree

7 files changed

+223
-103
lines changed

7 files changed

+223
-103
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { EXTERNAL } from '@/constant';
3+
import { getUserProfile, type UserProfile } from '@/lib/utils';
4+
5+
interface AuthGuardProps {
6+
children: React.ReactNode;
7+
}
8+
9+
export default function AuthGuard({ children }: AuthGuardProps) {
10+
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
11+
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
12+
13+
useEffect(() => {
14+
async function checkAuth() {
15+
const profile = await getUserProfile(EXTERNAL.directus_url);
16+
if (profile) {
17+
setUserProfile(profile);
18+
setIsAuthenticated(true);
19+
} else {
20+
setIsAuthenticated(false);
21+
// Redirect to login
22+
window.location.href = `${EXTERNAL.directus_url}/auth/login/authentik?redirect=${encodeURIComponent(window.location.href)}`;
23+
}
24+
}
25+
26+
checkAuth();
27+
}, []);
28+
29+
// Show loading while checking auth
30+
if (isAuthenticated === null) {
31+
return (
32+
<div className="min-h-screen flex items-center justify-center">
33+
<div className="text-center">
34+
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 mx-auto mb-4"></div>
35+
<p>Checking authentication...</p>
36+
</div>
37+
</div>
38+
);
39+
}
40+
41+
// Show children if authenticated
42+
if (isAuthenticated) {
43+
return <>{children}</>;
44+
}
45+
46+
// This shouldn't render as we redirect above, but just in case
47+
return (
48+
<div className="min-h-screen flex items-center justify-center">
49+
<div className="text-center">
50+
<p>Redirecting to login...</p>
51+
</div>
52+
</div>
53+
);
54+
}

src/components/interactive/RoleFitIndexForm.tsx

Lines changed: 54 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
1919
import DragAndDropUpload from "./DragAndDropUpload";
2020
import { Loader2, Check, X } from "lucide-react";
2121
import { EXTERNAL } from '@/constant';
22+
import { getUserProfile, type UserProfile } from '@/lib/utils';
2223

2324
const schema = z.object({
2425
jobDescription: z.string().min(1, "Paste the JD or provide a URL"),
@@ -27,7 +28,7 @@ const schema = z.object({
2728

2829
type FormValues = z.infer<typeof schema>;
2930

30-
type Me = { id: string } | null;
31+
type Me = UserProfile | null;
3132
type BalanceRow = {
3233
id: number;
3334
user: string;
@@ -78,25 +79,28 @@ export default function RoleFitForm() {
7879

7980
const DIRECTUS_URL = EXTERNAL.directus_url;
8081

82+
// Helper function to get authorization header
83+
const getAuthHeaders = (): Record<string, string> => {
84+
return isAuthed
85+
? {} // No auth header needed for authenticated users (using session cookies)
86+
: { Authorization: `Bearer ${EXTERNAL.directus_key}` }; // Guest token for unauthenticated users
87+
};
88+
8189
// --- Auth & Quota fetch (session cookies) ---
8290
useEffect(() => {
8391
let cancelled = false;
8492

8593
(async () => {
8694
try {
8795
// who am I?
88-
const meRes = await fetch(`${DIRECTUS_URL}/users/me`, {
89-
credentials: "include",
90-
});
91-
if (!meRes.ok) {
96+
const meData = await getUserProfile(DIRECTUS_URL);
97+
if (!meData) {
9298
setIsAuthed(false);
9399
setMe(null);
94100
setQuotaUsed(null);
95101
setQuotaRemaining(null);
96102
return;
97103
}
98-
const meJson = await meRes.json();
99-
const meData: Me = meJson?.data ?? null;
100104
if (cancelled) return;
101105

102106
setMe(meData);
@@ -167,7 +171,8 @@ export default function RoleFitForm() {
167171
url.searchParams.set("limit", "1");
168172

169173
const res = await fetch(url.toString(), {
170-
headers: { Authorization: `Bearer ${EXTERNAL.directus_key}` },
174+
credentials: isAuthed ? "include" : "omit",
175+
headers: getAuthHeaders(),
171176
});
172177
const js = await res.json();
173178
if (!res.ok) throw new Error(js?.errors?.[0]?.message || "Checksum lookup failed");
@@ -193,7 +198,8 @@ export default function RoleFitForm() {
193198
fd.append("file", file, file.name || "cv.pdf");
194199
const uploadRes = await fetch(`${DIRECTUS_URL}/files`, {
195200
method: "POST",
196-
headers: { Authorization: `Bearer ${EXTERNAL.directus_key}` },
201+
credentials: isAuthed ? "include" : "omit",
202+
headers: getAuthHeaders(),
197203
body: fd,
198204
});
199205
const uploadJs = await uploadRes.json();
@@ -207,17 +213,21 @@ export default function RoleFitForm() {
207213

208214
/** Create submission */
209215
const createSubmission = async (jd: string, fileId: string) => {
216+
const body = {
217+
cv_file: fileId,
218+
status: "submitted",
219+
job_description: { raw_input: jd },
220+
...(isAuthed && me?.id ? { user: me.id } : {}),
221+
};
222+
210223
const res = await fetch(`${DIRECTUS_URL}/items/role_fit_index_submission`, {
211224
method: "POST",
225+
credentials: isAuthed ? "include" : "omit",
212226
headers: {
213-
Authorization: `Bearer ${EXTERNAL.directus_key}`,
227+
...getAuthHeaders(),
214228
"Content-Type": "application/json",
215229
},
216-
body: JSON.stringify({
217-
cv_file: fileId,
218-
status: "submitted",
219-
job_description: { raw_input: jd },
220-
}),
230+
body: JSON.stringify(body),
221231
});
222232
const js = await res.json();
223233
if (!res.ok) throw new Error(js?.errors?.[0]?.message || "Submission failed");
@@ -235,7 +245,8 @@ export default function RoleFitForm() {
235245
url.searchParams.set("sort", "-date_created");
236246
url.searchParams.set("filter[submission][_eq]", id);
237247
const res = await fetch(url.toString(), {
238-
headers: { Authorization: `Bearer ${EXTERNAL.directus_key}` },
248+
credentials: isAuthed ? "include" : "omit",
249+
headers: getAuthHeaders(),
239250
});
240251
const js = await res.json();
241252
if (res.ok && js?.data?.length) {
@@ -279,7 +290,10 @@ export default function RoleFitForm() {
279290
}, 90_000);
280291

281292
ws.onopen = () => {
282-
ws.send(JSON.stringify({ type: "auth", access_token: EXTERNAL.directus_key }));
293+
const authToken = isAuthed ? undefined : EXTERNAL.directus_key;
294+
if (authToken) {
295+
ws.send(JSON.stringify({ type: "auth", access_token: authToken }));
296+
}
283297
};
284298

285299
ws.onmessage = async (evt) => {
@@ -313,7 +327,8 @@ export default function RoleFitForm() {
313327
url.searchParams.set("sort", "-date_created");
314328
url.searchParams.set("filter[submission][_eq]", String(id));
315329
const res = await fetch(url.toString(), {
316-
headers: { Authorization: `Bearer ${EXTERNAL.directus_key}` },
330+
credentials: isAuthed ? "include" : "omit",
331+
headers: getAuthHeaders(),
317332
});
318333
const js = await res.json();
319334
if (res.ok && js?.data?.length) {
@@ -519,14 +534,27 @@ export default function RoleFitForm() {
519534
) : null}
520535
</div>
521536

522-
<Button
523-
type="submit"
524-
variant="default"
525-
className="w-full"
526-
disabled={submitting || step !== 0}
527-
>
528-
{buttonText}
529-
</Button>
537+
{/* Conditionally show button or top-up link based on quota */}
538+
{isAuthed === true && quotaRemaining === 0 ? (
539+
<Button
540+
asChild
541+
variant="default"
542+
className="w-full"
543+
>
544+
<a href="/dashboard/role-fit-index/top-up">
545+
Top Up Credits to Continue
546+
</a>
547+
</Button>
548+
) : (
549+
<Button
550+
type="submit"
551+
variant="default"
552+
className="w-full"
553+
disabled={submitting || step !== 0}
554+
>
555+
{buttonText}
556+
</Button>
557+
)}
530558
</form>
531559
</Form>
532560
</CardContent>

src/lib/utils.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,29 @@ import { twMerge } from "tailwind-merge"
44
export function cn(...inputs: ClassValue[]) {
55
return twMerge(clsx(inputs))
66
}
7+
8+
export type UserProfile = {
9+
id: string;
10+
first_name?: string;
11+
last_name?: string;
12+
email?: string;
13+
[key: string]: any;
14+
}
15+
16+
export async function getUserProfile(directusUrl: string): Promise<UserProfile | null> {
17+
try {
18+
const res = await fetch(`${directusUrl}/users/me`, {
19+
credentials: "include",
20+
headers: { Accept: "application/json" },
21+
});
22+
23+
if (!res.ok) {
24+
return null;
25+
}
26+
27+
const json = await res.json();
28+
return json?.data ?? json;
29+
} catch {
30+
return null;
31+
}
32+
}

src/pages/dashboard/index.astro

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,31 @@ import Layout from "@/layouts/Layout.astro";
33
import Header from "@/components/Header.astro";
44
import Footer from "@/components/Footer.astro";
55
import { Sidebar } from "@/components/interactive/Sidebar";
6+
import AuthGuard from "@/components/interactive/AuthGuard";
67
---
78

89
<Layout title="Home - Bounteer">
9-
<Header />
10+
<AuthGuard client:load>
11+
<Header />
1012

11-
<main class="bg-gray-50">
12-
<section class="container-custom lg:max-w-none lg:px-0 py-10 lg:py-14">
13-
<div class="lg:flex">
14-
<Sidebar />
13+
<main class="bg-gray-50">
14+
<section class="container-custom lg:max-w-none lg:px-0 py-10 lg:py-14">
15+
<div class="lg:flex">
16+
<Sidebar />
1517

16-
<div class="w-full lg:pl-6 lg:pr-8">
17-
<div class="mb-6 py-6">
18-
<h1 class="text-2xl font-semibold tracking-tight">Dashboard</h1>
19-
<p class="text-sm text-muted-foreground">
20-
Choose a credit package to add funds to your account.
21-
</p>
18+
<div class="w-full lg:pl-6 lg:pr-8">
19+
<div class="mb-6 py-6">
20+
<h1 class="text-2xl font-semibold tracking-tight">Dashboard</h1>
21+
<p class="text-sm text-muted-foreground">
22+
Choose a credit package to add funds to your account.
23+
</p>
24+
</div>
25+
something here
2226
</div>
23-
something here
2427
</div>
25-
</div>
26-
</section>
27-
</main>
28+
</section>
29+
</main>
2830

29-
<Footer />
31+
<Footer />
32+
</AuthGuard>
3033
</Layout>

src/pages/dashboard/role-fit-index/index.astro

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,33 @@ import Footer from "@/components/Footer.astro";
55
import RoleFitForm from "@/components/interactive/RoleFitIndexForm";
66
import { Sidebar } from "@/components/interactive/Sidebar";
77
import UserReportTable from "@/components/interactive/UserReportTable";
8+
import AuthGuard from "@/components/interactive/AuthGuard";
89
910
const USER_ID = "51bad019-1d3b-4f7a-9a89-87355fbcde84";
1011
---
1112

1213
<Layout title="Role Fit Index - Bounteer">
13-
<Header />
14+
<AuthGuard client:load>
15+
<Header />
1416

15-
<main class="bg-gray-50">
16-
<section class="container-custom lg:max-w-none lg:px-0 py-10 lg:py-14">
17-
<div class="lg:flex">
18-
<Sidebar />
17+
<main class="bg-gray-50">
18+
<section class="container-custom lg:max-w-none lg:px-0 py-10 lg:py-14">
19+
<div class="lg:flex">
20+
<Sidebar />
1921

20-
<div class="w-full lg:pl-6 lg:pr-8">
21-
<div class="mb-6 py-6">
22-
<h1 class="text-2xl font-semibold tracking-tight">
23-
Role Fit Index Reports
24-
</h1>
25-
</div>
22+
<div class="w-full lg:pl-6 lg:pr-8">
23+
<div class="mb-6 py-6">
24+
<h1 class="text-2xl font-semibold tracking-tight">
25+
Role Fit Index Reports
26+
</h1>
27+
</div>
2628

27-
<UserReportTable userId={USER_ID} pageSize={10} client:load />
29+
<UserReportTable userId={USER_ID} pageSize={10} client:load />
30+
</div>
2831
</div>
29-
</div>
30-
</section>
31-
</main>
32+
</section>
33+
</main>
3234

33-
<Footer />
35+
<Footer />
36+
</AuthGuard>
3437
</Layout>

src/pages/dashboard/role-fit-index/top-up/confirm.astro

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@ import Footer from "@/components/Footer.astro";
44
import { Sidebar } from "@/components/interactive/Sidebar";
55
import { Button } from "@/components/ui/button";
66
import Header from "@/components/Header.astro";
7+
import AuthGuard from "@/components/interactive/AuthGuard";
78
---
89

910
<Layout title="Top Up Confirmation — Bounteer">
10-
<Header />
11+
<AuthGuard client:load>
12+
<Header />
1113

12-
<main class="bg-gray-50 min-h-screen flex">
13-
<Sidebar />
14+
<main class="bg-gray-50 min-h-screen flex">
15+
<Sidebar />
1416

15-
<section class="flex-1 flex flex-col items-center justify-center px-6">
16-
<h1 class="text-2xl font-semibold tracking-tight mb-6 text-center">
17-
Payment confirmed, Thank you!
18-
</h1>
19-
<Button className="w-fit">
20-
<a href="/dashboard/role-fit-index/top-up/">Return</a>
21-
</Button>
22-
</section>
23-
</main>
17+
<section class="flex-1 flex flex-col items-center justify-center px-6">
18+
<h1 class="text-2xl font-semibold tracking-tight mb-6 text-center">
19+
Payment confirmed, Thank you!
20+
</h1>
21+
<Button className="w-fit">
22+
<a href="/dashboard/role-fit-index/top-up/">Return</a>
23+
</Button>
24+
</section>
25+
</main>
2426

25-
<Footer />
27+
<Footer />
28+
</AuthGuard>
2629
</Layout>

0 commit comments

Comments
 (0)