diff --git a/client/src/app/api/auth/[...nextauth]/route.ts b/client/src/app/api/auth/[...nextauth]/route.ts index b0af1bb..a46d6a0 100644 --- a/client/src/app/api/auth/[...nextauth]/route.ts +++ b/client/src/app/api/auth/[...nextauth]/route.ts @@ -31,7 +31,7 @@ export const authOptions: AuthOptions = { let user: User; try { const { data: body } = await authClient.post( - "/api/accounts/login", + "/accounts/login", { email, password, @@ -88,7 +88,7 @@ export const authOptions: AuthOptions = { } // Refresh token - const { data: body } = await authClient.post("api/accounts/token", { + const { data: body } = await authClient.post("/accounts/token", { refreshToken: token.refreshToken, }); diff --git a/client/src/app/login/page.tsx b/client/src/app/login/page.tsx index 484edb6..b9d8a31 100644 --- a/client/src/app/login/page.tsx +++ b/client/src/app/login/page.tsx @@ -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, @@ -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(), @@ -30,6 +33,9 @@ type LoginFormValues = z.infer; export default function LoginForm() { const [showPassword, setShowPassword] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const router = useRouter(); const form = useForm({ resolver: zodResolver(formSchema), @@ -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"); + // 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 (
@@ -50,6 +76,11 @@ export default function LoginForm() {

Login

Enter your email and password below

+ {error && ( +
+ {error} +
+ )}
@@ -103,8 +134,9 @@ export default function LoginForm() {
diff --git a/client/src/app/profile/page.tsx b/client/src/app/profile/page.tsx index ece666a..59dcfcb 100644 --- a/client/src/app/profile/page.tsx +++ b/client/src/app/profile/page.tsx @@ -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
Loading...
; + } + + if (status === "unauthenticated") { + return
Redirecting to login...
; + } + return ( <> -

Hello John Doe

-
+

Hello {session?.user?.email}

+
+

Email: {session?.user?.email}

+

Access Token Available: {session?.accessToken ? "Yes" : "No"}

+ {session?.accessToken && ( +

Access Token (first 50 chars): {session.accessToken.substring(0, 50)}...

+ )} +
+ +
{[...Array(5).keys()].map((i) => (
(null); + const router = useRouter(); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { @@ -45,8 +51,30 @@ export default function SignUpForm() { }) // submit handler. - function onSubmit(values: z.infer) { - // Do something with the form values. + async function onSubmit(values: z.infer) { + 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"); + // 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 ( @@ -57,6 +85,11 @@ export default function SignUpForm() {

Sign Up

Create an account to continue!

+ {error && ( +
+ {error} +
+ )}
@@ -146,8 +179,9 @@ export default function SignUpForm() { )} />
diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index ece0b84..db1771f 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -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 (
@@ -18,14 +31,40 @@ function Navbar() {
    -
  • - Sign In -
  • -
  • - -
  • + {status === "loading" ? ( +
  • Loading...
  • + ) : session ? ( + <> +
  • + + Profile + +
  • +
  • + +
  • + + ) : ( + <> +
  • + + Sign In + +
  • +
  • + +
  • + + )}
); diff --git a/client/src/lib/authClient.ts b/client/src/lib/authClient.ts index e7783c2..c49bec1 100644 --- a/client/src/lib/authClient.ts +++ b/client/src/lib/authClient.ts @@ -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", }, diff --git a/client/src/services/authService.ts b/client/src/services/authService.ts index 7c9962a..1451eeb 100644 --- a/client/src/services/authService.ts +++ b/client/src/services/authService.ts @@ -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. @@ -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 { await authClient.post(withBase("/signup"), accountData); diff --git a/client/src/types/dto/accountDto.ts b/client/src/types/dto/accountDto.ts index 673b406..110a4b0 100644 --- a/client/src/types/dto/accountDto.ts +++ b/client/src/types/dto/accountDto.ts @@ -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 } \ No newline at end of file