-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Implement authentication actions with signin, signup, and signout #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughAdds 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
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. Comment |
There was a problem hiding this 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
⛔ 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 buildsVerification 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.0Next.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' . || trueIf 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-4import { 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()
withoutawait
. Iron Session’s destroy can be async; pleaseawait 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.
const normalisedEmail = email.toLowerCase().trim(); | ||
|
There was a problem hiding this comment.
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).
There was a problem hiding this 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.
- 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 throughhandleServerError
.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 extraawait
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'
andpath: '/'
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 ofstring
.-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
) fromheaders()
and attach to ctx for tracing logs across actions.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 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 ondestroy/save
.
src/lib/session.ts
Outdated
// 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" | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
// 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.
There was a problem hiding this 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
: PreferfindUnique
with minimalselect
.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 keysTrim 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 → SigninInputRename 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 tosignin(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-lengthPrevents 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 messagesHard-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
📒 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 preservethis
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 limitAdd .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 aliasesRename 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'
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') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
Description:
This PR adds backend logic for user authentication, including:
These changes lay the foundation for secure authentication in the application.
Summary by CodeRabbit
New Features
Security
Validation
Chores