diff --git a/__tests__/logout.test.ts b/__tests__/logout.test.ts new file mode 100644 index 0000000..4f6dafa --- /dev/null +++ b/__tests__/logout.test.ts @@ -0,0 +1,25 @@ +// @vitest-environment node +import { POST } from '@/app/api/auth/logout/route'; +import { describe, it, expect } from 'vitest'; + +describe('POST /api/auth/logout', () => { + it('should clear the auth cookie and return success', async () => { + const mockRequest = new Request('http://localhost:3000/api/auth/logout', { + method: 'POST', + headers: new Headers({ + 'cookie': 'auth_token=some-token-value' + }), + }); + + const res = await POST(mockRequest as any); + // Ensure response is JSON and success is true + expect(res.status).toBe(200); + const data = await res.json(); + expect(data).toHaveProperty('success', true); + + // Check that a Set-Cookie header was included (cookie cleared) + const setCookie = res.headers.get('set-cookie'); + expect(typeof setCookie).toBe('string'); + expect(setCookie).toContain('auth_token='); + }); +}); diff --git a/app/api/auth/signup/route.ts b/app/api/auth/signup/route.ts index b5b7b1f..ae0c617 100644 --- a/app/api/auth/signup/route.ts +++ b/app/api/auth/signup/route.ts @@ -1,4 +1,3 @@ -import { test } from "vitest"; import { NextRequest, NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { findUserByEmail, createUser, toPublicUser } from "@/lib/auth/users"; diff --git a/app/api/auth/verify-email/route.ts b/app/api/auth/verify-email/route.ts new file mode 100644 index 0000000..40a6cc0 --- /dev/null +++ b/app/api/auth/verify-email/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import { verifyEmailToken } from "@/lib/auth/users"; + +export async function GET(request: NextRequest) { + const token = request.nextUrl.searchParams.get("token"); + if (!token) { + return NextResponse.json({ success: false, error: "Missing verification token." }, { status: 400 }); + } + + const user = verifyEmailToken(token); + if (!user) { + return NextResponse.json({ success: false, error: "Invalid or expired verification token." }, { status: 400 }); + } + + return NextResponse.json({ success: true, message: "Email verified successfully." }); +} diff --git a/app/escrow/create/page.tsx b/app/escrow/create/page.tsx index 586a07d..d7b7fec 100644 --- a/app/escrow/create/page.tsx +++ b/app/escrow/create/page.tsx @@ -1,6 +1,51 @@ import CreateEscrowForm from "@/components/escrow/CreateEscrowForm"; +import { useEmailAuth } from "@/context/EmailAuthContext"; +import Link from "next/link"; export default function CreateEscrowPage() { + const { user, isLoading } = useEmailAuth(); + + if (isLoading) { + return null; + } + + if (!user) { + return ( +
+
+

Sign in to create an escrow

+

+ You need to be signed in to create escrow agreements. +

+ + Go to Login + +
+
+ ); + } + + if (!user.emailVerified) { + return ( +
+
+

Verify your email to create an escrow

+

+ Your email address must be verified before you can create a new escrow agreement. +

+
+ + Verify Email + + + Back to Home + +
+
+
+ ); + } + return (
diff --git a/app/verify-email/page.tsx b/app/verify-email/page.tsx new file mode 100644 index 0000000..3df9a42 --- /dev/null +++ b/app/verify-email/page.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; + +export default function VerifyEmailPage() { + const searchParams = useSearchParams(); + const router = useRouter(); + const token = searchParams.get("token"); + const [status, setStatus] = useState<"pending" | "success" | "error">("pending"); + const [message, setMessage] = useState("Verifying your email..."); + + useEffect(() => { + if (!token) { + setStatus("error"); + setMessage("Verification token is missing."); + return; + } + + const verify = async () => { + try { + const res = await fetch(`/api/auth/verify-email?token=${encodeURIComponent(token)}`); + const data = await res.json(); + + if (!res.ok || !data.success) { + setStatus("error"); + setMessage(data.error || "Verification failed. Please request a new link."); + return; + } + + setStatus("success"); + setMessage("Your email has been verified. You can now create escrows."); + } catch (error) { + setStatus("error"); + setMessage("Unable to verify email. Please try again later."); + } + }; + + verify(); + }, [token]); + + return ( +
+
+

Email Verification

+

{message}

+ +
+ + +
+
+
+ ); +} diff --git a/context/EmailAuthContext.tsx b/context/EmailAuthContext.tsx index 757f0cd..4438411 100644 --- a/context/EmailAuthContext.tsx +++ b/context/EmailAuthContext.tsx @@ -14,6 +14,7 @@ export interface AuthUser { id: string; email: string; name: string; + emailVerified: boolean; } interface EmailAuthContextValue { diff --git a/lib/auth/users.ts b/lib/auth/users.ts index 312c242..5e2abcb 100644 --- a/lib/auth/users.ts +++ b/lib/auth/users.ts @@ -17,6 +17,9 @@ export interface StoredUser { name: string; passwordHash: string; createdAt: string; + emailVerified: boolean; + verificationToken?: string; + verificationTokenExpiresAt?: string; notificationPreferences?: NotificationPreferences; // Added to store preferences } @@ -24,6 +27,7 @@ export interface PublicUser { id: string; email: string; name: string; + emailVerified: boolean; } const DEFAULT_NOTIFICATIONS: NotificationPreferences = { @@ -55,27 +59,69 @@ export function findUserById(id: string): StoredUser | undefined { return readUsers().find((u) => u.id === id); } +export function findUserByVerificationToken(token: string): StoredUser | undefined { + return readUsers().find( + (u) => + u.verificationToken === token && + u.verificationTokenExpiresAt && + new Date(u.verificationTokenExpiresAt) > new Date() + ); +} + +export function verifyEmailToken(token: string): StoredUser | null { + const users = readUsers(); + const userIndex = users.findIndex( + (u) => + u.verificationToken === token && + u.verificationTokenExpiresAt && + new Date(u.verificationTokenExpiresAt) > new Date() + ); + + if (userIndex === -1) { + return null; + } + + users[userIndex].emailVerified = true; + delete users[userIndex].verificationToken; + delete users[userIndex].verificationTokenExpiresAt; + writeUsers(users); + + return users[userIndex]; +} + export function createUser( email: string, name: string, passwordHash: string ): StoredUser { const users = readUsers(); + const verificationToken = randomUUID(); + const tokenExpiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(); + const user: StoredUser = { id: randomUUID(), email: email.toLowerCase().trim(), name: name.trim(), passwordHash, createdAt: new Date().toISOString(), + emailVerified: false, + verificationToken, + verificationTokenExpiresAt: tokenExpiresAt, notificationPreferences: DEFAULT_NOTIFICATIONS, // Apply defaults on creation }; + users.push(user); writeUsers(users); return user; } export function toPublicUser(user: StoredUser): PublicUser { - return { id: user.id, email: user.email, name: user.name }; + return { + id: user.id, + email: user.email, + name: user.name, + emailVerified: user.emailVerified, + }; } // ---------------------------------------------------------------------------