Authentication
CashGap uses NextAuth v5 with multiple authentication providers and a MongoDB adapter for session management.Overview
The authentication system supports:- Credentials authentication with email/password
- Google OAuth for social login
- Email verification for new registrations
- JWT-based sessions for stateless authentication
- Protected routes with middleware
NextAuth Configuration
Auth Setup
The main auth configuration is inauth.ts:
auth.ts
import NextAuth from "next-auth";
import { MongoDBAdapter } from "@auth/mongodb-adapter";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
import { z } from "zod";
import clientPromise from "@/lib/db/mongodb";
const credentialsSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const nextAuth = NextAuth({
adapter: MongoDBAdapter(clientPromise),
session: { strategy: "jwt" },
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
try {
const { email, password } = credentialsSchema.parse(credentials);
const client = await clientPromise;
const db = client.db();
const usersCollection = db.collection("users");
const user = await usersCollection.findOne({ email });
if (!user || !user.password) {
return null;
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return null;
}
return {
id: user._id.toString(),
email: user.email,
name: user.name,
};
} catch (error) {
console.error("Auth error:", error);
return null;
}
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
session({ session, token }) {
if (token && session.user) {
session.user.id = token.id as string;
}
return session;
},
},
});
export const { handlers, signIn, signOut, auth } = nextAuth;
Auth Config
Additional configuration inauth.config.ts:
auth.config.ts
import type { NextAuthConfig } from "next-auth";
export default {
pages: {
signIn: "/login",
newUser: "/register",
},
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
if (isOnDashboard) {
if (isLoggedIn) return true;
return false; // Redirect to login
} else if (isLoggedIn) {
return Response.redirect(new URL("/dashboard", nextUrl));
}
return true;
},
},
providers: [], // Providers defined in auth.ts
} satisfies NextAuthConfig;
Authentication Pages
Login Page
The login page uses the sharedLoginForm component from @repo/auth:
app/(auth)/login/page.tsx
import { LoginForm, AuthLayout } from "@repo/auth";
import { signIn } from "@/auth";
import Link from "next/link";
export default function LoginPage() {
const adapter = {
signIn: async ({ email, password }: { email: string; password: string }) => {
"use server";
const result = await signIn("credentials", {
email,
password,
redirect: false,
});
if (result?.error) throw new Error(result.error);
},
};
return (
<AuthLayout>
<LoginForm
adapter={adapter}
signInWithGoogle={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
/>
<p className="text-center text-sm text-muted-foreground mt-4">
Don't have an account?{" "}
<Link href="/register" className="text-primary hover:underline">
Sign up
</Link>
</p>
</AuthLayout>
);
}
Register Page
Registration with email verification:app/(auth)/register/page.tsx
import { RegisterForm, AuthLayout } from "@repo/auth";
import { signIn } from "@/auth";
import Link from "next/link";
export default function RegisterPage() {
const adapter = {
register: async ({
email,
password,
name,
}: {
email: string;
password: string;
name?: string;
}) => {
"use server";
// Call registration API
const response = await fetch(
`${process.env.NEXTAUTH_URL}/api/auth/register`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password, name }),
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || "Registration failed");
}
// Auto-login after registration
await signIn("credentials", {
email,
password,
redirectTo: "/dashboard",
});
},
};
return (
<AuthLayout>
<RegisterForm
adapter={adapter}
signInWithGoogle={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
/>
<p className="text-center text-sm text-muted-foreground mt-4">
Already have an account?{" "}
<Link href="/login" className="text-primary hover:underline">
Sign in
</Link>
</p>
</AuthLayout>
);
}
Registration API
User Registration Endpoint
The registration endpoint handles user creation:app/api/auth/register/route.ts
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";
import { z } from "zod";
import clientPromise from "@/lib/db/mongodb";
const registerSchema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
name: z.string().optional(),
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password, name } = registerSchema.parse(body);
const client = await clientPromise;
const db = client.db();
const usersCollection = db.collection("users");
// Check if user already exists
const existingUser = await usersCollection.findOne({ email });
if (existingUser) {
return NextResponse.json(
{ message: "User already exists" },
{ status: 400 }
);
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create user
const result = await usersCollection.insertOne({
email,
password: hashedPassword,
name: name || null,
emailVerified: null,
image: null,
createdAt: new Date(),
updatedAt: new Date(),
});
return NextResponse.json(
{
message: "User created successfully",
userId: result.insertedId.toString(),
},
{ status: 201 }
);
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ message: error.errors[0].message },
{ status: 400 }
);
}
console.error("Registration error:", error);
return NextResponse.json(
{ message: "Internal server error" },
{ status: 500 }
);
}
}
Email Verification
Verification Token Model
Email verification tokens are stored in MongoDB:lib/db/models.ts
export interface EmailVerificationTokenDocument extends Document {
email: string;
token: string;
code: string; // 6-digit verification code
expiresAt: Date;
verified: boolean;
registrationData?: {
password: string; // Hashed password
name?: string;
};
}
const emailVerificationTokenSchema = new Schema<EmailVerificationTokenDocument>({
email: { type: String, required: true, lowercase: true, trim: true },
token: { type: String, required: true, unique: true },
code: { type: String, required: true },
expiresAt: { type: Date, required: true },
verified: { type: Boolean, default: false },
registrationData: {
password: { type: String },
name: { type: String },
},
}, { timestamps: true });
// TTL index: automatically delete expired tokens
emailVerificationTokenSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });
Route Protection
Middleware
Protect routes with Next.js middleware:middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
const isAuthPage = nextUrl.pathname.startsWith("/login") ||
nextUrl.pathname.startsWith("/register");
const isProtectedRoute = nextUrl.pathname.startsWith("/dashboard") ||
nextUrl.pathname.startsWith("/income") ||
nextUrl.pathname.startsWith("/expenses") ||
nextUrl.pathname.startsWith("/subscriptions") ||
nextUrl.pathname.startsWith("/settings");
if (isProtectedRoute && !isLoggedIn) {
return NextResponse.redirect(new URL("/login", nextUrl));
}
if (isAuthPage && isLoggedIn) {
return NextResponse.redirect(new URL("/dashboard", nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
API Authentication
Request Authentication Helper
Authenticate API requests:lib/api/utils.ts
import { auth } from "@/auth";
import { NextRequest, NextResponse } from "next/server";
export async function authenticateRequest(request: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return {
auth: null,
error: NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
),
};
}
return {
auth: {
userId: session.user.id,
email: session.user.email,
name: session.user.name,
},
error: null,
};
}
Using in API Routes
app/api/income/route.ts
import { authenticateRequest } from "@/lib/api/utils";
export async function GET(request: NextRequest) {
const { auth, error: authError } = await authenticateRequest(request);
if (authError) return authError;
// Use auth.userId to fetch user-specific data
const incomes = await IncomeModel.find({ userId: auth.userId });
return NextResponse.json(incomes);
}
Session Management
Client-Side Session
Access session data in client components:import { useSession } from "next-auth/react";
export default function Component() {
const { data: session, status } = useSession();
if (status === "loading") {
return <div>Loading...</div>;
}
if (status === "unauthenticated") {
return <div>Please sign in</div>;
}
return <div>Welcome, {session.user.name}!</div>;
}
Server-Side Session
Access session in Server Components:import { auth } from "@/auth";
export default async function Page() {
const session = await auth();
if (!session) {
redirect("/login");
}
return <div>Hello, {session.user.name}</div>;
}
Sign Out
Implement sign out functionality:import { signOut } from "@/auth";
<button
onClick={async () => {
await signOut({ redirectTo: "/login" });
}}
>
Sign Out
</button>
Environment Variables
Required environment variables for authentication:.env.local
# MongoDB
MONGODB_URI=mongodb://localhost:27017/cashgap
# NextAuth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-secret-key-here
# Google OAuth (Optional)
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
Generate a secure
NEXTAUTH_SECRET using: openssl rand -base64 32Next Steps
Income Tracking
Implement income management features
Expense Management
Build expense tracking functionality