mirror of
https://github.com/zuiidea/antd-admin
synced 2026-10-23 08:23:54 +00:00
refactor(api): split modules under shared/ with API_PREFIX
Move auth and users into folder modules, centralize path constants, and align MSW with endpoint paths in both template apps. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,14 +17,14 @@ vp check --no-fmt
|
||||
pnpm run test:e2e -- e2e/<slug>.spec.ts
|
||||
```
|
||||
|
||||
## 1. Zod and types (`src/api/schemas.ts`)
|
||||
## 1. Zod and types (`src/api/<slug>/schemas.ts`)
|
||||
|
||||
- Define `<Entity>Schema`, `Create<Entity>RequestSchema`, `Update<Entity>RequestSchema` (`Update` = `Create.partial()`).
|
||||
- For list `output`, reuse `PaginatedDataSchema(<Entity>Schema)` (unwrapped `data`; envelope stays in `request.ts`).
|
||||
- Add list query schema (e.g. `List<Entity>QuerySchema`) when the table has filters/pagination.
|
||||
- Export `type <Entity> = z.infer<typeof ...>`.
|
||||
|
||||
## 2. Resource API module (`src/api/<slug>.ts`)
|
||||
## 2. Resource API module (`src/api/<slug>/index.ts`)
|
||||
|
||||
- Export path constants if useful for MSW/docs.
|
||||
- Define APIs with `defineApi` from `@/api/request`: `list` / `create` / `update` / `delete` (and `queryKeys`).
|
||||
|
||||
@@ -37,7 +37,7 @@ Run from **this app root** (directory with `package.json` / `vite.config.ts`):
|
||||
|
||||
**Purpose:** **schemas → `defineApi` modules → UI**; transport in `src/api/request.ts` only.
|
||||
|
||||
**Do:** Define APIs with `defineApi` in `src/api/<resource>.ts` (`method` default `GET`; `params`/`query`/`body` + `output`). Zod in `src/api/schemas.ts`. MSW envelopes `{ code, data, message }` aligned with schemas. Co-locate query keys (e.g. `userQueryKeys`). `hydrateSession` from `@/api/auth` after login/register/session restore.
|
||||
**Do:** Define APIs with `defineApi` in `src/api/<module>/index.ts` (`method` default `GET`; `params`/`query`/`body` + `output`). Zod in `src/api/<module>/schemas.ts`; shared helpers in `src/api/shared/`. MSW envelopes `{ code, data, message }` aligned with schemas. Co-locate query keys (e.g. `userQueryKeys`). `hydrateSession` from `@/api/auth` after login/register/session restore.
|
||||
|
||||
**Do not:** Call axios or `@/api/request` from routes/components — use resource functions (`listUsers`, …). Ad-hoc response shapes; skip schema parsing; mix unrelated domains in one module.
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ pnpm run test:e2e:ui # interactive UI mode
|
||||
AGENTS.md # Agent rules (frontend/api/testing/refactor)
|
||||
.agents/skills/add-resource/ # CRUD scaffold checklist (Skill)
|
||||
src/
|
||||
├── api/ # schemas, request (axios + defineApi), resource APIs
|
||||
├── api/ # request.ts, shared/, auth/, users/, … (defineApi per module)
|
||||
├── components/ # Reusable UI primitives and layout shells
|
||||
│ ├── Layout/ # MainLayout, Header, Sidebar, UserMenu, AppFooter
|
||||
│ ├── DataTable/ # Shared table frame/skeleton/empty state
|
||||
|
||||
@@ -6,26 +6,19 @@ import {
|
||||
LoginRequestSchema,
|
||||
PermissionsListSchema,
|
||||
RegisterRequestSchema,
|
||||
UserSchema,
|
||||
} from "@/api/schemas";
|
||||
} from "@/api/auth/schemas";
|
||||
import type {
|
||||
AuthTokens,
|
||||
LoginRequest,
|
||||
User,
|
||||
PermissionsList,
|
||||
RegisterRequest,
|
||||
} from "@/api/schemas";
|
||||
} from "@/api/auth/schemas";
|
||||
import { UserSchema, type User } from "@/api/users/schemas";
|
||||
import { AUTH_ENDPOINTS } from "@/api/auth/paths";
|
||||
import { APP_MENU_TREE, filterMenuTreeByPermissions } from "@/utils/appMenu";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
export const AUTH_ENDPOINTS = {
|
||||
login: "/api/auth/login",
|
||||
register: "/api/auth/register",
|
||||
refresh: "/api/auth/refresh",
|
||||
logout: "/api/auth/logout",
|
||||
user: "/api/auth/user",
|
||||
permissions: "/api/auth/permissions",
|
||||
} as const;
|
||||
export { AUTH_ENDPOINTS } from "@/api/auth/paths";
|
||||
|
||||
export const login = defineApi({
|
||||
method: "POST",
|
||||
@@ -0,0 +1,12 @@
|
||||
import { API_PREFIX } from "@/api/shared/prefix";
|
||||
|
||||
const AUTH_BASE = `${API_PREFIX}/auth`;
|
||||
|
||||
export const AUTH_ENDPOINTS = {
|
||||
login: `${AUTH_BASE}/login`,
|
||||
register: `${AUTH_BASE}/register`,
|
||||
refresh: `${AUTH_BASE}/refresh`,
|
||||
logout: `${AUTH_BASE}/logout`,
|
||||
user: `${AUTH_BASE}/user`,
|
||||
permissions: `${AUTH_BASE}/permissions`,
|
||||
} as const;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { z } from "zod/v4";
|
||||
import { UserSchema } from "@/api/users/schemas";
|
||||
|
||||
/** GET `/api/auth/user` body (no `permissions`; load via `/api/auth/permissions`). */
|
||||
export const AuthUserResponseSchema = UserSchema.omit({ permissions: true });
|
||||
export type AuthUserResponse = z.infer<typeof AuthUserResponseSchema>;
|
||||
|
||||
export const AuthTokensSchema = z.object({
|
||||
accessToken: z.string().min(1),
|
||||
refreshToken: z.string().min(1),
|
||||
});
|
||||
|
||||
export type AuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
||||
export const RefreshTokenRequestSchema = z.object({
|
||||
refreshToken: z.string().min(1),
|
||||
});
|
||||
|
||||
export type RefreshTokenRequest = z.infer<typeof RefreshTokenRequestSchema>;
|
||||
|
||||
export const LoginRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
|
||||
|
||||
/** Ant Design Form may submit `""` for an empty optional email */
|
||||
const registerOptionalEmailSchema = z
|
||||
.union([z.string(), z.undefined()])
|
||||
.transform((v) => {
|
||||
if (v === undefined) return undefined;
|
||||
const t = String(v).trim();
|
||||
return t === "" ? undefined : t;
|
||||
})
|
||||
.pipe(z.union([z.string().email(), z.undefined()]));
|
||||
|
||||
export const RegisterRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(6),
|
||||
email: registerOptionalEmailSchema,
|
||||
});
|
||||
|
||||
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>;
|
||||
|
||||
export const PermissionsListSchema = z.array(z.string());
|
||||
|
||||
export type PermissionsList = z.infer<typeof PermissionsListSchema>;
|
||||
@@ -1,11 +1,13 @@
|
||||
import axios, { type AxiosError, type InternalAxiosRequestConfig } from "axios";
|
||||
import { z } from "zod/v4";
|
||||
import { API_BASE_URL } from "@/utils/constants";
|
||||
import { AuthTokensSchema, RefreshTokenRequestSchema } from "@/api/schemas";
|
||||
import { AuthTokensSchema, RefreshTokenRequestSchema } from "@/api/auth/schemas";
|
||||
import { getAccessToken, useAuthStore } from "@/stores/auth";
|
||||
|
||||
/** Avoid importing `@/api/auth` here (circular with `defineApi` consumers). */
|
||||
const REFRESH_PATH = "/api/auth/refresh";
|
||||
import { AUTH_ENDPOINTS } from "@/api/auth/paths";
|
||||
|
||||
/** Avoid importing `@/api/auth` index here (circular with `defineApi` consumers). */
|
||||
const REFRESH_PATH = AUTH_ENDPOINTS.refresh;
|
||||
|
||||
export class ApiError extends Error {
|
||||
code: number;
|
||||
|
||||
@@ -1,180 +1,4 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
avatar: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
roles: z.array(z.string()),
|
||||
permissions: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
/** GET `/api/auth/user` body (no `permissions`; load via `/api/auth/permissions`). */
|
||||
export const AuthUserResponseSchema = UserSchema.omit({ permissions: true });
|
||||
export type AuthUserResponse = z.infer<typeof AuthUserResponseSchema>;
|
||||
|
||||
export const AuthTokensSchema = z.object({
|
||||
accessToken: z.string().min(1),
|
||||
refreshToken: z.string().min(1),
|
||||
});
|
||||
|
||||
export type AuthTokens = z.infer<typeof AuthTokensSchema>;
|
||||
|
||||
export const RefreshTokenRequestSchema = z.object({
|
||||
refreshToken: z.string().min(1),
|
||||
});
|
||||
|
||||
export type RefreshTokenRequest = z.infer<typeof RefreshTokenRequestSchema>;
|
||||
|
||||
export const LoginRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
});
|
||||
|
||||
export type LoginRequest = z.infer<typeof LoginRequestSchema>;
|
||||
|
||||
/** Ant Design Form may submit `""` for an empty optional email */
|
||||
const registerOptionalEmailSchema = z
|
||||
.union([z.string(), z.undefined()])
|
||||
.transform((v) => {
|
||||
if (v === undefined) return undefined;
|
||||
const t = String(v).trim();
|
||||
return t === "" ? undefined : t;
|
||||
})
|
||||
.pipe(z.union([z.string().email(), z.undefined()]));
|
||||
|
||||
export const RegisterRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(6),
|
||||
email: registerOptionalEmailSchema,
|
||||
});
|
||||
|
||||
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>;
|
||||
|
||||
export const PermissionsListSchema = z.array(z.string());
|
||||
|
||||
export type PermissionsList = z.infer<typeof PermissionsListSchema>;
|
||||
|
||||
const BaseMenuNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
icon: z.string().nullable(),
|
||||
permissions: z.array(z.string()).nullable(),
|
||||
sort: z.int(),
|
||||
hidden: z.boolean(),
|
||||
});
|
||||
|
||||
export type MenuItem =
|
||||
| (z.infer<typeof BaseMenuNodeSchema> & {
|
||||
kind: "item";
|
||||
path: string;
|
||||
children: MenuItem[] | null;
|
||||
})
|
||||
| (z.infer<typeof BaseMenuNodeSchema> & {
|
||||
kind: "group";
|
||||
path: null;
|
||||
children: MenuItem[];
|
||||
});
|
||||
|
||||
export const MenuItemSchema: z.ZodType<MenuItem> = z.lazy(() =>
|
||||
z.discriminatedUnion("kind", [
|
||||
BaseMenuNodeSchema.extend({
|
||||
kind: z.literal("item"),
|
||||
path: z.string(),
|
||||
children: z.array(MenuItemSchema).nullable(),
|
||||
}),
|
||||
BaseMenuNodeSchema.extend({
|
||||
kind: z.literal("group"),
|
||||
path: z.null(),
|
||||
children: z.array(MenuItemSchema),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
export function ApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
||||
return z.object({
|
||||
code: z.int(),
|
||||
data: dataSchema,
|
||||
message: z.string(),
|
||||
});
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function PaginatedResponseSchema<T extends z.ZodTypeAny>(itemSchema: T) {
|
||||
return z.object({
|
||||
code: z.int(),
|
||||
data: z.object({
|
||||
list: z.array(itemSchema),
|
||||
total: z.int(),
|
||||
}),
|
||||
message: z.string(),
|
||||
});
|
||||
}
|
||||
|
||||
export type PaginatedData<T> = {
|
||||
list: T[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
/** Unwrapped list payload (envelope `data`); use as `defineApi` `output`. */
|
||||
export function PaginatedDataSchema<T extends z.ZodTypeAny>(itemSchema: T) {
|
||||
return z.object({
|
||||
list: z.array(itemSchema),
|
||||
total: z.int(),
|
||||
});
|
||||
}
|
||||
|
||||
export const ListUsersQuerySchema = z.object({
|
||||
limit: z.number().int().positive().optional(),
|
||||
offset: z.number().int().nonnegative().optional(),
|
||||
keyword: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
sortField: z.string().optional(),
|
||||
sortOrder: z.enum(["ascend", "descend"]).optional(),
|
||||
});
|
||||
|
||||
export type ListUsersQuery = z.infer<typeof ListUsersQuerySchema>;
|
||||
|
||||
export const IdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export type IdParams = z.infer<typeof IdParamsSchema>;
|
||||
|
||||
export const SearchParamsSchema = z.object({
|
||||
page: z.number().int().positive().catch(1),
|
||||
pageSize: z.number().int().positive().catch(10),
|
||||
sortField: z.string().nullable().catch(null),
|
||||
sortOrder: z.enum(["ascend", "descend"]).nullable().catch(null),
|
||||
});
|
||||
|
||||
export type SearchParams = z.infer<typeof SearchParamsSchema>;
|
||||
|
||||
/** Ant Design Input submits `""` when empty; treat as null for optional email */
|
||||
const createUserEmailSchema = z
|
||||
.union([z.string(), z.null(), z.undefined()])
|
||||
.transform((v) => {
|
||||
if (v == null) return null;
|
||||
const t = String(v).trim();
|
||||
return t === "" ? null : t;
|
||||
})
|
||||
.pipe(z.union([z.string().email(), z.null()]));
|
||||
|
||||
export const CreateUserRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
email: createUserEmailSchema,
|
||||
roles: z.array(z.string()).min(1),
|
||||
});
|
||||
|
||||
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
|
||||
|
||||
export const UpdateUserRequestSchema = CreateUserRequestSchema.partial();
|
||||
|
||||
export type UpdateUserRequest = z.infer<typeof UpdateUserRequestSchema>;
|
||||
/** @deprecated Import from `@/api/<module>/schemas` or `@/api/shared`; re-exports for compatibility. */
|
||||
export * from "@/api/shared";
|
||||
export * from "@/api/users/schemas";
|
||||
export * from "@/api/auth/schemas";
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export function ApiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
||||
return z.object({
|
||||
code: z.int(),
|
||||
data: dataSchema,
|
||||
message: z.string(),
|
||||
});
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = {
|
||||
code: number;
|
||||
data: T;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export function PaginatedResponseSchema<T extends z.ZodTypeAny>(itemSchema: T) {
|
||||
return z.object({
|
||||
code: z.int(),
|
||||
data: z.object({
|
||||
list: z.array(itemSchema),
|
||||
total: z.int(),
|
||||
}),
|
||||
message: z.string(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from "./envelope";
|
||||
export * from "./menu";
|
||||
export * from "./pagination";
|
||||
export * from "./params";
|
||||
export * from "./prefix";
|
||||
@@ -0,0 +1,37 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
const BaseMenuNodeSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
icon: z.string().nullable(),
|
||||
permissions: z.array(z.string()).nullable(),
|
||||
sort: z.int(),
|
||||
hidden: z.boolean(),
|
||||
});
|
||||
|
||||
export type MenuItem =
|
||||
| (z.infer<typeof BaseMenuNodeSchema> & {
|
||||
kind: "item";
|
||||
path: string;
|
||||
children: MenuItem[] | null;
|
||||
})
|
||||
| (z.infer<typeof BaseMenuNodeSchema> & {
|
||||
kind: "group";
|
||||
path: null;
|
||||
children: MenuItem[];
|
||||
});
|
||||
|
||||
export const MenuItemSchema: z.ZodType<MenuItem> = z.lazy(() =>
|
||||
z.discriminatedUnion("kind", [
|
||||
BaseMenuNodeSchema.extend({
|
||||
kind: z.literal("item"),
|
||||
path: z.string(),
|
||||
children: z.array(MenuItemSchema).nullable(),
|
||||
}),
|
||||
BaseMenuNodeSchema.extend({
|
||||
kind: z.literal("group"),
|
||||
path: z.null(),
|
||||
children: z.array(MenuItemSchema),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export type PaginatedData<T> = {
|
||||
list: T[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
/** Unwrapped list payload (envelope `data`); use as `defineApi` `output`. */
|
||||
export function PaginatedDataSchema<T extends z.ZodTypeAny>(itemSchema: T) {
|
||||
return z.object({
|
||||
list: z.array(itemSchema),
|
||||
total: z.int(),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const IdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
export type IdParams = z.infer<typeof IdParamsSchema>;
|
||||
|
||||
export const SearchParamsSchema = z.object({
|
||||
page: z.number().int().positive().catch(1),
|
||||
pageSize: z.number().int().positive().catch(10),
|
||||
sortField: z.string().nullable().catch(null),
|
||||
sortOrder: z.enum(["ascend", "descend"]).nullable().catch(null),
|
||||
});
|
||||
|
||||
export type SearchParams = z.infer<typeof SearchParamsSchema>;
|
||||
@@ -0,0 +1,2 @@
|
||||
/** REST API path prefix (no trailing slash). */
|
||||
export const API_PREFIX = "/api" as const;
|
||||
@@ -1,21 +1,23 @@
|
||||
import { z } from "zod/v4";
|
||||
import { defineApi } from "@/api/request";
|
||||
import { IdParamsSchema } from "@/api/shared/params";
|
||||
import { PaginatedDataSchema } from "@/api/shared/pagination";
|
||||
import {
|
||||
CreateUserRequestSchema,
|
||||
IdParamsSchema,
|
||||
ListUsersQuerySchema,
|
||||
PaginatedDataSchema,
|
||||
UpdateUserRequestSchema,
|
||||
UserSchema,
|
||||
} from "@/api/schemas";
|
||||
import type { CreateUserRequest, ListUsersQuery, UpdateUserRequest, User } from "@/api/schemas";
|
||||
} from "@/api/users/schemas";
|
||||
import type {
|
||||
CreateUserRequest,
|
||||
ListUsersQuery,
|
||||
UpdateUserRequest,
|
||||
User,
|
||||
} from "@/api/users/schemas";
|
||||
import { API_PREFIX } from "@/api/shared/prefix";
|
||||
|
||||
export const USER_ENDPOINTS = {
|
||||
list: "/api/users",
|
||||
create: "/api/users",
|
||||
update: (id: string) => `/api/users/${id}`,
|
||||
delete: (id: string) => `/api/users/${id}`,
|
||||
} as const;
|
||||
export const USERS_PATH = `${API_PREFIX}/users`;
|
||||
export const USERS_MEMBER_PATH = `${USERS_PATH}/:id`;
|
||||
|
||||
export const userQueryKeys = {
|
||||
all: ["users"] as const,
|
||||
@@ -23,21 +25,21 @@ export const userQueryKeys = {
|
||||
};
|
||||
|
||||
export const listUsers = defineApi({
|
||||
path: USER_ENDPOINTS.list,
|
||||
path: USERS_PATH,
|
||||
query: ListUsersQuerySchema,
|
||||
output: PaginatedDataSchema(UserSchema),
|
||||
});
|
||||
|
||||
export const createUser = defineApi({
|
||||
method: "POST",
|
||||
path: USER_ENDPOINTS.list,
|
||||
path: USERS_PATH,
|
||||
body: CreateUserRequestSchema,
|
||||
output: UserSchema,
|
||||
});
|
||||
|
||||
export const updateUser = defineApi({
|
||||
method: "PUT",
|
||||
path: "/api/users/:id",
|
||||
path: USERS_MEMBER_PATH,
|
||||
params: IdParamsSchema,
|
||||
body: UpdateUserRequestSchema,
|
||||
output: UserSchema,
|
||||
@@ -45,7 +47,7 @@ export const updateUser = defineApi({
|
||||
|
||||
export const deleteUser = defineApi({
|
||||
method: "DELETE",
|
||||
path: "/api/users/:id",
|
||||
path: USERS_MEMBER_PATH,
|
||||
params: IdParamsSchema,
|
||||
output: z.null(),
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
avatar: z.string().nullable(),
|
||||
email: z.string().nullable(),
|
||||
roles: z.array(z.string()),
|
||||
permissions: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type User = z.infer<typeof UserSchema>;
|
||||
|
||||
export const ListUsersQuerySchema = z.object({
|
||||
limit: z.number().int().positive().optional(),
|
||||
offset: z.number().int().nonnegative().optional(),
|
||||
keyword: z.string().optional(),
|
||||
role: z.string().optional(),
|
||||
sortField: z.string().optional(),
|
||||
sortOrder: z.enum(["ascend", "descend"]).optional(),
|
||||
});
|
||||
|
||||
export type ListUsersQuery = z.infer<typeof ListUsersQuerySchema>;
|
||||
|
||||
/** Ant Design Input submits `""` when empty; treat as null for optional email */
|
||||
const createUserEmailSchema = z
|
||||
.union([z.string(), z.null(), z.undefined()])
|
||||
.transform((v) => {
|
||||
if (v == null) return null;
|
||||
const t = String(v).trim();
|
||||
return t === "" ? null : t;
|
||||
})
|
||||
.pipe(z.union([z.string().email(), z.null()]));
|
||||
|
||||
export const CreateUserRequestSchema = z.object({
|
||||
username: z.string().min(1),
|
||||
email: createUserEmailSchema,
|
||||
roles: z.array(z.string()).min(1),
|
||||
});
|
||||
|
||||
export type CreateUserRequest = z.infer<typeof CreateUserRequestSchema>;
|
||||
|
||||
export const UpdateUserRequestSchema = CreateUserRequestSchema.partial();
|
||||
|
||||
export type UpdateUserRequest = z.infer<typeof UpdateUserRequestSchema>;
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
RefreshTokenRequestSchema,
|
||||
RegisterRequestSchema,
|
||||
} from "@/api/schemas";
|
||||
import { AUTH_ENDPOINTS } from "@/api/auth/paths";
|
||||
|
||||
const GUEST_REFRESH = "mock-guest-refresh";
|
||||
const REGISTERED_ACCESS = "mock-registered-access";
|
||||
@@ -32,7 +33,7 @@ function isRegisteredAuth(authorization: string | null): boolean {
|
||||
}
|
||||
|
||||
export const authHandlers = [
|
||||
http.post("/api/auth/login", async ({ request }) => {
|
||||
http.post(AUTH_ENDPOINTS.login, async ({ request }) => {
|
||||
await withDelay(300);
|
||||
let json: unknown;
|
||||
try {
|
||||
@@ -60,7 +61,7 @@ export const authHandlers = [
|
||||
return errorResponse(ERROR_CODES.INVALID_CREDENTIALS, "Invalid username or password");
|
||||
}),
|
||||
|
||||
http.post("/api/auth/register", async ({ request }) => {
|
||||
http.post(AUTH_ENDPOINTS.register, async ({ request }) => {
|
||||
await withDelay(300);
|
||||
let json: unknown;
|
||||
try {
|
||||
@@ -90,7 +91,7 @@ export const authHandlers = [
|
||||
});
|
||||
}),
|
||||
|
||||
http.post("/api/auth/refresh", async ({ request }) => {
|
||||
http.post(AUTH_ENDPOINTS.refresh, async ({ request }) => {
|
||||
await withDelay(100);
|
||||
let json: unknown;
|
||||
try {
|
||||
@@ -123,9 +124,9 @@ export const authHandlers = [
|
||||
});
|
||||
}),
|
||||
|
||||
http.post("/api/auth/logout", () => successWithNullBody()),
|
||||
http.post(AUTH_ENDPOINTS.logout, () => successWithNullBody()),
|
||||
|
||||
http.get("/api/auth/user", ({ request }) => {
|
||||
http.get(AUTH_ENDPOINTS.user, ({ request }) => {
|
||||
const auth = request.headers.get("authorization");
|
||||
if (isRegisteredAuth(auth)) {
|
||||
const session = mockRegisteredSession;
|
||||
@@ -146,7 +147,7 @@ export const authHandlers = [
|
||||
return successWithSchema(AuthUserResponseSchema, userWithoutPermissions);
|
||||
}),
|
||||
|
||||
http.get("/api/auth/permissions", ({ request }) => {
|
||||
http.get(AUTH_ENDPOINTS.permissions, ({ request }) => {
|
||||
const auth = request.headers.get("authorization");
|
||||
if (isRegisteredAuth(auth) && mockRegisteredSession) {
|
||||
return successWithSchema(PermissionsListSchema, ["user:view"] as string[]);
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
successWithSchema,
|
||||
successWithNullBody,
|
||||
} from "../createHandler";
|
||||
import { USERS_MEMBER_PATH, USERS_PATH } from "@/api/users";
|
||||
|
||||
let users = [...MOCK_USERS];
|
||||
|
||||
export const EXPIRED_ACCESS_TOKEN = "__EXPIRED_ACCESS__";
|
||||
|
||||
export const userHandlers = [
|
||||
http.get("/api/users", async ({ request }) => {
|
||||
http.get(USERS_PATH, async ({ request }) => {
|
||||
await withDelay(200);
|
||||
const auth = request.headers.get("authorization");
|
||||
if (auth === `Bearer ${EXPIRED_ACCESS_TOKEN}`) {
|
||||
@@ -33,7 +34,7 @@ export const userHandlers = [
|
||||
return paginatedWithSchema(UserSchema, { list, total: filtered.length });
|
||||
}),
|
||||
|
||||
http.post("/api/users", async ({ request }) => {
|
||||
http.post(USERS_PATH, async ({ request }) => {
|
||||
await withDelay(200);
|
||||
const body = (await request.json()) as Record<string, unknown>;
|
||||
const newUser = {
|
||||
@@ -48,7 +49,7 @@ export const userHandlers = [
|
||||
return successWithSchema(UserSchema, newUser);
|
||||
}),
|
||||
|
||||
http.put("/api/users/:id", async ({ params, request }) => {
|
||||
http.put(USERS_MEMBER_PATH, async ({ params, request }) => {
|
||||
await withDelay(200);
|
||||
const body = (await request.json()) as Record<string, unknown>;
|
||||
const idx = users.findIndex((u) => u.id === params.id);
|
||||
@@ -59,7 +60,7 @@ export const userHandlers = [
|
||||
return successWithSchema(UserSchema, users[idx]);
|
||||
}),
|
||||
|
||||
http.delete("/api/users/:id", async ({ params }) => {
|
||||
http.delete(USERS_MEMBER_PATH, async ({ params }) => {
|
||||
await withDelay(200);
|
||||
users = users.filter((u) => u.id !== params.id);
|
||||
return successWithNullBody();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
|
||||
import { Avatar, Button, Space, Form, App, Dropdown, theme, Tag, Flex } from "antd";
|
||||
import type { TablePaginationConfig } from "antd/es/table/interface";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { createUser, deleteUser, listUsers, updateUser, userQueryKeys } from "@/api/user";
|
||||
import { createUser, deleteUser, listUsers, updateUser, userQueryKeys } from "@/api/users";
|
||||
import type { User, CreateUserRequest } from "@/api/schemas";
|
||||
import { z } from "zod/v4";
|
||||
import { MoreVertical, Pencil, Trash2 } from "lucide-react";
|
||||
|
||||
Reference in New Issue
Block a user