Skip to content

Conversation

kalidatony
Copy link
Collaborator

@kalidatony kalidatony commented Sep 22, 2025

Description:
This PR adds backend logic for user authentication, including:

  • Signup: User registration with password hashing and validation
  • Signin: User login with credential verification and session management
  • Signout: User session termination
  • Integration with Prisma for user data
  • Secure session handling using iron-session
  • Type safety improvements and alignment with Prisma schema

These changes lay the foundation for secure authentication in the application.

Summary by CodeRabbit

  • New Features

    • Added full user authentication: sign up, sign in, and sign out with session persistence.
  • Security

    • Enforced strong AUTH_SECRET requirements and production-aware cookie security.
  • Validation

    • Added input validation for signup/signin with clear email and password rules.
  • Chores

    • Added dependencies for sessions, safe actions, and schema validation; broadened Next.js version range.

Copy link

coderabbitai bot commented Sep 22, 2025

Walkthrough

Adds an action-based authentication stack: signup, signin, signout actions and logic with Zod validation, iron-session session management, a safe-action client with auth middleware, result utilities, environment/constants, and dependency updates in package.json.

Changes

Cohort / File(s) Change Summary
Dependencies
package.json
Added iron-session, next-safe-action, zod; changed next from 15.5.2 to ^15.5.2.
Sign-in
src/actions/auth/signin/*
New signinSchema (schema.ts), signinAction (action.ts), and signin logic (logic.ts): Zod validation, Prisma lookup, bcrypt verify, create/save session, Result return.
Sign-up
src/actions/auth/signup/*
New signupSchema (schema.ts), signupAction (action.ts), and Signup logic (logic.ts): Zod validation, uniqueness check, bcrypt hash, user creation, session init, Result return.
Sign-out
src/actions/auth/signout/*
New signoutAction (action.ts) and signout logic (logic.ts): get/destroy session and Result-based response.
Action client & middleware
src/lib/action.ts
Added defineMetadataSchema, actionClient (safe-action client), and authActionClient middleware enforcing authenticated context; removed previous addUser export/upsert flow.
Session & result utilities
src/lib/session.ts, src/lib/result.ts
Added getSession() and SessionData (iron-session setup using AUTH_SECRET and cookie options) and a generic Result<T> type with success/error factories.
Config & constants
src/lib/env.ts, src/lib/constants.ts
Added APP_ENV and validated AUTH_SECRET (env.ts); cookieName and IS_PRODUCTION constants (constants.ts).

Sequence Diagram(s)

sequenceDiagram
  rect rgb(245,250,255)
    participant Client
    participant ActionLayer as "Action (signin/signup/signout)"
    participant LogicLayer as "Logic (signin/Signup/signout)"
    participant DB as "Prisma DB"
    participant Session as "iron-session"
  end

  Client->>ActionLayer: invoke action with input
  ActionLayer->>LogicLayer: validate input (Zod) & call logic
  alt Signup / Signin
    LogicLayer->>DB: find or create user
    DB-->>LogicLayer: user record
    LogicLayer->>LogicLayer: hash/compare password (bcrypt)
    alt auth success
      LogicLayer->>Session: getSession(), set session.user.id, save
      Session-->>LogicLayer: session persisted
      LogicLayer-->>ActionLayer: Result.success(user sans password)
      ActionLayer-->>Client: success
    else auth failure
      LogicLayer-->>ActionLayer: Result.error(...)
      ActionLayer-->>Client: error
    end
  else Signout
    LogicLayer->>Session: getSession(), destroy
    Session-->>LogicLayer: destroyed
    LogicLayer-->>ActionLayer: Result.success(undefined)
    ActionLayer-->>Client: success
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • tasin2610

Poem

Hopping in with keys and cheer,
Schemas snug and sessions near,
Signup, signin, cookies set,
Errors handled, logs all met,
🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title succinctly and accurately summarizes the primary change—adding authentication actions for signin, signup, and signout—and aligns with the PR objectives and modified files, making it clear and appropriate for reviewers.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/auth-signin-signup-signout

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b4744b7 and ccb4db4.

📒 Files selected for processing (2)
  • src/actions/auth/signin/logic.ts (1 hunks)
  • src/actions/auth/signup/logic.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/actions/auth/signin/logic.ts
  • src/actions/auth/signup/logic.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 9

🧹 Nitpick comments (14)
package.json (1)

17-37: Add engines to prevent unsupported Node in prod/CI.

Lock Node range compatible with Next 15 and Prisma to avoid hard‑to‑debug failures.

Apply:

   "private": true,
   "type": "module",
+  "engines": {
+    "node": ">=18.18.0"
+  },

Adjust the floor if you officially require Node 20+.

src/lib/env.ts (1)

5-9: Trim and validate AUTH_SECRET more defensively.

Guard against whitespace and give actionable error text.

-const AUTH_SECRET = process.env.AUTH_SECRET as string;
+const AUTH_SECRET = (process.env.AUTH_SECRET ?? "").trim();
 
-if (!AUTH_SECRET || AUTH_SECRET.length < 32) {
-  throw new Error('AUTH_SECRET must be set and at least 32 characters long');
+if (AUTH_SECRET.length < 32) {
+  throw new Error('AUTH_SECRET must be set (>= 32 chars). Example: a 64+ char random string.');
 }
src/actions/auth/signup/schema.ts (1)

3-12: Normalize inputs at the schema boundary.

Trim/normalize now to keep logic slim and consistent, and cap lengths to sane limits.

-export const SignupSchema = z.object({
-    name: z.string().min(1, 'Name is required'),
-    email: z.string().email('Invalid email format'),
+export const SignupSchema = z.object({
+    name: z.string().trim().min(1, 'Name is required').max(100, 'Name is too long'),
+    email: z.string().trim().toLowerCase().email('Invalid email format').max(254, 'Email is too long'),
     password: z.string()
         .min(8, 'Password must be at least 8 characters long')
         .regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
         .regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
         .regex(/(?=.*\d)/, 'Password must contain at least one number')
 
 })
src/actions/auth/signin/schema.ts (1)

3-6: Trim email to reduce auth friction.

Avoids subtle login failures due to trailing spaces.

-export const SigninSchema = z.object({
-    email: z.string().email("Invalid Email Format"),
+export const SigninSchema = z.object({
+    email: z.string().trim().email("Invalid Email Format"),
     password: z.string().min(1,'Password is required')
 });
src/actions/auth/signup/action.ts (1)

10-30: Simplify error mapping and remove stale comment.

Throwing with custom cause then rethrowing adds noise without benefit. Return data or throw once with a user‑safe message.

   .action(async ({ parsedInput }) => {
-    try {
-      const result = await Signup(parsedInput);
-
-      if (result.success) {
-        return result.data;
-      }
-
-      // Set session
-      throw new Error(result.error, { cause: { internal: true } });
-    } catch (err) {
-      const error = err as Error;
-      const cause = error.cause as { internal: boolean } | undefined;
-
-      if (cause?.internal) {
-        throw new Error(error.message, { cause: error });
-      }
-
-      console.error('Sign up error:', error);
-      throw new Error('Something went wrong');
-    }
+    const result = await Signup(parsedInput);
+    if (result.success) return result.data;
+    // known domain error; surface a safe message
+    throw new Error(result.error || 'Something went wrong');
   });
src/lib/result.ts (1)

5-5: Consider lightweight error codes for UX without string parsing.

Optional: add a discriminant code to the error variant and helpers. Keeps client logic robust.

Example:

-export type Result<T> = { success: true; data: T } | { success: false; error: string };
+export type Result<T> =
+  | { success: true; data: T }
+  | { success: false; error: string; code?: string };

And optionally:

export const isSuccess = <T>(r: Result<T>): r is { success: true; data: T } => r.success;
export const isError = <T>(r: Result<T>): r is { success: false; error: string } => !r.success;
src/actions/auth/signup/logic.ts (1)

6-18: Avoid custom DTO; derive type from Prisma to prevent drift.

-import type { Role } from "@prisma/client";
+import type { Prisma } from "@prisma/client";
 
-interface User {
-    id: string;
-    email?: string | null;
-    name?: string | null;
-    role: Role;
-    createdAt: Date;
-    emailVerified?: Date | null;
-    subscriptionType: string;
-    lastLoginAt?: Date | null;
-    image?: string | null;
-}
+type User = Pick<
+  Prisma.User,
+  'id' | 'email' | 'name' | 'role' | 'createdAt' | 'emailVerified' | 'subscriptionType' | 'lastLoginAt' | 'image'
+>;
src/actions/auth/signout/action.ts (1)

6-27: Optional: sign out directly via ctx to avoid extra session fetch.

If you bind session methods (see src/lib/action.ts comment), you can inline:

-  const result = await Signout();
-  if (result.success) {
-    return result.data;
-  }
-  throw new Error(result.error, { cause: { internal: true } });
+  await ctx.session.destroy();
+  return undefined;
src/lib/session.ts (3)

2-6: Type the session helper precisely.

-import { getIronSession } from 'iron-session';
+import { getIronSession, type IronSession } from 'iron-session';
@@
-export async function getSession() {
+export async function getSession(): Promise<IronSession<SessionData>> {

Also applies to: 25-36


7-17: Stubbed verifyJWT: either implement or remove until needed.

I can wire real JWT verification (alg, issuer, audience) or strip this stub to avoid confusion.


25-33: Consider setting sameSite explicitly.

Add sameSite: 'lax' (explicitness aids audits), unless you intentionally need 'strict' or 'none' for cross-site flows.

src/actions/auth/signin/logic.ts (2)

63-69: Optionally update lastLoginAt after successful sign-in.

     const session = await getSession();
     session.user = { id: userWithoutPassword.id };
     await session.save();
 
+    // Best-effort; ignore failure
+    try {
+      await prisma.user.update({
+        where: { id: userWithoutPassword.id },
+        data: { lastLoginAt: new Date() }
+      });
+    } catch (e) {
+      console.error('Signin warning: failed to update lastLoginAt', e);
+    }

8-18: Prefer Prisma-derived type for return payload.

-import type { Role } from "@prisma/client";
+import type { Prisma } from "@prisma/client";
 
-type UserWithoutPassword = {
-    id: string;
-    email?: string | null;
-    name?: string | null;
-    role: Role;
-    createdAt: Date;
-    emailVerified?: Date | null;
-    subscriptionType: string;
-    lastLoginAt?: Date | null;
-    image?: string | null;
-}
+type UserWithoutPassword = Pick<
+  Prisma.User,
+  'id' | 'email' | 'name' | 'role' | 'createdAt' | 'emailVerified' | 'subscriptionType' | 'lastLoginAt' | 'image'
+>;
src/lib/action.ts (1)

39-44: Use findUnique for primary-key lookup.

-  const user = await prisma.user.findFirst({
-    where: {
-      id: userId
-    }
-  });
+  const user = await prisma.user.findUnique({ where: { id: userId } });
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 41f9e39 and 26474fa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (14)
  • package.json (1 hunks)
  • src/actions/auth/signin/action.ts (1 hunks)
  • src/actions/auth/signin/logic.ts (1 hunks)
  • src/actions/auth/signin/schema.ts (1 hunks)
  • src/actions/auth/signout/action.ts (1 hunks)
  • src/actions/auth/signout/logic.ts (1 hunks)
  • src/actions/auth/signup/action.ts (1 hunks)
  • src/actions/auth/signup/logic.ts (1 hunks)
  • src/actions/auth/signup/schema.ts (1 hunks)
  • src/lib/action.ts (1 hunks)
  • src/lib/constants.ts (1 hunks)
  • src/lib/env.ts (1 hunks)
  • src/lib/result.ts (1 hunks)
  • src/lib/session.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
src/actions/auth/signin/schema.ts (3)
src/components/LoginForm.tsx (1)
  • LoginForm (18-111)
src/lib/auth.ts (1)
  • authenticateUser (40-60)
src/app/api/auth/register/route.ts (1)
  • POST (6-64)
src/actions/auth/signup/schema.ts (2)
src/components/RegisterForm.tsx (3)
  • RegisterForm (17-187)
  • e (25-74)
  • e (142-142)
src/app/api/auth/register/route.ts (1)
  • POST (6-64)
src/actions/auth/signup/logic.ts (4)
src/actions/auth/signup/schema.ts (1)
  • SignupInput (14-14)
src/lib/result.ts (3)
  • Result (5-5)
  • error (20-25)
  • success (10-15)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/lib/session.ts (1)
  • getSession (25-36)
src/actions/auth/signout/action.ts (3)
src/lib/action.ts (1)
  • authActionClient (31-62)
src/actions/auth/signout/logic.ts (1)
  • Signout (6-10)
src/lib/result.ts (1)
  • error (20-25)
src/lib/constants.ts (1)
src/lib/env.ts (1)
  • APP_ENV (11-11)
src/actions/auth/signup/action.ts (4)
src/lib/action.ts (1)
  • actionClient (13-29)
src/actions/auth/signup/schema.ts (1)
  • SignupSchema (3-12)
src/actions/auth/signup/logic.ts (1)
  • Signup (20-59)
src/lib/result.ts (1)
  • error (20-25)
src/lib/session.ts (2)
src/lib/env.ts (1)
  • AUTH_SECRET (11-11)
src/lib/constants.ts (2)
  • cookieName (3-3)
  • IS_PRODUCTION (4-4)
src/lib/action.ts (2)
src/lib/session.ts (1)
  • getSession (25-36)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/actions/auth/signin/logic.ts (4)
src/actions/auth/signin/schema.ts (1)
  • SigninInput (8-8)
src/lib/result.ts (3)
  • Result (5-5)
  • error (20-25)
  • success (10-15)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/lib/session.ts (1)
  • getSession (25-36)
src/actions/auth/signout/logic.ts (2)
src/lib/result.ts (2)
  • Result (5-5)
  • success (10-15)
src/lib/session.ts (1)
  • getSession (25-36)
src/actions/auth/signin/action.ts (4)
src/lib/action.ts (1)
  • actionClient (13-29)
src/actions/auth/signin/schema.ts (1)
  • SigninSchema (3-6)
src/actions/auth/signin/logic.ts (1)
  • Signin (20-70)
src/lib/result.ts (1)
  • error (20-25)
🔇 Additional comments (7)
package.json (3)

8-8: Confirm Turbopack for production builds

Verification aborted: local run failed with "sh: 1: prisma: not found" — cannot compare Turbopack vs default Webpack.

File: package.json (lines 8)

"build": "prisma generate && next build --turbopack",

Re-run comparison in an environment with project deps installed and local Prisma available. Suggested commands (run from repo root and attach full stdout/stderr + timing):

npm ci
# Turbopack
npx prisma generate && time npx next build --turbopack
# Webpack
npx prisma generate && time npx next build

28-31: OK to merge — next@15.5.2 is compatible with React 19.1.0

Next.js v15 requires React/React‑DOM 19; package.json lists react and react-dom 19.1.0, which satisfy Next's peerDependencies.


22-22: Align bcrypt imports with installed dependency (bcryptjs)

package.json installs bcryptjs; importing "bcrypt" will fail at runtime. Automated search returned no files — run the check below and update any matches.

Run:

rg -n -S --hidden --glob '!node_modules/**' '\bbcrypt(js)?\b' || \
grep -RIn --exclude-dir=node_modules -E '\bbcrypt(js)?\b' . || true

If any imports/requires use "bcrypt", change to:

-import bcrypt from "bcrypt";
+import bcrypt from "bcryptjs";

or

-const bcrypt = require("bcrypt");
+const bcrypt = require("bcryptjs");
src/lib/constants.ts (1)

1-4: Avoid server-only coupling for widely used constants.

File: src/lib/constants.ts
Lines: 1-4

import { APP_ENV } from "./env";

export const cookieName = "authormaton-session";
export const IS_PRODUCTION = APP_ENV === "production";

Search inconclusive: ripgrep returned "unrecognized file type: tsx" and NO_MATCHES when scanning for imports of "@/lib/constants"; manual verification required to confirm whether any client components import this module.

Either compute IS_PRODUCTION from process.env.NODE_ENV (e.g. export const IS_PRODUCTION = process.env.NODE_ENV === "production";), or keep this file server-only and ensure it's never imported client-side.

src/actions/auth/signin/action.ts (1)

7-21: Pattern is sound; propagates user-facing errors only.

Confirm next-safe-action surfaces only messages you explicitly throw here (e.g., “Invalid credentials”) and not raw server errors, given the handleServerError change suggested in src/lib/action.ts.

src/actions/auth/signout/action.ts (1)

9-12: Ensure session destruction is awaited inside Signout().

The called logic currently uses session.destroy() without await. Iron Session’s destroy can be async; please await session.destroy() to guarantee cookie invalidation before responding.

src/lib/session.ts (1)

25-33: Verify AUTH_SECRET is set in CI/CD and local env and is at least 32 characters
src/lib/env.ts (lines 5–8) enforces presence and a 32-char minimum; src/lib/session.ts (lines 25–28) uses AUTH_SECRET as the iron-session password — rotating it will invalidate existing sessions. Confirm your CI/CD secret store and local .env.example/README include a secure >=32-char secret and do not commit real secrets.

Comment on lines +23 to +24
const normalisedEmail = email.toLowerCase().trim();

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

🧩 Analysis chain

Email normalization is good. Ensure a case-insensitive unique index on email.

Run to verify Prisma schema:


🏁 Script executed:

#!/bin/bash
# Expect: `@unique` (or a unique index) on User.email and a lowercase normalization in app code.
fd -HI --max-depth 3 'schema.prisma' \
  | xargs -I{} rg -nPA3 'model\s+User\b|@unique|@@unique|email' {}

Length of output: 401


Enforce DB-level case-insensitive uniqueness for User.email

  • Signup code already normalizes email: src/actions/auth/signup/logic.ts (lines 23–24).
  • schema.prisma shows model User with email String? @unique (schema.prisma). @unique exists but the column is nullable and uniqueness behavior depends on DB collation — this does not guarantee case-insensitive uniqueness.
  • Action required: make email non-nullable if uniqueness must apply to all users, and enforce case-insensitive uniqueness at the DB level (e.g., PostgreSQL CITEXT column or a unique index on lower(email) applied via migration/raw SQL).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (8)
src/actions/auth/signin/action.ts (2)

11-11: Remove unused variable to avoid accidental PII capture.

email is destructured but never used.

-    const { email } = parsedInput;

16-21: Decide UX: propagate sanitized domain errors vs. generic.

Currently all non-internal failures surface as “Something went wrong,” hiding safe domain errors like “Invalid credentials.” If product wants to show a safe message, return/propagate the sanitized domain error instead of throwing a generic one.

Option A (keep return type = data): map known domain failures to a typed result or discriminated union exposed by the action.
Option B (throw but allow safe messages): tag safe errors and pass them through handleServerError.

Example for Option B:

-      throw new Error(result.error, { cause: { internal: true } });
+      throw new Error(result.error, { cause: { safe: true } });
...
-      if (cause?.internal) {
-        throw new Error(error.message, { cause: error });
-      }
+      if ((cause as any)?.safe) {
+        // Let handleServerError return this safe message
+        throw error;
+      }

If you prefer the current fully-generic UX for anti-enumeration, resolve as Won’t Fix.

Also applies to: 21-31

src/lib/session.ts (2)

31-31: Drop unnecessary await on cookies().

cookies() is sync; the extra await is misleading.

-  const session = await getIronSession<SessionData>(await cookies(), {
+  const session = await getIronSession<SessionData>(cookies(), {

34-37: Harden cookie settings (sameSite + path).

Add sameSite: 'lax' and path: '/' to reduce CSRF risk and ensure global cookie scope.

       cookieOptions: {
         secure: IS_PRODUCTION,
-        httpOnly: true
+        httpOnly: true,
+        sameSite: 'lax',
+        path: '/'
       }
src/actions/auth/signup/logic.ts (2)

25-33: Fix typos and normalize logging.

Spelling (“exisitingUser”, “Singup”) and punctuation; keep logs consistent and sanitized.

-    const exisitingUser = await prisma.user.findUnique({
+    const existingUser = await prisma.user.findUnique({
         where: {
             email: normalisedEmail
         }
     });
-    if (exisitingUser) {
-        console.error('Singup error: User with this email already exists')
-        return error('Something went wrong')
+    if (existingUser) {
+        console.error('Signup error: User with this email already exists');
+        return error('Something went wrong');
     };

6-6: Type subscriptionType precisely (if enum).

If Prisma defines SubscriptionType, use it instead of string.

-import type { Role } from "@prisma/client";
+import type { Role, SubscriptionType } from "@prisma/client";
...
-    subscriptionType: string;
+    subscriptionType: SubscriptionType;

If it’s actually a string column, ignore.

Also applies to: 15-15

src/lib/action.ts (2)

42-46: Use findUnique with a narrow select for existence check.

Slightly clearer and cheaper than findFirst.

-  const user = await prisma.user.findFirst({
-    where: {
-      id: userId
-    }
-  });
+  const user = await prisma.user.findUnique({
+    where: { id: userId },
+    select: { id: true }
+  });

25-32: Optional: add request correlation id to ctx.

Consider extracting a request id (e.g., x-request-id) from headers() and attach to ctx for tracing logs across actions.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26474fa and c19521a.

📒 Files selected for processing (6)
  • src/actions/auth/signin/action.ts (1 hunks)
  • src/actions/auth/signout/logic.ts (1 hunks)
  • src/actions/auth/signup/logic.ts (1 hunks)
  • src/lib/action.ts (1 hunks)
  • src/lib/env.ts (1 hunks)
  • src/lib/session.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/env.ts
  • src/actions/auth/signout/logic.ts
🧰 Additional context used
🧬 Code graph analysis (4)
src/actions/auth/signup/logic.ts (4)
src/actions/auth/signup/schema.ts (1)
  • SignupInput (14-14)
src/lib/result.ts (3)
  • Result (5-5)
  • error (20-25)
  • success (10-15)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/lib/session.ts (1)
  • getSession (25-41)
src/lib/action.ts (2)
src/lib/session.ts (1)
  • getSession (25-41)
src/lib/prisma.ts (1)
  • prisma (13-14)
src/lib/session.ts (2)
src/lib/env.ts (1)
  • AUTH_SECRET (12-12)
src/lib/constants.ts (2)
  • cookieName (3-3)
  • IS_PRODUCTION (4-4)
src/actions/auth/signin/action.ts (4)
src/lib/action.ts (1)
  • actionClient (13-32)
src/actions/auth/signin/schema.ts (1)
  • SigninSchema (3-6)
src/actions/auth/signin/logic.ts (1)
  • Signin (20-70)
src/lib/result.ts (1)
  • error (20-25)
🔇 Additional comments (5)
src/actions/auth/signin/action.ts (1)

29-30: LGTM: PII removed from logs.

You removed the raw email from the error log. This addresses the earlier concern.

src/actions/auth/signup/logic.ts (2)

23-24: DB-level case-insensitive uniqueness for email.

App normalizes email, but ensure the DB enforces case-insensitive uniqueness (e.g., Postgres CITEXT or @@unique([lower(email)])).

#!/bin/bash
# Verify email field and unique index in Prisma schema
fd -HI 'schema.prisma' | xargs -I{} rg -nPA3 '(?s)model\s+User\b.*?email\b|@@unique|@unique|citext' {}

35-53: Handle unique-email race (P2002) and sanitize errors.

Pre-check isn’t safe under concurrency; prisma.user.create can still throw on unique violation.

-    const hashedPassword = await bcrypt.hash(password, 12);
-    const user = await prisma.user.create({
-        data: {
-            name,
-            email: normalisedEmail,
-            password: hashedPassword,
-        },
-        select: {
-            id: true,
-            email: true,
-            name: true,
-            role: true,
-            createdAt: true,
-            emailVerified: true,
-            subscriptionType: true,
-            lastLoginAt: true,
-            image: true,
-        }
-    });
+    const hashedPassword = await bcrypt.hash(password, 12);
+    let user;
+    try {
+      user = await prisma.user.create({
+        data: {
+          name,
+          email: normalisedEmail,
+          password: hashedPassword
+        },
+        select: {
+          id: true,
+          email: true,
+          name: true,
+          role: true,
+          createdAt: true,
+          emailVerified: true,
+          subscriptionType: true,
+          lastLoginAt: true,
+          image: true
+        }
+      });
+    } catch (e: any) {
+      if (e?.code === 'P2002' && e?.meta?.target?.includes?.('email')) {
+        console.error('Signup error: Email already in use');
+        return error('Something went wrong');
+      }
+      console.error('Signup error:', e);
+      return error('Something went wrong');
+    }
src/lib/action.ts (2)

15-18: LGTM: server errors are sanitized and logged.

This prevents leaking internals to clients while preserving server logs.


57-58: LGTM: bound session methods preserve context.

Binding avoids this loss on destroy/save.

Comment on lines 7 to 17
// Mock verifyJWT implementation for middleware usage
export async function verifyJWT(token: string): Promise<{ userId: string; email: string; role: string } | null> {
// TODO: Replace with real JWT verification logic
if (!token || token === "invalid") return null;
// Example payload
return {
userId: "example-id",
email: "user@example.com",
role: "user"
};
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Prevent mock JWT helper from being usable in production.

This returns a fixed payload; if invoked in prod it’s a security hole. Guard it.

 export async function verifyJWT(token: string): Promise<{ userId: string; email: string; role: string } | null> {
-  // TODO: Replace with real JWT verification logic
-  if (!token || token === "invalid") return null;
-  // Example payload
-  return {
-    userId: "example-id",
-    email: "user@example.com",
-    role: "user"
-  };
+  // TODO: Replace with real JWT verification logic
+  if (IS_PRODUCTION) {
+    throw new Error('verifyJWT is not implemented for production');
+  }
+  if (!token || token === "invalid") return null;
+  return { userId: "example-id", email: "user@example.com", role: "user" };
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Mock verifyJWT implementation for middleware usage
export async function verifyJWT(token: string): Promise<{ userId: string; email: string; role: string } | null> {
// TODO: Replace with real JWT verification logic
if (!token || token === "invalid") return null;
// Example payload
return {
userId: "example-id",
email: "user@example.com",
role: "user"
};
}
// Mock verifyJWT implementation for middleware usage
export async function verifyJWT(token: string): Promise<{ userId: string; email: string; role: string } | null> {
// TODO: Replace with real JWT verification logic
if (IS_PRODUCTION) {
throw new Error('verifyJWT is not implemented for production');
}
if (!token || token === "invalid") return null;
return { userId: "example-id", email: "user@example.com", role: "user" };
}
🤖 Prompt for AI Agents
In src/lib/session.ts around lines 7 to 17, the mock verifyJWT returns a fixed
payload which is unsafe for production; update it so the mock is only usable in
non-production: check process.env.NODE_ENV (or a dedicated FEATURE_FLAG like
ALLOW_MOCK_JWT) and if running in "production" (or flag is unset/false) throw an
explicit error or return null and log, otherwise keep the existing mock
behavior; add a clear error message instructing to replace with real
verification and ensure the function does not silently succeed in production.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (9)
src/lib/action.ts (4)

7-11: Schema OK; optionally tighten to known actions.

If the set of actions is stable, consider an enum to catch typos at compile-time. Otherwise, keep as-is.

Apply only if the action set is fixed:

-  return zod.object({
-    actionName: zod.string()
-  });
+  return zod.object({
+    actionName: zod.enum(['signin', 'signup', 'signout'])
+  });

31-37: Standardize Unauthorized handling/message.

Minor: use a consistent, terse message and log without constructing unused Error objects.

Apply:

-  if (!userId) {
-    console.error('Malformed cookie', new Error('Invalid user, not allowed'));
-    throw new Error('Not Authorised');
-  }
+  if (!userId) {
+    console.error('Unauthorized: missing user in session');
+    throw new Error('Unauthorized');
+  }

39-43: Prefer findUnique with minimal select.

Lookup by primary key should use findUnique; select only what you need.

Apply:

-  const user = await prisma.user.findFirst({
-    where: {
-      id: userId
-    }
-  });
+  const user = await prisma.user.findUnique({
+    where: { id: userId },
+    select: { id: true }
+  });

45-48: Minor: unify Unauthorized path.

Match the message from the missing-user branch; keeps logs/search consistent.

Apply:

-  if (!user) {
-    console.error('Not Authorised', new Error('Invalid user, not allowed'));
-    throw new Error('Not Authorised');
-  }
+  if (!user) {
+    console.error('Unauthorized: user id not found');
+    throw new Error('Unauthorized');
+  }
src/actions/auth/signin/schema.ts (2)

3-6: Harden and normalize inputs; reject unknown keys

Trim inputs, canonicalize email to lowercase for consistent lookups, ensure whitespace-only passwords fail, and disallow extraneous payload keys. Also align error message casing.

-export const signinSchema = z.object({
-    email: z.string().email("Invalid Email Format"),
-    password: z.string().min(1,'Password is required')
-});
+export const signinSchema = z.object({
+  email: z
+    .string({ required_error: 'Email is required' })
+    .trim()
+    .email('Invalid email format')
+    .transform((v) => v.toLowerCase()),
+  password: z
+    .string({ required_error: 'Password is required' })
+    .trim()
+    .min(1, 'Password is required'),
+}).strict();

8-8: Use PascalCase for the type alias — rename signinInput → SigninInput

Rename the exported type and update all imports/usages.

  • src/actions/auth/signin/schema.ts:
-export type signinInput = z.infer<typeof signinSchema>
+export type SigninInput = z.infer<typeof signinSchema>
  • src/actions/auth/signin/logic.ts: update import to import { SigninInput } from "./schema"; and change signature to signin(input: SigninInput).

Verified: signup and signin both normalize emails (email.toLowerCase().trim()) and lib/validation.ts applies the same zod transform; Prisma schema shows email String? @unique.

src/actions/auth/signup/schema.ts (3)

3-12: Reject unknown fields with .strict()

Prevents silent acceptance of extra properties from clients.

 export const signupSchema = z.object({
   name: z.string().min(1, 'Name is required'),
   email: z.string().email('Invalid email format'),
   password: z.string()
       .min(8, 'Password must be at least 8 characters long')
       .regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
       .regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
       .regex(/(?=.*\d)/, 'Password must contain at least one number')
 
-})
+}).strict()

5-5: Normalize email and enforce RFC-length

Prevents duplicate accounts differing by case/whitespace; caps to 254 chars.

-    email: z.string().email('Invalid email format'),
+    email: z.string().trim().toLowerCase().max(254, 'Email is too long').email('Invalid email format'),

Please confirm your signup logic and DB uniqueness use the normalized, lowercased email for lookups and uniqueness. If DB uniqueness must be case-insensitive, ensure consistent normalization on write/read.


3-12: Localize validation messages

Hard-coded English strings can block i18n. Consider centralizing messages or using a t() helper.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c19521a and b4744b7.

📒 Files selected for processing (9)
  • src/actions/auth/signin/action.ts (1 hunks)
  • src/actions/auth/signin/logic.ts (1 hunks)
  • src/actions/auth/signin/schema.ts (1 hunks)
  • src/actions/auth/signout/action.ts (1 hunks)
  • src/actions/auth/signout/logic.ts (1 hunks)
  • src/actions/auth/signup/logic.ts (1 hunks)
  • src/actions/auth/signup/schema.ts (1 hunks)
  • src/lib/action.ts (1 hunks)
  • src/lib/session.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/lib/session.ts
  • src/actions/auth/signup/logic.ts
  • src/actions/auth/signin/action.ts
  • src/actions/auth/signout/logic.ts
  • src/actions/auth/signin/logic.ts
  • src/actions/auth/signout/action.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/lib/action.ts (2)
src/lib/session.ts (1)
  • getSession (13-29)
src/lib/prisma.ts (1)
  • prisma (13-14)
🔇 Additional comments (5)
src/lib/action.ts (3)

22-29: Middleware shape LGTM.

Capturing session and headers into ctx is clean and keeps action code minimal.


13-17: Do not leak server error messages to clients. Sanitize and log.

Returning err.message exposes internals (e.g., DB or config errors) to users. Return a generic string and log the full error server-side.

Apply:

 export const actionClient = createSafeActionClient({
   defineMetadataSchema,
-  handleServerError: (err: Error) => err.message,
+  handleServerError: (err: Error) => {
+    console.error('Action server error:', err);
+    return 'Something went wrong';
+  },
   defaultValidationErrorsShape: 'flattened'
 })

50-59: Bind session methods to preserve this context.

destroy/save may rely on the session instance; copying unbound functions risks runtime errors.

Apply:

   return next({
     ctx: {
       ...ctx,
       session: {
-        destroy: ctx.session.destroy,
-        save: ctx.session.save,
+        destroy: ctx.session.destroy.bind(ctx.session),
+        save: ctx.session.save.bind(ctx.session),
         user: {
           id: userId
         }
       }
     }
   });

Run to confirm no other session fields are expected downstream:

#!/bin/bash
# Finds any ctx.session property usages other than user/destroy/save
rg -nP --type=ts -C2 '\bctx\.session\.(?!(user|destroy|save)\b)[a-zA-Z_]\w*'
src/actions/auth/signup/schema.ts (2)

4-4: Trim and cap name length — confirm DB column limit

Add .trim() and a sensible .max() to prevent space-only or overly long names. Prisma schema shows model User: name String? with no @db.VarChar length — confirm the DB column max and align the validation or add @db.VarChar in schema.prisma.

-    name: z.string().min(1, 'Name is required'),
+    name: z.string().trim().min(1, 'Name is required').max(100, 'Name must be at most 100 characters'),

14-14: Use PascalCase for exported type aliases

Rename exported type alias to SignupInput and update all references.
Location: src/actions/auth/signup/schema.ts:14

-export type signupInput = z.infer<typeof signupSchema>
+export type SignupInput = z.infer<typeof signupSchema>

Run a repo-wide search/rename and confirm references: rg -n 'signupInput' -S || git grep -n 'signupInput'

Comment on lines +6 to +10
password: z.string()
.min(8, 'Password must be at least 8 characters long')
.regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
.regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
.regex(/(?=.*\d)/, 'Password must contain at least one number')
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add a maximum password length to mitigate hashing DoS

Very long passwords can amplify Argon2/bcrypt cost. Cap to a sane upper bound (e.g., 128).

     password: z.string()
-        .min(8, 'Password must be at least 8 characters long')
+        .min(8, 'Password must be at least 8 characters long')
+        .max(128, 'Password must be at most 128 characters long')
         .regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
         .regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
         .regex(/(?=.*\d)/, 'Password must contain at least one number')

Optional: consider allowing long passphrases (e.g., ≥16 chars) as an alternative to composition rules.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
password: z.string()
.min(8, 'Password must be at least 8 characters long')
.regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
.regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
.regex(/(?=.*\d)/, 'Password must contain at least one number')
password: z.string()
.min(8, 'Password must be at least 8 characters long')
.max(128, 'Password must be at most 128 characters long')
.regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
.regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
.regex(/(?=.*\d)/, 'Password must contain at least one number')
🤖 Prompt for AI Agents
In src/actions/auth/signup/schema.ts lines 6 to 10, the password Zod schema
lacks an upper length bound which can enable hashing DoS; add a maximum length
check (e.g., .max(128, 'Password must be at most 128 characters long')) to cap
input size before hashing. Update the schema to enforce min and max together,
and if you want to permit long passphrases, implement a separate acceptance path
or rule (for example allow >=16 chars without strict composition) rather than
removing the max limit.

@tasin2610 tasin2610 merged commit aefdaff into main Sep 22, 2025
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants