diff --git a/.eslintrc.cjs b/.eslintrc.cjs index cb965c2..3bc3d6e 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -38,7 +38,7 @@ module.exports = { { name: "react-redux", importNames: ["useSelector", "useStore", "useDispatch"], - message: "Please use pre-typed versions from `src/app/hooks.ts` instead.", + message: "Please use pre-typed versions from `src/hooks/store.ts` instead.", }, ], }, diff --git a/src/app/services/api.ts b/src/app/services/api.ts new file mode 100644 index 0000000..8e677bc --- /dev/null +++ b/src/app/services/api.ts @@ -0,0 +1,20 @@ +import { createApi, fetchBaseQuery, retry } from "@reduxjs/toolkit/query/react"; +import type { RootState } from "../store"; + +import type { Wrap } from "./base"; + +const baseQuery = fetchBaseQuery({ + baseUrl: import.meta.env.MODE === "production" ? "/api/v1" : "http://127.0.0.1:8000/api/v1", + prepareHeaders: (headers, { getState }) => { + const token = (getState() as RootState).auth.token; + if (token) headers.set("Authorization", `Bearer ${token}`); + return headers; + }, + validateStatus: (response, result: Wrap) => response.status === 200 && result.code === 0, +}); + +export const api = createApi({ + baseQuery: retry(baseQuery, { maxRetries: 6 }), + tagTypes: ["User", "Status", "Submission"], + endpoints: () => ({}), +}); diff --git a/src/app/services/base.ts b/src/app/services/base.ts new file mode 100644 index 0000000..e4af929 --- /dev/null +++ b/src/app/services/base.ts @@ -0,0 +1,17 @@ +export interface Wrap { + code: number; + msg: string; + body: T; +} + +export interface WithCount { + count: number; + data: T[]; +} + +export interface Meta { + ID: number; + CreatedAt: Date; + UpdatedAt: Date; + DeletedAt: Date; +} diff --git a/src/app/services/user.ts b/src/app/services/user.ts new file mode 100644 index 0000000..567959f --- /dev/null +++ b/src/app/services/user.ts @@ -0,0 +1,66 @@ +import type { Meta, Wrap } from "./base"; +import { api } from "./api"; + +export enum UserRole { + RoleAdmin = 30, + RoleUser = 20, + RoleGuest = 10, +} + +export interface UserRequest { + email?: string; + password?: string; + nickname?: string; + uid?: number; +} + +export interface UserLoginResponse { + nickname: string; + token: string; +} + +export interface UserProfile { + meta: Meta; + email: string; + nick_name: string; + role: number; + is_enabled: boolean; +} + +export const userApi = api.injectEndpoints({ + endpoints: (builder) => ({ + create: builder.mutation, UserRequest>({ + query: (data: UserRequest) => ({ + url: "/user/create", + method: "POST", + body: data, + }), + invalidatesTags: ["User", "Status", "Submission"], + }), + login: builder.mutation, UserRequest>({ + query: (data: UserRequest) => ({ + url: "/user/login", + method: "POST", + body: data, + }), + invalidatesTags: ["User", "Status", "Submission"], + }), + logout: builder.mutation, void>({ + query: () => ({ + url: "/user/logout", + method: "POST", + }), + invalidatesTags: ["User", "Status", "Submission"], + }), + profile: builder.query, UserRequest>({ + query: (data: UserRequest) => ({ + url: "/user/profile", + method: "POST", + body: data, + }), + providesTags: ["User"], + }), + }), +}); + +export const { useCreateMutation, useLoginMutation, useLogoutMutation, useProfileQuery } = userApi; diff --git a/src/app/store.ts b/src/app/store.ts index dc555f7..f8100e7 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -2,17 +2,23 @@ import type { Action, ThunkAction } from "@reduxjs/toolkit"; import { combineSlices, configureStore } from "@reduxjs/toolkit"; import { setupListeners } from "@reduxjs/toolkit/query"; +import { userApi } from "./services/user"; + import { counterSlice } from "../features/counter/counterSlice"; import { quotesApiSlice } from "../features/quotes/quotesApiSlice"; -const rootReducer = combineSlices(counterSlice, quotesApiSlice); +const dataSlices = [counterSlice]; +const middlewareSlices = [quotesApiSlice, userApi]; +const slices = [...dataSlices, ...middlewareSlices]; + +const rootReducer = combineSlices(...slices); export type RootState = ReturnType; export const makeStore = (preloadedState?: Partial) => { const store = configureStore({ reducer: rootReducer, middleware: (getDefaultMiddleware) => { - return getDefaultMiddleware().concat(quotesApiSlice.middleware); + return getDefaultMiddleware().concat(middlewareSlices.map((x) => x.middleware)); }, preloadedState, }); diff --git a/src/features/counter/Counter.tsx b/src/features/counter/Counter.tsx index aace700..6be7255 100644 --- a/src/features/counter/Counter.tsx +++ b/src/features/counter/Counter.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { useAppDispatch, useAppSelector } from "../../app/hooks"; +import { useAppDispatch, useAppSelector } from "../../hooks/store"; import styles from "./Counter.module.css"; import { decrement, diff --git a/src/app/hooks.ts b/src/hooks/store.ts similarity index 80% rename from src/app/hooks.ts rename to src/hooks/store.ts index 90c9339..bf229e9 100644 --- a/src/app/hooks.ts +++ b/src/hooks/store.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-restricted-imports */ import { useDispatch, useSelector } from "react-redux"; -import type { AppDispatch, RootState } from "./store"; +import type { AppDispatch, RootState } from "../app/store"; export const useAppDispatch = useDispatch.withTypes(); export const useAppSelector = useSelector.withTypes();