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
57 changes: 32 additions & 25 deletions frontend/app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
'use client';
"use client";

import { Suspense } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { toast } from 'sonner';
import { Button } from '../../../components/ui/button';
import { Input } from '../../../components/ui/input';
import { Label } from '../../../components/ui/label';
import { Suspense } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useRouter, useSearchParams } from "next/navigation";
import Link from "next/link";
import { toast } from "sonner";
import { Button } from "../../../components/ui/button";
import { Input } from "../../../components/ui/input";
import { Label } from "../../../components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '../../../components/ui/card';
import { useAuthStore } from '../../../stores/auth.store';
} from "../../../components/ui/card";
import { useAuthStore } from "../../../stores/auth.store";

const loginSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});

type LoginFormData = z.infer<typeof loginSchema>;

function LoginForm() {
const router = useRouter();
const searchParams = useSearchParams();
const callbackUrl = searchParams.get('callbackUrl') ?? '/dashboard';
const callbackUrl = searchParams?.get("callbackUrl") ?? "/dashboard";
const { login, isLoading } = useAuthStore();

const {
Expand All @@ -44,13 +44,13 @@ function LoginForm() {
const onSubmit = async (data: LoginFormData) => {
try {
await login(data);
toast.success('Welcome back!');
toast.success("Welcome back!");
router.push(callbackUrl);
} catch (err: unknown) {
const error = err as { message?: string | string[] };
const message = Array.isArray(error?.message)
? error.message[0]
: error?.message ?? 'Login failed. Please check your credentials.';
: (error?.message ?? "Login failed. Please check your credentials.");
toast.error(message);
}
};
Expand All @@ -59,7 +59,9 @@ function LoginForm() {
<Card>
<CardHeader>
<CardTitle className="text-2xl">Sign in</CardTitle>
<CardDescription>Enter your email and password to access your account</CardDescription>
<CardDescription>
Enter your email and password to access your account
</CardDescription>
</CardHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<CardContent className="space-y-4">
Expand All @@ -70,7 +72,7 @@ function LoginForm() {
type="email"
placeholder="you@example.com"
autoComplete="email"
{...register('email')}
{...register("email")}
/>
{errors.email && (
<p className="text-sm text-destructive">{errors.email.message}</p>
Expand All @@ -91,20 +93,25 @@ function LoginForm() {
type="password"
placeholder="••••••••"
autoComplete="current-password"
{...register('password')}
{...register("password")}
/>
{errors.password && (
<p className="text-sm text-destructive">{errors.password.message}</p>
<p className="text-sm text-destructive">
{errors.password.message}
</p>
)}
</div>
</CardContent>
<CardFooter className="flex flex-col gap-4">
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? 'Signing in…' : 'Sign in'}
{isLoading ? "Signing in…" : "Sign in"}
</Button>
<p className="text-sm text-muted-foreground text-center">
Don&apos;t have an account?{' '}
<Link href="/register" className="text-primary underline underline-offset-4">
Don&apos;t have an account?{" "}
<Link
href="/register"
className="text-primary underline underline-offset-4"
>
Sign up
</Link>
</p>
Expand Down
75 changes: 47 additions & 28 deletions frontend/lib/api/auth.api.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import { apiClient, setAccessToken } from './client';
import type { AuthResponse, LoginPayload, RegisterPayload, User } from '../../types/auth.types';
import { apiClient, setAccessToken } from "./client";
import type {
AuthResponse,
LoginPayload,
RegisterPayload,
User,
} from "../../types/auth.types";

function persistTokens(data: AuthResponse) {
setAccessToken(data.accessToken);
if (typeof window !== 'undefined') {
sessionStorage.setItem('refreshToken', data.refreshToken);
sessionStorage.setItem('userId', data.user.id);
if (typeof window !== "undefined") {
sessionStorage.setItem("refreshToken", data.refreshToken);
sessionStorage.setItem("userId", data.user.id);
}
}

function clearTokens() {
setAccessToken(null);
if (typeof window !== 'undefined') {
sessionStorage.removeItem('refreshToken');
sessionStorage.removeItem('userId');
if (typeof window !== "undefined") {
sessionStorage.removeItem("refreshToken");
sessionStorage.removeItem("userId");
}
}

export async function register(payload: RegisterPayload): Promise<AuthResponse> {
const data = await apiClient<AuthResponse>('/auth/register', {
method: 'POST',
export async function register(
payload: RegisterPayload,
): Promise<AuthResponse> {
const data = await apiClient<AuthResponse>("/auth/register", {
method: "POST",
body: JSON.stringify(payload),
skipAuth: true,
});
Expand All @@ -28,8 +35,8 @@ export async function register(payload: RegisterPayload): Promise<AuthResponse>
}

export async function login(payload: LoginPayload): Promise<AuthResponse> {
const data = await apiClient<AuthResponse>('/auth/login', {
method: 'POST',
const data = await apiClient<AuthResponse>("/auth/login", {
method: "POST",
body: JSON.stringify(payload),
skipAuth: true,
});
Expand All @@ -39,18 +46,22 @@ export async function login(payload: LoginPayload): Promise<AuthResponse> {

export async function logout(): Promise<void> {
try {
await apiClient('/auth/logout', { method: 'POST' });
await apiClient("/auth/logout", { method: "POST" });
} finally {
clearTokens();
}
}

export async function refreshToken(): Promise<AuthResponse> {
const userId = typeof window !== 'undefined' ? sessionStorage.getItem('userId') : null;
const refresh = typeof window !== 'undefined' ? sessionStorage.getItem('refreshToken') : null;
const userId =
typeof window !== "undefined" ? sessionStorage.getItem("userId") : null;
const refresh =
typeof window !== "undefined"
? sessionStorage.getItem("refreshToken")
: null;

const data = await apiClient<AuthResponse>('/auth/refresh', {
method: 'POST',
const data = await apiClient<AuthResponse>("/auth/refresh", {
method: "POST",
body: JSON.stringify({ userId, refreshToken: refresh }),
skipAuth: true,
});
Expand All @@ -59,12 +70,14 @@ export async function refreshToken(): Promise<AuthResponse> {
}

export async function getCurrentUser(): Promise<User> {
return apiClient<User>('/auth/me');
return apiClient<User>("/auth/me");
}

export async function forgotPassword(email: string): Promise<{ message: string }> {
return apiClient<{ message: string }>('/auth/forgot-password', {
method: 'POST',
export async function forgotPassword(
email: string,
): Promise<{ message: string }> {
return apiClient<{ message: string }>("/auth/forgot-password", {
method: "POST",
body: JSON.stringify({ email }),
skipAuth: true,
});
Expand All @@ -74,8 +87,8 @@ export async function resetPassword(
token: string,
newPassword: string,
): Promise<{ message: string }> {
return apiClient<{ message: string }>('/auth/reset-password', {
method: 'POST',
return apiClient<{ message: string }>("/auth/reset-password", {
method: "POST",
body: JSON.stringify({ token, newPassword }),
skipAuth: true,
});
Expand All @@ -86,8 +99,8 @@ export async function updateProfile(dto: {
lastName?: string;
walletAddress?: string;
}): Promise<User> {
return apiClient<User>('/auth/profile', {
method: 'PATCH',
return apiClient<User>("/auth/profile", {
method: "PATCH",
body: JSON.stringify(dto),
});
}
Expand All @@ -96,8 +109,14 @@ export async function changePassword(
currentPassword: string,
newPassword: string,
): Promise<{ message: string }> {
return apiClient<{ message: string }>('/auth/change-password', {
method: 'PATCH',
return apiClient<{ message: string }>("/auth/change-password", {
method: "PATCH",
body: JSON.stringify({ currentPassword, newPassword }),
});
}

export async function resendVerificationEmail(): Promise<{ message: string }> {
return apiClient<{ message: string }>("/auth/resend-verification", {
method: "POST",
});
}
17 changes: 8 additions & 9 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-slot": "^1.2.3",
"@swc/helpers": "^0.5.23",
"@tanstack/react-query": "^5.90.2",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
Expand Down
Loading
Loading