added auth

This commit is contained in:
Sianida26 2024-01-09 22:47:20 +07:00
parent 02680d057f
commit 9ad195ad78
9 changed files with 184 additions and 74 deletions

View File

@ -9,17 +9,18 @@ import {
TextInput,
Group,
Anchor,
Button,
Button,
Alert,
} from "@mantine/core";
import { useForm } from "@mantine/form";
import React from "react";
import React, { useState } from "react";
/**
* Type definition for login form values.
*/
interface LoginFormType {
email: string,
password: string
email: string;
password: string;
}
/**
@ -36,18 +37,26 @@ export default function LoginPage() {
},
});
const [errorMessage, setErrorMessage] = useState("");
/**
* Handles form submission by calling Next-Auth signIn function with credentials.
*
* @param values - Object containing email and password entered by the user.
*/
const handleFormSubmit = async (values: LoginFormType) => {
await signIn("credentials", {
email: values.email,
password: values.password,
callbackUrl: "/"
})
}
try {
await signIn("credentials", {
email: values.email,
password: values.password,
callbackUrl: "/",
redirect: false,
});
} catch (e) {
// TODO: Handle proper error message
setErrorMessage("Email/Password does not match");
}
};
return (
<div className="w-screen h-screen flex items-center justify-center">
@ -57,6 +66,16 @@ export default function LoginPage() {
</Text>
<form onSubmit={form.onSubmit(handleFormSubmit)}>
<Stack>
{errorMessage ? (
<Alert
variant="filled"
color="pink"
title=""
// icon={icon}
>
{errorMessage}
</Alert>
) : null}
<TextInput
label="Email"
placeholder="Enter your email"
@ -84,9 +103,9 @@ export default function LoginPage() {
Don&apos;t have an account? Register
</Anchor>
<Button type="submit" radius="xl">
Login
</Button>
<Button type="submit" radius="xl">
Login
</Button>
</Group>
</form>
</Paper>

View File

@ -16,7 +16,7 @@ import { useForm } from "@mantine/form";
import React, { useEffect } from "react";
import { api } from "@/trpc/utils";
interface RegisterFormType {
export interface RegisterFormSchema {
email: string,
password: string,
passwordConfirmation: string,
@ -25,13 +25,7 @@ interface RegisterFormType {
export default function RegisterPage() {
const {data, isLoading} = api.auth.register.useQuery();
useEffect(() => {
console.log("data", data)
}, [data])
const form = useForm<RegisterFormType>({
const form = useForm<RegisterFormSchema>({
initialValues: {
email: "",
password: "",
@ -41,14 +35,26 @@ export default function RegisterPage() {
validate: {
email: (value: string) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
password: (value: string) => (value.length > 6 ? null : 'Password should be at least 6 characters'),
passwordConfirmation: (value: string, values: RegisterFormType) => value === values.password ? null : 'Passwords should match',
passwordConfirmation: (value: string, values: RegisterFormSchema) => value === values.password ? null : 'Passwords should match',
name: (value: string) => (value.length > 0 ? null : 'Name is required'),
}
});
const handleFormSubmit = async (values: RegisterFormType) => {
// await
const registerMutation = api.auth.register.useMutation({
onSuccess: async () => {
console.log("success. signing in")
await signIn("credentials", {
email: form.values.email,
password: form.values.password,
callbackUrl: "/dashboard"
})
console.log("signed in")
}
})
const handleFormSubmit = (values: RegisterFormSchema) => {
// await
registerMutation.mutate(values)
}
return (

View File

@ -2,8 +2,9 @@ import BaseError from "@/BaseError";
export enum AuthErrorCode {
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
EMPTY_USER_HASH = "EMPTY_USER_HASH",
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
EMPTY_USER_HASH = "EMPTY_USER_HASH"
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS"
}
export default class AuthError extends BaseError {

View File

@ -0,0 +1,31 @@
import prisma from "@/db"
import AuthError, { AuthErrorCode } from "../AuthError";
import { hashPassword } from "../authUtils";
interface CreateUserSchema {
name: string,
email: string,
plainPassword: string,
}
const register = async (inputData: CreateUserSchema) => {
const existingUser = await prisma.user.findUnique({
where: {
email: inputData.email
}
});
if (existingUser) throw new AuthError(AuthErrorCode.USER_ALREADY_EXISTS, 419, "Email already exists")
const user = await prisma.user.create({
data: {
name: inputData.name,
email: inputData.email,
passwordHash: await hashPassword(inputData.plainPassword)
}
});
return user;
}
export default register;

View File

@ -0,0 +1,34 @@
import prisma from "@/db";
import { User } from "@prisma/client";
import AuthError, { AuthErrorCode } from "../AuthError";
import { comparePassword } from "../authUtils";
/**
* Validates the user by their email and password.
* If the user is found and the password is correct, it returns the user.
* Throws an AuthError if any authentication step fails.
*
* @param email - The email of the user to validate.
* @param password - The password to validate against the user's stored hash.
* @returns The authenticated user object.
* @throws {AuthError} - EMAIL_NOT_FOUND if no user is found, INVALID_CREDENTIALS if the password doesn't match, or other auth-related errors.
*/
export default async function signIn(email: string, password: string): Promise<User> {
// Retrieve user from the database by email
const user = await prisma.user.findUnique({
where: { email }
});
// Throw if user not found
if (!user) throw new AuthError(AuthErrorCode.EMAIL_NOT_FOUND, 401);
// Throw if user has no password hash
// TODO: Add check if the user uses another provider
if (!user.passwordHash) throw new AuthError(AuthErrorCode.EMPTY_USER_HASH, 500);
// Compare the provided password with the user's stored password hash
const isMatch = await comparePassword(password, user.passwordHash);
if (!isMatch) throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401);
return user;
}

View File

@ -4,36 +4,6 @@ import * as bcrypt from "bcrypt";
import AuthError, { AuthErrorCode } from "./AuthError";
import authConfig from "@/config/auth";
/**
* Validates the user by their email and password.
* If the user is found and the password is correct, it returns the user.
* Throws an AuthError if any authentication step fails.
*
* @param email - The email of the user to validate.
* @param password - The password to validate against the user's stored hash.
* @returns The authenticated user object.
* @throws {AuthError} - EMAIL_NOT_FOUND if no user is found, INVALID_CREDENTIALS if the password doesn't match, or other auth-related errors.
*/
export async function validateUser(email: string, password: string): Promise<User> {
// Retrieve user from the database by email
const user = await prisma.user.findUnique({
where: { email }
});
// Throw if user not found
if (!user) throw new AuthError(AuthErrorCode.EMAIL_NOT_FOUND, 401);
// Throw if user has no password hash
// TODO: Add check if the user uses another provider
if (!user.passwordHash) throw new AuthError(AuthErrorCode.EMPTY_USER_HASH, 500);
// Compare the provided password with the user's stored password hash
const isMatch = await comparePassword(password, user.passwordHash);
if (!isMatch) throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401);
return user;
}
/**
* Hashes a plain text password using bcrypt.
*

View File

@ -12,18 +12,28 @@ const nextAuth = NextAuth({
emailPasswordProvider
],
callbacks: {
session: async({session, user, token}) => {
if (session.user){
session.user.id = token.userId as string;
}
return session;
},
jwt: async ({ token, user, account, profile }) => {
if(account && account.type === "credentials") {
token.userId = account.providerAccountId; // this is Id that coming from authorize() callback
}
return token
}
// signIn: ({ user, account, profile, email, credentials}) => {
// console.log("sign in callback")
// console.table({user, account, profile, email, credentials})
// return true;
// },
// session: async({session, user, token}) => {
// if (session.user){
// session.user.id = token.userId as string;
// }
// return session;
// },
// jwt: async ({ token, user, account, profile }) => {
// if(account && account.type === "credentials") {
// token.userId = account.providerAccountId; // this is Id that coming from authorize() callback
// }
// return token
// },
// redirect: async ({url, baseUrl}) => {
// console.log("redirect callback called")
// console.table({url, baseUrl})
// return url
// }
},
pages: {
signIn: "/login"

View File

@ -1,7 +1,7 @@
import CredentialsProvider from "next-auth/providers/credentials"
import { validateUser } from "../authUtils";
import AuthError, { AuthErrorCode } from "../AuthError";
import BaseError from "@/BaseError";
import signIn from "../actions/signIn";
/**
* Factory function to create a credential provider.
@ -30,13 +30,14 @@ const credential = CredentialsProvider({
}
// Validate user with provided credentials
const user = await validateUser(credentials.email, credentials.password);
const user = await signIn(credentials.email, credentials.password);
return user;
} catch (e: unknown){
// Handle specific authentication errors, re-throw others
if (e instanceof AuthError){
// Generalize error message for security
throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401, "Invalid email/password.");
// Auth invalid
if ([AuthErrorCode.EMAIL_NOT_FOUND, AuthErrorCode.EMPTY_USER_HASH, AuthErrorCode.INVALID_CREDENTIALS].includes(e.errorCode as AuthErrorCode))
return null;
}
throw e;
}

View File

@ -1,8 +1,46 @@
import { z } from "zod";
import { procedure, router } from "..";
import prisma from "@/db";
import createUser from "@/features/auth/actions/createUser";
import { AuthError } from "next-auth";
import { TRPCError } from "@trpc/server";
const authRouter = router({
register: procedure.query(() => "hi, register")
register: procedure
.input(
z.object({
name: z.string(),
email: z.string().email(),
password: z.string(),
passwordConfirmation: z.string(),
})
.refine(data => data.password === data.passwordConfirmation, {
message: "Password don't match",
path: ["passwordConfirmation"]
})
)
.mutation(async ({input}) => {
try {
const user = await createUser({
email: input.email,
name: input.name,
plainPassword: input.password
})
return "ok"
} catch (e: unknown) {
if (e instanceof AuthError){
throw new TRPCError({
code: "BAD_REQUEST",
message: e.message,
cause: e
})
}
throw new TRPCError({
code: "INTERNAL_SERVER_ERROR"
})
}
})
})
export default authRouter;