Skip to content

Commit aefdaff

Browse files
authored
feat: Implement authentication actions with signin, signup, and signout (#26)
* feat: Implement authentication actions with signin, signup, and signout logic * fix: Improve error handling and logging in authentication actions * refactor: Standardize naming conventions for signin and signout actions and schemas * refactor: Add 'server-only' import to signin and signup logic files
1 parent 41f9e39 commit aefdaff

File tree

15 files changed

+400
-65
lines changed

15 files changed

+400
-65
lines changed

package-lock.json

Lines changed: 66 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
"bcryptjs": "^3.0.2",
2323
"class-variance-authority": "^0.7.1",
2424
"clsx": "^2.1.1",
25+
"iron-session": "^8.0.4",
2526
"lucide-react": "^0.542.0",
2627
"luxon": "^3.7.2",
27-
"next": "15.5.2",
28+
"next": "^15.5.2",
29+
"next-safe-action": "^8.0.11",
2830
"react": "19.1.0",
2931
"react-dom": "19.1.0",
3032
"react-icons": "^5.5.0",
3133
"tailwind-merge": "^3.3.1",
3234
"viem": "^2.37.5",
33-
"wagmi": "^2.16.9"
35+
"wagmi": "^2.16.9",
36+
"zod": "^3.25.76"
3437
},
3538
"devDependencies": {
3639
"@eslint/eslintrc": "^3",

src/actions/auth/signin/action.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use server';
2+
3+
import { actionClient } from '@/lib/action';
4+
import { signin} from './logic';
5+
import { signinSchema} from './schema';
6+
7+
export const signinAction = actionClient
8+
.inputSchema(signinSchema)
9+
.metadata({ actionName: 'signin' })
10+
.action(async ({ parsedInput }) => {
11+
const { email } = parsedInput;
12+
13+
try {
14+
const result = await signin(parsedInput);
15+
16+
if (result.success) {
17+
return result.data;
18+
}
19+
20+
throw new Error(result.error, { cause: { internal: true } });
21+
} catch (err) {
22+
const error = err as Error;
23+
const cause = error.cause as { internal: boolean } | undefined;
24+
25+
if (cause?.internal) {
26+
throw new Error(error.message, { cause: error });
27+
}
28+
29+
console.error('Sign in error:', error);
30+
throw new Error('Something went wrong');
31+
}
32+
});

src/actions/auth/signin/logic.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import 'server-only';
2+
3+
import { prisma } from "@/lib/prisma";
4+
import { signinInput } from "./schema";
5+
import bcrypt from "bcryptjs";
6+
import { error, Result, success } from "@/lib/result";
7+
import { getSession } from "@/lib/session";
8+
import type { User } from "@prisma/client";
9+
10+
export async function signin(input: signinInput): Promise<Result<Omit<User, 'password'>>> {
11+
const { email, password } = input;
12+
13+
const normalisedEmail = email.toLowerCase().trim();
14+
15+
//Find user by email
16+
const user = await prisma.user.findUnique({
17+
where: {
18+
email: normalisedEmail
19+
}
20+
})
21+
if (!user) {
22+
console.error('signin error: User not found');
23+
return error('Invalid credentials');
24+
}
25+
26+
//Verify Password
27+
const isValidPassword = await bcrypt.compare(password, user.password!);
28+
29+
if (!isValidPassword) {
30+
console.error('signin error: Invalid Password');
31+
return error('Invalid credentials');
32+
}
33+
34+
// Omit password from returned user object
35+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
36+
const { password: _password, ...userWithoutPassword } = user;
37+
38+
//Session
39+
const session = await getSession();
40+
session.user = { id: userWithoutPassword.id };
41+
await session.save();
42+
43+
return success(userWithoutPassword);
44+
}

src/actions/auth/signin/schema.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {z} from 'zod';
2+
3+
export const signinSchema = z.object({
4+
email: z.string().email("Invalid Email Format"),
5+
password: z.string().min(1,'Password is required')
6+
});
7+
8+
export type signinInput = z.infer<typeof signinSchema>

src/actions/auth/signout/action.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
'use server';
2+
3+
import { authActionClient } from '@/lib/action';
4+
import { signout} from './logic';
5+
6+
export const signoutAction = authActionClient.metadata({ actionName: 'signout' }).action(async ({ ctx }) => {
7+
const userId = ctx.session.user.id;
8+
9+
try {
10+
const result = await signout();
11+
12+
if (result.success) {
13+
return result.data;
14+
}
15+
16+
throw new Error(result.error, { cause: { internal: true } });
17+
} catch (err) {
18+
const error = err as Error;
19+
const cause = error.cause as { internal: boolean } | undefined;
20+
21+
if (cause?.internal) {
22+
throw new Error(error.message, { cause: error });
23+
}
24+
25+
console.error('Sign out error:', error, { userId });
26+
throw new Error('Something went wrong');
27+
}
28+
});

src/actions/auth/signout/logic.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import 'server-only';
2+
3+
import { Result, success } from '@/lib/result';
4+
import { getSession } from '@/lib/session';
5+
6+
export async function signout(): Promise<Result<undefined>> {
7+
const session = await getSession();
8+
await session.destroy();
9+
return success(undefined);
10+
}

src/actions/auth/signup/action.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use server';
2+
3+
import { actionClient } from '@/lib/action';
4+
import { Signup} from './logic';
5+
import { SignupSchema} from './schema';
6+
7+
export const signupAction = actionClient
8+
.inputSchema(SignupSchema)
9+
.metadata({ actionName: 'signup' })
10+
.action(async ({ parsedInput }) => {
11+
try {
12+
const result = await Signup(parsedInput);
13+
14+
if (result.success) {
15+
return result.data;
16+
}
17+
18+
// Set session
19+
throw new Error(result.error, { cause: { internal: true } });
20+
} catch (err) {
21+
const error = err as Error;
22+
const cause = error.cause as { internal: boolean } | undefined;
23+
24+
if (cause?.internal) {
25+
throw new Error(error.message, { cause: error });
26+
}
27+
28+
console.error('Sign up error:', error);
29+
throw new Error('Something went wrong');
30+
}
31+
});

src/actions/auth/signup/logic.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import 'server-only';
2+
3+
import { prisma } from "@/lib/prisma";
4+
import bcrypt from "bcryptjs";
5+
import { signupInput } from "./schema";
6+
import { error, Result, success } from "@/lib/result";
7+
import { getSession } from "@/lib/session";
8+
import type { User } from "@prisma/client";
9+
10+
export async function Signup(input: signupInput): Promise<Result<User>> {
11+
const { email, name, password } = input;
12+
13+
const normalisedEmail = email.toLowerCase().trim();
14+
15+
const exisitingUser = await prisma.user.findUnique({
16+
where: {
17+
email: normalisedEmail
18+
}
19+
});
20+
if (exisitingUser) {
21+
console.error('Singup error: User with this email already exists')
22+
return error('Something went wrong')
23+
};
24+
25+
const hashedPassword = await bcrypt.hash(password, 12);
26+
const user = await prisma.user.create({
27+
data: {
28+
name,
29+
email: normalisedEmail,
30+
password: hashedPassword
31+
}
32+
});
33+
const session = await getSession();
34+
session.user = { id: user.id };
35+
await session.save();
36+
37+
return success(user);
38+
}

src/actions/auth/signup/schema.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { z } from "zod";
2+
3+
export const signupSchema = z.object({
4+
name: z.string().min(1, 'Name is required'),
5+
email: z.string().email('Invalid email format'),
6+
password: z.string()
7+
.min(8, 'Password must be at least 8 characters long')
8+
.regex(/(?=.*[a-z])/, 'Password must contain at least one lowercase letter')
9+
.regex(/(?=.*[A-Z])/, 'Password must contain at least one uppercase letter')
10+
.regex(/(?=.*\d)/, 'Password must contain at least one number')
11+
12+
})
13+
14+
export type signupInput = z.infer<typeof signupSchema>

0 commit comments

Comments
 (0)