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",