diff --git a/.env b/.env
index 644eec4..591b242 100644
--- a/.env
+++ b/.env
@@ -1,3 +1,4 @@
+NEXT_PUBLIC_API_BASE_URL=
NEXT_PUBLIC_DOMAIN=
NEXT_PUBLIC_GOOGLE_CLIENT_ID=
NEXT_ANALYZE=false
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..860cc50
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+v18.17.1
diff --git a/package.json b/package.json
index f5ca674..da545fa 100644
--- a/package.json
+++ b/package.json
@@ -24,6 +24,7 @@
"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",
@@ -36,8 +37,11 @@
"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..5ff1ce6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5,6 +5,9 @@ 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)
@@ -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'}
@@ -9845,6 +9865,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 +10071,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 +11016,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 +11208,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 +11246,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 +11362,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==}
@@ -11872,6 +11922,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/(login)/login/page.tsx b/src/app/(login)/login/page.tsx
deleted file mode 100644
index 3409287..0000000
--- a/src/app/(login)/login/page.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import LoginPage from 'presentation/component/page/Login';
-
-const Login = () => {
- return ;
-};
-
-export default Login;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 927cc0f..940110c 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,7 +1,6 @@
import { PropsWithChildren } from 'react';
import { Inter, Kodchasan } from 'next/font/google';
import clsx from 'clsx';
-import GoogleOAuthProvider from './providers/GoogleOAuthProvider';
import './globals.css';
const kodchasan = Kodchasan({ subsets: ['latin'], variable: '--font-title', weight: ['600'] });
@@ -18,9 +17,7 @@ export default function RootLayout(props: PropsWithChildren) {
return (
-
- {children}
-
+ {children}
);
diff --git a/src/app/sign-in/page.tsx b/src/app/sign-in/page.tsx
new file mode 100644
index 0000000..ad64630
--- /dev/null
+++ b/src/app/sign-in/page.tsx
@@ -0,0 +1,7 @@
+import SignInPage from 'presentation/component/page/SignIn';
+
+const SignIn = () => {
+ return ;
+};
+
+export default SignIn;
diff --git a/src/app/sign-up/page.tsx b/src/app/sign-up/page.tsx
new file mode 100644
index 0000000..886d88f
--- /dev/null
+++ b/src/app/sign-up/page.tsx
@@ -0,0 +1,7 @@
+import SignUpPage from 'presentation/component/page/SignUp';
+
+const SignUp = () => {
+ return ;
+};
+
+export default SignUp;
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..919ffa2
--- /dev/null
+++ b/src/data/driver/validation/auth/messages.ts
@@ -0,0 +1,7 @@
+import getPlural from 'helper/string/getPlural';
+
+export const requiredField = 'Поле обязательно для заполнения';
+export const minSymbols = ({ min }: { min: number }) =>
+ `Минимум ${min} ${getPlural(min, ['символ', 'символа', 'символов'])}`;
+export const maxSymbols = ({ max }: { max: number }) =>
+ `Максимум ${max} ${getPlural(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..5b6cdad
--- /dev/null
+++ b/src/data/driver/validation/auth/schema.ts
@@ -0,0 +1,30 @@
+import { object, ObjectSchema, ref, string } from 'yup';
+import { SignInForm, SignUpForm } 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 signInFormValidationSchema: ObjectSchema = object({
+ email: emailValidationSchema,
+ password: passwordValidationSchema,
+});
+
+export const signUpFormValidationSchema: ObjectSchema = object({
+ email: emailValidationSchema,
+ password: passwordValidationSchema,
+ confirmPassword: passwordValidationSchema.oneOf([ref('password')], 'Пароли должны совпадать'),
+});
diff --git a/src/data/driver/validation/auth/types.ts b/src/data/driver/validation/auth/types.ts
new file mode 100644
index 0000000..2ee5b68
--- /dev/null
+++ b/src/data/driver/validation/auth/types.ts
@@ -0,0 +1,10 @@
+export interface SignUpForm {
+ email: string;
+ password: string;
+ confirmPassword: string;
+}
+
+export interface SignInForm {
+ 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