create-blitzpack 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +452 -0
- package/package.json +57 -0
- package/template/.dockerignore +59 -0
- package/template/.github/workflows/ci.yml +157 -0
- package/template/.husky/pre-commit +1 -0
- package/template/.husky/pre-push +1 -0
- package/template/.lintstagedrc.cjs +4 -0
- package/template/.nvmrc +1 -0
- package/template/.prettierrc +9 -0
- package/template/.vscode/settings.json +13 -0
- package/template/CLAUDE.md +175 -0
- package/template/CONTRIBUTING.md +32 -0
- package/template/Dockerfile +90 -0
- package/template/GETTING_STARTED.md +35 -0
- package/template/LICENSE +21 -0
- package/template/README.md +116 -0
- package/template/apps/api/.dockerignore +51 -0
- package/template/apps/api/.env.local.example +62 -0
- package/template/apps/api/emails/account-deleted-email.tsx +69 -0
- package/template/apps/api/emails/components/email-layout.tsx +154 -0
- package/template/apps/api/emails/config.ts +22 -0
- package/template/apps/api/emails/password-changed-email.tsx +88 -0
- package/template/apps/api/emails/password-reset-email.tsx +86 -0
- package/template/apps/api/emails/verification-email.tsx +85 -0
- package/template/apps/api/emails/welcome-email.tsx +70 -0
- package/template/apps/api/package.json +84 -0
- package/template/apps/api/prisma/migrations/20251012111439_init/migration.sql +13 -0
- package/template/apps/api/prisma/migrations/20251018162629_add_better_auth_fields/migration.sql +67 -0
- package/template/apps/api/prisma/migrations/20251019142208_add_user_role_enum/migration.sql +5 -0
- package/template/apps/api/prisma/migrations/20251019182151_user_auth/migration.sql +7 -0
- package/template/apps/api/prisma/migrations/20251019211416_faster_session_lookup/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251119124337_add_upload_model/migration.sql +26 -0
- package/template/apps/api/prisma/migrations/20251120071241_add_scope_to_account/migration.sql +2 -0
- package/template/apps/api/prisma/migrations/20251120072608_add_oauth_token_expiration_fields/migration.sql +10 -0
- package/template/apps/api/prisma/migrations/20251120144705_add_audit_logs/migration.sql +29 -0
- package/template/apps/api/prisma/migrations/20251127123614_remove_impersonated_by/migration.sql +8 -0
- package/template/apps/api/prisma/migrations/20251127125630_remove_audit_logs/migration.sql +11 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +116 -0
- package/template/apps/api/prisma/seed.ts +159 -0
- package/template/apps/api/prisma.config.ts +14 -0
- package/template/apps/api/src/app.ts +377 -0
- package/template/apps/api/src/common/logger.service.ts +227 -0
- package/template/apps/api/src/config/env.ts +60 -0
- package/template/apps/api/src/config/rate-limit.ts +29 -0
- package/template/apps/api/src/hooks/auth.ts +122 -0
- package/template/apps/api/src/plugins/auth.ts +198 -0
- package/template/apps/api/src/plugins/database.ts +45 -0
- package/template/apps/api/src/plugins/logger.ts +33 -0
- package/template/apps/api/src/plugins/multipart.ts +16 -0
- package/template/apps/api/src/plugins/scalar.ts +20 -0
- package/template/apps/api/src/plugins/schedule.ts +52 -0
- package/template/apps/api/src/plugins/services.ts +66 -0
- package/template/apps/api/src/plugins/swagger.ts +56 -0
- package/template/apps/api/src/routes/accounts.ts +91 -0
- package/template/apps/api/src/routes/admin-sessions.ts +92 -0
- package/template/apps/api/src/routes/metrics.ts +71 -0
- package/template/apps/api/src/routes/password.ts +46 -0
- package/template/apps/api/src/routes/sessions.ts +53 -0
- package/template/apps/api/src/routes/stats.ts +38 -0
- package/template/apps/api/src/routes/uploads-serve.ts +27 -0
- package/template/apps/api/src/routes/uploads.ts +154 -0
- package/template/apps/api/src/routes/users.ts +114 -0
- package/template/apps/api/src/routes/verification.ts +90 -0
- package/template/apps/api/src/server.ts +34 -0
- package/template/apps/api/src/services/accounts.service.ts +125 -0
- package/template/apps/api/src/services/authorization.service.ts +162 -0
- package/template/apps/api/src/services/email.service.ts +170 -0
- package/template/apps/api/src/services/file-storage.service.ts +267 -0
- package/template/apps/api/src/services/metrics.service.ts +175 -0
- package/template/apps/api/src/services/password.service.ts +56 -0
- package/template/apps/api/src/services/sessions.service.spec.ts +134 -0
- package/template/apps/api/src/services/sessions.service.ts +276 -0
- package/template/apps/api/src/services/stats.service.ts +273 -0
- package/template/apps/api/src/services/uploads.service.ts +163 -0
- package/template/apps/api/src/services/users.service.spec.ts +249 -0
- package/template/apps/api/src/services/users.service.ts +198 -0
- package/template/apps/api/src/utils/file-validation.ts +108 -0
- package/template/apps/api/start.sh +33 -0
- package/template/apps/api/test/helpers/fastify-app.ts +24 -0
- package/template/apps/api/test/helpers/mock-authorization.ts +16 -0
- package/template/apps/api/test/helpers/mock-logger.ts +28 -0
- package/template/apps/api/test/helpers/mock-prisma.ts +30 -0
- package/template/apps/api/test/helpers/test-db.ts +125 -0
- package/template/apps/api/test/integration/auth-flow.integration.spec.ts +449 -0
- package/template/apps/api/test/integration/password.integration.spec.ts +427 -0
- package/template/apps/api/test/integration/rate-limit.integration.spec.ts +51 -0
- package/template/apps/api/test/integration/sessions.integration.spec.ts +445 -0
- package/template/apps/api/test/integration/users.integration.spec.ts +211 -0
- package/template/apps/api/test/setup.ts +31 -0
- package/template/apps/api/tsconfig.json +26 -0
- package/template/apps/api/vitest.config.ts +35 -0
- package/template/apps/web/.env.local.example +11 -0
- package/template/apps/web/components.json +24 -0
- package/template/apps/web/next.config.ts +22 -0
- package/template/apps/web/package.json +56 -0
- package/template/apps/web/postcss.config.js +5 -0
- package/template/apps/web/public/apple-icon.png +0 -0
- package/template/apps/web/public/icon.png +0 -0
- package/template/apps/web/public/robots.txt +3 -0
- package/template/apps/web/src/app/(admin)/admin/layout.tsx +222 -0
- package/template/apps/web/src/app/(admin)/admin/page.tsx +157 -0
- package/template/apps/web/src/app/(admin)/admin/sessions/page.tsx +18 -0
- package/template/apps/web/src/app/(admin)/admin/users/page.tsx +20 -0
- package/template/apps/web/src/app/(auth)/forgot-password/page.tsx +177 -0
- package/template/apps/web/src/app/(auth)/login/page.tsx +159 -0
- package/template/apps/web/src/app/(auth)/reset-password/page.tsx +245 -0
- package/template/apps/web/src/app/(auth)/signup/page.tsx +153 -0
- package/template/apps/web/src/app/dashboard/change-password/page.tsx +255 -0
- package/template/apps/web/src/app/dashboard/page.tsx +296 -0
- package/template/apps/web/src/app/error.tsx +32 -0
- package/template/apps/web/src/app/examples/file-upload/page.tsx +200 -0
- package/template/apps/web/src/app/favicon.ico +0 -0
- package/template/apps/web/src/app/global-error.tsx +96 -0
- package/template/apps/web/src/app/globals.css +22 -0
- package/template/apps/web/src/app/icon.png +0 -0
- package/template/apps/web/src/app/layout.tsx +34 -0
- package/template/apps/web/src/app/not-found.tsx +28 -0
- package/template/apps/web/src/app/page.tsx +192 -0
- package/template/apps/web/src/components/admin/activity-feed.tsx +101 -0
- package/template/apps/web/src/components/admin/charts/auth-breakdown-chart.tsx +114 -0
- package/template/apps/web/src/components/admin/charts/chart-tooltip.tsx +124 -0
- package/template/apps/web/src/components/admin/charts/realtime-metrics-chart.tsx +511 -0
- package/template/apps/web/src/components/admin/charts/role-distribution-chart.tsx +102 -0
- package/template/apps/web/src/components/admin/charts/session-activity-chart.tsx +90 -0
- package/template/apps/web/src/components/admin/charts/user-growth-chart.tsx +108 -0
- package/template/apps/web/src/components/admin/health-indicator.tsx +175 -0
- package/template/apps/web/src/components/admin/refresh-control.tsx +90 -0
- package/template/apps/web/src/components/admin/session-revoke-all-dialog.tsx +79 -0
- package/template/apps/web/src/components/admin/session-revoke-dialog.tsx +74 -0
- package/template/apps/web/src/components/admin/sessions-management-table.tsx +372 -0
- package/template/apps/web/src/components/admin/stat-card.tsx +137 -0
- package/template/apps/web/src/components/admin/user-create-dialog.tsx +152 -0
- package/template/apps/web/src/components/admin/user-delete-dialog.tsx +73 -0
- package/template/apps/web/src/components/admin/user-edit-dialog.tsx +170 -0
- package/template/apps/web/src/components/admin/users-management-table.tsx +285 -0
- package/template/apps/web/src/components/auth/email-verification-banner.tsx +85 -0
- package/template/apps/web/src/components/auth/github-button.tsx +40 -0
- package/template/apps/web/src/components/auth/google-button.tsx +54 -0
- package/template/apps/web/src/components/auth/protected-route.tsx +66 -0
- package/template/apps/web/src/components/auth/redirect-if-authenticated.tsx +31 -0
- package/template/apps/web/src/components/auth/with-auth.tsx +30 -0
- package/template/apps/web/src/components/error/error-card.tsx +47 -0
- package/template/apps/web/src/components/error/forbidden.tsx +25 -0
- package/template/apps/web/src/components/landing/command-block.tsx +64 -0
- package/template/apps/web/src/components/landing/feature-card.tsx +60 -0
- package/template/apps/web/src/components/landing/included-feature-card.tsx +63 -0
- package/template/apps/web/src/components/landing/logo.tsx +41 -0
- package/template/apps/web/src/components/landing/tech-badge.tsx +11 -0
- package/template/apps/web/src/components/layout/auth-nav.tsx +58 -0
- package/template/apps/web/src/components/layout/footer.tsx +3 -0
- package/template/apps/web/src/config/landing-data.ts +152 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/hooks/api/__tests__/use-users.test.tsx +181 -0
- package/template/apps/web/src/hooks/api/use-admin-sessions.ts +75 -0
- package/template/apps/web/src/hooks/api/use-admin-stats.ts +33 -0
- package/template/apps/web/src/hooks/api/use-sessions.ts +52 -0
- package/template/apps/web/src/hooks/api/use-uploads.ts +156 -0
- package/template/apps/web/src/hooks/api/use-users.ts +149 -0
- package/template/apps/web/src/hooks/use-mobile.ts +21 -0
- package/template/apps/web/src/hooks/use-realtime-metrics.ts +120 -0
- package/template/apps/web/src/lib/__tests__/utils.test.ts +29 -0
- package/template/apps/web/src/lib/api.ts +151 -0
- package/template/apps/web/src/lib/auth.ts +13 -0
- package/template/apps/web/src/lib/env.ts +52 -0
- package/template/apps/web/src/lib/form-utils.ts +11 -0
- package/template/apps/web/src/lib/utils.ts +1 -0
- package/template/apps/web/src/providers.tsx +34 -0
- package/template/apps/web/src/store/atoms.ts +15 -0
- package/template/apps/web/src/test/helpers/test-utils.tsx +44 -0
- package/template/apps/web/src/test/setup.ts +8 -0
- package/template/apps/web/tailwind.config.ts +5 -0
- package/template/apps/web/tsconfig.json +26 -0
- package/template/apps/web/vitest.config.ts +32 -0
- package/template/assets/logo-512.png +0 -0
- package/template/assets/logo.svg +4 -0
- package/template/docker-compose.prod.yml +66 -0
- package/template/docker-compose.yml +36 -0
- package/template/eslint.config.ts +119 -0
- package/template/package.json +77 -0
- package/template/packages/tailwind-config/package.json +9 -0
- package/template/packages/tailwind-config/theme.css +179 -0
- package/template/packages/types/package.json +29 -0
- package/template/packages/types/src/__tests__/schemas.test.ts +255 -0
- package/template/packages/types/src/api-response.ts +53 -0
- package/template/packages/types/src/health-check.ts +11 -0
- package/template/packages/types/src/pagination.ts +41 -0
- package/template/packages/types/src/role.ts +5 -0
- package/template/packages/types/src/session.ts +48 -0
- package/template/packages/types/src/stats.ts +113 -0
- package/template/packages/types/src/upload.ts +51 -0
- package/template/packages/types/src/user.ts +36 -0
- package/template/packages/types/tsconfig.json +5 -0
- package/template/packages/types/vitest.config.ts +21 -0
- package/template/packages/ui/components.json +21 -0
- package/template/packages/ui/package.json +108 -0
- package/template/packages/ui/src/__tests__/button.test.tsx +70 -0
- package/template/packages/ui/src/alert-dialog.tsx +141 -0
- package/template/packages/ui/src/alert.tsx +66 -0
- package/template/packages/ui/src/animated-theme-toggler.tsx +167 -0
- package/template/packages/ui/src/avatar.tsx +53 -0
- package/template/packages/ui/src/badge.tsx +36 -0
- package/template/packages/ui/src/button.tsx +84 -0
- package/template/packages/ui/src/card.tsx +92 -0
- package/template/packages/ui/src/checkbox.tsx +32 -0
- package/template/packages/ui/src/data-table/data-table-column-header.tsx +68 -0
- package/template/packages/ui/src/data-table/data-table-pagination.tsx +99 -0
- package/template/packages/ui/src/data-table/data-table-toolbar.tsx +55 -0
- package/template/packages/ui/src/data-table/data-table-view-options.tsx +63 -0
- package/template/packages/ui/src/data-table/data-table.tsx +167 -0
- package/template/packages/ui/src/dialog.tsx +143 -0
- package/template/packages/ui/src/dropdown-menu.tsx +257 -0
- package/template/packages/ui/src/empty-state.tsx +52 -0
- package/template/packages/ui/src/file-upload-input.tsx +202 -0
- package/template/packages/ui/src/form.tsx +168 -0
- package/template/packages/ui/src/hooks/use-mobile.ts +19 -0
- package/template/packages/ui/src/icons/brand-icons.tsx +16 -0
- package/template/packages/ui/src/input.tsx +21 -0
- package/template/packages/ui/src/label.tsx +24 -0
- package/template/packages/ui/src/lib/utils.ts +6 -0
- package/template/packages/ui/src/password-input.tsx +102 -0
- package/template/packages/ui/src/popover.tsx +48 -0
- package/template/packages/ui/src/radio-group.tsx +45 -0
- package/template/packages/ui/src/scroll-area.tsx +58 -0
- package/template/packages/ui/src/select.tsx +187 -0
- package/template/packages/ui/src/separator.tsx +28 -0
- package/template/packages/ui/src/sheet.tsx +139 -0
- package/template/packages/ui/src/sidebar.tsx +726 -0
- package/template/packages/ui/src/skeleton-variants.tsx +87 -0
- package/template/packages/ui/src/skeleton.tsx +13 -0
- package/template/packages/ui/src/slider.tsx +63 -0
- package/template/packages/ui/src/sonner.tsx +25 -0
- package/template/packages/ui/src/spinner.tsx +16 -0
- package/template/packages/ui/src/switch.tsx +31 -0
- package/template/packages/ui/src/table.tsx +116 -0
- package/template/packages/ui/src/tabs.tsx +66 -0
- package/template/packages/ui/src/textarea.tsx +18 -0
- package/template/packages/ui/src/tooltip.tsx +61 -0
- package/template/packages/ui/src/user-avatar.tsx +97 -0
- package/template/packages/ui/test-config.js +3 -0
- package/template/packages/ui/tsconfig.json +12 -0
- package/template/packages/ui/turbo.json +18 -0
- package/template/packages/ui/vitest.config.ts +17 -0
- package/template/packages/ui/vitest.setup.ts +1 -0
- package/template/packages/utils/package.json +23 -0
- package/template/packages/utils/src/__tests__/utils.test.ts +223 -0
- package/template/packages/utils/src/array.ts +18 -0
- package/template/packages/utils/src/async.ts +3 -0
- package/template/packages/utils/src/date.ts +77 -0
- package/template/packages/utils/src/errors.ts +73 -0
- package/template/packages/utils/src/number.ts +11 -0
- package/template/packages/utils/src/string.ts +13 -0
- package/template/packages/utils/tsconfig.json +5 -0
- package/template/packages/utils/vitest.config.ts +21 -0
- package/template/pnpm-workspace.yaml +4 -0
- package/template/tsconfig.base.json +32 -0
- package/template/turbo.json +133 -0
- package/template/vitest.shared.ts +26 -0
- package/template/vitest.workspace.ts +9 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ListResponseSchema,
|
|
3
|
+
MessageResponseSchema,
|
|
4
|
+
} from '@repo/packages-types/api-response';
|
|
5
|
+
import { HealthCheckSchema } from '@repo/packages-types/health-check';
|
|
6
|
+
import { CreateUserSchema, UserSchema } from '@repo/packages-types/user';
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
describe('HealthCheckSchema', () => {
|
|
10
|
+
it('should validate a valid health check object', () => {
|
|
11
|
+
const validHealthCheck = {
|
|
12
|
+
status: 'ok' as const,
|
|
13
|
+
timestamp: new Date().toISOString(),
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
uptime: 12345,
|
|
16
|
+
environment: 'development' as const,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = HealthCheckSchema.safeParse(validHealthCheck);
|
|
20
|
+
expect(result.success).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should reject invalid status', () => {
|
|
24
|
+
const invalidHealthCheck = {
|
|
25
|
+
status: 'invalid',
|
|
26
|
+
timestamp: new Date().toISOString(),
|
|
27
|
+
version: '1.0.0',
|
|
28
|
+
uptime: 12345,
|
|
29
|
+
environment: 'development',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const result = HealthCheckSchema.safeParse(invalidHealthCheck);
|
|
33
|
+
expect(result.success).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should reject negative uptime', () => {
|
|
37
|
+
const invalidHealthCheck = {
|
|
38
|
+
status: 'ok',
|
|
39
|
+
timestamp: new Date().toISOString(),
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
uptime: -100,
|
|
42
|
+
environment: 'development',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = HealthCheckSchema.safeParse(invalidHealthCheck);
|
|
46
|
+
expect(result.success).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should reject invalid environment', () => {
|
|
50
|
+
const invalidHealthCheck = {
|
|
51
|
+
status: 'ok',
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
version: '1.0.0',
|
|
54
|
+
uptime: 12345,
|
|
55
|
+
environment: 'invalid',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const result = HealthCheckSchema.safeParse(invalidHealthCheck);
|
|
59
|
+
expect(result.success).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('UserSchema', () => {
|
|
64
|
+
it('should validate a valid user object with Date', () => {
|
|
65
|
+
const validUser = {
|
|
66
|
+
id: 'cm1abc2def3ghi4jkl',
|
|
67
|
+
email: 'test@example.com',
|
|
68
|
+
name: 'John Doe',
|
|
69
|
+
role: 'user' as const,
|
|
70
|
+
createdAt: new Date(),
|
|
71
|
+
updatedAt: new Date(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = UserSchema.safeParse(validUser);
|
|
75
|
+
expect(result.success).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should validate a valid user object with ISO string dates', () => {
|
|
79
|
+
const validUser = {
|
|
80
|
+
id: 'cm1abc2def3ghi4jkl',
|
|
81
|
+
email: 'test@example.com',
|
|
82
|
+
name: 'John Doe',
|
|
83
|
+
role: 'user' as const,
|
|
84
|
+
createdAt: new Date().toISOString(),
|
|
85
|
+
updatedAt: new Date().toISOString(),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = UserSchema.safeParse(validUser);
|
|
89
|
+
expect(result.success).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should accept Better Auth ID format', () => {
|
|
93
|
+
const validUser = {
|
|
94
|
+
id: 'auth-1763496954387-0tr9175',
|
|
95
|
+
email: 'test@example.com',
|
|
96
|
+
name: 'John Doe',
|
|
97
|
+
role: 'user' as const,
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
updatedAt: new Date(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const result = UserSchema.safeParse(validUser);
|
|
103
|
+
expect(result.success).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should reject empty id', () => {
|
|
107
|
+
const invalidUser = {
|
|
108
|
+
id: '',
|
|
109
|
+
email: 'test@example.com',
|
|
110
|
+
name: 'John Doe',
|
|
111
|
+
role: 'user' as const,
|
|
112
|
+
createdAt: new Date(),
|
|
113
|
+
updatedAt: new Date(),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const result = UserSchema.safeParse(invalidUser);
|
|
117
|
+
expect(result.success).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should reject invalid email', () => {
|
|
121
|
+
const invalidUser = {
|
|
122
|
+
id: 'cm1abc2def3ghi4jkl',
|
|
123
|
+
email: 'not-an-email',
|
|
124
|
+
name: 'John Doe',
|
|
125
|
+
role: 'user' as const,
|
|
126
|
+
createdAt: new Date(),
|
|
127
|
+
updatedAt: new Date(),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const result = UserSchema.safeParse(invalidUser);
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should reject empty name', () => {
|
|
135
|
+
const invalidUser = {
|
|
136
|
+
id: 'cm1abc2def3ghi4jkl',
|
|
137
|
+
email: 'test@example.com',
|
|
138
|
+
name: '',
|
|
139
|
+
role: 'user' as const,
|
|
140
|
+
createdAt: new Date(),
|
|
141
|
+
updatedAt: new Date(),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const result = UserSchema.safeParse(invalidUser);
|
|
145
|
+
expect(result.success).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('CreateUserSchema', () => {
|
|
150
|
+
it('should validate user creation without id and timestamps', () => {
|
|
151
|
+
const validCreateUser = {
|
|
152
|
+
email: 'new@example.com',
|
|
153
|
+
name: 'Jane Doe',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const result = CreateUserSchema.safeParse(validCreateUser);
|
|
157
|
+
expect(result.success).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should omit id, createdAt, and updatedAt fields', () => {
|
|
161
|
+
const createUserWithExtra = {
|
|
162
|
+
id: '123e4567-e89b-12d3-a456-426614174000',
|
|
163
|
+
email: 'new@example.com',
|
|
164
|
+
name: 'Jane Doe',
|
|
165
|
+
createdAt: new Date(),
|
|
166
|
+
updatedAt: new Date(),
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = CreateUserSchema.safeParse(createUserWithExtra);
|
|
170
|
+
expect(result.success).toBe(true);
|
|
171
|
+
if (result.success) {
|
|
172
|
+
expect(result.data).toEqual({
|
|
173
|
+
email: 'new@example.com',
|
|
174
|
+
name: 'Jane Doe',
|
|
175
|
+
role: 'user',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should reject invalid email in create', () => {
|
|
181
|
+
const invalidCreateUser = {
|
|
182
|
+
email: 'invalid-email',
|
|
183
|
+
name: 'Jane Doe',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = CreateUserSchema.safeParse(invalidCreateUser);
|
|
187
|
+
expect(result.success).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('MessageResponseSchema', () => {
|
|
192
|
+
it('should validate message response', () => {
|
|
193
|
+
const validResponse = {
|
|
194
|
+
message: 'Operation successful',
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = MessageResponseSchema.safeParse(validResponse);
|
|
198
|
+
expect(result.success).toBe(true);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should reject response without message field', () => {
|
|
202
|
+
const invalidResponse = {
|
|
203
|
+
data: 'something',
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = MessageResponseSchema.safeParse(invalidResponse);
|
|
207
|
+
expect(result.success).toBe(false);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('ListResponseSchema', () => {
|
|
212
|
+
it('should validate list response with data array', () => {
|
|
213
|
+
const validResponse = {
|
|
214
|
+
data: [
|
|
215
|
+
{
|
|
216
|
+
id: 'cm1abc2def3ghi4jkl',
|
|
217
|
+
email: 'user1@example.com',
|
|
218
|
+
name: 'User One',
|
|
219
|
+
role: 'user' as const,
|
|
220
|
+
createdAt: new Date(),
|
|
221
|
+
updatedAt: new Date(),
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
id: 'cm2xyz3abc4def5ghi',
|
|
225
|
+
email: 'user2@example.com',
|
|
226
|
+
name: 'User Two',
|
|
227
|
+
role: 'admin' as const,
|
|
228
|
+
createdAt: new Date(),
|
|
229
|
+
updatedAt: new Date(),
|
|
230
|
+
},
|
|
231
|
+
],
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = ListResponseSchema(UserSchema).safeParse(validResponse);
|
|
235
|
+
expect(result.success).toBe(true);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('should reject response without data field', () => {
|
|
239
|
+
const invalidResponse = {
|
|
240
|
+
items: [
|
|
241
|
+
{
|
|
242
|
+
id: 'cm1abc2def3ghi4jkl',
|
|
243
|
+
email: 'user1@example.com',
|
|
244
|
+
name: 'User One',
|
|
245
|
+
role: 'user' as const,
|
|
246
|
+
createdAt: new Date(),
|
|
247
|
+
updatedAt: new Date(),
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const result = ListResponseSchema(UserSchema).safeParse(invalidResponse);
|
|
253
|
+
expect(result.success).toBe(false);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Standard error response returned by all API endpoints
|
|
5
|
+
* Used by global error handler for consistent error formatting
|
|
6
|
+
*/
|
|
7
|
+
export const ErrorResponseSchema = z.object({
|
|
8
|
+
error: z.object({
|
|
9
|
+
message: z.string(),
|
|
10
|
+
code: z.string(),
|
|
11
|
+
details: z.record(z.string(), z.unknown()).optional(),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Standard success response wrapper for single items
|
|
19
|
+
* Used for: GET /users/:id, POST /users, etc.
|
|
20
|
+
*/
|
|
21
|
+
export const SuccessResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
|
22
|
+
z.object({
|
|
23
|
+
data: dataSchema,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type SuccessResponse<T> = {
|
|
27
|
+
data: T;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Simple message response for action/command operations
|
|
32
|
+
* Used for: password changes, session revocations, deletions with confirmation
|
|
33
|
+
*/
|
|
34
|
+
export const MessageResponseSchema = z.object({
|
|
35
|
+
message: z.string(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type MessageResponse = {
|
|
39
|
+
message: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List response for collections without pagination
|
|
44
|
+
* Used for: sessions list, etc.
|
|
45
|
+
*/
|
|
46
|
+
export const ListResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
|
|
47
|
+
z.object({
|
|
48
|
+
data: z.array(dataSchema),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type ListResponse<T> = {
|
|
52
|
+
data: T[];
|
|
53
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const HealthCheckSchema = z.object({
|
|
4
|
+
status: z.enum(['ok', 'error']),
|
|
5
|
+
timestamp: z.string().datetime(),
|
|
6
|
+
version: z.string(),
|
|
7
|
+
uptime: z.number().nonnegative(),
|
|
8
|
+
environment: z.enum(['development', 'staging', 'production']),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export type HealthCheck = z.infer<typeof HealthCheckSchema>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const PaginationSchema = z.object({
|
|
4
|
+
page: z.coerce.number().int().positive().default(1),
|
|
5
|
+
limit: z.coerce.number().int().positive().max(100).default(10),
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export type Pagination = z.infer<typeof PaginationSchema>;
|
|
9
|
+
|
|
10
|
+
export const QueryUsersSchema = PaginationSchema.extend({
|
|
11
|
+
search: z.string().optional(),
|
|
12
|
+
sortBy: z.enum(['name', 'email', 'createdAt']).default('createdAt'),
|
|
13
|
+
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type QueryUsers = z.infer<typeof QueryUsersSchema>;
|
|
17
|
+
|
|
18
|
+
export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(
|
|
19
|
+
dataSchema: T
|
|
20
|
+
) =>
|
|
21
|
+
z.object({
|
|
22
|
+
data: z.array(dataSchema),
|
|
23
|
+
pagination: z.object({
|
|
24
|
+
page: z.number().int().positive(),
|
|
25
|
+
limit: z.number().int().positive(),
|
|
26
|
+
total: z.number().int().nonnegative(),
|
|
27
|
+
totalPages: z.number().int().nonnegative(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type PaginationMeta = {
|
|
32
|
+
page: number;
|
|
33
|
+
limit: number;
|
|
34
|
+
total: number;
|
|
35
|
+
totalPages: number;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type PaginatedResponse<T> = {
|
|
39
|
+
data: T[];
|
|
40
|
+
pagination: PaginationMeta;
|
|
41
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { PaginationSchema } from './pagination';
|
|
4
|
+
|
|
5
|
+
export const AdminSessionSchema = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
userId: z.string(),
|
|
8
|
+
ipAddress: z.string().nullable(),
|
|
9
|
+
userAgent: z.string().nullable(),
|
|
10
|
+
createdAt: z.coerce.date(),
|
|
11
|
+
updatedAt: z.coerce.date(),
|
|
12
|
+
expiresAt: z.coerce.date(),
|
|
13
|
+
user: z.object({
|
|
14
|
+
id: z.string(),
|
|
15
|
+
name: z.string().nullable(),
|
|
16
|
+
email: z.string(),
|
|
17
|
+
image: z.string().nullable(),
|
|
18
|
+
}),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type AdminSession = z.infer<typeof AdminSessionSchema>;
|
|
22
|
+
|
|
23
|
+
export const QuerySessionsSchema = PaginationSchema.extend({
|
|
24
|
+
search: z.string().optional(),
|
|
25
|
+
status: z.enum(['active', 'expired', 'all']).default('active'),
|
|
26
|
+
userId: z.string().optional(),
|
|
27
|
+
sortBy: z.enum(['createdAt', 'expiresAt']).default('createdAt'),
|
|
28
|
+
sortOrder: z.enum(['asc', 'desc']).default('desc'),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type QuerySessions = z.infer<typeof QuerySessionsSchema>;
|
|
32
|
+
|
|
33
|
+
export const SessionStatsSchema = z.object({
|
|
34
|
+
activeSessions: z.number(),
|
|
35
|
+
uniqueUsers: z.number(),
|
|
36
|
+
sessionsToday: z.number(),
|
|
37
|
+
expiringSoon: z.number(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type SessionStats = z.infer<typeof SessionStatsSchema>;
|
|
41
|
+
|
|
42
|
+
export const RevokeSessionParamsSchema = z.object({
|
|
43
|
+
sessionId: z.string(),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const RevokeUserSessionsParamsSchema = z.object({
|
|
47
|
+
userId: z.string(),
|
|
48
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const StatsOverviewSchema = z.object({
|
|
4
|
+
totalUsers: z.number(),
|
|
5
|
+
activeSessionsLast24h: z.number(),
|
|
6
|
+
totalUploads: z.number(),
|
|
7
|
+
storageUsedBytes: z.number(),
|
|
8
|
+
newUsersToday: z.number(),
|
|
9
|
+
newUsersThisWeek: z.number(),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type StatsOverview = z.infer<typeof StatsOverviewSchema>;
|
|
13
|
+
|
|
14
|
+
export const UserGrowthPointSchema = z.object({
|
|
15
|
+
date: z.string(),
|
|
16
|
+
count: z.number(),
|
|
17
|
+
cumulative: z.number(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export type UserGrowthPoint = z.infer<typeof UserGrowthPointSchema>;
|
|
21
|
+
|
|
22
|
+
export const SessionActivityPointSchema = z.object({
|
|
23
|
+
date: z.string(),
|
|
24
|
+
count: z.number(),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type SessionActivityPoint = z.infer<typeof SessionActivityPointSchema>;
|
|
28
|
+
|
|
29
|
+
export const AuthBreakdownSchema = z.object({
|
|
30
|
+
verified: z.number(),
|
|
31
|
+
unverified: z.number(),
|
|
32
|
+
banned: z.number(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type AuthBreakdown = z.infer<typeof AuthBreakdownSchema>;
|
|
36
|
+
|
|
37
|
+
export const RoleDistributionItemSchema = z.object({
|
|
38
|
+
role: z.string(),
|
|
39
|
+
count: z.number(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export type RoleDistributionItem = z.infer<typeof RoleDistributionItemSchema>;
|
|
43
|
+
|
|
44
|
+
export const RecentSignupSchema = z.object({
|
|
45
|
+
id: z.string(),
|
|
46
|
+
email: z.string(),
|
|
47
|
+
name: z.string().nullable(),
|
|
48
|
+
image: z.string().nullable(),
|
|
49
|
+
createdAt: z.string(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type RecentSignup = z.infer<typeof RecentSignupSchema>;
|
|
53
|
+
|
|
54
|
+
export const DatabaseHealthSchema = z.enum([
|
|
55
|
+
'connected',
|
|
56
|
+
'degraded',
|
|
57
|
+
'disconnected',
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
export type DatabaseHealth = z.infer<typeof DatabaseHealthSchema>;
|
|
61
|
+
|
|
62
|
+
export const SystemHealthSchema = z.object({
|
|
63
|
+
database: DatabaseHealthSchema,
|
|
64
|
+
uptime: z.number(),
|
|
65
|
+
lastChecked: z.string(),
|
|
66
|
+
dbLatencyMs: z.number().optional(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
export type SystemHealth = z.infer<typeof SystemHealthSchema>;
|
|
70
|
+
|
|
71
|
+
export const SystemStatsSchema = z.object({
|
|
72
|
+
overview: StatsOverviewSchema,
|
|
73
|
+
userGrowth: z.array(UserGrowthPointSchema),
|
|
74
|
+
sessionActivity: z.array(SessionActivityPointSchema),
|
|
75
|
+
authBreakdown: AuthBreakdownSchema,
|
|
76
|
+
roleDistribution: z.array(RoleDistributionItemSchema),
|
|
77
|
+
recentSignups: z.array(RecentSignupSchema),
|
|
78
|
+
systemHealth: SystemHealthSchema,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export type SystemStats = z.infer<typeof SystemStatsSchema>;
|
|
82
|
+
|
|
83
|
+
export const HealthCheckResponseSchema = z.object({
|
|
84
|
+
status: z.enum(['ok', 'degraded', 'error']),
|
|
85
|
+
database: DatabaseHealthSchema,
|
|
86
|
+
dbLatencyMs: z.number(),
|
|
87
|
+
uptime: z.number(),
|
|
88
|
+
timestamp: z.string(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
export type HealthCheckResponse = z.infer<typeof HealthCheckResponseSchema>;
|
|
92
|
+
|
|
93
|
+
export const RealtimeMetricsPointSchema = z.object({
|
|
94
|
+
timestamp: z.number(),
|
|
95
|
+
memory: z.object({
|
|
96
|
+
heapUsedMB: z.number(),
|
|
97
|
+
heapTotalMB: z.number(),
|
|
98
|
+
rssMB: z.number(),
|
|
99
|
+
usedPercent: z.number(),
|
|
100
|
+
}),
|
|
101
|
+
cpu: z.object({
|
|
102
|
+
percentage: z.number(),
|
|
103
|
+
}),
|
|
104
|
+
errors: z.object({
|
|
105
|
+
rate: z.number(),
|
|
106
|
+
}),
|
|
107
|
+
requests: z.object({
|
|
108
|
+
perSecond: z.number(),
|
|
109
|
+
avgResponseTimeMs: z.number(),
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export type RealtimeMetricsPoint = z.infer<typeof RealtimeMetricsPointSchema>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const UploadSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
filename: z.string(),
|
|
6
|
+
originalName: z.string(),
|
|
7
|
+
mimeType: z.string(),
|
|
8
|
+
size: z.number().int().positive(),
|
|
9
|
+
url: z.string().url(),
|
|
10
|
+
userId: z.string(),
|
|
11
|
+
createdAt: z.coerce.date(),
|
|
12
|
+
updatedAt: z.coerce.date(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type Upload = z.infer<typeof UploadSchema>;
|
|
16
|
+
|
|
17
|
+
export const UploadResponseSchema = z.object({
|
|
18
|
+
id: z.string(),
|
|
19
|
+
filename: z.string(),
|
|
20
|
+
originalName: z.string(),
|
|
21
|
+
mimeType: z.string(),
|
|
22
|
+
size: z.number().int().positive(),
|
|
23
|
+
url: z.string().url(),
|
|
24
|
+
createdAt: z.coerce.date(),
|
|
25
|
+
user: z.object({
|
|
26
|
+
id: z.string(),
|
|
27
|
+
name: z.string().nullable(),
|
|
28
|
+
}),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export type UploadResponse = z.infer<typeof UploadResponseSchema>;
|
|
32
|
+
|
|
33
|
+
export const GetUploadsQuerySchema = z.object({
|
|
34
|
+
limit: z.coerce.number().int().positive().max(100).default(50),
|
|
35
|
+
offset: z.coerce.number().int().nonnegative().default(0),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type GetUploadsQuery = z.infer<typeof GetUploadsQuerySchema>;
|
|
39
|
+
|
|
40
|
+
export const DeleteUploadParamsSchema = z.object({
|
|
41
|
+
id: z.string().min(1),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export type DeleteUploadParams = z.infer<typeof DeleteUploadParamsSchema>;
|
|
45
|
+
|
|
46
|
+
export const UploadStatsSchema = z.object({
|
|
47
|
+
totalFiles: z.number().int().nonnegative(),
|
|
48
|
+
totalSize: z.number().int().nonnegative(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
export type UploadStats = z.infer<typeof UploadStatsSchema>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { RoleSchema } from './role';
|
|
4
|
+
|
|
5
|
+
export const UserSchema = z.object({
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
email: z.string().email(),
|
|
8
|
+
name: z.string().min(1),
|
|
9
|
+
role: RoleSchema.default('user'),
|
|
10
|
+
createdAt: z.date().or(z.string().datetime()),
|
|
11
|
+
updatedAt: z.date().or(z.string().datetime()),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export type User = z.infer<typeof UserSchema>;
|
|
15
|
+
|
|
16
|
+
export const CreateUserSchema = UserSchema.omit({
|
|
17
|
+
id: true,
|
|
18
|
+
createdAt: true,
|
|
19
|
+
updatedAt: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export type CreateUser = z.infer<typeof CreateUserSchema>;
|
|
23
|
+
|
|
24
|
+
export const UpdateUserSchema = UserSchema.omit({
|
|
25
|
+
id: true,
|
|
26
|
+
createdAt: true,
|
|
27
|
+
updatedAt: true,
|
|
28
|
+
}).partial();
|
|
29
|
+
|
|
30
|
+
export type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
|
31
|
+
|
|
32
|
+
export const GetUserByIdSchema = z.object({
|
|
33
|
+
id: z.string().min(1),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type GetUserById = z.infer<typeof GetUserByIdSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig, mergeConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
import { sharedConfig } from '../../vitest.shared';
|
|
4
|
+
|
|
5
|
+
export default mergeConfig(
|
|
6
|
+
sharedConfig,
|
|
7
|
+
defineConfig({
|
|
8
|
+
test: {
|
|
9
|
+
name: '@repo/packages-types',
|
|
10
|
+
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
|
11
|
+
coverage: {
|
|
12
|
+
thresholds: {
|
|
13
|
+
lines: 70,
|
|
14
|
+
functions: 70,
|
|
15
|
+
branches: 60,
|
|
16
|
+
statements: 70,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": ".",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
}
|
|
21
|
+
}
|