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() {
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;