diff --git a/.gitignore b/.gitignore index 5ef6a520..3eb32276 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,5 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +certificates diff --git a/README.md b/README.md index 2b001613..14a243b5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,12 @@ To clone and run this application, [Node.js](https://nodejs.org/en/download/) (w npm install npm@latest -g ``` +### `.env` Setup + +Create a file called `.env` in the root directory, copying the contents of `example.env`. + +Replace all values in the file with the relevant information. + ### Installation ```bash diff --git a/app/_utils/format-api-error.tsx b/app/_utils/format-api-error.tsx new file mode 100644 index 00000000..3b06fc13 --- /dev/null +++ b/app/_utils/format-api-error.tsx @@ -0,0 +1,36 @@ +function snakeToTitleCase(str: string): string { + return str + .split("_") + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); +} + +export default function formatApiError(errors: any): string { + let errorMessage = ""; + let generalMessage = ""; + errors = errors.error; + + if (errors.general) { + generalMessage = errors.general[0]; + } + + for (const field in errors) { + if (field !== "general" && Array.isArray(errors[field])) { + for (const msg of errors[field]) { + const fieldTitle = snakeToTitleCase(field); + errorMessage += `${fieldTitle}: ${msg}\n`; + } + } + } + + if (errorMessage) { + if (generalMessage) { + return generalMessage + "\n" + errorMessage.trim(); + } + return errorMessage.trim(); + } else if (generalMessage) { + return generalMessage; + } + + return "An unknown error has occurred."; +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index ceb0f03e..5733a087 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,4 +1,8 @@ -import React from "react"; +"use client"; + +import { useRouter } from "next/navigation"; +import { useRef } from "react"; +import formatApiError from "../_utils/format-api-error"; type Event = { id: string; @@ -7,6 +11,9 @@ type Event = { }; export default function Page() { + const router = useRouter(); + const isSubmitting = useRef(false); + // Mock data for testing const joinedEvents: Event[] = [ { @@ -60,6 +67,29 @@ export default function Page() { ); }; + const logout = async () => { + if (isSubmitting.current) return; + isSubmitting.current = true; + + await fetch("/api/auth/logout/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + }) + .then(async (res) => { + if (res.ok) { + router.push("/login"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; + }; + return (
{/* Events You Joined */} @@ -95,6 +125,16 @@ export default function Page() { ))}
+ + {/* Logout Button */} +
+ +
); } diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx index 806b9e1b..c05be2ab 100644 --- a/app/forgot-password/page.tsx +++ b/app/forgot-password/page.tsx @@ -1,23 +1,47 @@ "use client"; -import React, { useState } from "react"; import Link from "next/link"; -import MessagePage from "../ui/layout/message-page"; import { useRouter } from "next/navigation"; +import React, { useRef, useState } from "react"; +import formatApiError from "../_utils/format-api-error"; +import MessagePage from "../ui/layout/message-page"; export default function Page() { const [email, setEmail] = useState(""); const [emailSent, setEmailSent] = useState(false); + const isSubmitting = useRef(false); const router = useRouter(); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (isSubmitting.current) return; + isSubmitting.current = true; + if (!email) { - alert("Please enter an email address."); + alert("Missing email"); + isSubmitting.current = false; return; } - setEmailSent(true); + + await fetch("/api/auth/start-password-reset/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }) + .then(async (res) => { + if (res.ok) { + setEmailSent(true); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; }; return ( diff --git a/app/login/page.tsx b/app/login/page.tsx index 9fb51139..c5c44c06 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -1,24 +1,51 @@ "use client"; -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import formatApiError from "../_utils/format-api-error"; export default function Page() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const isSubmitting = useRef(false); const router = useRouter(); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - console.log("Login attempt:", { email, password }); - // TODO: Replace with real authentication - if (email === "test" && password === "1234") { - router.push("/dashboard"); - } else { - alert("Invalid email or password"); + if (isSubmitting.current) return; + isSubmitting.current = true; + + if (!email) { + alert("Missing email"); + isSubmitting.current = false; + return; + } + if (!password) { + alert("Missing password"); + isSubmitting.current = false; + return; } + + await fetch("/api/auth/login/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + .then(async (res) => { + if (res.ok) { + router.push("/dashboard"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; }; return ( diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx index 15e94f87..455787da 100644 --- a/app/reset-password/page.tsx +++ b/app/reset-password/page.tsx @@ -1,35 +1,62 @@ "use client"; import { useRouter, useSearchParams } from "next/navigation"; -import React, { useState } from "react"; +import React, { useRef, useState } from "react"; +import formatApiError from "../_utils/format-api-error"; export default function Page() { const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const isSubmitting = useRef(false); const router = useRouter(); const searchParams = useSearchParams(); const pwdResetToken = searchParams.get("token"); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); + if (isSubmitting.current) return; + isSubmitting.current = true; + if (!pwdResetToken) { - alert("Invalid or missing password reset token."); + alert("This link is expired or invalid."); + isSubmitting.current = false; return; } - if (!newPassword || !confirmPassword) { - alert("Please fill in all fields."); + if (!newPassword) { + alert("Missing new password."); + isSubmitting.current = false; return; } - // TODO: Replace with real password reset API call if (newPassword !== confirmPassword) { alert("Passwords do not match."); - } else { - router.push("/reset-password/success"); + isSubmitting.current = false; + return; } + await fetch("/api/auth/reset-password/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + reset_token: pwdResetToken, + new_password: newPassword, + }), + }) + .then(async (res) => { + if (res.ok) { + router.push("/reset-password/success"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; }; return ( diff --git a/app/sign-up/email-sent/page.tsx b/app/sign-up/email-sent/page.tsx index 8f892600..32e3c9a2 100644 --- a/app/sign-up/email-sent/page.tsx +++ b/app/sign-up/email-sent/page.tsx @@ -1,5 +1,6 @@ "use client"; +import formatApiError from "@/app/_utils/format-api-error"; import { useRouter } from "next/navigation"; import { useEffect, useRef } from "react"; import MessagePage from "../../ui/layout/message-page"; @@ -23,7 +24,7 @@ export default function Page() { return null; } - const handleResendEmail = () => { + const handleResendEmail = async () => { const emailResendCooldown = 30000; // 30 seconds let timeLeft = (emailResendCooldown - (Date.now() - lastEmailResend.current)) / 1000; @@ -32,8 +33,24 @@ export default function Page() { alert(`Slow down! ${timeLeft} seconds until you can send again.`); return; } - // TODO: Replace with real resend email API logic - console.log("Resending email to:", email); + + await fetch("/api/auth/resend-register-email/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email }), + }) + .then(async (res) => { + if (res.ok) { + alert("Email resent. Please check your inbox."); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + lastEmailResend.current = Date.now(); }; diff --git a/app/sign-up/page.tsx b/app/sign-up/page.tsx index 9de47d29..cf1fed8a 100644 --- a/app/sign-up/page.tsx +++ b/app/sign-up/page.tsx @@ -3,30 +3,56 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import React, { useState } from "react"; +import formatApiError from "../_utils/format-api-error"; export default function Page() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); + const isSubmitting = React.useRef(false); const router = useRouter(); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - console.log("Sign up attempt:", { - email, - password, - confirmPassword, - }); - // TODO: Replace with real sign up API logic - if (email && password && confirmPassword === password) { - // add the email to session storage to have it in the email-sent page without - // putting it in the URL - sessionStorage.setItem("sign_up_email", email); - router.push("/sign-up/email-sent"); - } else { - alert("WOMP WOMP NO ACCOUNT FOR YOU"); + if (isSubmitting.current) return; + isSubmitting.current = true; + + if (!email) { + alert("Missing email"); + isSubmitting.current = false; + return; + } + if (!password) { + alert("Missing password"); + isSubmitting.current = false; + return; + } + if (confirmPassword !== password) { + alert("Passwords do not match"); + isSubmitting.current = false; + return; } + + await fetch("/api/auth/register/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }), + }) + .then(async (res) => { + if (res.ok) { + sessionStorage.setItem("sign_up_email", email); + router.push("/sign-up/email-sent"); + } else { + alert(formatApiError(await res.json())); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + + isSubmitting.current = false; }; return ( diff --git a/app/verify-email/page.tsx b/app/verify-email/page.tsx index 2009f810..e599c33d 100644 --- a/app/verify-email/page.tsx +++ b/app/verify-email/page.tsx @@ -2,6 +2,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; +import formatApiError from "../_utils/format-api-error"; import MessagePage from "../ui/layout/message-page"; export default function Page() { @@ -14,18 +15,28 @@ export default function Page() { useEffect(() => { const verifyEmail = async () => { - // simulate network delay - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (!token) { setVerifying(false); setEmailVerified(false); return; } - // TODO: Replace with an actual API call + await fetch("/api/auth/verify-email/", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ verification_code: token }), + }) + .then(async (res) => { + if (res.ok) { + setEmailVerified(true); + } + }) + .catch((err) => { + console.error("Fetch error:", err); + alert("An error occurred. Please try again."); + }); + setVerifying(false); - setEmailVerified(true); }; verifyEmail(); diff --git a/example.env b/example.env new file mode 100644 index 00000000..d5f10262 --- /dev/null +++ b/example.env @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL=https://example.com diff --git a/next.config.ts b/next.config.ts index 28f40ec5..a9f37824 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,16 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { allowedDevOrigins: ["129.161.139.75"], + + // Redirect API calls to the backend server for CORS stuff + async rewrites() { + return [ + { + source: "/api/:path*", + destination: `${process.env.NEXT_PUBLIC_API_URL}/:path*/`, + }, + ]; + }, }; export default nextConfig; diff --git a/package.json b/package.json index f537f45e..85bd1c35 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev --turbopack --experimental-https", "build": "next build", "start": "next start", "lint": "next lint",