Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions client/src/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const authOptions: AuthOptions = {
let user: User;
try {
const { data: body } = await authClient.post<AccountWithTokens>(
"/api/accounts/login",
"/accounts/login",
{
email,
password,
Expand Down Expand Up @@ -88,7 +88,7 @@ export const authOptions: AuthOptions = {
}

// Refresh token
const { data: body } = await authClient.post<Tokens>("api/accounts/token", {
const { data: body } = await authClient.post<Tokens>("/accounts/token", {
refreshToken: token.refreshToken,
});

Expand Down
38 changes: 35 additions & 3 deletions client/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import {
Form,
Expand All @@ -19,6 +20,8 @@ import { Checkbox } from "@/components/ui/checkbox";
import Image from "next/image";
import google from "@/assets/google-icon.png";
import linkedin from "@/assets/linkedin-icon.png";
import { login } from "@/services/authService";
import { CreateAccountInput } from "@/types/dto/accountDto";

const formSchema = z.object({
email: z.string().email(),
Expand All @@ -30,6 +33,9 @@ type LoginFormValues = z.infer<typeof formSchema>;

export default function LoginForm() {
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();

const form = useForm<LoginFormValues>({
resolver: zodResolver(formSchema),
Expand All @@ -40,7 +46,27 @@ export default function LoginForm() {
},
});

const onSubmit = (values: LoginFormValues) => console.log(values);
const onSubmit = async (values: LoginFormValues) => {
setIsLoading(true);
setError(null);

try {
const loginData: CreateAccountInput = {
email: values.email,
password: values.password,
};

await login(loginData, "/profile");
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The login function is called with a hardcoded redirect URL '/profile'. Consider making this configurable or using a constant to improve maintainability and consistency with other parts of the application.

Copilot uses AI. Check for mistakes.
// If we reach here and no redirect happened, the login was successful
// but we chose not to redirect, so we can manually navigate
router.push("/profile");
} catch (err) {
console.error("Login error:", err);
setError(err instanceof Error ? err.message : "Login failed. Please try again.");
} finally {
setIsLoading(false);
}
};

return (
<div className="flex items-center justify-center min-h-screen bg-gradient-to-br from-[#B1D2F6] to-[#DFEDFB]">
Expand All @@ -50,6 +76,11 @@ export default function LoginForm() {
<div className="text-center">
<h2 className="text-3xl font-extrabold text-gray-800">Login</h2>
<p className="mt-4 text-xs text-gray-500">Enter your email and password below</p>
{error && (
<div className="mt-2 p-3 bg-red-100 border border-red-400 text-red-700 text-sm rounded">
{error}
</div>
)}
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
Expand Down Expand Up @@ -103,8 +134,9 @@ export default function LoginForm() {
</Link>
</div>
<Button type="submit"
className="w-full py-7 rounded-xl bg-[#735AFB] hover:bg-white text-white font-semibold">
Login
disabled={isLoading}
className="w-full py-7 rounded-xl bg-[#735AFB] hover:bg-white text-white font-semibold disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? "Logging in..." : "Login"}
</Button>
</form>
</Form>
Expand Down
49 changes: 47 additions & 2 deletions client/src/app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,53 @@
"use client";

import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import { useEffect } from "react";
import { logout } from "@/services/authService";

export default function Profile() {
const { data: session, status } = useSession();
const router = useRouter();

useEffect(() => {
if (status === "unauthenticated") {
router.push("/login");
}
}, [status, router]);

const handleLogout = async () => {
try {
await logout("/login");
} catch (error) {
console.error("Logout error:", error);
}
};

if (status === "loading") {
return <div>Loading...</div>;
}

if (status === "unauthenticated") {
return <div>Redirecting to login...</div>;
}

return (
<>
<h1 className="text-4xl font-semibold mb-4">Hello John Doe</h1>
<div className="grid grid-cols-3 gap-4">
<h1 className="text-4xl font-semibold mb-4">Hello {session?.user?.email}</h1>
<div className="mb-4">
<p><strong>Email:</strong> {session?.user?.email}</p>
<p><strong>Access Token Available:</strong> {session?.accessToken ? "Yes" : "No"}</p>
{session?.accessToken && (
<p><strong>Access Token (first 50 chars):</strong> {session.accessToken.substring(0, 50)}...</p>
)}
</div>
<button
onClick={handleLogout}
className="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600"
>
Logout
</button>
<div className="mt-8 grid grid-cols-3 gap-4">
{[...Array(5).keys()].map((i) => (
<div
key={i}
Expand Down
42 changes: 38 additions & 4 deletions client/src/app/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from 'next/link';
import { z } from "zod"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button"
import {
Form,
Expand All @@ -16,6 +17,8 @@ import { Input } from "@/components/ui/input"
import Image from "next/image";
import google from "@/assets/google-icon.png";
import linkedin from "@/assets/linkedin-icon.png";
import { signup } from "@/services/authService";
import { SignupInput } from "@/types/dto/accountDto";


// zod defines form shape and fields
Expand All @@ -31,6 +34,9 @@ const formSchema = z.object({

export default function SignUpForm() {
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
Expand All @@ -45,8 +51,30 @@ export default function SignUpForm() {
})

// submit handler.
function onSubmit(values: z.infer<typeof formSchema>) {
// Do something with the form values.
async function onSubmit(values: z.infer<typeof formSchema>) {
setIsLoading(true);
setError(null);

try {
const signupData: SignupInput = {
first: values.first,
last: values.last,
phone: values.phone,
birthday: values.birthday,
email: values.email,
password: values.password,
};

await signup(signupData, "/profile");
Copy link

Copilot AI Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The signup function is called with a hardcoded redirect URL '/profile'. Consider making this configurable or using a constant to improve maintainability and flexibility.

Copilot uses AI. Check for mistakes.
// If we reach here and no redirect happened, the signup was successful
// but we chose not to redirect, so we can manually navigate
router.push("/profile");
} catch (err) {
console.error("Signup error:", err);
setError(err instanceof Error ? err.message : "Signup failed. Please try again.");
} finally {
setIsLoading(false);
}
}

return (
Expand All @@ -57,6 +85,11 @@ export default function SignUpForm() {
<div className="text-center">
<h2 className="text-3xl font-extrabold text-gray-800">Sign Up</h2>
<p className="mt-4 text-xs text-gray-500">Create an account to continue!</p>
{error && (
<div className="mt-2 p-3 bg-red-100 border border-red-400 text-red-700 text-sm rounded">
{error}
</div>
)}
</div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
Expand Down Expand Up @@ -146,8 +179,9 @@ export default function SignUpForm() {
)}
/>
<Button type="submit"
className="w-full py-7 rounded-xl bg-[#735AFB] hover:bg-white text-white font-semibold">
Register
disabled={isLoading}
className="w-full py-7 rounded-xl bg-[#735AFB] hover:bg-white text-white font-semibold disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? "Creating account..." : "Register"}
</Button>
</form>
</Form>
Expand Down
59 changes: 49 additions & 10 deletions client/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
"use client";

import Link from "next/link";
import Image from "next/image";
import { useSession } from "next-auth/react";
import title from "@/assets/medmatch_.svg";
import { Button } from "@/components/ui/button"

import { Button } from "@/components/ui/button";
import { logout } from "@/services/authService";

function Navbar() {
const { data: session, status } = useSession();

const handleLogout = async () => {
try {
await logout("/");
} catch (error) {
console.error("Logout error:", error);
}
};

return (
<div className="bg-[#ffffff] shadow-md flex justify-between items-center py-2 px-4 gap-8 pl-10 pr-10">
<div className="flex items-center">
Expand All @@ -18,14 +31,40 @@ function Navbar() {
</Link>
</div>
<ul className="flex items-center p-0 m-0 list-none gap-4">
<li>
<Link href="/" className="text-primary-blue text-lg pr-10 font-semibold">Sign In</Link>
</li>
<li>
<Button className="w-36 h-12 text-lg font-semibold bg-primary-blue hover:bg-[#ffffff] hover:text-primary-blue hover:outline hover:outline-primary-blue hover:outline-[2]">
<span>Sign Up</span>
</Button>
</li>
{status === "loading" ? (
<li>Loading...</li>
) : session ? (
<>
<li>
<Link href="/profile" className="text-primary-blue text-lg pr-10 font-semibold">
Profile
</Link>
</li>
<li>
<Button
onClick={handleLogout}
className="w-36 h-12 text-lg font-semibold bg-primary-blue hover:bg-[#ffffff] hover:text-primary-blue hover:outline hover:outline-primary-blue hover:outline-[2]"
>
<span>Log Out</span>
</Button>
</li>
</>
) : (
<>
<li>
<Link href="/login" className="text-primary-blue text-lg pr-10 font-semibold">
Sign In
</Link>
</li>
<li>
<Button className="w-36 h-12 text-lg font-semibold bg-primary-blue hover:bg-[#ffffff] hover:text-primary-blue hover:outline hover:outline-primary-blue hover:outline-[2]">
<Link href="/signup">
<span>Sign Up</span>
</Link>
</Button>
</li>
</>
)}
</ul>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/lib/authClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import axios from "axios";
*/
const authClient = axios.create({
withCredentials: true,
baseURL: `${process.env.NEXT_PUBLIC_API_BASE_URL!}/api`,
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:4000/api",
headers: {
"Content-Type": "application/json",
},
Expand Down
6 changes: 3 additions & 3 deletions client/src/services/authService.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { authClient } from "@/lib/authClient";
import { signIn, signOut } from "next-auth/react";
import { AccountWithTokens, CreateAccountInput } from "@/types/dto/accountDto";
import { AccountWithTokens, CreateAccountInput, SignupInput } from "@/types/dto/accountDto";

const withBase = (path: string) => `/api/auth${path}`;
const withBase = (path: string) => `/accounts${path}`;

/**
* Authenticates a user based on email and password, adding the user to the session.
Expand Down Expand Up @@ -44,7 +44,7 @@ async function login(
* @throws An `AxiosError` if there is a login conflict. Generic error if the NextAuth request fails.
*/
async function signup(
accountData: CreateAccountInput,
accountData: SignupInput,
callbackUrl: string | null = "/"
): Promise<void> {
await authClient.post<AccountWithTokens>(withBase("/signup"), accountData);
Expand Down
9 changes: 9 additions & 0 deletions client/src/types/dto/accountDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ export type AccountWithTokens = Account & Tokens
export type CreateAccountInput = {
email: string,
password: string
}

export type SignupInput = {
first: string,
last: string,
phone: string,
birthday: string,
email: string,
password: string
}