added auth
This commit is contained in:
parent
02680d057f
commit
9ad195ad78
|
|
@ -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 (
|
||||
<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't have an account? Register
|
||||
</Anchor>
|
||||
|
||||
<Button type="submit" radius="xl">
|
||||
Login
|
||||
</Button>
|
||||
<Button type="submit" radius="xl">
|
||||
Login
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Paper>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
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 (
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
31
src/features/auth/actions/createUser.ts
Normal file
31
src/features/auth/actions/createUser.ts
Normal 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;
|
||||
34
src/features/auth/actions/signIn.ts
Normal file
34
src/features/auth/actions/signIn.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user