-
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
Changes from all commits
26474fa
c19521a
b4744b7
ccb4db4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
throw new Error('Something went wrong'); | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import 'server-only'; | ||
|
||
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 { User } from "@prisma/client"; | ||
|
||
export async function signin(input: signinInput): Promise<Result<Omit<User, 'password'>>> { | ||
const { email, password } = input; | ||
|
||
const normalisedEmail = email.toLowerCase().trim(); | ||
|
||
//Find user by email | ||
const user = await prisma.user.findUnique({ | ||
where: { | ||
email: normalisedEmail | ||
} | ||
}) | ||
if (!user) { | ||
console.error('signin error: User not found'); | ||
return error('Invalid credentials'); | ||
} | ||
|
||
//Verify Password | ||
const isValidPassword = await bcrypt.compare(password, user.password!); | ||
|
||
if (!isValidPassword) { | ||
console.error('signin error: Invalid Password'); | ||
return error('Invalid credentials'); | ||
} | ||
|
||
// Omit password from returned user object | ||
// 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); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof signinSchema> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import 'server-only'; | ||
|
||
import { Result, success } from '@/lib/result'; | ||
import { getSession } from '@/lib/session'; | ||
|
||
export async function signout(): Promise<Result<undefined>> { | ||
const session = await getSession(); | ||
await session.destroy(); | ||
return success(undefined); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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'); | ||
} | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import 'server-only'; | ||
|
||
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 { User } from "@prisma/client"; | ||
|
||
export async function Signup(input: signupInput): Promise<Result<User>> { | ||
const { email, name, password } = input; | ||
|
||
const normalisedEmail = email.toLowerCase().trim(); | ||
|
||
const exisitingUser = await prisma.user.findUnique({ | ||
where: { | ||
email: normalisedEmail | ||
} | ||
}); | ||
if (exisitingUser) { | ||
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 | ||
} | ||
}); | ||
const session = await getSession(); | ||
session.user = { id: user.id }; | ||
await session.save(); | ||
|
||
return success(user); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -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') | ||||||||||||||||||||||||
Comment on lines
+6
to
+10
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||
|
||||||||||||||||||||||||
}) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
export type signupInput = z.infer<typeof signupSchema> |
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:
Length of output: 401
Enforce DB-level case-insensitive uniqueness for User.email
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.