From 26474fac2ae37b81e698c53d0b732308010e796e Mon Sep 17 00:00:00 2001 From: Kalida Tony Date: Mon, 22 Sep 2025 05:44:20 -0600 Subject: [PATCH 1/4] feat: Implement authentication actions with signin, signup, and signout logic --- package-lock.json | 71 ++++++++++++++++-- package.json | 7 +- src/actions/auth/signin/action.ts | 32 +++++++++ src/actions/auth/signin/logic.ts | 70 ++++++++++++++++++ src/actions/auth/signin/schema.ts | 8 +++ src/actions/auth/signout/action.ts | 28 ++++++++ src/actions/auth/signout/logic.ts | 10 +++ src/actions/auth/signup/action.ts | 31 ++++++++ src/actions/auth/signup/logic.ts | 59 +++++++++++++++ src/actions/auth/signup/schema.ts | 14 ++++ src/lib/action.ts | 112 ++++++++++++++--------------- src/lib/constants.ts | 4 ++ src/lib/env.ts | 11 +++ src/lib/result.ts | 25 +++++++ src/lib/session.ts | 36 ++++++++++ 15 files changed, 453 insertions(+), 65 deletions(-) create mode 100644 src/actions/auth/signin/action.ts create mode 100644 src/actions/auth/signin/logic.ts create mode 100644 src/actions/auth/signin/schema.ts create mode 100644 src/actions/auth/signout/action.ts create mode 100644 src/actions/auth/signout/logic.ts create mode 100644 src/actions/auth/signup/action.ts create mode 100644 src/actions/auth/signup/logic.ts create mode 100644 src/actions/auth/signup/schema.ts create mode 100644 src/lib/constants.ts create mode 100644 src/lib/env.ts create mode 100644 src/lib/result.ts create mode 100644 src/lib/session.ts diff --git a/package-lock.json b/package-lock.json index e84e3c6..8411e4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,15 +16,18 @@ "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "iron-session": "^8.0.4", "lucide-react": "^0.542.0", "luxon": "^3.7.2", - "next": "15.5.2", + "next": "^15.5.2", + "next-safe-action": "^8.0.11", "react": "19.1.0", "react-dom": "19.1.0", "react-icons": "^5.5.0", "tailwind-merge": "^3.3.1", "viem": "^2.37.5", - "wagmi": "^2.16.9" + "wagmi": "^2.16.9", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -2733,6 +2736,15 @@ "zod": "3.22.4" } }, + "node_modules/@reown/appkit-wallet/node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@reown/appkit/node_modules/@noble/ciphers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.2.1.tgz", @@ -5106,6 +5118,15 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-es": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", @@ -7034,6 +7055,21 @@ "node": ">= 0.4" } }, + "node_modules/iron-session": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/iron-session/-/iron-session-8.0.4.tgz", + "integrity": "sha512-9ivNnaKOd08osD0lJ3i6If23GFS2LsxyMU8Gf/uBUEgm8/8CC1hrrCHFDpMo3IFbpBgwoo/eairRsaD3c5itxA==", + "funding": [ + "https://github.com/sponsors/vvo", + "https://github.com/sponsors/brc-dd" + ], + "license": "MIT", + "dependencies": { + "cookie": "^0.7.2", + "iron-webcrypto": "^1.2.1", + "uncrypto": "^0.1.3" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -7991,6 +8027,7 @@ "version": "15.5.2", "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "license": "MIT", "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", @@ -8038,6 +8075,30 @@ } } }, + "node_modules/next-safe-action": { + "version": "8.0.11", + "resolved": "https://registry.npmjs.org/next-safe-action/-/next-safe-action-8.0.11.tgz", + "integrity": "sha512-gqJLmnQLAoFCq1kRBopN46New+vx1n9J9Y/qDQLXpv/VqU40AWxDakvshwwnWAt8R0kLvlakNYNLX5PqlXWSMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/TheEdoRan" + }, + { + "type": "paypal", + "url": "https://www.paypal.com/donate/?hosted_button_id=ES9JRPSC66XKW" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.17" + }, + "peerDependencies": { + "next": ">= 14.0.0", + "react": ">= 18.2.0", + "react-dom": ">= 18.2.0" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -10815,9 +10876,9 @@ } }, "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 0629f0d..b78e3e5 100644 --- a/package.json +++ b/package.json @@ -22,15 +22,18 @@ "bcryptjs": "^3.0.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "iron-session": "^8.0.4", "lucide-react": "^0.542.0", "luxon": "^3.7.2", - "next": "15.5.2", + "next": "^15.5.2", + "next-safe-action": "^8.0.11", "react": "19.1.0", "react-dom": "19.1.0", "react-icons": "^5.5.0", "tailwind-merge": "^3.3.1", "viem": "^2.37.5", - "wagmi": "^2.16.9" + "wagmi": "^2.16.9", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/actions/auth/signin/action.ts b/src/actions/auth/signin/action.ts new file mode 100644 index 0000000..9cca7da --- /dev/null +++ b/src/actions/auth/signin/action.ts @@ -0,0 +1,32 @@ +'use server'; + +import { actionClient } from '@/lib/action'; +import { Signin} from './logic'; +import { SigninSchema} from './schema'; + +export const signinAction = actionClient + .inputSchema(SigninSchema) + .metadata({ actionName: 'signin' }) + .action(async ({ parsedInput }) => { + const { email } = parsedInput; + + try { + const result = await Signin(parsedInput); + + if (result.success) { + return result.data; + } + + 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 in error:', error, { email }); + throw new Error('Something went wrong'); + } + }); diff --git a/src/actions/auth/signin/logic.ts b/src/actions/auth/signin/logic.ts new file mode 100644 index 0000000..b2008b4 --- /dev/null +++ b/src/actions/auth/signin/logic.ts @@ -0,0 +1,70 @@ +import { prisma } from "@/lib/prisma"; +import { SigninInput } from "./schema"; +import bcrypt from "bcryptjs"; +import { error, Result, success } from "@/lib/result"; +import { getSession } from "@/lib/session"; +import type { Role } 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; +} + +export async function Signin(input: SigninInput): Promise> { + const { email, password } = input; + + const normalisedEmail = email.toLowerCase().trim(); + + //Find user by email + const user = await prisma.user.findUnique({ + where: { + email: normalisedEmail + }, + select: { + id: true, + email: true, + name: true, + password: true, + role: true, + createdAt: true, + isActive: true, + emailVerified: true, + subscriptionType: true, + lastLoginAt: true, + image: true, + } + }) + if (!user) { + console.error('Signin error: User not found'); + return error('Invalid credentials'); + } + + //Verify Password + if (!user.password) { + console.error('Signin error: No password set for user'); + return error('Invalid credentials'); + } + const isValidPassword = await bcrypt.compare(password, user.password); + if (!isValidPassword) { + console.error('Signin error: Invalid Password'); + return error('Invalid credentials'); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { password: _password, ...userWithoutPassword } = user; + + //Session + + const session = await getSession(); + session.user = { id: userWithoutPassword.id }; + await session.save(); + + return success(userWithoutPassword); +} \ No newline at end of file diff --git a/src/actions/auth/signin/schema.ts b/src/actions/auth/signin/schema.ts new file mode 100644 index 0000000..d44803d --- /dev/null +++ b/src/actions/auth/signin/schema.ts @@ -0,0 +1,8 @@ +import {z} from 'zod'; + +export const SigninSchema = z.object({ + email: z.string().email("Invalid Email Format"), + password: z.string().min(1,'Password is required') +}); + +export type SigninInput = z.infer \ No newline at end of file diff --git a/src/actions/auth/signout/action.ts b/src/actions/auth/signout/action.ts new file mode 100644 index 0000000..201c29c --- /dev/null +++ b/src/actions/auth/signout/action.ts @@ -0,0 +1,28 @@ +'use server'; + +import { authActionClient } from '@/lib/action'; +import { Signout} from './logic'; + +export const signoutAction = authActionClient.metadata({ actionName: 'signout' }).action(async ({ ctx }) => { + const userId = ctx.session.user.id; + + try { + const result = await Signout(); + + if (result.success) { + return result.data; + } + + 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 out error:', error, { userId }); + throw new Error('Something went wrong'); + } +}); diff --git a/src/actions/auth/signout/logic.ts b/src/actions/auth/signout/logic.ts new file mode 100644 index 0000000..8da6295 --- /dev/null +++ b/src/actions/auth/signout/logic.ts @@ -0,0 +1,10 @@ +import 'server-only'; + +import { Result, success } from '@/lib/result'; +import { getSession } from '@/lib/session'; + +export async function Signout(): Promise> { + const session = await getSession(); + session.destroy(); + return success(undefined); +} diff --git a/src/actions/auth/signup/action.ts b/src/actions/auth/signup/action.ts new file mode 100644 index 0000000..9edaffc --- /dev/null +++ b/src/actions/auth/signup/action.ts @@ -0,0 +1,31 @@ +'use server'; + +import { actionClient } from '@/lib/action'; +import { Signup} from './logic'; +import { SignupSchema} from './schema'; + +export const signupAction = actionClient + .inputSchema(SignupSchema) + .metadata({ actionName: 'signup' }) + .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'); + } + }); diff --git a/src/actions/auth/signup/logic.ts b/src/actions/auth/signup/logic.ts new file mode 100644 index 0000000..37daf4b --- /dev/null +++ b/src/actions/auth/signup/logic.ts @@ -0,0 +1,59 @@ +import { prisma } from "@/lib/prisma"; +import bcrypt from "bcryptjs"; +import { SignupInput } from "./schema"; +import { error, Result, success } from "@/lib/result"; +import { getSession } from "@/lib/session"; +import type { Role } 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; +} + +export async function Signup(input: SignupInput): Promise> { + const { email, name, password } = input; + + const normalisedEmail = email.toLowerCase().trim(); + + const exisitngUser = await prisma.user.findUnique({ + where: { + email: normalisedEmail + } + }); + if (exisitngUser) { + console.error('Singup error: User with this email already exists') + return error('Something went wrong') + }; + + 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 session = await getSession(); + session.user = { id: user.id }; + await session.save(); + + return success(user); +} \ No newline at end of file diff --git a/src/actions/auth/signup/schema.ts b/src/actions/auth/signup/schema.ts new file mode 100644 index 0000000..35c4e8a --- /dev/null +++ b/src/actions/auth/signup/schema.ts @@ -0,0 +1,14 @@ +import { z } from "zod"; + +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') + +}) + +export type SignupInput = z.infer \ No newline at end of file diff --git a/src/lib/action.ts b/src/lib/action.ts index 6266a12..9ba1c57 100644 --- a/src/lib/action.ts +++ b/src/lib/action.ts @@ -1,66 +1,62 @@ -"use server"; -import { prisma } from "./prisma"; +import { getSession } from './session'; +import { prisma } from './prisma'; +import { createSafeActionClient } from 'next-safe-action'; +import { headers } from 'next/headers'; +import * as zod from 'zod'; +export function defineMetadataSchema() { + return zod.object({ + actionName: zod.string() + }); +} -export async function addUser(address: string, email?: string) { +export const actionClient = createSafeActionClient({ + defineMetadataSchema, + handleServerError: (err: Error) => err.message, + defaultValidationErrorsShape: 'flattened' +}) + /** + * Middleware used for auth purposes. + * Returns the context with the session object. + */ + .use(async ({ next }) => { + const session = await getSession(); + const headerList = headers(); - const normalizedAddress = address ? address.trim().toLowerCase() : undefined; - if (!normalizedAddress && !email) { - throw new Error("Must provide wallet address or email"); + return next({ + ctx: { session, headers: headerList } + }); + }); + +export const authActionClient = actionClient.use(async ({ next, ctx }) => { + const userId = ctx.session.user?.id; + + if (!userId) { + console.error('Malformed cookie', new Error('Invalid user, not allowed')); + throw new Error('Not Authorised'); + } + + const user = await prisma.user.findFirst({ + where: { + id: userId } + }); - const selectSafe = { - id: true, - email: true, - walletAddress: true, - role: true, - name: true, - image: true, - subscriptionType: true, - createdAt: true, - updatedAt: true, - lastLoginAt: true, - isActive: true, - } as const; + if (!user) { + console.error('Not Authorised', new Error('Invalid user, not allowed')); + throw new Error('Not Authorised'); + } - // Prefer wallet path when available (idempotent via upsert) - if (normalizedAddress) { - try { - const user = await prisma.user.upsert({ - where: { walletAddress: normalizedAddress }, - update: { lastLoginAt: new Date() }, - create: { - walletAddress: normalizedAddress, - email: email ?? null, - lastLoginAt: new Date(), - }, - select: selectSafe, - }); - // Optionally link email if provided and not set yet - if (email && !user.email) { - return await prisma.user.update({ - where: { id: user.id }, - data: { email }, - select: selectSafe, - }); - } - return user; - } catch (e) { - // Handle race: if unique constraint hit, re-fetch - const existing = await prisma.user.findUnique({ - where: { walletAddress: normalizedAddress }, - select: selectSafe, - }); - if (existing) return existing; - throw e; + return next({ + ctx: { + ...ctx, + session: { + destroy: ctx.session.destroy, + save: ctx.session.save, + user: { + id: userId } + } } - - // Email-only path (idempotent) - return prisma.user.upsert({ - where: { email: email as string }, - update: { lastLoginAt: new Date() }, - create: { email: email as string }, - select: selectSafe, - }); -} \ No newline at end of file + }); +}); diff --git a/src/lib/constants.ts b/src/lib/constants.ts new file mode 100644 index 0000000..70289ea --- /dev/null +++ b/src/lib/constants.ts @@ -0,0 +1,4 @@ +import { APP_ENV } from "./env"; + +export const cookieName = "authormaton-session"; +export const IS_PRODUCTION = APP_ENV === "production"; diff --git a/src/lib/env.ts b/src/lib/env.ts new file mode 100644 index 0000000..9ccdc86 --- /dev/null +++ b/src/lib/env.ts @@ -0,0 +1,11 @@ +import 'server-only'; + +const APP_ENV = (process.env.APP_ENV as 'development' | 'production') ?? 'production'; + +const AUTH_SECRET = process.env.AUTH_SECRET as string; + +if (!AUTH_SECRET || AUTH_SECRET.length < 32) { + throw new Error('AUTH_SECRET must be set and at least 32 characters long'); +} + +export { APP_ENV, AUTH_SECRET }; diff --git a/src/lib/result.ts b/src/lib/result.ts new file mode 100644 index 0000000..76a7252 --- /dev/null +++ b/src/lib/result.ts @@ -0,0 +1,25 @@ +/** + * Universal return type for server actions + * Provides a consistent structure for success and error responses + */ +export type Result = { success: true; data: T } | { success: false; error: string }; + +/** + * Creates a success result with data + */ +export function success(data: T): Result { + return { + data, + success: true + }; +} + +/** + * Creates an error result with error message + */ +export function error(message: string): Result { + return { + success: false, + error: message + }; +} diff --git a/src/lib/session.ts b/src/lib/session.ts new file mode 100644 index 0000000..28129a8 --- /dev/null +++ b/src/lib/session.ts @@ -0,0 +1,36 @@ + +import { getIronSession } from 'iron-session'; +import { cookies } from 'next/headers'; +import { cookieName, IS_PRODUCTION } from './constants'; +import { AUTH_SECRET } from './env'; + +// 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" + }; +} + +export type SessionData = { + user?: { + id: string; + }; +}; + +export async function getSession() { + const session = await getIronSession(await cookies(), { + password: AUTH_SECRET, + cookieName: cookieName, + cookieOptions: { + secure: IS_PRODUCTION, + httpOnly: true + } + }); + + return session; +} From c19521a857e4eb2564d2ffd8b11db00e9b4067f7 Mon Sep 17 00:00:00 2001 From: Kalida Tony Date: Mon, 22 Sep 2025 06:15:16 -0600 Subject: [PATCH 2/4] fix: Improve error handling and logging in authentication actions --- src/actions/auth/signin/action.ts | 2 +- src/actions/auth/signout/logic.ts | 2 +- src/actions/auth/signup/logic.ts | 4 ++-- src/lib/action.ts | 9 ++++++--- src/lib/env.ts | 3 ++- src/lib/session.ts | 5 +++++ 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/actions/auth/signin/action.ts b/src/actions/auth/signin/action.ts index 9cca7da..3b02d73 100644 --- a/src/actions/auth/signin/action.ts +++ b/src/actions/auth/signin/action.ts @@ -26,7 +26,7 @@ export const signinAction = actionClient throw new Error(error.message, { cause: error }); } - console.error('Sign in error:', error, { email }); + console.error('Sign in error:', error); throw new Error('Something went wrong'); } }); diff --git a/src/actions/auth/signout/logic.ts b/src/actions/auth/signout/logic.ts index 8da6295..17c795c 100644 --- a/src/actions/auth/signout/logic.ts +++ b/src/actions/auth/signout/logic.ts @@ -5,6 +5,6 @@ import { getSession } from '@/lib/session'; export async function Signout(): Promise> { const session = await getSession(); - session.destroy(); + await session.destroy(); return success(undefined); } diff --git a/src/actions/auth/signup/logic.ts b/src/actions/auth/signup/logic.ts index 37daf4b..25c3461 100644 --- a/src/actions/auth/signup/logic.ts +++ b/src/actions/auth/signup/logic.ts @@ -22,12 +22,12 @@ export async function Signup(input: SignupInput): Promise> { const normalisedEmail = email.toLowerCase().trim(); - const exisitngUser = await prisma.user.findUnique({ + const exisitingUser = await prisma.user.findUnique({ where: { email: normalisedEmail } }); - if (exisitngUser) { + if (exisitingUser) { console.error('Singup error: User with this email already exists') return error('Something went wrong') }; diff --git a/src/lib/action.ts b/src/lib/action.ts index 9ba1c57..aa652a2 100644 --- a/src/lib/action.ts +++ b/src/lib/action.ts @@ -12,7 +12,10 @@ export function defineMetadataSchema() { 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' }) /** @@ -51,8 +54,8 @@ export const authActionClient = actionClient.use(async ({ next, ctx }) => { 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 } diff --git a/src/lib/env.ts b/src/lib/env.ts index 9ccdc86..e588fe9 100644 --- a/src/lib/env.ts +++ b/src/lib/env.ts @@ -1,6 +1,7 @@ import 'server-only'; -const APP_ENV = (process.env.APP_ENV as 'development' | 'production') ?? 'production'; +const rawEnv = process.env.APP_ENV ?? process.env.NODE_ENV ?? 'development'; +const APP_ENV: 'development' | 'production' = rawEnv === 'production' ? 'production' : 'development'; const AUTH_SECRET = process.env.AUTH_SECRET as string; diff --git a/src/lib/session.ts b/src/lib/session.ts index 28129a8..faea00d 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -23,6 +23,11 @@ export type SessionData = { }; export async function getSession() { + if (!AUTH_SECRET || AUTH_SECRET.length < 32) { + // Do not leak exact reason to clients; log server-side only. + console.error('Invalid AUTH_SECRET: must be at least 32 characters.'); + throw new Error('Server misconfiguration'); + } const session = await getIronSession(await cookies(), { password: AUTH_SECRET, cookieName: cookieName, From b4744b758319111e110c7297025599561374df17 Mon Sep 17 00:00:00 2001 From: Kalida Tony Date: Mon, 22 Sep 2025 06:41:49 -0600 Subject: [PATCH 3/4] refactor: Standardize naming conventions for signin and signout actions and schemas --- src/actions/auth/signin/action.ts | 8 +++--- src/actions/auth/signin/logic.ts | 44 ++++++------------------------ src/actions/auth/signin/schema.ts | 4 +-- src/actions/auth/signout/action.ts | 4 +-- src/actions/auth/signout/logic.ts | 2 +- src/actions/auth/signup/logic.ts | 31 +++------------------ src/actions/auth/signup/schema.ts | 4 +-- src/lib/action.ts | 9 ++---- src/lib/session.ts | 12 -------- 9 files changed, 26 insertions(+), 92 deletions(-) diff --git a/src/actions/auth/signin/action.ts b/src/actions/auth/signin/action.ts index 3b02d73..896ae1c 100644 --- a/src/actions/auth/signin/action.ts +++ b/src/actions/auth/signin/action.ts @@ -1,17 +1,17 @@ 'use server'; import { actionClient } from '@/lib/action'; -import { Signin} from './logic'; -import { SigninSchema} from './schema'; +import { signin} from './logic'; +import { signinSchema} from './schema'; export const signinAction = actionClient - .inputSchema(SigninSchema) + .inputSchema(signinSchema) .metadata({ actionName: 'signin' }) .action(async ({ parsedInput }) => { const { email } = parsedInput; try { - const result = await Signin(parsedInput); + const result = await signin(parsedInput); if (result.success) { return result.data; diff --git a/src/actions/auth/signin/logic.ts b/src/actions/auth/signin/logic.ts index b2008b4..2074d71 100644 --- a/src/actions/auth/signin/logic.ts +++ b/src/actions/auth/signin/logic.ts @@ -1,23 +1,11 @@ import { prisma } from "@/lib/prisma"; -import { SigninInput } from "./schema"; +import { signinInput } from "./schema"; import bcrypt from "bcryptjs"; import { error, Result, success } from "@/lib/result"; import { getSession } from "@/lib/session"; -import type { Role } from "@prisma/client"; +import type { User } 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; -} - -export async function Signin(input: SigninInput): Promise> { +export async function signin(input: signinInput): Promise>> { const { email, password } = input; const normalisedEmail = email.toLowerCase().trim(); @@ -26,42 +14,26 @@ export async function Signin(input: SigninInput): Promise \ No newline at end of file +export type signinInput = z.infer \ No newline at end of file diff --git a/src/actions/auth/signout/action.ts b/src/actions/auth/signout/action.ts index 201c29c..737d704 100644 --- a/src/actions/auth/signout/action.ts +++ b/src/actions/auth/signout/action.ts @@ -1,13 +1,13 @@ 'use server'; import { authActionClient } from '@/lib/action'; -import { Signout} from './logic'; +import { signout} from './logic'; export const signoutAction = authActionClient.metadata({ actionName: 'signout' }).action(async ({ ctx }) => { const userId = ctx.session.user.id; try { - const result = await Signout(); + const result = await signout(); if (result.success) { return result.data; diff --git a/src/actions/auth/signout/logic.ts b/src/actions/auth/signout/logic.ts index 17c795c..98b6aae 100644 --- a/src/actions/auth/signout/logic.ts +++ b/src/actions/auth/signout/logic.ts @@ -3,7 +3,7 @@ import 'server-only'; import { Result, success } from '@/lib/result'; import { getSession } from '@/lib/session'; -export async function Signout(): Promise> { +export async function signout(): Promise> { const session = await getSession(); await session.destroy(); return success(undefined); diff --git a/src/actions/auth/signup/logic.ts b/src/actions/auth/signup/logic.ts index 25c3461..f67ad76 100644 --- a/src/actions/auth/signup/logic.ts +++ b/src/actions/auth/signup/logic.ts @@ -1,23 +1,11 @@ import { prisma } from "@/lib/prisma"; import bcrypt from "bcryptjs"; -import { SignupInput } from "./schema"; +import { signupInput } from "./schema"; import { error, Result, success } from "@/lib/result"; import { getSession } from "@/lib/session"; -import type { Role } from "@prisma/client"; +import type { User } 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; -} - -export async function Signup(input: SignupInput): Promise> { +export async function Signup(input: signupInput): Promise> { const { email, name, password } = input; const normalisedEmail = email.toLowerCase().trim(); @@ -37,18 +25,7 @@ export async function Signup(input: SignupInput): Promise> { 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, + password: hashedPassword } }); const session = await getSession(); diff --git a/src/actions/auth/signup/schema.ts b/src/actions/auth/signup/schema.ts index 35c4e8a..208e79b 100644 --- a/src/actions/auth/signup/schema.ts +++ b/src/actions/auth/signup/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -export const SignupSchema = z.object({ +export const signupSchema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Invalid email format'), password: z.string() @@ -11,4 +11,4 @@ export const SignupSchema = z.object({ }) -export type SignupInput = z.infer \ No newline at end of file +export type signupInput = z.infer \ No newline at end of file diff --git a/src/lib/action.ts b/src/lib/action.ts index aa652a2..f2c153c 100644 --- a/src/lib/action.ts +++ b/src/lib/action.ts @@ -12,10 +12,7 @@ export function defineMetadataSchema() { export const actionClient = createSafeActionClient({ defineMetadataSchema, - handleServerError: (err: Error) => { - console.error('Action server error:', err); - return 'Something went wrong'; - }, + handleServerError: (err: Error) => err.message, defaultValidationErrorsShape: 'flattened' }) /** @@ -54,8 +51,8 @@ export const authActionClient = actionClient.use(async ({ next, ctx }) => { ctx: { ...ctx, session: { - destroy: ctx.session.destroy.bind(ctx.session), - save: ctx.session.save.bind(ctx.session), + destroy: ctx.session.destroy, + save: ctx.session.save, user: { id: userId } diff --git a/src/lib/session.ts b/src/lib/session.ts index faea00d..28877dd 100644 --- a/src/lib/session.ts +++ b/src/lib/session.ts @@ -4,18 +4,6 @@ import { cookies } from 'next/headers'; import { cookieName, IS_PRODUCTION } from './constants'; import { AUTH_SECRET } from './env'; -// 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" - }; -} - export type SessionData = { user?: { id: string; From ccb4db44d2e01c4e4e9fff801e7ef10951e86ef0 Mon Sep 17 00:00:00 2001 From: Kalida Tony Date: Mon, 22 Sep 2025 06:59:18 -0600 Subject: [PATCH 4/4] refactor: Add 'server-only' import to signin and signup logic files --- src/actions/auth/signin/logic.ts | 4 +++- src/actions/auth/signup/logic.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/actions/auth/signin/logic.ts b/src/actions/auth/signin/logic.ts index 2074d71..7f89554 100644 --- a/src/actions/auth/signin/logic.ts +++ b/src/actions/auth/signin/logic.ts @@ -1,3 +1,5 @@ +import 'server-only'; + import { prisma } from "@/lib/prisma"; import { signinInput } from "./schema"; import bcrypt from "bcryptjs"; @@ -23,7 +25,7 @@ export async function signin(input: signinInput): Promise