diff --git a/.env b/.env
index 644eec4..136c492 100644
--- a/.env
+++ b/.env
@@ -1,3 +1,7 @@
+NEXT_PUBLIC_API_BASE_URL=
NEXT_PUBLIC_DOMAIN=
-NEXT_PUBLIC_GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_ID=
+GOOGLE_CLIENT_SECRET=
+NEXTAUTH_SECRET=
+NEXTAUTH_URL=
NEXT_ANALYZE=false
diff --git a/package.json b/package.json
index f5ca674..c80e4f8 100644
--- a/package.json
+++ b/package.json
@@ -24,8 +24,8 @@
"pnpm": "^8.7.0"
},
"dependencies": {
+ "@hookform/resolvers": "^3.3.2",
"@phosphor-icons/react": "^2.0.14",
- "@react-oauth/google": "^0.11.1",
"@tw-classed/react": "^1.6.1",
"@types/node": "18.15.11",
"@types/react": "18.0.32",
@@ -33,11 +33,15 @@
"axios": "^1.6.1",
"clsx": "^2.0.0",
"next": "13.4.19",
+ "next-auth": "^4.24.5",
"polished": "^4.2.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-hook-form": "^7.48.2",
+ "swr": "^2.2.4",
"tailwindcss": "0.0.0-insiders.803d7b5",
"typescript": "5.0.3",
+ "yup": "^1.3.2",
"zustand": "^4.4.6"
},
"devDependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3e41143..eadb5bd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,12 +5,12 @@ settings:
excludeLinksFromLockfile: false
dependencies:
+ '@hookform/resolvers':
+ specifier: ^3.3.2
+ version: 3.3.2(react-hook-form@7.48.2)
'@phosphor-icons/react':
specifier: ^2.0.14
version: 2.0.14(react-dom@18.2.0)(react@18.2.0)
- '@react-oauth/google':
- specifier: ^0.11.1
- version: 0.11.1(react-dom@18.2.0)(react@18.2.0)
'@tw-classed/react':
specifier: ^1.6.1
version: 1.6.1(react@18.2.0)
@@ -32,6 +32,9 @@ dependencies:
next:
specifier: 13.4.19
version: 13.4.19(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
+ next-auth:
+ specifier: ^4.24.5
+ version: 4.24.5(next@13.4.19)(react-dom@18.2.0)(react@18.2.0)
polished:
specifier: ^4.2.2
version: 4.2.2
@@ -41,12 +44,21 @@ dependencies:
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
+ react-hook-form:
+ specifier: ^7.48.2
+ version: 7.48.2(react@18.2.0)
+ swr:
+ specifier: ^2.2.4
+ version: 2.2.4(react@18.2.0)
tailwindcss:
specifier: 0.0.0-insiders.803d7b5
version: 0.0.0-insiders.803d7b5
typescript:
specifier: 5.0.3
version: 5.0.3
+ yup:
+ specifier: ^1.3.2
+ version: 1.3.2
zustand:
specifier: ^4.4.6
version: 4.4.6(@types/react@18.0.32)(react@18.2.0)
@@ -1838,6 +1850,14 @@ packages:
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
dev: true
+ /@hookform/resolvers@3.3.2(react-hook-form@7.48.2):
+ resolution: {integrity: sha512-Tw+GGPnBp+5DOsSg4ek3LCPgkBOuOgS5DsDV7qsWNH9LZc433kgsWICjlsh2J9p04H2K66hsXPPb9qn9ILdUtA==}
+ peerDependencies:
+ react-hook-form: ^7.0.0
+ dependencies:
+ react-hook-form: 7.48.2(react@18.2.0)
+ dev: false
+
/@humanwhocodes/config-array@0.11.13:
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
engines: {node: '>=10.10.0'}
@@ -2114,6 +2134,10 @@ packages:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.15.0
+ /@panva/hkdf@1.1.1:
+ resolution: {integrity: sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==}
+ dev: false
+
/@phosphor-icons/react@2.0.14(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-VaZ7/JEQ7dW+Up23l7t6lqJ3dPJupM03916Pat+ZOLX1vex9OeX9t8RZLJWt0oVrdc/GcrAyRD5FESDeP+M4tQ==}
engines: {node: '>=10'}
@@ -2731,16 +2755,6 @@ packages:
'@babel/runtime': 7.23.2
dev: true
- /@react-oauth/google@0.11.1(react-dom@18.2.0)(react@18.2.0):
- resolution: {integrity: sha512-tywZisXbsdaRBVbEu0VX6dRbOSL2I6DgY97woq5NMOOOz+xtDsm418vqq+Vx10KMtra3kdHMRMf0hXLWrk2RMg==}
- peerDependencies:
- react: '>=16.8.0'
- react-dom: '>=16.8.0'
- dependencies:
- react: 18.2.0
- react-dom: 18.2.0(react@18.2.0)
- dev: false
-
/@rushstack/eslint-patch@1.5.1:
resolution: {integrity: sha512-6i/8UoL0P5y4leBIGzvkZdS85RDMG9y1ihZzmTZQ5LdHUYmZ7pKFoj8X0236s3lusPs1Fa5HTQUpwI+UfTcmeA==}
dev: true
@@ -5870,7 +5884,6 @@ packages:
/cookie@0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
- dev: true
/core-js-compat@3.33.2:
resolution: {integrity: sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw==}
@@ -8330,6 +8343,10 @@ packages:
resolution: {integrity: sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==}
hasBin: true
+ /jose@4.15.4:
+ resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==}
+ dev: false
+
/js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -8739,7 +8756,6 @@ packages:
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
- dev: true
/lz-string@1.5.0:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
@@ -9019,6 +9035,31 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: true
+ /next-auth@4.24.5(next@13.4.19)(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==}
+ peerDependencies:
+ next: ^12.2.5 || ^13 || ^14
+ nodemailer: ^6.6.5
+ react: ^17.0.2 || ^18
+ react-dom: ^17.0.2 || ^18
+ peerDependenciesMeta:
+ nodemailer:
+ optional: true
+ dependencies:
+ '@babel/runtime': 7.23.2
+ '@panva/hkdf': 1.1.1
+ cookie: 0.5.0
+ jose: 4.15.4
+ next: 13.4.19(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0)
+ oauth: 0.9.15
+ openid-client: 5.6.1
+ preact: 10.19.2
+ preact-render-to-string: 5.2.6(preact@10.19.2)
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ uuid: 8.3.2
+ dev: false
+
/next@13.4.19(@babel/core@7.23.2)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-HuPSzzAbJ1T4BD8e0bs6B9C1kWQ6gv8ykZoRWs5AQoiIuqbGHHdQO7Ljuvg05Q0Z24E2ABozHe6FxDvI6HfyAw==}
engines: {node: '>=16.8.0'}
@@ -9171,10 +9212,19 @@ packages:
boolbase: 1.0.0
dev: true
+ /oauth@0.9.15:
+ resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
+ dev: false
+
/object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ /object-hash@2.2.0:
+ resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
+ engines: {node: '>= 6'}
+ dev: false
+
/object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@@ -9254,6 +9304,11 @@ packages:
resolution: {integrity: sha512-eJJDYkhJFFbBBAxeh8xW+weHlkI28n2ZdQV/J/DNfWfSKlGEf2xcfAbZTv3riEXHAhL9SVOTs2pRmXiSTf78xg==}
dev: true
+ /oidc-token-hash@5.0.3:
+ resolution: {integrity: sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==}
+ engines: {node: ^10.13.0 || >=12.0.0}
+ dev: false
+
/on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@@ -9294,6 +9349,15 @@ packages:
is-wsl: 2.2.0
dev: true
+ /openid-client@5.6.1:
+ resolution: {integrity: sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ==}
+ dependencies:
+ jose: 4.15.4
+ lru-cache: 6.0.0
+ object-hash: 2.2.0
+ oidc-token-hash: 5.0.3
+ dev: false
+
/optionator@0.9.3:
resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==}
engines: {node: '>= 0.8.0'}
@@ -9722,6 +9786,19 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
+ /preact-render-to-string@5.2.6(preact@10.19.2):
+ resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
+ peerDependencies:
+ preact: '>=10'
+ dependencies:
+ preact: 10.19.2
+ pretty-format: 3.8.0
+ dev: false
+
+ /preact@10.19.2:
+ resolution: {integrity: sha512-UA9DX/OJwv6YwP9Vn7Ti/vF80XL+YA5H2l7BpCtUr3ya8LWHFzpiO5R+N7dN16ujpIxhekRFuOOF82bXX7K/lg==}
+ dev: false
+
/prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@@ -9810,6 +9887,10 @@ packages:
react-is: 17.0.2
dev: true
+ /pretty-format@3.8.0:
+ resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
+ dev: false
+
/pretty-hrtime@1.0.3:
resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==}
engines: {node: '>= 0.8'}
@@ -9845,6 +9926,10 @@ packages:
react-is: 16.13.1
dev: true
+ /property-expr@2.0.6:
+ resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
+ dev: false
+
/proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
@@ -10047,6 +10132,15 @@ packages:
react-is: 18.1.0
dev: true
+ /react-hook-form@7.48.2(react@18.2.0):
+ resolution: {integrity: sha512-H0T2InFQb1hX7qKtDIZmvpU1Xfn/bdahWBN1fH19gSe4bBEqTfmlr7H3XWTaVtiK4/tpPaI1F3355GPMZYge+A==}
+ engines: {node: '>=12.22.0'}
+ peerDependencies:
+ react: ^16.8.0 || ^17 || ^18
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/react-inspector@6.0.2(react@18.2.0):
resolution: {integrity: sha512-x+b7LxhmHXjHoU/VrFAzw5iutsILRoYyDq97EDYdFpPLcvqtEzk4ZSZSQjnFPbr5T57tLXnHcqFYoN1pI6u8uQ==}
peerDependencies:
@@ -10983,6 +11077,16 @@ packages:
webpack: 5.89.0(@swc/core@1.3.95)(esbuild@0.18.20)
dev: true
+ /swr@2.2.4(react@18.2.0):
+ resolution: {integrity: sha512-njiZ/4RiIhoOlAaLYDqwz5qH/KZXVilRLvomrx83HjzCWTfa+InyfAjv05PSFxnmLzZkNO9ZfvgoqzAaEI4sGQ==}
+ peerDependencies:
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ client-only: 0.0.1
+ react: 18.2.0
+ use-sync-external-store: 1.2.0(react@18.2.0)
+ dev: false
+
/synchronous-promise@2.0.17:
resolution: {integrity: sha512-AsS729u2RHUfEra9xJrE39peJcc2stq2+poBXX8bcM08Y6g9j/i/PUzwNQqkaJde7Ntg1TO7bSREbR5sdosQ+g==}
dev: true
@@ -11165,6 +11269,10 @@ packages:
setimmediate: 1.0.5
dev: true
+ /tiny-case@1.0.3:
+ resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
+ dev: false
+
/tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
dev: true
@@ -11199,6 +11307,10 @@ packages:
engines: {node: '>=0.6'}
dev: true
+ /toposort@2.0.2:
+ resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
+ dev: false
+
/tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: true
@@ -11311,7 +11423,6 @@ packages:
/type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
- dev: true
/type-is@1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
@@ -11553,6 +11664,11 @@ packages:
engines: {node: '>= 0.4.0'}
dev: true
+ /uuid@8.3.2:
+ resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ hasBin: true
+ dev: false
+
/uuid@9.0.1:
resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==}
hasBin: true
@@ -11826,7 +11942,6 @@ packages:
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
- dev: true
/yaml@1.10.2:
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
@@ -11872,6 +11987,15 @@ packages:
engines: {node: '>=12.20'}
dev: true
+ /yup@1.3.2:
+ resolution: {integrity: sha512-6KCM971iQtJ+/KUaHdrhVr2LDkfhBtFPRnsG1P8F4q3uUVQ2RfEM9xekpha9aA4GXWJevjM10eDcPQ1FfWlmaQ==}
+ dependencies:
+ property-expr: 2.0.6
+ tiny-case: 1.0.3
+ toposort: 2.0.2
+ type-fest: 2.19.0
+ dev: false
+
/zod@3.21.4:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
diff --git a/src/app/(authorized)/layout.tsx b/src/app/(authorized)/layout.tsx
index a3acbd7..57354c4 100644
--- a/src/app/(authorized)/layout.tsx
+++ b/src/app/(authorized)/layout.tsx
@@ -1,8 +1,12 @@
+'use client';
+
import { PropsWithChildren } from 'react';
+import RoleEnum from 'domain/entity/RoleEnum';
import Sidebar from 'presentation/component/layout/Sidebar';
import AuthorizedHeader from 'presentation/component/layout/AuthorizedHeader';
+import createAuthorizedLayout from 'presentation/component/layout/AuthorizedLayout';
-export default function AuthorizedLayout(props: PropsWithChildren) {
+const BaseAuthorizedLayout = (props: PropsWithChildren) => {
const { children } = props;
return (
@@ -14,4 +18,8 @@ export default function AuthorizedLayout(props: PropsWithChildren) {
);
-}
+};
+
+export default createAuthorizedLayout(BaseAuthorizedLayout, {
+ roles: [RoleEnum.User],
+});
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..8656176
--- /dev/null
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,13 @@
+import NextAuth from 'next-auth';
+import GoogleProvider from 'next-auth/providers/google';
+
+const handler = NextAuth({
+ providers: [
+ GoogleProvider({
+ clientId: process.env.GOOGLE_CLIENT_ID ?? '',
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET ?? '',
+ }),
+ ],
+});
+
+export { handler as GET, handler as POST };
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 927cc0f..5df0715 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,7 +1,7 @@
import { PropsWithChildren } from 'react';
import { Inter, Kodchasan } from 'next/font/google';
import clsx from 'clsx';
-import GoogleOAuthProvider from './providers/GoogleOAuthProvider';
+import AuthProvider from './providers/AuthProvider';
import './globals.css';
const kodchasan = Kodchasan({ subsets: ['latin'], variable: '--font-title', weight: ['600'] });
@@ -18,9 +18,9 @@ export default function RootLayout(props: PropsWithChildren) {
return (
-
+
{children}
-
+
);
diff --git a/src/app/providers/AuthProvider/index.tsx b/src/app/providers/AuthProvider/index.tsx
new file mode 100644
index 0000000..6451c1f
--- /dev/null
+++ b/src/app/providers/AuthProvider/index.tsx
@@ -0,0 +1,8 @@
+'use client';
+
+import { ReactNode } from 'react';
+import { SessionProvider } from 'next-auth/react';
+
+export default function AuthProvider({ children }: { children: ReactNode }) {
+ return {children};
+}
diff --git a/src/app/providers/GoogleOAuthProvider/index.tsx b/src/app/providers/GoogleOAuthProvider/index.tsx
deleted file mode 100644
index 546395a..0000000
--- a/src/app/providers/GoogleOAuthProvider/index.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-'use client';
-
-import { GoogleOAuthProvider as BaseGoogleOAuthProvider } from '@react-oauth/google';
-import { GOOGLE_CLIENT_ID } from 'constant/env';
-
-export default function GoogleOAuthProvider({ children }: { children: React.ReactNode }) {
- return (
- {children}
- );
-}
diff --git a/src/app/(login)/login/page.tsx b/src/app/sign-in/page.tsx
similarity index 100%
rename from src/app/(login)/login/page.tsx
rename to src/app/sign-in/page.tsx
diff --git a/src/constant/env.ts b/src/constant/env.ts
index d8fe56b..9d562d7 100644
--- a/src/constant/env.ts
+++ b/src/constant/env.ts
@@ -1,2 +1,3 @@
export const IS_PRODUCTION = process.env.NODE_ENV === 'production';
+export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? '';
export const GOOGLE_CLIENT_ID = process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID ?? '';
diff --git a/src/constant/httpCode.ts b/src/constant/httpCode.ts
new file mode 100644
index 0000000..b236365
--- /dev/null
+++ b/src/constant/httpCode.ts
@@ -0,0 +1,5 @@
+export const enum HttpCode {
+ Ok = 200,
+ Unauthorized = 401,
+ Forbidden = 403,
+}
diff --git a/src/constant/route.ts b/src/constant/route.ts
index ffa0db1..1c909ef 100644
--- a/src/constant/route.ts
+++ b/src/constant/route.ts
@@ -1,12 +1,12 @@
+export const SIGN_UP = '/sign-up';
+export const SIGN_IN = '/sign-in';
+
export const HOME = '/';
export const NEWS = '/news';
export const DASHBOARD = '/dashboard';
export const STUDY = '/study';
export const SETTINGS = '/settings';
-export const SIGN_UP = '/sign-up';
-export const SIGN_IN = '/sign-in';
-
/**
* Dashboard sidebar items
*/
@@ -26,3 +26,11 @@ export const DICTIONARY = `${DASHBOARD}/dictionary`;
export const PROFILE = `${SETTINGS}/profile`;
export const SECURITY = `${SETTINGS}/security`;
export const ADVANCED = `${SETTINGS}/advanced`;
+
+/**
+ * API routes
+ */
+const USER = (url: string) => `/user/${url}`;
+export const GET_USER = USER('');
+export const USER_LOGIN = USER('login');
+export const USER_REFRESH = USER('refresh');
diff --git a/src/data/driver/ApiClient/AbstractApiClient.ts b/src/data/driver/ApiClient/AbstractApiClient.ts
new file mode 100644
index 0000000..39bd827
--- /dev/null
+++ b/src/data/driver/ApiClient/AbstractApiClient.ts
@@ -0,0 +1,26 @@
+import type { AxiosInstance } from 'axios';
+import REST from './REST';
+
+export default abstract class AbstractApiClient {
+ public abstract rest: REST;
+
+ private accessToken?: string;
+
+ public setAccessToken(token?: string) {
+ this.accessToken = token;
+ }
+
+ public getAccessToken() {
+ return this.accessToken;
+ }
+
+ protected useCredentialsInterceptor(client: AxiosInstance) {
+ client.interceptors.request.use((request) => {
+ if (this.accessToken && request.headers) {
+ request.headers.Authorization = `Bearer ${this.accessToken}`;
+ }
+
+ return request;
+ }, Promise.reject);
+ }
+}
diff --git a/src/data/driver/ApiClient/FrontendApiClient.ts b/src/data/driver/ApiClient/FrontendApiClient.ts
new file mode 100644
index 0000000..8d96d36
--- /dev/null
+++ b/src/data/driver/ApiClient/FrontendApiClient.ts
@@ -0,0 +1,14 @@
+import { API_BASE_URL } from 'constant/env';
+import AbstractApiClient from './AbstractApiClient';
+import REST from './REST';
+
+export default class FrontendApiClient extends AbstractApiClient {
+ public readonly rest: REST;
+
+ constructor() {
+ super();
+ this.rest = new REST(`${API_BASE_URL}/api`);
+
+ this.useCredentialsInterceptor(this.rest.client);
+ }
+}
diff --git a/src/data/driver/ApiClient/REST.ts b/src/data/driver/ApiClient/REST.ts
new file mode 100644
index 0000000..e6bb33f
--- /dev/null
+++ b/src/data/driver/ApiClient/REST.ts
@@ -0,0 +1,13 @@
+import AbstractAxiosClient from './axios';
+
+export default class REST extends AbstractAxiosClient {
+ public post = this._client.post;
+
+ public postForm = this._client.postForm;
+
+ public get = this._client.get;
+
+ public delete = this._client.delete;
+
+ public patch = this._client.patch;
+}
diff --git a/src/data/driver/ApiClient/axios.ts b/src/data/driver/ApiClient/axios.ts
new file mode 100644
index 0000000..d4acaf5
--- /dev/null
+++ b/src/data/driver/ApiClient/axios.ts
@@ -0,0 +1,32 @@
+import axios, { AxiosInstance } from 'axios';
+
+const TIMEOUT_ERROR_MESSAGE = 'TimeoutError';
+
+export default abstract class AbstractAxiosClient {
+ protected readonly _client: AxiosInstance;
+
+ public constructor(apiBaseURL: string) {
+ this._client = axios.create({
+ baseURL: apiBaseURL,
+ timeout: 30000,
+ timeoutErrorMessage: TIMEOUT_ERROR_MESSAGE,
+ });
+
+ this.useTimeoutErrorInterceptor();
+ }
+
+ private useTimeoutErrorInterceptor(): void {
+ this._client.interceptors.response.use(undefined, (error) => {
+ if (error?.message === TIMEOUT_ERROR_MESSAGE) {
+ // eslint-disable-next-line no-console
+ console.log(JSON.stringify({ TIMEOUT_ERROR_MESSAGE, error }, null, 2));
+ }
+
+ throw error;
+ });
+ }
+
+ public get client(): AxiosInstance {
+ return this._client;
+ }
+}
diff --git a/src/data/driver/ApiClient/frontend.ts b/src/data/driver/ApiClient/frontend.ts
new file mode 100644
index 0000000..712839d
--- /dev/null
+++ b/src/data/driver/ApiClient/frontend.ts
@@ -0,0 +1,3 @@
+import FrontendApiClient from './FrontendApiClient';
+
+export const frontendApiClient = new FrontendApiClient();
diff --git a/src/data/driver/validation/auth/messages.ts b/src/data/driver/validation/auth/messages.ts
new file mode 100644
index 0000000..1faa1c4
--- /dev/null
+++ b/src/data/driver/validation/auth/messages.ts
@@ -0,0 +1,7 @@
+import getStringByNumber from 'helper/string/getStringByNumber';
+
+export const requiredField = 'Поле обязательно для заполнения';
+export const minSymbols = ({ min }: { min: number }) =>
+ `Минимум ${min} ${getStringByNumber(min, ['символ', 'символа', 'символов'])}`;
+export const maxSymbols = ({ max }: { max: number }) =>
+ `Максимум ${max} ${getStringByNumber(max, ['символ', 'символа', 'символов'])}`;
diff --git a/src/data/driver/validation/auth/schema.ts b/src/data/driver/validation/auth/schema.ts
new file mode 100644
index 0000000..8d5b205
--- /dev/null
+++ b/src/data/driver/validation/auth/schema.ts
@@ -0,0 +1,24 @@
+import { object, ObjectSchema, string } from 'yup';
+import { LoginForm } from './types';
+import { requiredField } from './messages';
+
+const emailValidationSchema = string().required(requiredField);
+const passwordOptionalValidationSchema = string()
+ .matches(
+ /^[\w#?!@$%^&*-]+$|^$/i,
+ 'Password can only contain Latin letters, numbers and special characters',
+ )
+ .test('min-password-length', 'The password must contain at least 4 characters', (value) => {
+ if (!value) {
+ return true;
+ }
+
+ return value.length >= 4 || value.length === 0;
+ })
+ .defined();
+const passwordValidationSchema = passwordOptionalValidationSchema.required(requiredField);
+
+export const loginFormValidationSchema: ObjectSchema = object({
+ email: emailValidationSchema,
+ password: passwordValidationSchema,
+});
diff --git a/src/data/driver/validation/auth/types.ts b/src/data/driver/validation/auth/types.ts
new file mode 100644
index 0000000..85a202f
--- /dev/null
+++ b/src/data/driver/validation/auth/types.ts
@@ -0,0 +1,4 @@
+export interface LoginForm {
+ email: string;
+ password: string;
+}
diff --git a/src/domain/entity/RoleEnum.ts b/src/domain/entity/RoleEnum.ts
new file mode 100644
index 0000000..52e3fff
--- /dev/null
+++ b/src/domain/entity/RoleEnum.ts
@@ -0,0 +1,7 @@
+const enum RoleEnum {
+ Visitor = 'visitor',
+ User = 'user',
+ Admin = 'admin',
+}
+
+export default RoleEnum;
diff --git a/src/domain/entity/User.ts b/src/domain/entity/User.ts
index 9375e1d..c390fd8 100644
--- a/src/domain/entity/User.ts
+++ b/src/domain/entity/User.ts
@@ -1,11 +1,14 @@
+import RoleEnum from './RoleEnum';
+
export default class User {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public static Hydrate(data: any): User {
try {
return new User(
- data.id ?? NaN,
+ data.id ?? '',
data.name ?? '',
data.avatar ?? '',
+ data.visitor ?? [RoleEnum.Visitor],
data.lvl ?? '',
data.location ?? '',
data.email ?? '',
@@ -19,13 +22,14 @@ export default class User {
}
public static CreateEmpty(): User {
- return new User(NaN, '', '', '', '', '', NaN, NaN, NaN);
+ return new User('', '', '', [RoleEnum.Visitor], '', '', '', NaN, NaN, NaN);
}
constructor(
- public readonly id: number,
+ public readonly id: string,
public readonly name: string,
public readonly avatar: string,
+ public readonly roles: RoleEnum[],
public readonly lvl: string,
public readonly location: string,
public readonly email: string,
diff --git a/src/domain/service/auth/useLoginSWRMutation.ts b/src/domain/service/auth/useLoginSWRMutation.ts
new file mode 100644
index 0000000..6d2317a
--- /dev/null
+++ b/src/domain/service/auth/useLoginSWRMutation.ts
@@ -0,0 +1,40 @@
+import { GET_USER, USER_LOGIN } from 'constant/route';
+import type User from 'domain/entity/User';
+import { frontendApiClient } from 'data/driver/ApiClient/frontend';
+import useUserStore from 'domain/store/user/useUserStore';
+import useCustomSWRMutation from 'domain/service/useCustomSWRMutation';
+
+interface LoginPayload {
+ email: string;
+ password: string;
+}
+
+interface LoginResponse {
+ token: string;
+}
+
+const useLoginSWRMutation = () => {
+ const setUser = useUserStore((state) => state.setUser);
+ const { trigger, ...restProps } = useCustomSWRMutation(USER_LOGIN);
+
+ return {
+ ...restProps,
+ trigger: async (loginPayload: LoginPayload) => {
+ const loginResponse = await trigger(loginPayload);
+ const { headers } = loginResponse;
+ const { authorization } = headers;
+ frontendApiClient.setAccessToken(authorization);
+ const { data: user } = await frontendApiClient.rest.get(GET_USER);
+
+ if (user) {
+ setUser(user);
+
+ return;
+ }
+
+ frontendApiClient.setAccessToken(undefined);
+ },
+ };
+};
+
+export default useLoginSWRMutation;
diff --git a/src/domain/service/auth/useUserRefreshTokenSWR.ts b/src/domain/service/auth/useUserRefreshTokenSWR.ts
new file mode 100644
index 0000000..c9c7e0d
--- /dev/null
+++ b/src/domain/service/auth/useUserRefreshTokenSWR.ts
@@ -0,0 +1,31 @@
+import useSWRMutation from 'swr/mutation';
+import { GET_USER, USER_REFRESH } from 'constant/route';
+import User from 'domain/entity/User';
+import useUserStore from 'domain/store/user/useUserStore';
+import { frontendApiClient } from 'data/driver/ApiClient/frontend';
+
+interface RefreshResponse {
+ token: string;
+}
+
+export function useUserRefreshTokenSWR() {
+ const { setUser, getIsAuthorized } = useUserStore((state) => state);
+ const isAuthorized = getIsAuthorized();
+
+ return useSWRMutation(
+ () => (isAuthorized ? null : USER_REFRESH),
+ async (baseUrl) => {
+ const response = await frontendApiClient.rest.post(baseUrl);
+
+ const { headers } = response;
+ const { authorization } = headers;
+
+ frontendApiClient.setAccessToken(authorization);
+
+ const { data } = await frontendApiClient.rest.get(GET_USER);
+
+ setUser(data);
+ },
+ // TODO: error handling
+ );
+}
diff --git a/src/domain/service/useCustomSWRMutation.ts b/src/domain/service/useCustomSWRMutation.ts
new file mode 100644
index 0000000..0133a73
--- /dev/null
+++ b/src/domain/service/useCustomSWRMutation.ts
@@ -0,0 +1,14 @@
+import { AxiosResponse } from 'axios';
+import useSWRMutation from 'swr/mutation';
+import { frontendApiClient } from 'data/driver/ApiClient/frontend';
+
+const useCustomSWRMutation = (url: string) => {
+ return useSWRMutation, unknown, string, Input>(
+ url,
+ async (baseUrl, options) => {
+ return frontendApiClient.rest.post