Added sign in action
This commit is contained in:
parent
9ad195ad78
commit
d05fa7e27d
2
.env
2
.env
|
|
@ -1,3 +1,3 @@
|
||||||
DATABASE_URL=mysql://root:root@localhost:3306/dashboard_template
|
DATABASE_URL=mysql://root:root@localhost:3306/dashboard_template
|
||||||
|
|
||||||
NEXTAUTH_SECRET=
|
JWT_SECRET=
|
||||||
|
|
@ -21,9 +21,10 @@
|
||||||
"@trpc/react-query": "^10.45.0",
|
"@trpc/react-query": "^10.45.0",
|
||||||
"@trpc/server": "^10.45.0",
|
"@trpc/server": "^10.45.0",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/jsonwebtoken": "^9.0.5",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"next": "14.0.4",
|
"next": "14.0.4",
|
||||||
"next-auth": "beta",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
|
|
|
||||||
116
pnpm-lock.yaml
116
pnpm-lock.yaml
|
|
@ -41,15 +41,18 @@ dependencies:
|
||||||
'@types/bcrypt':
|
'@types/bcrypt':
|
||||||
specifier: ^5.0.2
|
specifier: ^5.0.2
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
'@types/jsonwebtoken':
|
||||||
|
specifier: ^9.0.5
|
||||||
|
version: 9.0.5
|
||||||
bcrypt:
|
bcrypt:
|
||||||
specifier: ^5.1.1
|
specifier: ^5.1.1
|
||||||
version: 5.1.1
|
version: 5.1.1
|
||||||
|
jsonwebtoken:
|
||||||
|
specifier: ^9.0.2
|
||||||
|
version: 9.0.2
|
||||||
next:
|
next:
|
||||||
specifier: 14.0.4
|
specifier: 14.0.4
|
||||||
version: 14.0.4(react-dom@18.2.0)(react@18.2.0)
|
version: 14.0.4(react-dom@18.2.0)(react@18.2.0)
|
||||||
next-auth:
|
|
||||||
specifier: beta
|
|
||||||
version: 5.0.0-beta.4(next@14.0.4)(react@18.2.0)
|
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
|
@ -113,22 +116,6 @@ packages:
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@auth/core@0.18.4:
|
|
||||||
resolution: {integrity: sha512-GsNhsP1xE/3FoNS3dVkPjqRljLNJ4iyL2OLv3klQGNvw3bMpROFcK4lqhx7+pPHiamnVaYt2vg1xbB+lsNaevg==}
|
|
||||||
peerDependencies:
|
|
||||||
nodemailer: ^6.8.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
nodemailer:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@panva/hkdf': 1.1.1
|
|
||||||
cookie: 0.6.0
|
|
||||||
jose: 5.2.0
|
|
||||||
oauth4webapi: 2.4.3
|
|
||||||
preact: 10.11.3
|
|
||||||
preact-render-to-string: 5.2.3(preact@10.11.3)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@auth/core@0.20.0:
|
/@auth/core@0.20.0:
|
||||||
resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==}
|
resolution: {integrity: sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -646,6 +633,12 @@ packages:
|
||||||
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@types/jsonwebtoken@9.0.5:
|
||||||
|
resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==}
|
||||||
|
dependencies:
|
||||||
|
'@types/node': 20.10.6
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@types/node@20.10.6:
|
/@types/node@20.10.6:
|
||||||
resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==}
|
resolution: {integrity: sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -1009,6 +1002,10 @@ packages:
|
||||||
update-browserslist-db: 1.0.13(browserslist@4.22.2)
|
update-browserslist-db: 1.0.13(browserslist@4.22.2)
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/buffer-equal-constant-time@1.0.1:
|
||||||
|
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/busboy@1.6.0:
|
/busboy@1.6.0:
|
||||||
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==}
|
||||||
engines: {node: '>=10.16.0'}
|
engines: {node: '>=10.16.0'}
|
||||||
|
|
@ -1231,6 +1228,12 @@ packages:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/ecdsa-sig-formatter@1.0.11:
|
||||||
|
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/electron-to-chromium@1.4.623:
|
/electron-to-chromium@1.4.623:
|
||||||
resolution: {integrity: sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==}
|
resolution: {integrity: sha512-lKoz10iCYlP1WtRYdh5MvocQPWVRoI7ysp6qf18bmeBgR8abE6+I2CsfyNKztRDZvhdWc+krKT6wS7Neg8sw3A==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
@ -2203,6 +2206,22 @@ packages:
|
||||||
minimist: 1.2.8
|
minimist: 1.2.8
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jsonwebtoken@9.0.2:
|
||||||
|
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||||
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
dependencies:
|
||||||
|
jws: 3.2.2
|
||||||
|
lodash.includes: 4.3.0
|
||||||
|
lodash.isboolean: 3.0.3
|
||||||
|
lodash.isinteger: 4.0.4
|
||||||
|
lodash.isnumber: 3.0.3
|
||||||
|
lodash.isplainobject: 4.0.6
|
||||||
|
lodash.isstring: 4.0.1
|
||||||
|
lodash.once: 4.1.1
|
||||||
|
ms: 2.1.3
|
||||||
|
semver: 7.5.4
|
||||||
|
dev: false
|
||||||
|
|
||||||
/jsx-ast-utils@3.3.5:
|
/jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
|
|
@ -2213,6 +2232,21 @@ packages:
|
||||||
object.values: 1.1.7
|
object.values: 1.1.7
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/jwa@1.4.1:
|
||||||
|
resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==}
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: 1.0.1
|
||||||
|
ecdsa-sig-formatter: 1.0.11
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/jws@3.2.2:
|
||||||
|
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||||
|
dependencies:
|
||||||
|
jwa: 1.4.1
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/keyv@4.5.4:
|
/keyv@4.5.4:
|
||||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -2264,10 +2298,38 @@ packages:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.includes@4.3.0:
|
||||||
|
resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isboolean@3.0.3:
|
||||||
|
resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isinteger@4.0.4:
|
||||||
|
resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isnumber@3.0.3:
|
||||||
|
resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isplainobject@4.0.6:
|
||||||
|
resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
|
/lodash.isstring@4.0.1:
|
||||||
|
resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/lodash.merge@4.6.2:
|
/lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/lodash.once@4.1.1:
|
||||||
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/loose-envify@1.4.0:
|
/loose-envify@1.4.0:
|
||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -2357,7 +2419,6 @@ packages:
|
||||||
|
|
||||||
/ms@2.1.3:
|
/ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
dev: true
|
|
||||||
|
|
||||||
/mz@2.7.0:
|
/mz@2.7.0:
|
||||||
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
|
||||||
|
|
@ -2376,21 +2437,6 @@ packages:
|
||||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/next-auth@5.0.0-beta.4(next@14.0.4)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-vgocjvwPA8gxd/zrIP/vr9lJ/HeNe+C56lPP1D3sdyenHt8KncQV6ro7q0xCsDp1fcOKx7WAWVZH5o8aMxDzgw==}
|
|
||||||
peerDependencies:
|
|
||||||
next: ^14
|
|
||||||
nodemailer: ^6.6.5
|
|
||||||
react: ^18.2.0
|
|
||||||
peerDependenciesMeta:
|
|
||||||
nodemailer:
|
|
||||||
optional: true
|
|
||||||
dependencies:
|
|
||||||
'@auth/core': 0.18.4
|
|
||||||
next: 14.0.4(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
react: 18.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/next@14.0.4(react-dom@18.2.0)(react@18.2.0):
|
/next@14.0.4(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==}
|
resolution: {integrity: sha512-qbwypnM7327SadwFtxXnQdGiKpkuhaRLE2uq62/nRul9cj9KhQ5LhHmlziTNqUidZotw/Q1I9OjirBROdUJNgA==}
|
||||||
engines: {node: '>=18.17.0'}
|
engines: {node: '>=18.17.0'}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { signIn } from "next-auth/react";
|
import signIn from "@/features/auth/actions/signIn";
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
|
|
@ -12,51 +12,17 @@ import {
|
||||||
Button,
|
Button,
|
||||||
Alert,
|
Alert,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import React from "react";
|
||||||
import React, { useState } from "react";
|
import { useFormState } from "react-dom";
|
||||||
|
|
||||||
/**
|
const initialState = {
|
||||||
* Type definition for login form values.
|
errors: {
|
||||||
*/
|
message: "",
|
||||||
interface LoginFormType {
|
},
|
||||||
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() {
|
export default function LoginPage() {
|
||||||
const form = useForm<LoginFormType>({
|
const [state, formAction] = useFormState(signIn, initialState);
|
||||||
initialValues: {
|
|
||||||
email: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
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 (
|
return (
|
||||||
<div className="w-screen h-screen flex items-center justify-center">
|
<div className="w-screen h-screen flex items-center justify-center">
|
||||||
|
|
@ -64,16 +30,16 @@ export default function LoginPage() {
|
||||||
<Text size="lg" fw={500} mb={30}>
|
<Text size="lg" fw={500} mb={30}>
|
||||||
Welcome
|
Welcome
|
||||||
</Text>
|
</Text>
|
||||||
<form onSubmit={form.onSubmit(handleFormSubmit)}>
|
<form action={formAction}>
|
||||||
<Stack>
|
<Stack>
|
||||||
{errorMessage ? (
|
{state.errors.message ? (
|
||||||
<Alert
|
<Alert
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="pink"
|
color="pink"
|
||||||
title=""
|
title=""
|
||||||
// icon={icon}
|
// icon={icon}
|
||||||
>
|
>
|
||||||
{errorMessage}
|
{state.errors.message}
|
||||||
</Alert>
|
</Alert>
|
||||||
) : null}
|
) : null}
|
||||||
<TextInput
|
<TextInput
|
||||||
|
|
@ -81,14 +47,12 @@ export default function LoginPage() {
|
||||||
placeholder="Enter your email"
|
placeholder="Enter your email"
|
||||||
name="email"
|
name="email"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
{...form.getInputProps("email")}
|
|
||||||
/>
|
/>
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Password"
|
label="Password"
|
||||||
placeholder="Your password"
|
placeholder="Your password"
|
||||||
name="password"
|
name="password"
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
{...form.getInputProps("password")}
|
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { auth } from "@/features/auth";
|
|
||||||
import { signIn } from "next-auth/react";
|
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
PasswordInput,
|
PasswordInput,
|
||||||
|
|
@ -14,7 +12,6 @@ import {
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useForm } from "@mantine/form";
|
import { useForm } from "@mantine/form";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { api } from "@/trpc/utils";
|
|
||||||
|
|
||||||
export interface RegisterFormSchema {
|
export interface RegisterFormSchema {
|
||||||
email: string,
|
email: string,
|
||||||
|
|
@ -40,30 +37,13 @@ export default function RegisterPage() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<div className="w-screen h-screen flex items-center justify-center">
|
<div className="w-screen h-screen flex items-center justify-center">
|
||||||
<Paper radius="md" p="xl" withBorder w={400}>
|
<Paper radius="md" p="xl" withBorder w={400}>
|
||||||
<Text size="lg" fw={500} mb={30}>
|
<Text size="lg" fw={500} mb={30}>
|
||||||
Register
|
Register
|
||||||
</Text>
|
</Text>
|
||||||
<form onSubmit={form.onSubmit(handleFormSubmit)}>
|
<form onSubmit={form.onSubmit(() => {})}>
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name"
|
label="Name"
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export {GET, POST} from "@/features/auth"
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|
||||||
import { appRouter } from '@/trpc/routes/_app';
|
|
||||||
|
|
||||||
const handler = (req: Request) =>
|
|
||||||
fetchRequestHandler({
|
|
||||||
endpoint: '/api/trpc',
|
|
||||||
req,
|
|
||||||
router: appRouter,
|
|
||||||
createContext: () => ({ })
|
|
||||||
});
|
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
|
||||||
|
|
@ -5,7 +5,6 @@ import "./globals.css"
|
||||||
import '@mantine/core/styles.css';
|
import '@mantine/core/styles.css';
|
||||||
|
|
||||||
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
|
import { ColorSchemeScript, MantineProvider } from '@mantine/core';
|
||||||
import { TrpcProvider } from '@/trpc/TrpcProvider';
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] })
|
const inter = Inter({ subsets: ['latin'] })
|
||||||
|
|
||||||
|
|
@ -26,9 +25,7 @@ export default function RootLayout({
|
||||||
</head>
|
</head>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<MantineProvider>
|
<MantineProvider>
|
||||||
<TrpcProvider>
|
|
||||||
{children}
|
{children}
|
||||||
</TrpcProvider>
|
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ export enum AuthErrorCode {
|
||||||
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
|
EMAIL_NOT_FOUND = "EMAIL_NOT_FOUND",
|
||||||
EMPTY_USER_HASH = "EMPTY_USER_HASH",
|
EMPTY_USER_HASH = "EMPTY_USER_HASH",
|
||||||
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
INVALID_CREDENTIALS = "INVALID_CREDENTIALS",
|
||||||
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS"
|
JWT_SECRET_EMPTY = "JWT_SECRET_NOT_EMPTY",
|
||||||
|
USER_ALREADY_EXISTS = "USER_ALREADY_EXISTS",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AuthError extends BaseError {
|
export default class AuthError extends BaseError {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
"use server"
|
||||||
import prisma from "@/db";
|
import prisma from "@/db";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import AuthError, { AuthErrorCode } from "../AuthError";
|
import AuthError, { AuthErrorCode } from "../AuthError";
|
||||||
import { comparePassword } from "../authUtils";
|
import { comparePassword, createJwtToken } from "../authUtils";
|
||||||
|
import { cookies } from "next/headers";
|
||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the user by their email and password.
|
* Validates the user by their email and password.
|
||||||
|
|
@ -13,22 +16,55 @@ import { comparePassword } from "../authUtils";
|
||||||
* @returns The authenticated user object.
|
* @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.
|
* @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> {
|
export default async function signIn(prevState: any, rawFormData: FormData) {
|
||||||
// Retrieve user from the database by email
|
//TODO: Add Throttling
|
||||||
const user = await prisma.user.findUnique({
|
try {
|
||||||
where: { email }
|
const formData = {
|
||||||
});
|
email: rawFormData.get("email") as string,
|
||||||
|
password: rawFormData.get("password") as string
|
||||||
|
}
|
||||||
|
|
||||||
// Throw if user not found
|
// Retrieve user from the database by email
|
||||||
if (!user) throw new AuthError(AuthErrorCode.EMAIL_NOT_FOUND, 401);
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { email: formData.email }
|
||||||
|
});
|
||||||
|
|
||||||
// Throw if user has no password hash
|
// Throw if user not found
|
||||||
// TODO: Add check if the user uses another provider
|
if (!user) throw new AuthError(AuthErrorCode.EMAIL_NOT_FOUND, 401);
|
||||||
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;
|
// 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(formData.password, user.passwordHash);
|
||||||
|
if (!isMatch) throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401);
|
||||||
|
|
||||||
|
//Set cookie
|
||||||
|
//TODO: Auth: Add expiry
|
||||||
|
const token = createJwtToken({id: user.id});
|
||||||
|
|
||||||
|
cookies().set("token",token);
|
||||||
|
|
||||||
|
redirect("/dashboard");
|
||||||
|
|
||||||
|
} catch (e: unknown){
|
||||||
|
if (e instanceof AuthError){
|
||||||
|
if ([
|
||||||
|
AuthErrorCode.EMAIL_NOT_FOUND, AuthErrorCode.INVALID_CREDENTIALS
|
||||||
|
]) {
|
||||||
|
return {
|
||||||
|
errors: {
|
||||||
|
message: "Email/Password combination is not match. Please try again"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors: {
|
||||||
|
message: "There's something wrong happened on the server. Please try again or contact administrator"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { User } from "@prisma/client";
|
||||||
import * as bcrypt from "bcrypt";
|
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";
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
import UserClaims from "./types/UserClaims";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hashes a plain text password using bcrypt.
|
* Hashes a plain text password using bcrypt.
|
||||||
|
|
@ -24,3 +26,10 @@ export async function hashPassword(password: string): Promise<string> {
|
||||||
export async function comparePassword(password: string, hash: string): Promise<boolean> {
|
export async function comparePassword(password: string, hash: string): Promise<boolean> {
|
||||||
return bcrypt.compare(password, hash);
|
return bcrypt.compare(password, hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createJwtToken(userclaims: UserClaims, options?: jwt.SignOptions){
|
||||||
|
const secret = process.env.JWT_SECRET;
|
||||||
|
if (!secret) throw new AuthError(AuthErrorCode.JWT_SECRET_EMPTY);
|
||||||
|
const token = jwt.sign(userclaims, secret, options);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import NextAuth from "next-auth";
|
|
||||||
import emailPasswordProvider from "./providers/emailPasswordProvider";
|
|
||||||
import prisma from "@/db";
|
|
||||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
|
||||||
|
|
||||||
const nextAuth = NextAuth({
|
|
||||||
adapter: PrismaAdapter(prisma),
|
|
||||||
session: {
|
|
||||||
strategy: "jwt"
|
|
||||||
},
|
|
||||||
providers:[
|
|
||||||
emailPasswordProvider
|
|
||||||
],
|
|
||||||
callbacks: {
|
|
||||||
// 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"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const {
|
|
||||||
signIn,
|
|
||||||
signOut,
|
|
||||||
handlers: { GET, POST},
|
|
||||||
auth
|
|
||||||
} = nextAuth;
|
|
||||||
|
|
||||||
export default nextAuth;
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import CredentialsProvider from "next-auth/providers/credentials"
|
|
||||||
import AuthError, { AuthErrorCode } from "../AuthError";
|
|
||||||
import BaseError from "@/BaseError";
|
|
||||||
import signIn from "../actions/signIn";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Factory function to create a credential provider.
|
|
||||||
* It defines the structure of the credentials and includes an authorization function
|
|
||||||
* to validate the user's credentials.
|
|
||||||
*
|
|
||||||
* @returns A CredentialsProvider instance configured for email-password authentication.
|
|
||||||
*/
|
|
||||||
const credential = CredentialsProvider({
|
|
||||||
name: "email-password",
|
|
||||||
credentials: {
|
|
||||||
email: {
|
|
||||||
label: "Email",
|
|
||||||
type: "email",
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
label: "password",
|
|
||||||
type: "password"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
authorize: async (credentials) => {
|
|
||||||
try {
|
|
||||||
// Ensure credentials are properly formatted strings
|
|
||||||
if (typeof credentials.email !== "string" || typeof credentials.password !== "string"){
|
|
||||||
throw new AuthError(AuthErrorCode.INVALID_CREDENTIALS, 401);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate user with provided credentials
|
|
||||||
const user = await signIn(credentials.email, credentials.password);
|
|
||||||
return user;
|
|
||||||
} catch (e: unknown){
|
|
||||||
// Handle specific authentication errors, re-throw others
|
|
||||||
if (e instanceof AuthError){
|
|
||||||
// Auth invalid
|
|
||||||
if ([AuthErrorCode.EMAIL_NOT_FOUND, AuthErrorCode.EMPTY_USER_HASH, AuthErrorCode.INVALID_CREDENTIALS].includes(e.errorCode as AuthErrorCode))
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default credential;
|
|
||||||
7
src/features/auth/types/UserClaims.d.ts
vendored
Normal file
7
src/features/auth/types/UserClaims.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { User } from "@prisma/client"
|
||||||
|
|
||||||
|
type UserClaims = {
|
||||||
|
id: User["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserClaims
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { httpBatchLink, getFetch, loggerLink } from "@trpc/client";
|
|
||||||
import { useState } from "react";
|
|
||||||
import superjson from "superjson";
|
|
||||||
import { api as trpc } from "@/trpc/utils";
|
|
||||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
|
||||||
|
|
||||||
export const TrpcProvider: React.FC<{ children: React.ReactNode }> = ({
|
|
||||||
children,
|
|
||||||
}) => {
|
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
|
||||||
const [trpcClient] = useState(() =>
|
|
||||||
trpc.createClient({
|
|
||||||
links: [
|
|
||||||
httpBatchLink({
|
|
||||||
url: 'http://localhost:3000/api/trpc',
|
|
||||||
// You can pass any HTTP headers you wish here
|
|
||||||
async headers() {
|
|
||||||
return {
|
|
||||||
// authorization: getAuthCookie(),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
{children}
|
|
||||||
<ReactQueryDevtools />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</trpc.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
import { initTRPC } from '@trpc/server';
|
|
||||||
const t = initTRPC.create();
|
|
||||||
|
|
||||||
// Base router and procedure helpers
|
|
||||||
export const router = t.router;
|
|
||||||
export const procedure = t.procedure;
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { z } from 'zod';
|
|
||||||
import { procedure, router } from '..';
|
|
||||||
import authRouter from './auth';
|
|
||||||
|
|
||||||
export const appRouter = router({
|
|
||||||
hello: procedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
text: z.string(),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.query((opts) => {
|
|
||||||
return {
|
|
||||||
greeting: `hello ${opts.input.text}`,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
auth: authRouter,
|
|
||||||
});
|
|
||||||
|
|
||||||
// export type definition of API
|
|
||||||
export type AppRouter = typeof appRouter;
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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
|
|
||||||
.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;
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { httpBatchLink } from '@trpc/client';
|
|
||||||
import { createTRPCNext } from '@trpc/next';
|
|
||||||
import type { AppRouter } from './routes/_app';
|
|
||||||
import { createTRPCReact } from '@trpc/react-query';
|
|
||||||
|
|
||||||
function getBaseUrl() {
|
|
||||||
if (typeof window !== 'undefined')
|
|
||||||
// browser should use relative path
|
|
||||||
return '';
|
|
||||||
|
|
||||||
if (process.env.VERCEL_URL)
|
|
||||||
// reference for vercel.com
|
|
||||||
return `https://${process.env.VERCEL_URL}`;
|
|
||||||
|
|
||||||
if (process.env.RENDER_INTERNAL_HOSTNAME)
|
|
||||||
// reference for render.com
|
|
||||||
return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
|
|
||||||
|
|
||||||
// assume localhost
|
|
||||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const api = createTRPCReact<AppRouter>({
|
|
||||||
|
|
||||||
})
|
|
||||||
Loading…
Reference in New Issue
Block a user