diff --git a/app/context/auth-context.tsx b/app/context/auth-context.tsx index 2e55407..a60785a 100644 --- a/app/context/auth-context.tsx +++ b/app/context/auth-context.tsx @@ -1,6 +1,7 @@ import { createContext, useContext, useState, useEffect, type ReactNode } from "react" import { useNavigate } from "react-router" -import { type User, getUser, getUserAsync, logout as logoutUser } from "@/lib/auth" +import { type User, getUser } from "@/lib/auth" +import { getUserAsync, logout as logoutUser } from "@/lib/api" interface AuthContextType { user: User | null diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 6207898..a024e9f 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -4,6 +4,7 @@ import { FormInput } from "@/components/shared/form-input" import { FormButton } from "@/components/shared/form-button" import { useAuth } from "@/app/context/auth-context" import { setTokens } from "@/lib/auth" +import { login } from "@/lib/api" export default function LoginPage() { const navigate = useNavigate() @@ -42,29 +43,12 @@ export default function LoginPage() { setLoading(true) try { - const res = await fetch("http://localhost:8080/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email: form.email, - password: form.password, - }), - }) - - if (!res.ok) { - const data = await res.json() - setErrors({ general: data.message ?? "Invalid email or password" }) - return - } - - const data = await res.json() + const data = await login(form.email, form.password) setTokens(data.access_token, data.refresh_token, rememberMe) refreshUser() - navigate("/forms") } catch (err) { - console.error(err) - setErrors({ general: "Something went wrong. Please try again." }) + setErrors({ general: err instanceof Error ? err.message : "Something went wrong. Please try again." }) } finally { setLoading(false) } diff --git a/app/routes/register.tsx b/app/routes/register.tsx index af53515..eaf3fa4 100644 --- a/app/routes/register.tsx +++ b/app/routes/register.tsx @@ -3,6 +3,7 @@ import { Link, useNavigate } from "react-router" import { FormInput } from "@/components/shared/form-input" import { FormButton } from "@/components/shared/form-button" import { useAuth } from "@/app/context/auth-context" +import { register } from "@/lib/api" export default function RegisterPage() { const navigate = useNavigate() @@ -52,25 +53,10 @@ export default function RegisterPage() { } try { - const res = await fetch("http://localhost:8080/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - email: form.email, - password: form.password, - }), - }) - - if (!res.ok) { - const data = await res.json() - setErrors({ email: data.message ?? "Registration failed" }) - return - } - + await register(form.email, form.password) navigate("/login") } catch (err) { - console.error(err) - setErrors({ email: "Something went wrong. Please try again." }) + setErrors({ email: err instanceof Error ? err.message : "Something went wrong. Please try again." }) } } diff --git a/lib/api.ts b/lib/api.ts index 0868592..e9e3ae9 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -1,10 +1,152 @@ -import { fetchWithAuth } from "@/lib/auth" +import { + type User, + getToken, + setTokens, + clearTokens, + isTokenExpired, + decodeJWT, +} from "@/lib/auth" import type { FormSummary, FormDetail, CreateFormPayload, UpdateFormPayload } from "@/lib/types" -const API_BASE = "http://localhost:8080" +export async function refreshAccessToken(): Promise { + if (typeof window === "undefined") return null + + const refreshToken = getToken("refresh_token") + if (!refreshToken) { + clearTokens() + return null + } + + try { + const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/refresh`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ refresh_token: refreshToken }), + }) + + if (!res.ok) { + clearTokens() + return null + } + + const data = await res.json() + setTokens(data.access_token, data.refresh_token) + return data.access_token + } catch { + clearTokens() + return null + } +} + +async function getAccessTokenAsync(): Promise { + if (typeof window === "undefined") return null + + const token = getToken("access_token") + if (token && !isTokenExpired(token)) return token + + return refreshAccessToken() +} + +export async function getUserAsync(): Promise { + if (typeof window === "undefined") return null + + let token = getToken("access_token") + + if (!token || isTokenExpired(token)) { + token = await refreshAccessToken() + if (!token) return null + } + + const payload = decodeJWT(token) + if (!payload) return null + + return { + id: payload.sub, + email: payload.email, + name: payload.name, + } +} + +export async function fetchWithAuth(url: string, options: RequestInit = {}) { + let token = await getAccessTokenAsync() + + const res = await fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: token ? `Bearer ${token}` : "", + }, + }) + + if (res.status === 401) { + token = await refreshAccessToken() + if (token) { + return fetch(url, { + ...options, + headers: { + ...options.headers, + Authorization: `Bearer ${token}`, + }, + }) + } + } + + return res +} + +export async function login(email: string, password: string) { + const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + + if (!res.ok) { + const data = await res.json().catch(() => null) + throw new Error(data?.message ?? "Invalid email or password") + } + + return res.json() as Promise<{ access_token: string; refresh_token: string }> +} + +export async function register(email: string, password: string) { + const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/register`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + + if (!res.ok) { + const data = await res.json().catch(() => null) + throw new Error(data?.message ?? "Registration failed") + } + + return res.json() +} + +export async function logout() { + if (typeof window === "undefined") return + + const token = getToken("access_token") + const refreshToken = getToken("refresh_token") + + try { + await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/auth/logout`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: token ? `Bearer ${token}` : "", + }, + body: JSON.stringify({ refresh_token: refreshToken }), + }) + } catch { + } + + clearTokens() +} export async function getForms(): Promise { - const res = await fetchWithAuth(`${API_BASE}/api/forms`) + const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/forms`) if (!res.ok) { throw new Error("Failed to fetch forms") @@ -14,7 +156,7 @@ export async function getForms(): Promise { } export async function getFormById(id: string): Promise { - const res = await fetchWithAuth(`${API_BASE}/api/form/${id}`) + const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/form/${id}`) if (!res.ok) { if (res.status === 404) { @@ -27,7 +169,7 @@ export async function getFormById(id: string): Promise { } export async function createForm(payload: CreateFormPayload): Promise { - const res = await fetchWithAuth(`${API_BASE}/api/form`, { + const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/form`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -42,7 +184,7 @@ export async function createForm(payload: CreateFormPayload): Promise { - const res = await fetchWithAuth(`${API_BASE}/api/form/${id}`, { + const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/form/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), @@ -57,7 +199,7 @@ export async function updateForm(id: string, payload: UpdateFormPayload): Promis } export async function deleteForm(id: string): Promise { - const res = await fetchWithAuth(`${API_BASE}/api/form/${id}`, { + const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/form/${id}`, { method: "DELETE", }) diff --git a/lib/auth.ts b/lib/auth.ts index 5a3fb5a..4ad3f0f 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -41,7 +41,7 @@ function getStorage(): Storage { : sessionStorage } -function getToken(key: string): string | null { +export function getToken(key: string): string | null { return localStorage.getItem(key) ?? sessionStorage.getItem(key) } @@ -65,7 +65,7 @@ export function setTokens(accessToken: string, refreshToken: string, remember?: storage.setItem("refresh_token", refreshToken) } -function clearTokens() { +export function clearTokens() { localStorage.removeItem("access_token") localStorage.removeItem("refresh_token") localStorage.removeItem("remember_me") @@ -73,36 +73,6 @@ function clearTokens() { sessionStorage.removeItem("refresh_token") } -export async function refreshAccessToken(): Promise { - if (typeof window === "undefined") return null - - const refreshToken = getToken("refresh_token") - if (!refreshToken) { - clearTokens() - return null - } - - try { - const res = await fetch("http://localhost:8080/api/auth/refresh", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ refresh_token: refreshToken }), - }) - - if (!res.ok) { - clearTokens() - return null - } - - const data = await res.json() - setTokens(data.access_token, data.refresh_token) - return data.access_token - } catch { - clearTokens() - return null - } -} - export function getUser(): User | null { if (typeof window === "undefined") return null @@ -118,89 +88,3 @@ export function getUser(): User | null { name: payload.name, } } - -export async function getUserAsync(): Promise { - if (typeof window === "undefined") return null - - let token = getToken("access_token") - - if (!token || isTokenExpired(token)) { - token = await refreshAccessToken() - if (!token) return null - } - - const payload = decodeJWT(token) - if (!payload) return null - - return { - id: payload.sub, - email: payload.email, - name: payload.name, - } -} - -export function getAccessToken(): string | null { - if (typeof window === "undefined") return null - - const token = getToken("access_token") - if (!token || isTokenExpired(token)) return null - return token -} - -export async function getAccessTokenAsync(): Promise { - if (typeof window === "undefined") return null - - let token = getToken("access_token") - if (token && !isTokenExpired(token)) return token - - token = await refreshAccessToken() - return token -} - -export async function logout() { - if (typeof window === "undefined") return - - const token = getToken("access_token") - const refreshToken = getToken("refresh_token") - - try { - await fetch("http://localhost:8080/api/auth/logout", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: token ? `Bearer ${token}` : "", - }, - body: JSON.stringify({ refresh_token: refreshToken }), - }) - } catch { - } - - clearTokens() -} - -export async function fetchWithAuth(url: string, options: RequestInit = {}) { - let token = await getAccessTokenAsync() - - const res = await fetch(url, { - ...options, - headers: { - ...options.headers, - Authorization: token ? `Bearer ${token}` : "", - }, - }) - - if (res.status === 401) { - token = await refreshAccessToken() - if (token) { - return fetch(url, { - ...options, - headers: { - ...options.headers, - Authorization: `Bearer ${token}`, - }, - }) - } - } - - return res -}