Yvonne Priscilla commited on
Commit
e394370
·
1 Parent(s): 8f32a26

update login and redirect

Browse files
src/app/api/agentic/create_weight/route.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { NextRequest, NextResponse } from "next/server"
 
2
 
3
  export async function POST(request: NextRequest) {
4
- const token = request.headers.get("Authorization")
 
5
  const { searchParams } = new URL(request.url)
6
  const criteria_id = searchParams.get("criteria_id")
7
 
 
1
+ import { cookies } from "next/headers";
2
+ import { NextRequest, NextResponse } from "next/server";
3
 
4
  export async function POST(request: NextRequest) {
5
+ const cookieStore = await cookies();
6
+ const token = cookieStore.get('auth_token')?.value;
7
  const { searchParams } = new URL(request.url)
8
  const criteria_id = searchParams.get("criteria_id")
9
 
src/app/api/auth/login/route.ts CHANGED
@@ -1,97 +1,83 @@
1
- /**
2
- * Login API Route
3
- * POST /api/auth/login
4
- *
5
- * Accepts credentials, exchanges for token with backend,
6
- * stores token in HTTP-only cookie, returns user data
7
- */
8
-
9
- import { NextRequest, NextResponse } from "next/server";
10
- import { cookies } from "next/headers";
11
-
12
- const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || "https://byteriot-candidateexplorer.hf.space/CandidateExplorer";
13
 
14
  export async function POST(request: NextRequest) {
15
  try {
16
  const body = await request.json();
17
  const { username, password } = body;
18
 
 
 
19
  if (!username || !password) {
20
  return NextResponse.json(
21
- { message: "Username and password are required" },
22
  { status: 400 }
23
  );
24
  }
25
 
26
- // 1. Call backend /admin/login endpoint
27
- const loginResponse = await fetch(`${BACKEND_URL}/admin/login`, {
28
- method: "POST",
29
- headers: {
30
- "Content-Type": "application/x-www-form-urlencoded",
31
- },
32
- body: new URLSearchParams({
33
- username,
34
- password,
35
- }).toString(),
36
- });
 
37
 
38
  if (!loginResponse.ok) {
39
- const error = await loginResponse.text();
40
- console.error("[Login] Backend login failed:", error);
41
  return NextResponse.json(
42
- { message: "Invalid credentials" },
43
  { status: 401 }
44
  );
45
  }
46
 
47
- const loginData = await loginResponse.json();
48
- const accessToken = loginData.access_token;
49
 
50
- if (!accessToken) {
51
- console.error("[Login] No access_token in response");
52
- return NextResponse.json(
53
- { message: "Invalid login response from backend" },
54
- { status: 500 }
55
- );
56
- }
57
-
58
- // 2. Fetch user data from backend /admin/me
59
- const meResponse = await fetch(`${BACKEND_URL}/admin/me`, {
60
- method: "GET",
61
- headers: {
62
- Authorization: `Bearer ${accessToken}`,
63
- },
64
- });
65
 
66
- if (!meResponse.ok) {
67
- console.error("[Login] Failed to fetch user data");
68
  return NextResponse.json(
69
- { message: "Failed to fetch user data" },
70
  { status: 500 }
71
  );
72
  }
73
 
74
- const userData = await meResponse.json();
 
75
 
76
- // 3. Set token in HTTP-only cookie
77
  const cookieStore = await cookies();
78
- cookieStore.set("auth_token", accessToken, {
79
  httpOnly: true,
80
- secure: process.env.NODE_ENV === "production",
81
- sameSite: "lax",
82
- path: "/",
83
  maxAge: 7 * 24 * 60 * 60, // 7 days
84
  });
85
 
86
- // 4. Return user data and access token to client
87
- // NOTE: access_token is returned so caller may (optionally) persist it.
88
- const payload = { ...userData, access_token: accessToken };
89
- return NextResponse.json(payload, { status: 200 });
90
  } catch (error) {
91
- console.error("[Login] Error:", error);
92
  return NextResponse.json(
93
- { message: "Login failed" },
94
  { status: 500 }
95
  );
96
  }
97
- }
 
1
+ // app/api/auth/login/route.ts
2
+ import { cookies } from 'next/headers';
3
+ import { NextRequest, NextResponse } from 'next/server';
 
 
 
 
 
 
 
 
 
4
 
5
  export async function POST(request: NextRequest) {
6
  try {
7
  const body = await request.json();
8
  const { username, password } = body;
9
 
10
+ console.log('🟢 [API] Login attempt for:', username);
11
+
12
  if (!username || !password) {
13
  return NextResponse.json(
14
+ { message: 'Username and password are required' },
15
  { status: 400 }
16
  );
17
  }
18
 
19
+ const formdata = new FormData()
20
+ formdata.append("username", username)
21
+ formdata.append("password", password)
22
+
23
+ // Call backend
24
+ const loginResponse = await fetch(
25
+ 'https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/login',
26
+ {
27
+ method: 'POST',
28
+ body: formdata,
29
+ }
30
+ );
31
 
32
  if (!loginResponse.ok) {
33
+ console.log('🟢 [API] Backend rejected login');
 
34
  return NextResponse.json(
35
+ { message: 'Invalid credentials' },
36
  { status: 401 }
37
  );
38
  }
39
 
40
+ const { access_token } = await loginResponse.json();
41
+ console.log('🟢 [API] Got token from backend');
42
 
43
+ // Get user data
44
+ const userResponse = await fetch(
45
+ 'https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me',
46
+ {
47
+ headers: { Authorization: `Bearer ${access_token}` },
48
+ }
49
+ );
 
 
 
 
 
 
 
 
50
 
51
+ if (!userResponse.ok) {
52
+ console.log('🟢 [API] Failed to get user data');
53
  return NextResponse.json(
54
+ { message: 'Failed to get user data' },
55
  { status: 500 }
56
  );
57
  }
58
 
59
+ const userData = await userResponse.json();
60
+ console.log('🟢 [API] Got user data:', userData);
61
 
62
+ // Set cookie using Next.js cookies API (no 'cookie' package needed!)
63
  const cookieStore = await cookies();
64
+ cookieStore.set('auth_token', access_token, {
65
  httpOnly: true,
66
+ secure: process.env.NODE_ENV === 'production',
67
+ sameSite: 'lax',
68
+ path: '/',
69
  maxAge: 7 * 24 * 60 * 60, // 7 days
70
  });
71
 
72
+ console.log('🟢 [API] Cookie set successfully');
73
+
74
+ return NextResponse.json(userData, { status: 200 });
75
+
76
  } catch (error) {
77
+ console.error('🟢 [API] Login error:', error);
78
  return NextResponse.json(
79
+ { message: 'Login failed' },
80
  { status: 500 }
81
  );
82
  }
83
+ }
src/app/api/auth/logout/route.ts CHANGED
@@ -1,28 +1,21 @@
1
- /**
2
- * Logout API Route
3
- * POST /api/auth/logout
4
- *
5
- * Clears the HTTP-only auth token cookie
6
- */
7
 
8
- import { NextRequest, NextResponse } from "next/server";
9
- import { cookies } from "next/headers";
10
-
11
- export async function POST(request: NextRequest) {
12
  try {
13
  const cookieStore = await cookies();
14
- cookieStore.delete("auth_token");
15
- localStorage.removeItem("token")
16
 
17
  return NextResponse.json(
18
- { message: "Logged out successfully" },
19
  { status: 200 }
20
  );
21
  } catch (error) {
22
- console.error("[Logout] Error:", error);
23
  return NextResponse.json(
24
- { message: "Logout failed" },
25
  { status: 500 }
26
  );
27
  }
28
- }
 
1
+ // app/api/auth/logout/route.ts
2
+ import { cookies } from 'next/headers';
3
+ import { NextResponse } from 'next/server';
 
 
 
4
 
5
+ export async function POST() {
 
 
 
6
  try {
7
  const cookieStore = await cookies();
8
+ cookieStore.delete('auth_token');
 
9
 
10
  return NextResponse.json(
11
+ { message: 'Logged out successfully' },
12
  { status: 200 }
13
  );
14
  } catch (error) {
15
+ console.error('Logout error:', error);
16
  return NextResponse.json(
17
+ { message: 'Logout failed' },
18
  { status: 500 }
19
  );
20
  }
21
+ }
src/app/api/auth/me/route.ts CHANGED
@@ -1,55 +1,40 @@
1
- /**
2
- * User Info API Route
3
- * GET /api/auth/me
4
- *
5
- * Fetches user data from backend /admin/me endpoint
6
- * Uses token from HTTP-only cookie
7
- */
8
 
9
- import { NextRequest, NextResponse } from "next/server";
10
- import { cookies } from "next/headers";
11
-
12
- const BACKEND_URL = process.env.NEXT_PUBLIC_API_URL || "https://byteriot-candidateexplorer.hf.space/CandidateExplorer";
13
-
14
- export async function GET(request: NextRequest) {
15
  try {
16
  const cookieStore = await cookies();
17
- const token = cookieStore.get("auth_token")?.value;
18
 
19
  if (!token) {
20
  return NextResponse.json(
21
- { message: "Unauthorized" },
22
  { status: 401 }
23
  );
24
  }
25
 
26
- // Call backend /admin/me endpoint
27
- const response = await fetch(`${BACKEND_URL}/admin/me`, {
28
- method: "GET",
29
- headers: {
30
- Authorization: `Bearer ${token}`,
31
- },
32
- });
33
 
34
  if (!response.ok) {
35
- if (response.status === 401) {
36
- return NextResponse.json(
37
- { message: "Unauthorized" },
38
- { status: 401 }
39
- );
40
- }
41
  return NextResponse.json(
42
- { message: "Failed to fetch user data" },
43
- { status: response.status }
44
  );
45
  }
46
 
47
  const userData = await response.json();
48
  return NextResponse.json(userData, { status: 200 });
 
49
  } catch (error) {
50
- console.error("[Auth/Me] Error:", error);
51
  return NextResponse.json(
52
- { message: "Failed to fetch user data" },
53
  { status: 500 }
54
  );
55
  }
 
1
+ // app/api/auth/me/route.ts
2
+ import { cookies } from 'next/headers';
3
+ import { NextResponse } from 'next/server';
 
 
 
 
4
 
5
+ export async function GET() {
 
 
 
 
 
6
  try {
7
  const cookieStore = await cookies();
8
+ const token = cookieStore.get('auth_token')?.value;
9
 
10
  if (!token) {
11
  return NextResponse.json(
12
+ { message: 'Not authenticated' },
13
  { status: 401 }
14
  );
15
  }
16
 
17
+ const response = await fetch(
18
+ 'https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me',
19
+ {
20
+ headers: { Authorization: `Bearer ${token}` },
21
+ }
22
+ );
 
23
 
24
  if (!response.ok) {
 
 
 
 
 
 
25
  return NextResponse.json(
26
+ { message: 'Invalid token' },
27
+ { status: 401 }
28
  );
29
  }
30
 
31
  const userData = await response.json();
32
  return NextResponse.json(userData, { status: 200 });
33
+
34
  } catch (error) {
35
+ console.error('Get user error:', error);
36
  return NextResponse.json(
37
+ { message: 'Failed to get user' },
38
  { status: 500 }
39
  );
40
  }
src/app/api/file/score_card/route.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // app/api/auth/me/route.ts
2
+ import { cookies } from 'next/headers';
3
+ import { NextResponse } from 'next/server';
4
+
5
+ export async function GET() {
6
+ try {
7
+ const cookieStore = await cookies();
8
+ const token = cookieStore.get('auth_token')?.value;
9
+
10
+ if (!token) {
11
+ return NextResponse.json(
12
+ { message: 'Not authenticated' },
13
+ { status: 401 }
14
+ );
15
+ }
16
+
17
+ const response = await fetch(
18
+ 'https://byteriot-candidateexplorer.hf.space/CandidateExplorer/file/score_card',
19
+ {
20
+ headers: { Authorization: `Bearer ${token}` },
21
+ }
22
+ );
23
+
24
+ if (!response.ok) {
25
+ return NextResponse.json(
26
+ { message: 'Invalid token' },
27
+ { status: 401 }
28
+ );
29
+ }
30
+
31
+ const scoreCardData = await response.json();
32
+ return NextResponse.json(scoreCardData, { status: 200 });
33
+
34
+ } catch (error) {
35
+ console.error('Get score card error:', error);
36
+ return NextResponse.json(
37
+ { message: 'Failed to get score card' },
38
+ { status: 500 }
39
+ );
40
+ }
41
+ }
src/app/api/me/route.ts DELETED
@@ -1,22 +0,0 @@
1
- import { NextRequest, NextResponse } from "next/server"
2
-
3
- export async function GET(request: NextRequest) {
4
- const token = request.headers.get("Authorization")
5
-
6
- if (!token) {
7
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
8
- }
9
-
10
- const res = await fetch("https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me", {
11
- headers: {
12
- Authorization: token,
13
- },
14
- })
15
-
16
- if (!res.ok) {
17
- return NextResponse.json({ error: "Failed to fetch user" }, { status: res.status })
18
- }
19
-
20
- const data = await res.json()
21
- return NextResponse.json(data)
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/app/login/page.tsx CHANGED
@@ -1,40 +1,21 @@
1
- /**
2
- * Login Page
3
- * Route: /login
4
- * Publicly accessible page for user authentication
5
- */
6
-
7
- import { LoginForm } from "@/components/LoginForm";
8
-
9
- export const metadata = {
10
- title: "Login - Candidate Explorer",
11
- description: "Sign in to your account",
12
- };
13
 
14
  export default function LoginPage() {
15
  return (
16
- <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
17
  <div className="w-full max-w-md space-y-8">
18
- {/* Header */}
19
  <div className="text-center">
20
- <h2 className="text-3xl font-extrabold text-gray-900">
21
- Candidate Explorer
22
- </h2>
23
  <p className="mt-2 text-sm text-gray-600">
24
- Sign in to access the recruitment dashboard
25
  </p>
26
  </div>
27
 
28
- {/* Login Form */}
29
  <div className="bg-white py-8 px-6 shadow rounded-lg">
30
  <LoginForm />
31
  </div>
32
-
33
- {/* Footer */}
34
- <p className="text-center text-xs text-gray-500">
35
- byteriot - Candidate Explorer &copy; {new Date().getFullYear()} |{" "}
36
- </p>
37
  </div>
38
  </div>
39
  );
40
- }
 
1
+ // pages/login.tsx
2
+ import { LoginForm } from '@/components/LoginForm';
 
 
 
 
 
 
 
 
 
 
3
 
4
  export default function LoginPage() {
5
  return (
6
+ <div className="min-h-screen flex items-center justify-center bg-gray-50">
7
  <div className="w-full max-w-md space-y-8">
 
8
  <div className="text-center">
9
+ <h2 className="text-3xl font-bold">Candidate Explorer</h2>
 
 
10
  <p className="mt-2 text-sm text-gray-600">
11
+ Sign in to your account
12
  </p>
13
  </div>
14
 
 
15
  <div className="bg-white py-8 px-6 shadow rounded-lg">
16
  <LoginForm />
17
  </div>
 
 
 
 
 
18
  </div>
19
  </div>
20
  );
21
+ }
src/app/providers.tsx CHANGED
@@ -5,9 +5,8 @@
5
  * Wraps the entire app with necessary providers
6
  */
7
 
 
8
  import React from "react";
9
- import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
10
- import { AuthProvider } from "@/lib/auth-context";
11
 
12
  const queryClient = new QueryClient({
13
  defaultOptions: {
@@ -21,7 +20,7 @@ const queryClient = new QueryClient({
21
  export function Providers({ children }: { children: React.ReactNode }) {
22
  return (
23
  <QueryClientProvider client={queryClient}>
24
- <AuthProvider>{children}</AuthProvider>
25
  </QueryClientProvider>
26
  );
27
  }
 
5
  * Wraps the entire app with necessary providers
6
  */
7
 
8
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
9
  import React from "react";
 
 
10
 
11
  const queryClient = new QueryClient({
12
  defaultOptions: {
 
20
  export function Providers({ children }: { children: React.ReactNode }) {
21
  return (
22
  <QueryClientProvider client={queryClient}>
23
+ {children}
24
  </QueryClientProvider>
25
  );
26
  }
src/app/recruitment/layout.tsx CHANGED
@@ -1,16 +1,69 @@
1
- 'use client';
2
-
3
  import { Header } from '@/components/dashboard/header';
4
  import { HeaderMenu } from '@/components/dashboard/header-menu';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- export default function RootLayout({
 
 
 
 
 
 
 
 
 
 
7
  children,
8
  }: {
9
  children: React.ReactNode;
10
  }) {
 
 
 
 
 
 
 
 
 
 
 
11
  return (
12
  <div className="flex h-screen bg-background">
13
- {/* <Sidebar /> */}
14
  <div className="flex-1 flex flex-col overflow-hidden">
15
  <Header />
16
  <HeaderMenu />
@@ -22,4 +75,4 @@ export default function RootLayout({
22
  </div>
23
  </div>
24
  );
25
- }
 
1
+ // app/recruitment/layout.tsx
 
2
  import { Header } from '@/components/dashboard/header';
3
  import { HeaderMenu } from '@/components/dashboard/header-menu';
4
+ import { cookies } from 'next/headers';
5
+ import { redirect } from 'next/navigation';
6
+
7
+ async function getUser() {
8
+ console.log('🟡 [Layout] Checking authentication...');
9
+
10
+ const cookieStore = await cookies();
11
+ const token = cookieStore.get('auth_token')?.value;
12
+
13
+ console.log('🟡 [Layout] Token exists:', token ? 'YES' : 'NO');
14
+ console.log('🟡 [Layout] Token value:', token ? token.substring(0, 20) + '...' : 'none');
15
+
16
+ if (!token) {
17
+ console.log('🟡 [Layout] No token found, returning null');
18
+ return null;
19
+ }
20
+
21
+ try {
22
+ console.log('🟡 [Layout] Verifying token with backend...');
23
+
24
+ const response = await fetch(
25
+ 'https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me',
26
+ {
27
+ headers: { Authorization: `Bearer ${token}` },
28
+ cache: 'no-store',
29
+ }
30
+ );
31
+
32
+ console.log('🟡 [Layout] Backend response status:', response.status);
33
+
34
+ if (!response.ok) {
35
+ console.log('🟡 [Layout] Token invalid, backend returned:', response.status);
36
+ return null;
37
+ }
38
 
39
+ const userData = await response.json();
40
+ console.log('🟡 [Layout] User authenticated:', userData);
41
+ return userData;
42
+
43
+ } catch (error) {
44
+ console.error('🟡 [Layout] Error fetching user:', error);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ export default async function RecruitmentLayout({
50
  children,
51
  }: {
52
  children: React.ReactNode;
53
  }) {
54
+ console.log('🟡 [Layout] Layout rendering...');
55
+
56
+ const user = await getUser();
57
+
58
+ if (!user) {
59
+ console.log('🟡 [Layout] No user, redirecting to login');
60
+ redirect('/login');
61
+ }
62
+
63
+ console.log('🟡 [Layout] User found, rendering layout');
64
+
65
  return (
66
  <div className="flex h-screen bg-background">
 
67
  <div className="flex-1 flex flex-col overflow-hidden">
68
  <Header />
69
  <HeaderMenu />
 
75
  </div>
76
  </div>
77
  );
78
+ }
src/components/LoginForm.tsx CHANGED
@@ -1,145 +1,104 @@
1
- "use client";
 
2
 
3
- /**
4
- * Login Form Component
5
- * Handles user authentication with username/password
6
- * Uses React Hook Form + Zod validation + Radix UI
7
- */
8
 
9
- import React, { useState } from "react";
10
- import { useForm } from "react-hook-form";
11
- import { zodResolver } from "@hookform/resolvers/zod";
12
- import { z } from "zod";
13
- import {
14
- Form,
15
- FormField,
16
- FormItem,
17
- FormLabel,
18
- FormControl,
19
- FormMessage,
20
- } from "@/components/ui/form";
21
- import { Input } from "@/components/ui/input";
22
- import { Button } from "@/components/ui/button";
23
- import { Alert, AlertDescription } from "@/components/ui/alert";
24
- import { Spinner } from "@/components/ui/spinner";
25
- import { useAuth } from "@/lib/auth-context";
26
- import { AuthError } from "@/types/auth";
27
-
28
- // Validation schema
29
- const loginSchema = z.object({
30
- username: z
31
- .string()
32
- .min(1, "Username is required")
33
- .min(3, "Username must be at least 3 characters"),
34
- password: z
35
- .string()
36
- .min(1, "Password is required")
37
- .min(6, "Password must be at least 6 characters"),
38
- });
39
 
40
- type LoginFormData = z.infer<typeof loginSchema>;
41
 
42
- export function LoginForm() {
43
- const [apiError, setApiError] = useState<string | null>(null);
44
- const { login, isLoading } = useAuth();
 
45
 
46
- const form = useForm<LoginFormData>({
47
- resolver: zodResolver(loginSchema),
48
- defaultValues: {
49
- username: "",
50
- password: "",
51
- },
52
- });
53
 
54
- const onSubmit = async (data: LoginFormData) => {
55
- setApiError(null);
56
  try {
57
- await login(data.username, data.password);
58
- } catch (error) {
59
- const message =
60
- error instanceof AuthError ? error.message : "Login failed. Please try again.";
61
- setApiError(message);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  }
63
  };
64
 
65
  return (
66
- <Form {...form}>
67
- <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6 w-full max-w-md">
68
- {/* API Error Alert */}
69
- {apiError && (
70
- <Alert variant="destructive">
71
- <AlertDescription>{apiError}</AlertDescription>
72
- </Alert>
73
  )}
74
 
75
- {/* Username Field */}
76
- <FormField
77
- control={form.control}
78
- name="username"
79
- render={({ field, fieldState }) => (
80
- <FormItem>
81
- <FormLabel>Username</FormLabel>
82
- <FormControl>
83
- <Input
84
- {...field}
85
- type="text"
86
- placeholder="Enter your username"
87
- disabled={isLoading}
88
- autoComplete="username"
89
- />
90
- </FormControl>
91
- {fieldState.error && (
92
- <FormMessage>{fieldState.error.message}</FormMessage>
93
- )}
94
- </FormItem>
95
- )}
96
- />
97
 
98
- {/* Password Field */}
99
- <FormField
100
- control={form.control}
101
- name="password"
102
- render={({ field, fieldState }) => (
103
- <FormItem>
104
- <FormLabel>Password</FormLabel>
105
- <FormControl>
106
- <Input
107
- {...field}
108
- type="password"
109
- placeholder="Enter your password"
110
- disabled={isLoading}
111
- autoComplete="current-password"
112
- />
113
- </FormControl>
114
- {fieldState.error && (
115
- <FormMessage>{fieldState.error.message}</FormMessage>
116
- )}
117
- </FormItem>
118
- )}
119
- />
120
 
121
- {/* Submit Button */}
122
- <Button
123
  type="submit"
124
  disabled={isLoading}
125
- className="w-full"
126
- size="lg"
127
  >
128
- {isLoading ? (
129
- <div className="flex items-center gap-2">
130
- <Spinner className="w-4 h-4" />
131
- <span>Signing in...</span>
132
- </div>
133
- ) : (
134
- "Sign In"
135
- )}
136
- </Button>
137
-
138
- {/* Info Text */}
139
- <p className="text-center text-sm text-gray-500">
140
- Use your admin credentials to login
141
- </p>
142
- </form>
143
- </Form>
144
  );
145
  }
 
1
+ // components/LoginForm.tsx
2
+ 'use client';
3
 
4
+ import { useRouter } from 'next/navigation';
5
+ import { useState } from 'react';
 
 
 
6
 
7
+ export function LoginForm() {
8
+ const [username, setUsername] = useState('');
9
+ const [password, setPassword] = useState('');
10
+ const [error, setError] = useState('');
11
+ const [isLoading, setIsLoading] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ const router = useRouter()
14
 
15
+ const handleSubmit = async (e: React.FormEvent) => {
16
+ e.preventDefault();
17
+ setError('');
18
+ setIsLoading(true);
19
 
20
+ console.log('🔵 [Form] Submitting login...');
 
 
 
 
 
 
21
 
 
 
22
  try {
23
+ const response = await fetch('/api/auth/login', {
24
+ method: 'POST',
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ body: JSON.stringify({ username, password }),
29
+ });
30
+
31
+ console.log('🔵 [Form] Response status:', response.status);
32
+
33
+ const data = await response.json();
34
+
35
+ if (!response.ok) {
36
+ console.log('🔵 [Form] Login failed:', data.message);
37
+ setError(data.message || 'Login failed');
38
+ setIsLoading(false);
39
+ return;
40
+ }
41
+
42
+ console.log('🔵 [Form] Login successful!');
43
+ console.log('🔵 [Form] User data:', data);
44
+
45
+ // Use window.location for a full page reload to ensure cookie is loaded
46
+ console.log('🔵 [Form] Redirecting to /recruitment...');
47
+ // window.location.href = '/recruitment';
48
+ router.push("/recruitment")
49
+
50
+ } catch (err) {
51
+ console.error('🔵 [Form] Error:', err);
52
+ setError('Something went wrong. Please try again.');
53
+ setIsLoading(false);
54
  }
55
  };
56
 
57
  return (
58
+ <form onSubmit={handleSubmit} className="space-y-6">
59
+ {error && (
60
+ <div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm">
61
+ {error}
62
+ </div>
 
 
63
  )}
64
 
65
+ <div>
66
+ <label htmlFor="username" className="block text-sm font-medium text-gray-700">
67
+ Username
68
+ </label>
69
+ <input
70
+ id="username"
71
+ type="text"
72
+ value={username}
73
+ onChange={(e) => setUsername(e.target.value)}
74
+ required
75
+ disabled={isLoading}
76
+ className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
77
+ />
78
+ </div>
 
 
 
 
 
 
 
 
79
 
80
+ <div>
81
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700">
82
+ Password
83
+ </label>
84
+ <input
85
+ id="password"
86
+ type="password"
87
+ value={password}
88
+ onChange={(e) => setPassword(e.target.value)}
89
+ required
90
+ disabled={isLoading}
91
+ className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
92
+ />
93
+ </div>
 
 
 
 
 
 
 
 
94
 
95
+ <button
 
96
  type="submit"
97
  disabled={isLoading}
98
+ className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
 
99
  >
100
+ {isLoading ? 'Signing in...' : 'Sign in'}
101
+ </button>
102
+ </form>
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  );
104
  }
src/components/button.tsx CHANGED
@@ -24,7 +24,7 @@ export default function Button({
24
  <button
25
  onClick={onClick}
26
  type={type}
27
- className={`rounded-[8px] px-4 py-2 ${buttonStyles} mt-4 hover:cursor-pointer ${className} ${
28
  disabled ? "opacity-50 cursor-not-allowed" : ""
29
  }`}
30
  >
 
24
  <button
25
  onClick={onClick}
26
  type={type}
27
+ className={`rounded-[8px] px-4 py-2 ${buttonStyles} hover:cursor-pointer ${className} ${
28
  disabled ? "opacity-50 cursor-not-allowed" : ""
29
  }`}
30
  >
src/components/dashboard/candidates-table.tsx CHANGED
@@ -6,7 +6,7 @@ import { Checkbox } from "@/components/ui/checkbox"
6
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
7
  import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form"
8
  import { Input } from "@/components/ui/input"
9
- import { useUser } from "@/composables/useUser"
10
  import { authFetch } from "@/lib/api"
11
  import { exportCandidatesToCSV } from "@/lib/export-service"
12
  import { createAndCalculateScore } from "@/lib/scoring-service"
@@ -894,7 +894,7 @@ export default function CandidateTable() {
894
  const [debouncedSearch, setDebouncedSearch] = useState("")
895
  const [currentPage, setCurrentPage] = useState(1)
896
  const [sortConfig, setSortConfig] = useState<{ key: string | null; direction: "asc" | "desc" }>({ key: null, direction: "asc" })
897
- const { user } = useUser()
898
 
899
  const defaultColumns = allColumns.reduce(
900
  (acc, col) => ({ ...acc, [col.id]: col.visible }),
 
6
  import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
7
  import { Form, FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form"
8
  import { Input } from "@/components/ui/input"
9
+ import { useAuth } from "@/composables/useAuth"
10
  import { authFetch } from "@/lib/api"
11
  import { exportCandidatesToCSV } from "@/lib/export-service"
12
  import { createAndCalculateScore } from "@/lib/scoring-service"
 
894
  const [debouncedSearch, setDebouncedSearch] = useState("")
895
  const [currentPage, setCurrentPage] = useState(1)
896
  const [sortConfig, setSortConfig] = useState<{ key: string | null; direction: "asc" | "desc" }>({ key: null, direction: "asc" })
897
+ const { user } = useAuth()
898
 
899
  const defaultColumns = allColumns.reduce(
900
  (acc, col) => ({ ...acc, [col.id]: col.visible }),
src/components/dashboard/header.tsx CHANGED
@@ -1,8 +1,7 @@
1
  "use client";
2
 
3
- import { Button } from "@/components/ui/button";
4
- import { useAuth } from "@/lib/auth-context";
5
- import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
6
  import { LogOut } from "lucide-react";
7
 
8
  function getInitials(name: string) {
@@ -15,7 +14,7 @@ function getInitials(name: string) {
15
  }
16
 
17
  export function Header() {
18
- const { user, logout, isLoading } = useAuth();
19
 
20
  const initials = user ? getInitials(user.full_name || user.username) : "?";
21
  const displayName = user?.full_name || user?.username || "Loading...";
@@ -32,9 +31,9 @@ export function Header() {
32
  return (
33
  <header className="h-16 bg-white border-b border-gray-200 px-8 flex items-center justify-between">
34
  <div></div>
35
- <DropdownMenu.Root>
36
- <DropdownMenu.Trigger asChild>
37
- <Button variant="ghost" className="flex items-center gap-2">
38
  <div className="w-8 h-8 bg-gradient-to-br from-pink-300 to-orange-300 rounded-full flex items-center justify-center text-white text-sm font-semibold">
39
  {initials}
40
  </div>
@@ -42,19 +41,19 @@ export function Header() {
42
  <span className="text-sm">{displayName}</span>
43
  <span className="text-xs text-gray-500">{displayRole}</span>
44
  </div>
45
- </Button>
46
- </DropdownMenu.Trigger>
47
- <DropdownMenu.Content align="end" className="bg-white border border-gray-200 rounded-md shadow-lg">
48
- <DropdownMenu.Item
49
- disabled={isLoading}
50
  onClick={handleLogout}
51
  className="px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
52
  >
53
  <LogOut className="w-4 h-4" />
54
  Logout
55
- </DropdownMenu.Item>
56
- </DropdownMenu.Content>
57
- </DropdownMenu.Root>
58
  </header>
59
  );
60
  }
 
1
  "use client";
2
 
3
+ import { useAuth } from "@/composables/useAuth";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
 
5
  import { LogOut } from "lucide-react";
6
 
7
  function getInitials(name: string) {
 
14
  }
15
 
16
  export function Header() {
17
+ const { user, logout, isLoadingUser } = useAuth();
18
 
19
  const initials = user ? getInitials(user.full_name || user.username) : "?";
20
  const displayName = user?.full_name || user?.username || "Loading...";
 
31
  return (
32
  <header className="h-16 bg-white border-b border-gray-200 px-8 flex items-center justify-between">
33
  <div></div>
34
+ <DropdownMenu>
35
+ <DropdownMenuTrigger asChild>
36
+ <div className="flex items-center gap-2 mt-0 cursor-pointer">
37
  <div className="w-8 h-8 bg-gradient-to-br from-pink-300 to-orange-300 rounded-full flex items-center justify-center text-white text-sm font-semibold">
38
  {initials}
39
  </div>
 
41
  <span className="text-sm">{displayName}</span>
42
  <span className="text-xs text-gray-500">{displayRole}</span>
43
  </div>
44
+ </div>
45
+ </DropdownMenuTrigger>
46
+ <DropdownMenuContent align="end" className="bg-white border border-gray-200 rounded-md shadow-lg">
47
+ <DropdownMenuItem
48
+ disabled={isLoadingUser}
49
  onClick={handleLogout}
50
  className="px-4 py-2 text-sm text-red-600 hover:bg-red-50 flex items-center gap-2 cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
51
  >
52
  <LogOut className="w-4 h-4" />
53
  Logout
54
+ </DropdownMenuItem>
55
+ </DropdownMenuContent>
56
+ </DropdownMenu>
57
  </header>
58
  );
59
  }
src/components/dashboard/metrics-row.tsx CHANGED
@@ -4,8 +4,6 @@ import { MetricsCard } from "@/components/ui/metrics-card";
4
  import { authFetch } from "@/lib/api";
5
  import { useQuery } from "@tanstack/react-query";
6
 
7
- const BASE_URL = "https://byteriot-candidateexplorer.hf.space/CandidateExplorer"
8
-
9
  type ScoreData = {
10
  data: {
11
  total_file: number
@@ -25,7 +23,7 @@ const fallbackScore: ScoreData = {
25
  export function MetricsRow() {
26
 
27
  const fetchScore = async (): Promise<ScoreData> => {
28
- const res = await authFetch(`${BASE_URL}/file/score_card`)
29
  if (!res.ok) throw new Error("Failed to fetch score")
30
  return res.json()
31
  }
 
4
  import { authFetch } from "@/lib/api";
5
  import { useQuery } from "@tanstack/react-query";
6
 
 
 
7
  type ScoreData = {
8
  data: {
9
  total_file: number
 
23
  export function MetricsRow() {
24
 
25
  const fetchScore = async (): Promise<ScoreData> => {
26
+ const res = await authFetch(`/api/file/score_card`)
27
  if (!res.ok) throw new Error("Failed to fetch score")
28
  return res.json()
29
  }
src/composables/useAuth.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // hooks/useAuth.ts
2
+ import { authFetch } from "@/lib/api";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useRouter } from "next/navigation";
5
+
6
+ interface MeResponse {
7
+ user_id: string;
8
+ username: string;
9
+ email: string;
10
+ full_name: string;
11
+ role: string;
12
+ is_active: boolean;
13
+ tenant_id: string;
14
+ created_at: string;
15
+ }
16
+
17
+ export const useAuth = () => {
18
+ const queryClient = useQueryClient();
19
+ const router = useRouter();
20
+
21
+ // Get current user
22
+ const {
23
+ data: user,
24
+ isLoading: isLoadingUser,
25
+ error: userError,
26
+ } = useQuery<MeResponse>({
27
+ queryKey: ["me"],
28
+ queryFn: async () => {
29
+ const res = await authFetch("/api/auth/me");
30
+ if (!res.ok) throw new Error("Failed to fetch user");
31
+ return res.json();
32
+ },
33
+ staleTime: 1000 * 60 * 5,
34
+ retry: false,
35
+ });
36
+
37
+ // Logout mutation
38
+ const logoutMutation = useMutation({
39
+ mutationFn: async () => {
40
+ const res = await fetch("/api/auth/logout", {
41
+ method: "POST",
42
+ });
43
+ if (!res.ok) throw new Error("Logout failed");
44
+ return res.json();
45
+ },
46
+ onSuccess: () => {
47
+ queryClient.clear();
48
+ router.push("/login");
49
+ router.refresh();
50
+ },
51
+ });
52
+
53
+ return {
54
+ user,
55
+ isLoadingUser,
56
+ userError,
57
+ logout: logoutMutation.mutate,
58
+ isLoggingOut: logoutMutation.isPending,
59
+ isAuthenticated: !!user,
60
+ };
61
+ };
src/composables/useUser.ts DELETED
@@ -1,30 +0,0 @@
1
- import { authFetch } from "@/lib/api";
2
- import { useQuery } from "@tanstack/react-query";
3
-
4
- interface MeResponse {
5
- user_id: string;
6
- username: string;
7
- email: string;
8
- full_name: string;
9
- role: string;
10
- is_active: boolean;
11
- tenant_id: string;
12
- created_at: string;
13
- }
14
-
15
- export const useUser = () => {
16
- const { data: user } = useQuery<MeResponse>({
17
- queryKey: ["me"],
18
- queryFn: async () => {
19
- const res = await authFetch("/api/me");
20
- if (!res.ok) throw new Error("Failed to fetch user");
21
- const json = await res.json();
22
- return json;
23
- },
24
- staleTime: 1000 * 60 * 5, // cache 5 minutes
25
- });
26
-
27
- return {
28
- user,
29
- };
30
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/api.ts CHANGED
@@ -1,12 +1,26 @@
1
- export function authFetch(url: string, options?: RequestInit) {
2
- const token = localStorage.getItem("token") // or wherever you store it
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  return fetch(url, {
5
  ...options,
 
6
  headers: {
7
  "Content-Type": "application/json",
8
- Authorization: `Bearer ${token}`,
9
  ...options?.headers,
10
  },
11
- })
12
  }
 
1
+ function getCookie(name: string) {
2
+ if (typeof document === "undefined") return null;
3
+
4
+ const match = document.cookie
5
+ .split("; ")
6
+ .find((row) => row.startsWith(name + "="));
7
+
8
+ return match?.split("=")[1] ?? null;
9
+ }
10
+
11
+ export async function authFetch(
12
+ url: string,
13
+ options?: RequestInit
14
+ ) {
15
+ const token = getCookie("auth_token");
16
 
17
  return fetch(url, {
18
  ...options,
19
+ credentials: "include",
20
  headers: {
21
  "Content-Type": "application/json",
22
+ ...(token && { Authorization: `Bearer ${token}` }),
23
  ...options?.headers,
24
  },
25
+ });
26
  }
src/lib/auth-context.tsx DELETED
@@ -1,150 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Auth Context & Provider
5
- * Provides user state and auth methods throughout the app
6
- * Wraps the entire app at root layout
7
- */
8
-
9
- import React, { createContext, useContext, useEffect, useState, useCallback } from "react";
10
- import { User, AuthContextType, AuthError } from "@/types/auth";
11
- import { getUser, logout as logoutAuth } from "@/lib/auth";
12
- import { useQueryClient } from "@tanstack/react-query";
13
-
14
- const AuthContext = createContext<AuthContextType | undefined>(undefined);
15
-
16
- export function AuthProvider({ children }: { children: React.ReactNode }) {
17
- const [user, setUser] = useState<User | null>(null);
18
- const [isLoading, setIsLoading] = useState(true);
19
- const [isAuthenticated, setIsAuthenticated] = useState(false);
20
- const queryClient = useQueryClient();
21
-
22
- // Fetch user data on mount
23
- useEffect(() => {
24
- const initAuth = async () => {
25
- try {
26
- setIsLoading(true);
27
- const userData = await getUser();
28
- setUser(userData);
29
- setIsAuthenticated(true);
30
- } catch (error) {
31
- // User not authenticated
32
- setUser(null);
33
- setIsAuthenticated(false);
34
- } finally {
35
- setIsLoading(false);
36
- }
37
- };
38
-
39
- initAuth();
40
- }, []);
41
-
42
- // Login function
43
- const login = useCallback(
44
- async (username: string, password: string) => {
45
- setIsLoading(true);
46
-
47
- const formData = new FormData();
48
- formData.append('username', username);
49
- formData.append('password', password);
50
-
51
- try {
52
- const response = await fetch("https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/login", {
53
- method: "POST",
54
- credentials: "include",
55
- body: formData,
56
- });
57
-
58
- if (!response.ok) {
59
- const error = await response.json().catch(() => ({}));
60
- throw new AuthError(error.message || "Login failed", "LOGIN_ERROR", response.status);
61
- }
62
-
63
- const userData = await response.json();
64
- setUser(userData);
65
- // preserve this line as requested (access_token is returned by the API route)
66
- try {
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
68
- (localStorage as any).setItem("token", (userData as any).access_token);
69
- } catch (e) {
70
- // ignore storage errors
71
- }
72
- setIsAuthenticated(true);
73
-
74
- // Redirect to recruitment page
75
- if (typeof window !== "undefined") {
76
- window.location.href = "/recruitment";
77
- }
78
- } catch (error) {
79
- setUser(null);
80
- setIsAuthenticated(false);
81
- throw error instanceof AuthError ? error : new AuthError("Login failed");
82
- } finally {
83
- setIsLoading(false);
84
- }
85
- },
86
- []
87
- );
88
-
89
- // Logout function
90
- const logout = useCallback(async () => {
91
- setIsLoading(true);
92
- try {
93
- await logoutAuth();
94
- setUser(null);
95
- setIsAuthenticated(false);
96
- // Clear and invalidate queries to prevent stale data
97
- try {
98
- queryClient.clear();
99
- } catch (e) {
100
- // ignore if not available
101
- }
102
- await queryClient.invalidateQueries();
103
- // Redirect to login
104
- if (typeof window !== "undefined") {
105
- window.location.href = "/login";
106
- }
107
- } catch (error) {
108
- console.error("Logout error:", error);
109
- // Still clear local state even if API call fails
110
- setUser(null);
111
- setIsAuthenticated(false);
112
- if (typeof window !== "undefined") {
113
- window.location.href = "/login";
114
- }
115
- } finally {
116
- setIsLoading(false);
117
- }
118
- }, [queryClient]);
119
-
120
- // Refresh user data
121
- const refreshUser = useCallback(async () => {
122
- try {
123
- const userData = await getUser();
124
- setUser(userData);
125
- setIsAuthenticated(true);
126
- } catch (error) {
127
- setUser(null);
128
- setIsAuthenticated(false);
129
- }
130
- }, []);
131
-
132
- const value: AuthContextType = {
133
- user,
134
- isLoading,
135
- isAuthenticated,
136
- login,
137
- logout,
138
- refreshUser,
139
- };
140
-
141
- return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
142
- }
143
-
144
- export function useAuth(): AuthContextType {
145
- const context = useContext(AuthContext);
146
- if (context === undefined) {
147
- throw new Error("useAuth must be used within AuthProvider");
148
- }
149
- return context;
150
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/auth.ts DELETED
@@ -1,96 +0,0 @@
1
- /**
2
- * Core Authentication Utilities
3
- * Handles token management via HTTP-only cookies (server-side)
4
- * Never exposes token to client-side JavaScript
5
- */
6
-
7
- import { User, AuthError } from "@/types/auth";
8
-
9
- const API_URL = process.env.NEXT_PUBLIC_API_URL || "https://byteriot-candidateexplorer.hf.space/CandidateExplorer";
10
-
11
- /**
12
- * Call backend /admin/me endpoint to fetch user data
13
- * Uses cookies for token storage (set by API route)
14
- */
15
- export async function getUser(): Promise<User> {
16
- try {
17
- const response = await fetch("https://byteriot-candidateexplorer.hf.space/CandidateExplorer/admin/me", {
18
- method: "GET",
19
- credentials: "include", // Include cookies
20
- headers: {
21
- "Content-Type": "application/json",
22
- },
23
- });
24
-
25
- if (!response.ok) {
26
- if (response.status === 401) {
27
- throw new AuthError("Unauthorized", "INVALID_TOKEN", 401);
28
- }
29
- const error = await response.json();
30
- throw new AuthError(error.message || "Failed to fetch user", "FETCH_USER_ERROR", response.status);
31
- }
32
-
33
- return await response.json();
34
- } catch (error) {
35
- if (error instanceof AuthError) throw error;
36
- throw new AuthError("Failed to fetch user data", "FETCH_USER_ERROR");
37
- }
38
- }
39
-
40
- /**
41
- * Logout: Clear auth cookie on server via API route
42
- */
43
- export async function logout(): Promise<void> {
44
- try {
45
- const response = await fetch(`${new URL(window?.location?.href || "").origin}/api/auth/logout`, {
46
- method: "POST",
47
- credentials: "include",
48
- headers: {
49
- "Content-Type": "application/json",
50
- },
51
- });
52
-
53
- if (!response.ok) {
54
- throw new AuthError("Failed to logout", "LOGOUT_ERROR", response.status);
55
- }
56
- } catch (error) {
57
- if (error instanceof AuthError) throw error;
58
- throw new AuthError("Logout failed", "LOGOUT_ERROR");
59
- }
60
- }
61
-
62
- /**
63
- * Check if token exists in HTTP-only cookie (server-side only)
64
- * Note: Cannot directly check cookies from client-side due to httpOnly flag
65
- * Use AuthContext.isAuthenticated instead
66
- */
67
- export function isAuthenticated(token?: string): boolean {
68
- return !!token;
69
- }
70
-
71
- /**
72
- * Generic fetch wrapper for API calls (not auth endpoints)
73
- * Automatically includes Bearer token from cookie (set by login route)
74
- */
75
- export async function authFetch(
76
- url: string,
77
- options: RequestInit = {}
78
- ): Promise<Response> {
79
- const response = await fetch(`${API_URL}${url}`, {
80
- ...options,
81
- credentials: "include",
82
- headers: {
83
- "Content-Type": "application/json",
84
- ...options.headers,
85
- },
86
- });
87
-
88
- if (!response.ok) {
89
- if (response.status === 401) {
90
- throw new AuthError("Unauthorized", "INVALID_TOKEN", 401);
91
- }
92
- throw new AuthError(`API request failed: ${response.statusText}`, "API_ERROR", response.status);
93
- }
94
-
95
- return response;
96
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/lib/rbac.ts DELETED
@@ -1,72 +0,0 @@
1
- /**
2
- * Role-Based Access Control (RBAC)
3
- * Check user permissions based on role from authentication context
4
- */
5
-
6
- import { User } from "@/types/auth";
7
-
8
- export const ROLES = {
9
- ADMIN: "admin",
10
- RECRUITER: "recruiter",
11
- VIEWER: "viewer",
12
- } as const;
13
-
14
- export type Role = (typeof ROLES)[keyof typeof ROLES];
15
-
16
- /**
17
- * Check if user has a specific role
18
- * @param user - User object from auth context
19
- * @param role - Role to check
20
- * @returns true if user has the role
21
- */
22
- export function hasRole(user: User | null, role: Role): boolean {
23
- if (!user) return false;
24
- return user.role.toLowerCase() === role.toLowerCase();
25
- }
26
-
27
- /**
28
- * Check if user has any of the provided roles
29
- * @param user - User object from auth context
30
- * @param roles - Array of roles to check
31
- * @returns true if user has any of the roles
32
- */
33
- export function hasAnyRole(user: User | null, roles: Role[]): boolean {
34
- if (!user) return false;
35
- return roles.some((role) => hasRole(user, role));
36
- }
37
-
38
- /**
39
- * Check if user has all of the provided roles
40
- * @param user - User object from auth context
41
- * @param roles - Array of roles to check
42
- * @returns true if user has all the roles
43
- */
44
- export function hasAllRoles(user: User | null, roles: Role[]): boolean {
45
- if (!user) return false;
46
- return roles.every((role) => hasRole(user, role));
47
- }
48
-
49
- /**
50
- * Require a specific role - throw error if user doesn't have it
51
- * Suitable for server-side authorization checks
52
- * @param user - User object
53
- * @param role - Required role
54
- * @throws Error if user doesn't have the role
55
- */
56
- export function requireRole(user: User | null, role: Role): void {
57
- if (!hasRole(user, role)) {
58
- throw new Error(`User does not have required role: ${role}`);
59
- }
60
- }
61
-
62
- /**
63
- * Require any of the provided roles
64
- * @param user - User object
65
- * @param roles - Array of acceptable roles
66
- * @throws Error if user doesn't have any of the roles
67
- */
68
- export function requireAnyRole(user: User | null, roles: Role[]): void {
69
- if (!hasAnyRole(user, roles)) {
70
- throw new Error(`User does not have any of the required roles: ${roles.join(", ")}`);
71
- }
72
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/middleware.ts CHANGED
@@ -1,68 +1,68 @@
1
  import { NextRequest, NextResponse } from "next/server"
2
 
3
- // Routes that require authentication
4
- const protectedRoutes = [
5
- "/recruitment",
6
- "/dashboard",
7
- ]
8
-
9
- // API routes that require authentication
10
- const protectedApiRoutes = [
11
- "/api/cv-profile",
12
- "/api/cv-profile/options",
13
  ]
14
 
15
- // Public routes (can be accessed without auth)
16
- const publicRoutes = [
17
  "/login",
18
- "/api/auth/login",
19
- "/api/auth/logout",
20
  ]
21
 
22
  export function middleware(request: NextRequest) {
23
- const pathname = request.nextUrl.pathname
24
  const token = request.cookies.get("auth_token")?.value
25
 
26
- // Check if it's an API route
27
- const isApiRoute = pathname.startsWith("/api/")
28
- const isProtectedApi = protectedApiRoutes.some((route) => pathname.startsWith(route))
29
- const isPublicRoute = publicRoutes.some((route) => pathname.startsWith(route))
30
 
31
- // Handle API routes
32
- if (isApiRoute) {
33
- // Protected API routes require token
34
- if (isProtectedApi && !token) {
35
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
 
 
 
36
  }
37
- return NextResponse.next()
 
 
38
  }
39
 
40
- // Handle public routes
41
- if (isPublicRoute) {
42
- // If user is logged in and visiting /login, redirect to /recruitment
43
- if (pathname === "/login" && token) {
44
- return NextResponse.redirect(new URL("/recruitment", request.url))
 
 
 
 
 
 
45
  }
 
46
  return NextResponse.next()
47
  }
48
 
49
- // Handle protected page routes
50
- const isProtected = protectedRoutes.some((route) => pathname.startsWith(route))
51
-
52
- if (isProtected && !token) {
53
- // Redirect to login if no token
54
- return NextResponse.redirect(new URL("/login", request.url))
 
 
 
55
  }
56
 
57
- // Handle root path
58
- if (pathname === "/") {
59
- if (token) {
60
- // If logged in, redirect to recruitment
61
- return NextResponse.redirect(new URL("/recruitment", request.url))
62
- } else {
63
- // If not logged in, redirect to login
64
- return NextResponse.redirect(new URL("/login", request.url))
65
- }
66
  }
67
 
68
  return NextResponse.next()
@@ -70,14 +70,9 @@ export function middleware(request: NextRequest) {
70
 
71
  export const config = {
72
  matcher: [
73
- // API routes
74
- "/api/:path*",
75
- // Protected pages
76
- "/recruitment/:path*",
77
- "/dashboard/:path*",
78
- // Auth pages
79
- "/login",
80
- // Root
81
  "/",
 
 
 
82
  ],
83
  }
 
1
  import { NextRequest, NextResponse } from "next/server"
2
 
3
+ const publicApiRoutes = [
4
+ "/api/auth/login",
 
 
 
 
 
 
 
 
5
  ]
6
 
7
+ const publicPages = [
 
8
  "/login",
 
 
9
  ]
10
 
11
  export function middleware(request: NextRequest) {
12
+ const { pathname } = request.nextUrl
13
  const token = request.cookies.get("auth_token")?.value
14
 
15
+ const isApiRoute = pathname.startsWith("/api")
16
+ const isPublicApi = publicApiRoutes.some(route =>
17
+ pathname.startsWith(route)
18
+ )
19
 
20
+ // -----------------------
21
+ // 🔹 ROOT HANDLING
22
+ // -----------------------
23
+ if (pathname === "/") {
24
+ if (token) {
25
+ return NextResponse.redirect(
26
+ new URL("/recruitment", request.url)
27
+ )
28
  }
29
+ return NextResponse.redirect(
30
+ new URL("/login", request.url)
31
+ )
32
  }
33
 
34
+ // -----------------------
35
+ // 🔹 API PROTECTION
36
+ // -----------------------
37
+ if (isApiRoute) {
38
+ if (isPublicApi) return NextResponse.next()
39
+
40
+ if (!token) {
41
+ return NextResponse.json(
42
+ { error: "Unauthorized" },
43
+ { status: 401 }
44
+ )
45
  }
46
+
47
  return NextResponse.next()
48
  }
49
 
50
+ // -----------------------
51
+ // 🔹 PAGE PROTECTION
52
+ // -----------------------
53
+
54
+ // Logged in & accessing login
55
+ if (token && pathname === "/login") {
56
+ return NextResponse.redirect(
57
+ new URL("/recruitment", request.url)
58
+ )
59
  }
60
 
61
+ // Not logged in & accessing protected pages
62
+ if (!token && !publicPages.includes(pathname)) {
63
+ return NextResponse.redirect(
64
+ new URL("/login", request.url)
65
+ )
 
 
 
 
66
  }
67
 
68
  return NextResponse.next()
 
70
 
71
  export const config = {
72
  matcher: [
 
 
 
 
 
 
 
 
73
  "/",
74
+ "/login",
75
+ "/recruitment/:path*",
76
+ "/api/:path*",
77
  ],
78
  }