refactor: move auth api call to api file
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
|
import { createContext, useContext, useState, useEffect, type ReactNode } from "react"
|
||||||
import { useNavigate } from "react-router"
|
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 {
|
interface AuthContextType {
|
||||||
user: User | null
|
user: User | null
|
||||||
|
|||||||
+3
-19
@@ -4,6 +4,7 @@ import { FormInput } from "@/components/shared/form-input"
|
|||||||
import { FormButton } from "@/components/shared/form-button"
|
import { FormButton } from "@/components/shared/form-button"
|
||||||
import { useAuth } from "@/app/context/auth-context"
|
import { useAuth } from "@/app/context/auth-context"
|
||||||
import { setTokens } from "@/lib/auth"
|
import { setTokens } from "@/lib/auth"
|
||||||
|
import { login } from "@/lib/api"
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -42,29 +43,12 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch("http://localhost:8080/api/auth/login", {
|
const data = await login(form.email, form.password)
|
||||||
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()
|
|
||||||
setTokens(data.access_token, data.refresh_token, rememberMe)
|
setTokens(data.access_token, data.refresh_token, rememberMe)
|
||||||
refreshUser()
|
refreshUser()
|
||||||
|
|
||||||
navigate("/forms")
|
navigate("/forms")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
setErrors({ general: err instanceof Error ? err.message : "Something went wrong. Please try again." })
|
||||||
setErrors({ general: "Something went wrong. Please try again." })
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-17
@@ -3,6 +3,7 @@ import { Link, useNavigate } from "react-router"
|
|||||||
import { FormInput } from "@/components/shared/form-input"
|
import { FormInput } from "@/components/shared/form-input"
|
||||||
import { FormButton } from "@/components/shared/form-button"
|
import { FormButton } from "@/components/shared/form-button"
|
||||||
import { useAuth } from "@/app/context/auth-context"
|
import { useAuth } from "@/app/context/auth-context"
|
||||||
|
import { register } from "@/lib/api"
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -52,25 +53,10 @@ export default function RegisterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("http://localhost:8080/api/auth/register", {
|
await register(form.email, form.password)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate("/login")
|
navigate("/login")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
setErrors({ email: err instanceof Error ? err.message : "Something went wrong. Please try again." })
|
||||||
setErrors({ email: "Something went wrong. Please try again." })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+149
-7
@@ -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"
|
import type { FormSummary, FormDetail, CreateFormPayload, UpdateFormPayload } from "@/lib/types"
|
||||||
|
|
||||||
const API_BASE = "http://localhost:8080"
|
export async function refreshAccessToken(): Promise<string | null> {
|
||||||
|
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<string | null> {
|
||||||
|
if (typeof window === "undefined") return null
|
||||||
|
|
||||||
|
const token = getToken("access_token")
|
||||||
|
if (token && !isTokenExpired(token)) return token
|
||||||
|
|
||||||
|
return refreshAccessToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUserAsync(): Promise<User | null> {
|
||||||
|
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<FormSummary[]> {
|
export async function getForms(): Promise<FormSummary[]> {
|
||||||
const res = await fetchWithAuth(`${API_BASE}/api/forms`)
|
const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/forms`)
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error("Failed to fetch forms")
|
throw new Error("Failed to fetch forms")
|
||||||
@@ -14,7 +156,7 @@ export async function getForms(): Promise<FormSummary[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getFormById(id: string): Promise<FormDetail> {
|
export async function getFormById(id: string): Promise<FormDetail> {
|
||||||
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.ok) {
|
||||||
if (res.status === 404) {
|
if (res.status === 404) {
|
||||||
@@ -27,7 +169,7 @@ export async function getFormById(id: string): Promise<FormDetail> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createForm(payload: CreateFormPayload): Promise<FormDetail> {
|
export async function createForm(payload: CreateFormPayload): Promise<FormDetail> {
|
||||||
const res = await fetchWithAuth(`${API_BASE}/api/form`, {
|
const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/form`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -42,7 +184,7 @@ export async function createForm(payload: CreateFormPayload): Promise<FormDetail
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function updateForm(id: string, payload: UpdateFormPayload): Promise<FormDetail> {
|
export async function updateForm(id: string, payload: UpdateFormPayload): Promise<FormDetail> {
|
||||||
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",
|
method: "PUT",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
@@ -57,7 +199,7 @@ export async function updateForm(id: string, payload: UpdateFormPayload): Promis
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteForm(id: string): Promise<void> {
|
export async function deleteForm(id: string): Promise<void> {
|
||||||
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",
|
method: "DELETE",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+2
-118
@@ -41,7 +41,7 @@ function getStorage(): Storage {
|
|||||||
: sessionStorage
|
: sessionStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
function getToken(key: string): string | null {
|
export function getToken(key: string): string | null {
|
||||||
return localStorage.getItem(key) ?? sessionStorage.getItem(key)
|
return localStorage.getItem(key) ?? sessionStorage.getItem(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export function setTokens(accessToken: string, refreshToken: string, remember?:
|
|||||||
storage.setItem("refresh_token", refreshToken)
|
storage.setItem("refresh_token", refreshToken)
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearTokens() {
|
export function clearTokens() {
|
||||||
localStorage.removeItem("access_token")
|
localStorage.removeItem("access_token")
|
||||||
localStorage.removeItem("refresh_token")
|
localStorage.removeItem("refresh_token")
|
||||||
localStorage.removeItem("remember_me")
|
localStorage.removeItem("remember_me")
|
||||||
@@ -73,36 +73,6 @@ function clearTokens() {
|
|||||||
sessionStorage.removeItem("refresh_token")
|
sessionStorage.removeItem("refresh_token")
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAccessToken(): Promise<string | null> {
|
|
||||||
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 {
|
export function getUser(): User | null {
|
||||||
if (typeof window === "undefined") return null
|
if (typeof window === "undefined") return null
|
||||||
|
|
||||||
@@ -118,89 +88,3 @@ export function getUser(): User | null {
|
|||||||
name: payload.name,
|
name: payload.name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserAsync(): Promise<User | null> {
|
|
||||||
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<string | null> {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user