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}
} +
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+

+ 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}
} +
+
+ + setUsername(e.target.value)} + required + /> +
+
+ + setEmail(e.target.value)} + required + /> +
+
+
+ + setFirstName(e.target.value)} + required + /> +
+
+ + setLastName(e.target.value)} + required + /> +
+
+
+ + setPassword(e.target.value)} + required + /> +
+ +
+

+ 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