From 9ad195ad784aeb55cea55c68f929d5538ae32f28 Mon Sep 17 00:00:00 2001 From: Sianida26 Date: Tue, 9 Jan 2024 22:47:20 +0700 Subject: [PATCH] added auth --- src/app/(auth)/login/page.tsx | 49 +++++++++++++------ src/app/(auth)/register/page.tsx | 28 ++++++----- src/features/auth/AuthError.ts | 3 +- src/features/auth/actions/createUser.ts | 31 ++++++++++++ src/features/auth/actions/signIn.ts | 34 +++++++++++++ src/features/auth/authUtils.ts | 30 ------------ src/features/auth/index.ts | 34 ++++++++----- .../auth/providers/emailPasswordProvider.ts | 9 ++-- src/trpc/routes/auth.ts | 40 ++++++++++++++- 9 files changed, 184 insertions(+), 74 deletions(-) create mode 100644 src/features/auth/actions/createUser.ts create mode 100644 src/features/auth/actions/signIn.ts diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index 78dfe18..ac4fef7 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -9,23 +9,24 @@ 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; } /** * LoginPage component: Renders a login form allowing users to authenticate using their credentials. * Utilizes Mantine for UI components and Next-Auth for authentication handling. - * + * * @returns React functional component representing the login page. */ export default function LoginPage() { @@ -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 (
@@ -57,6 +66,16 @@ export default function LoginPage() {
+ {errorMessage ? ( + + {errorMessage} + + ) : null} - + diff --git a/src/app/(auth)/register/page.tsx b/src/app/(auth)/register/page.tsx index 1f08518..deb5c3c 100644 --- a/src/app/(auth)/register/page.tsx +++ b/src/app/(auth)/register/page.tsx @@ -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({ + const form = useForm({ 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) => { + 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 ( diff --git a/src/features/auth/AuthError.ts b/src/features/auth/AuthError.ts index 10aca55..33a87f8 100644 --- a/src/features/auth/AuthError.ts +++ b/src/features/auth/AuthError.ts @@ -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 { diff --git a/src/features/auth/actions/createUser.ts b/src/features/auth/actions/createUser.ts new file mode 100644 index 0000000..8bd29e6 --- /dev/null +++ b/src/features/auth/actions/createUser.ts @@ -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; diff --git a/src/features/auth/actions/signIn.ts b/src/features/auth/actions/signIn.ts new file mode 100644 index 0000000..db4b9f6 --- /dev/null +++ b/src/features/auth/actions/signIn.ts @@ -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 { + // 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; +} diff --git a/src/features/auth/authUtils.ts b/src/features/auth/authUtils.ts index e41e118..8cde8f1 100644 --- a/src/features/auth/authUtils.ts +++ b/src/features/auth/authUtils.ts @@ -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 { - // 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. * diff --git a/src/features/auth/index.ts b/src/features/auth/index.ts index 43e6485..5b7e553 100644 --- a/src/features/auth/index.ts +++ b/src/features/auth/index.ts @@ -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" diff --git a/src/features/auth/providers/emailPasswordProvider.ts b/src/features/auth/providers/emailPasswordProvider.ts index f193ef0..81efc07 100644 --- a/src/features/auth/providers/emailPasswordProvider.ts +++ b/src/features/auth/providers/emailPasswordProvider.ts @@ -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; } diff --git a/src/trpc/routes/auth.ts b/src/trpc/routes/auth.ts index 4b7a3f6..64de428 100644 --- a/src/trpc/routes/auth.ts +++ b/src/trpc/routes/auth.ts @@ -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;