added auth
This commit is contained in:
parent
02680d057f
commit
9ad195ad78
|
|
@ -9,17 +9,18 @@ import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Group,
|
Group,
|
||||||
Anchor,
|
Anchor,
|
||||||
Button,
|
Button,
|
||||||
|
Alert,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Type definition for login form values.
|
* Type definition for login form values.
|
||||||
*/
|
*/
|
||||||
interface LoginFormType {
|
interface LoginFormType {
|
||||||
email: string,
|
email: string;
|
||||||
password: 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.
|
* Handles form submission by calling Next-Auth signIn function with credentials.
|
||||||
*
|
*
|
||||||
* @param values - Object containing email and password entered by the user.
|
* @param values - Object containing email and password entered by the user.
|
||||||
*/
|
*/
|
||||||
const handleFormSubmit = async (values: LoginFormType) => {
|
const handleFormSubmit = async (values: LoginFormType) => {
|
||||||
await signIn("credentials", {
|
try {
|
||||||
email: values.email,
|
await signIn("credentials", {
|
||||||
password: values.password,
|
email: values.email,
|
||||||
callbackUrl: "/"
|
password: values.password,
|
||||||
})
|
callbackUrl: "/",
|
||||||
}
|
redirect: false,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: Handle proper error message
|
||||||
|
setErrorMessage("Email/Password does not match");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen flex items-center justify-center">
|
<div className="w-screen h-screen flex items-center justify-center">
|
||||||
|
|
@ -57,6 +66,16 @@ export default function LoginPage() {
|
||||||
</Text>
|
</Text>
|
||||||
<form onSubmit={form.onSubmit(handleFormSubmit)}>
|
<form onSubmit={form.onSubmit(handleFormSubmit)}>
|
||||||
<Stack>
|
<Stack>
|
||||||
|
{errorMessage ? (
|
||||||
|
<Alert
|
||||||
|
variant="filled"
|
||||||
|
color="pink"
|
||||||
|
title=""
|
||||||
|
// icon={icon}
|
||||||
|
>
|
||||||
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Email"
|
label="Email"
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
|
|
@ -84,9 +103,9 @@ export default function LoginPage() {
|
||||||
Don't have an account? Register
|
Don't have an account? Register
|
||||||
</Anchor>
|
</Anchor>
|
||||||
|
|
||||||
<Button type="submit" radius="xl">
|
<Button type="submit" radius="xl">
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</form>
|
</form>
|
||||||
</Paper>
|
</Paper>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { useForm } from "@mantine/form";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { api } from "@/trpc/utils";
|
import { api } from "@/trpc/utils";
|
||||||
|
|
||||||
interface RegisterFormType {
|
export interface RegisterFormSchema {
|
||||||
email: string,
|
email: string,
|
||||||
password: string,
|
password: string,
|
||||||
passwordConfirmation: string,
|
passwordConfirmation: string,
|
||||||
|
|
@ -25,13 +25,7 @@ interface RegisterFormType {
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
|
|
||||||
const {data, isLoading} = api.auth.register.useQuery();
|
const form = useForm<RegisterFormSchema>({
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log("data", data)
|
|
||||||
}, [data])
|
|
||||||
|
|
||||||
const form = useForm<RegisterFormType>({
|
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
|
@ -41,14 +35,26 @@ export default function RegisterPage() {
|
||||||
validate: {
|
validate: {
|
||||||
email: (value: string) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
|
email: (value: string) => (/^\S+@\S+$/.test(value) ? null : 'Invalid email'),
|
||||||
password: (value: string) => (value.length > 6 ? null : 'Password should be at least 6 characters'),
|
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'),
|
name: (value: string) => (value.length > 0 ? null : 'Name is required'),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFormSubmit = async (values: RegisterFormType) => {
|
const registerMutation = api.auth.register.useMutation({
|
||||||
// await
|
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 (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,9 @@ import BaseError from "@/BaseError";
|
||||||
|
|
||||||
export enum AuthErrorCode {
|
export enum AuthErrorCode {
|
||||||
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
|
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
|
||||||
|
EMPTY_USER_HASH = "EMPTY_USER_HASH",
|
||||||
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
||||||
EMPTY_USER_HASH = "EMPTY_USER_HASH"
|
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS"
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AuthError extends BaseError {
|
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 AuthError, { AuthErrorCode } from "./AuthError";
|
||||||
import authConfig from "@/config/auth";
|
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.
|
* Hashes a plain text password using bcrypt.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -12,18 +12,28 @@ const nextAuth = NextAuth({
|
||||||
emailPasswordProvider
|
emailPasswordProvider
|
||||||
],
|
],
|
||||||
callbacks: {
|
callbacks: {
|
||||||
session: async({session, user, token}) => {
|
// signIn: ({ user, account, profile, email, credentials}) => {
|
||||||
if (session.user){
|
// console.log("sign in callback")
|
||||||
session.user.id = token.userId as string;
|
// console.table({user, account, profile, email, credentials})
|
||||||
}
|
// return true;
|
||||||
return session;
|
// },
|
||||||
},
|
// session: async({session, user, token}) => {
|
||||||
jwt: async ({ token, user, account, profile }) => {
|
// if (session.user){
|
||||||
if(account && account.type === "credentials") {
|
// session.user.id = token.userId as string;
|
||||||
token.userId = account.providerAccountId; // this is Id that coming from authorize() callback
|
// }
|
||||||
}
|
// return session;
|
||||||
return token
|
// },
|
||||||
}
|
// 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: {
|
pages: {
|
||||||
signIn: "/login"
|
signIn: "/login"
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import CredentialsProvider from "next-auth/providers/credentials"
|
import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
import { validateUser } from "../authUtils";
|
|
||||||
import AuthError, { AuthErrorCode } from "../AuthError";
|
import AuthError, { AuthErrorCode } from "../AuthError";
|
||||||
import BaseError from "@/BaseError";
|
import BaseError from "@/BaseError";
|
||||||
|
import signIn from "../actions/signIn";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory function to create a credential provider.
|
* Factory function to create a credential provider.
|
||||||
|
|
@ -30,13 +30,14 @@ const credential = CredentialsProvider({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate user with provided credentials
|
// Validate user with provided credentials
|
||||||
const user = await validateUser(credentials.email, credentials.password);
|
const user = await signIn(credentials.email, credentials.password);
|
||||||
return user;
|
return user;
|
||||||
} catch (e: unknown){
|
} catch (e: unknown){
|
||||||
// Handle specific authentication errors, re-throw others
|
// Handle specific authentication errors, re-throw others
|
||||||
if (e instanceof AuthError){
|
if (e instanceof AuthError){
|
||||||
// Generalize error message for security
|
// Auth invalid
|
||||||
throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401, "Invalid email/password.");
|
if ([AuthErrorCode.EMAIL_NOT_FOUND, AuthErrorCode.EMPTY_USER_HASH, AuthErrorCode.INVALID_CREDENTIALS].includes(e.errorCode as AuthErrorCode))
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,46 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { procedure, router } from "..";
|
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({
|
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;
|
export default authRouter;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user