diff --git a/frontend/api/authentication.ts b/frontend/api/authentication.ts new file mode 100644 index 0000000..218c996 --- /dev/null +++ b/frontend/api/authentication.ts @@ -0,0 +1,48 @@ +import axios from "axios"; +import axiosInstance from "./config"; + +export interface LoginResponse { + Token: string; + message?: string; +} + +export interface RegisterResponse { + token: string; + message?: string; +} + +/** + * Registers a new user by sending a POST request to the backend. + */ +export async function registerUser(email: string, password: string): Promise { + try { + const response = await axiosInstance.post("/auth/register", { + Email: email, + Password: password, + }); + return response.data; + } catch (error: any) { + if (axios.isAxiosError(error)) { + throw new Error(error.response?.data?.message || "Failed to register."); + } + throw error; + } +} + +/** + * Logs in a user by sending a POST request to the backend. + */ +export async function loginUser(email: string, password: string): Promise { + try { + const response = await axiosInstance.post("/auth/login", { + Email: email, + Password: password, + }); + return response.data; + } catch (error: any) { + if (axios.isAxiosError(error)) { + throw new Error(error.response?.data?.message || "Failed to log in."); + } + throw error; + } +} diff --git a/frontend/api/config.ts b/frontend/api/config.ts new file mode 100644 index 0000000..a48150b --- /dev/null +++ b/frontend/api/config.ts @@ -0,0 +1,28 @@ +import axios from "axios"; + +const axiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:8000", + headers: { + "Content-Type": "application/json", + }, +}); + +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem("token"); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/frontend/app/auth/signin/page.tsx b/frontend/app/auth/signin/page.tsx index 5e6dd18..5891049 100644 --- a/frontend/app/auth/signin/page.tsx +++ b/frontend/app/auth/signin/page.tsx @@ -18,8 +18,11 @@ import { z } from "zod"; import { useState } from "react"; import { useRouter } from "next/navigation"; +import { loginUser } from "@/api/authentication"; + export default function SigninPage() { - const [serverError, setServerError] = useState(null); + const [serverError, setServerError] = useState(null); + const [isLoading, setIsLoading] = useState(false); const router = useRouter(); const { @@ -35,32 +38,20 @@ export default function SigninPage() { }); const onSubmit = async (values: z.infer) => { - setServerError(null); // reset previous errors + setServerError(null); + setIsLoading(true); + try { - const response = await fetch("http://localhost:8000/auth/login", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - Email: values.email, - Password: values.password, - }), - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Failed to log in."); - } - + const data = await loginUser(values.email, values.password); localStorage.setItem("token", data.Token); localStorage.setItem("user", values.email); router.push("/setup"); - } catch (error) { + } catch (error: any) { console.error("Error logging in:", error); setServerError(error.message); + } finally { + setIsLoading(false); } }; @@ -99,8 +90,8 @@ export default function SigninPage() { {errors.password &&

{errors.password.message}

} - diff --git a/frontend/app/auth/signup/page.tsx b/frontend/app/auth/signup/page.tsx index 2c3d0e7..360e237 100644 --- a/frontend/app/auth/signup/page.tsx +++ b/frontend/app/auth/signup/page.tsx @@ -17,9 +17,11 @@ import { z } from "zod"; import { useRouter } from "next/navigation"; +import { registerUser } from "@/api/authentication"; + export default function SignupPage() { - const [serverError, setServerError] = useState(null); - const [successMessage, setSuccessMessage] = useState(null); + const [serverError, setServerError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); const router = useRouter(); const { @@ -38,28 +40,13 @@ export default function SignupPage() { const onSubmit = async (values: z.infer) => { setServerError(null); setSuccessMessage(null); + try { - const response = await fetch("http://localhost:8000/auth/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - Email: values.email, - Password: values.password, - }), - }); - - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || "Failed to register."); - } + const data = await registerUser(values.email, values.password); localStorage.setItem("token", data.token); localStorage.setItem("user", values.email); - // Assume registration returns a token or user data. console.log("Registration successful:", data); setSuccessMessage("Registration successful! You can now sign in."); diff --git a/frontend/package.json b/frontend/package.json index 6797fb8..d2fbb29 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@hookform/resolvers": "^4.0.0", "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -20,6 +21,7 @@ "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/typography": "^0.5.16", "@tanstack/react-query": "^5.66.0", + "axios": "^1.7.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 5ab3af8..a9fcf02 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: '@radix-ui/react-checkbox': specifier: ^1.1.4 version: 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-collapsible': + specifier: ^1.1.3 + version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.6 version: 1.1.6(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -41,6 +44,9 @@ importers: '@tanstack/react-query': specifier: ^5.66.0 version: 5.66.0(react@19.0.0) + axios: + specifier: ^1.7.9 + version: 1.7.9 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -443,6 +449,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-collapsible@1.1.3': + resolution: {integrity: sha512-jFSerheto1X03MUC0g6R7LedNW9EEGWdg9W1+MlpkMLwGkgkbUXLPBH/KIuWKXUoeYRVY11llqbTBDzuLg7qrw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -939,6 +958,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -947,6 +969,9 @@ packages: resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==} engines: {node: '>=4'} + axios@1.7.9: + resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==} + axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} @@ -1027,6 +1052,10 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1093,6 +1122,10 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + detect-libc@2.0.3: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} @@ -1318,6 +1351,15 @@ packages: flatted@3.3.2: resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==} + follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -1326,6 +1368,10 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} + engines: {node: '>= 6'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1649,6 +1695,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -1896,6 +1950,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2582,6 +2639,22 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-collapsible@1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-presence': 1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0) @@ -3106,12 +3179,22 @@ snapshots: async-function@1.0.0: {} + asynckit@0.4.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 axe-core@4.10.2: {} + axios@1.7.9: + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.1 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axobject-query@4.1.0: {} balanced-match@1.0.2: {} @@ -3201,6 +3284,10 @@ snapshots: color-string: 1.9.1 optional: true + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} concat-map@0.0.1: {} @@ -3259,6 +3346,8 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delayed-stream@1.0.0: {} + detect-libc@2.0.3: optional: true @@ -3632,6 +3721,8 @@ snapshots: flatted@3.3.2: {} + follow-redirects@1.15.9: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -3641,6 +3732,12 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.1: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + fsevents@2.3.3: optional: true @@ -3970,6 +4067,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -4212,6 +4315,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + proxy-from-env@1.1.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {}