diff --git a/frontend/src/App.js b/frontend/src/App.js
index 2d8fd5e..98738a0 100644
--- a/frontend/src/App.js
+++ b/frontend/src/App.js
@@ -1,31 +1,89 @@
-import React, { useEffect, useState } from "react";
-import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
+import React from "react";
+import { BrowserRouter, Routes, Route, Link, Navigate } from "react-router-dom";
+import { AuthProvider, useAuth } from "./contexts/AuthContext";
import CompanyList from "./components/CompanyList";
import JobList from "./components/JobList";
import CompanyForm from "./components/CompanyForm";
import JobForm from "./components/JobForm";
import CompanyJobs from "./components/CompanyJobs";
+import Login from "./components/Login";
+import Register from "./components/Register";
+import Profile from "./components/Profile";
-function App() {
- return (
+function PrivateRoute({ children }) {
+ const { isAuthenticated } = useAuth();
+ return isAuthenticated ? children : ;
+}
+
+function AppContent() {
+ const { isAuthenticated, logout } = useAuth();
+ return (
- } />
- } />
- } />
-
- } />
- } />
- } />
- } />
+ } />
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+
+
+
+ } />
+ } />
+ } />
);
}
+function App() {
+ return (
+
+
+
+ );
+}
+
export default App;
diff --git a/frontend/src/api.js b/frontend/src/api.js
index 425be09..a876bac 100644
--- a/frontend/src/api.js
+++ b/frontend/src/api.js
@@ -4,15 +4,36 @@
});
export async function apiGet(path) {
- const res=await fetch(path);
- if(!res.ok) throw new Error(await res.text());
+ const token = localStorage.getItem('token');
+ const headers = { 'Content-Type': 'application/json' };
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ const res = await fetch(path, { headers });
+ if (!res.ok) throw new Error(await res.text());
return res.json();
}
+export async function apiExport(path) {
+ const token = localStorage.getItem('token');
+ const headers = {};
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ const res = await fetch(path, { headers });
+ if (!res.ok) throw new Error('Export failed');
+ return res;
+}
+
export async function apiPost(path, body) {
- const res=await fetch(path, {
+ const token = localStorage.getItem('token');
+ const headers = { 'Content-Type': 'application/json' };
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ const res = await fetch(path, {
method: "POST",
- headers: {"Content-Type": "application/json"},
+ headers,
body: JSON.stringify(body)
});
if(!res.ok) throw new Error(await res.text());
@@ -20,9 +41,14 @@
}
export async function apiPut(path, body) {
- const res=await fetch(path, {
+ const token = localStorage.getItem('token');
+ const headers = { 'Content-Type': 'application/json' };
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
+ const res = await fetch(path, {
method: "PUT",
- headers: {"Content-Type": "application/json"},
+ headers,
body: JSON.stringify(body)
});
if(!res.ok) throw new Error(await res.text());
@@ -30,12 +56,18 @@
}
export async function apiDelete(path) {
+ const token = localStorage.getItem('token');
+ const headers = {};
+ if (token) {
+ headers['Authorization'] = `Bearer ${token}`;
+ }
const response = await fetch(path, {
- method: 'DELETE'
+ method: 'DELETE',
+ headers
});
if (!response.ok) {
throw new Error(`DELETE ${path} failed: ${response.statusText}`);
}
- return true; // optional: return anything you need
+ return true;
}
export default api;
\ No newline at end of file
diff --git a/frontend/src/components/JobList.js b/frontend/src/components/JobList.js
index 869efe5..652c400 100644
--- a/frontend/src/components/JobList.js
+++ b/frontend/src/components/JobList.js
@@ -1,5 +1,5 @@
import React from 'react';
-import { apiDelete, apiGet } from '../api';
+import { apiDelete, apiGet, apiExport } from '../api';
import { Link } from 'react-router-dom';
function JobList() {
@@ -9,7 +9,7 @@
const handleExportClick = async () => {
try {
- const response = await fetch('/api/jobs/export');
+ const response = await apiExport('/api/jobs/export');
if (!response.ok) throw new Error('Export failed');
// Extract filename with timestamp from header
diff --git a/frontend/src/components/Login.js b/frontend/src/components/Login.js
new file mode 100644
index 0000000..e40390e
--- /dev/null
+++ b/frontend/src/components/Login.js
@@ -0,0 +1,62 @@
+import React, { useState } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import { apiPost } from '../api';
+
+function Login() {
+ const [username, setUsername] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const { login } = useAuth();
+ const navigate = useNavigate();
+ const location = useLocation();
+ const from = location.state?.from?.pathname || '/';
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const response = await apiPost('/api/auth/login', { username, password });
+ login(response.token);
+ navigate(from, { replace: true });
+ } catch (err) {
+ setError(err.message);
+ }
+ };
+
+ return (
+
+
Login
+ {error &&
{error}
}
+
+
+ Don't have an account? Register
+
+
+ );
+}
+
+export default Login;
\ No newline at end of file
diff --git a/frontend/src/components/PrivateRoute.js b/frontend/src/components/PrivateRoute.js
new file mode 100644
index 0000000..32cbcf2
--- /dev/null
+++ b/frontend/src/components/PrivateRoute.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Navigate, Outlet } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+
+function PrivateRoute() {
+ const { isAuthenticated } = useAuth();
+ return isAuthenticated ? : ;
+}
+
+export default PrivateRoute;
\ No newline at end of file
diff --git a/frontend/src/components/Profile.js b/frontend/src/components/Profile.js
new file mode 100644
index 0000000..e3d2ec4
--- /dev/null
+++ b/frontend/src/components/Profile.js
@@ -0,0 +1,41 @@
+import React, { useState, useEffect } from 'react';
+import { useAuth } from '../contexts/AuthContext';
+import { apiGet } from '../api.js';
+
+const Profile = () => {
+ const { isAuthenticated } = useAuth();
+ const [user, setUser] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (isAuthenticated) {
+ apiGet('/api/user/profile')
+ .then(data => {
+ setUser(data);
+ setLoading(false);
+ })
+ .catch(err => {
+ setError(err.message);
+ setLoading(false);
+ });
+ }
+ }, [isAuthenticated]);
+
+ if (loading) return Loading...
;
+ if (error) return Error: {error}
;
+ if (!user) return No user data
;
+
+ return (
+
+
Profile
+
Username: {user.username}
+
Email: {user.email}
+
First Name: {user.firstName || 'N/A'}
+
Last Name: {user.lastName || 'N/A'}
+
Status: {user.status}
+
+ );
+};
+
+export default Profile;
\ No newline at end of file
diff --git a/frontend/src/components/Register.js b/frontend/src/components/Register.js
new file mode 100644
index 0000000..4bf3234
--- /dev/null
+++ b/frontend/src/components/Register.js
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { useAuth } from '../contexts/AuthContext';
+import { apiPost } from '../api';
+
+function Register() {
+ const [username, setUsername] = useState('');
+ const [email, setEmail] = useState('');
+ const [firstName, setFirstName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [password, setPassword] = useState('');
+ const [error, setError] = useState('');
+ const { login } = useAuth();
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const response = await apiPost('/api/auth/register', { username, email, firstName, lastName, password });
+ setError('');
+ alert('Registration successful! Please check your email for confirmation link.');
+ navigate('/login');
+ } catch (err) {
+ setError(err.message || 'Registration failed');
+ }
+ };
+
+ return (
+
+
Register
+ {error &&
{error}
}
+
+
+ Already have an account? Login
+
+
+ );
+}
+
+export default Register;
\ No newline at end of file
diff --git a/frontend/src/contexts/AuthContext.js b/frontend/src/contexts/AuthContext.js
new file mode 100644
index 0000000..bfe129f
--- /dev/null
+++ b/frontend/src/contexts/AuthContext.js
@@ -0,0 +1,40 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+
+const AuthContext = createContext();
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider');
+ }
+ return context;
+}
+
+export function AuthProvider({ children }) {
+ const [token, setToken] = useState(localStorage.getItem('token'));
+ const [isAuthenticated, setIsAuthenticated] = useState(!!token);
+
+ useEffect(() => {
+ if (token) {
+ localStorage.setItem('token', token);
+ setIsAuthenticated(true);
+ } else {
+ localStorage.removeItem('token');
+ setIsAuthenticated(false);
+ }
+ }, [token]);
+
+ const login = (newToken) => {
+ setToken(newToken);
+ };
+
+ const logout = () => {
+ setToken(null);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
\ No newline at end of file