create-pardx-scaffold 0.1.10 → 0.1.11

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.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/template/apps/api/libs/domain/auth/src/auth.service.ts +27 -1
  3. package/template/apps/api/libs/infra/clients/internal/email/dto/email.dto.ts +3 -3
  4. package/template/apps/api/libs/infra/clients/internal/volcengine-tts/dto/tts.dto.ts +4 -4
  5. package/template/apps/api/libs/infra/common/common.module.ts +10 -0
  6. package/template/apps/api/libs/infra/common/config/validation/env.validation.ts +1 -1
  7. package/template/apps/api/libs/infra/common/config/validation/keys.validation.ts +2 -2
  8. package/template/apps/api/libs/infra/common/config/validation/yaml.validation.ts +3 -3
  9. package/template/apps/api/libs/infra/common/decorators/device-info.decorator.ts +58 -0
  10. package/template/apps/api/libs/infra/common/decorators/team-info.decorator.ts +122 -0
  11. package/template/apps/api/libs/infra/common/encryption.service.ts +70 -0
  12. package/template/apps/api/libs/infra/common/index.ts +9 -0
  13. package/template/apps/api/libs/infra/shared-services/email/dto/email.dto.ts +3 -3
  14. package/template/apps/api/libs/infra/shared-services/email/email.service.ts +5 -1
  15. package/template/apps/api/libs/infra/shared-services/notification/index.ts +13 -0
  16. package/template/apps/api/libs/infra/shared-services/notification/notification.module.ts +10 -0
  17. package/template/apps/api/libs/infra/shared-services/notification/notification.service.ts +791 -0
  18. package/template/apps/web/components/client-only.tsx +28 -0
  19. package/template/apps/web/components/index.ts +23 -0
  20. package/template/apps/web/components/layout/app-navbar.tsx +109 -0
  21. package/template/apps/web/components/layout/app-shell.tsx +30 -0
  22. package/template/apps/web/components/layout/app-sidebar.tsx +206 -0
  23. package/template/apps/web/components/layout/index.ts +4 -0
  24. package/template/apps/web/components/layout/locale-switcher.tsx +57 -0
  25. package/template/apps/web/components/runtime-i18n-bridge.tsx +32 -0
  26. package/template/apps/web/components/state-components.tsx +214 -0
  27. package/template/apps/web/config.ts +22 -2
  28. package/template/apps/web/lib/api/cache-config.ts +32 -0
  29. package/template/apps/web/lib/api/contracts/client.ts +43 -1
  30. package/template/apps/web/lib/api/contracts/hooks/analytics.ts +32 -0
  31. package/template/apps/web/lib/api/contracts/hooks/index.ts +41 -2
  32. package/template/apps/web/lib/api/contracts/hooks/message.ts +60 -0
  33. package/template/apps/web/lib/api/contracts/hooks/system.ts +42 -0
  34. package/template/apps/web/lib/api/contracts/hooks/task.ts +54 -0
  35. package/template/apps/web/lib/api/contracts/hooks/user.ts +45 -0
  36. package/template/apps/web/lib/api/contracts/server-client.ts +1 -1
  37. package/template/apps/web/lib/api/prefetch.ts +128 -0
  38. package/template/apps/web/lib/api/query-client.ts +37 -0
  39. package/template/apps/web/lib/i18n/runtime-translator.ts +48 -0
  40. package/template/apps/web/lib/requests.ts +1 -1
  41. package/template/apps/web/providers/app-provider.tsx +1 -1
  42. package/template/apps/web/providers/auth-provider.tsx +228 -0
  43. package/template/apps/web/providers/index.tsx +28 -9
  44. package/template/apps/web/providers/intl-client-provider.tsx +43 -0
  45. package/template/apps/web/vitest.config.ts +4 -0
@@ -0,0 +1,128 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * 数据预加载工具
5
+ * 用于在用户可能需要数据之前预先加载,提升用户体验
6
+ */
7
+
8
+ import { useEffect, useRef, useCallback, useMemo } from 'react';
9
+ import { usePathname } from '@/i18n/navigation';
10
+ import { queryClient } from './query-client';
11
+ import { cacheTime } from './cache-config';
12
+
13
+ // ============================================================================
14
+ // 单项预加载函数(示例)
15
+ // ============================================================================
16
+
17
+ /**
18
+ * 预加载用户信息
19
+ * 在应用初始化或登录后调用
20
+ */
21
+ export async function prefetchUserInfo() {
22
+ // Implement based on your API contracts
23
+ // Example:
24
+ // return queryClient.prefetchQuery({
25
+ // queryKey: ['user', 'info'],
26
+ // queryFn: async () => {
27
+ // return userClient.getInfo({});
28
+ // },
29
+ // staleTime: cacheTime.short,
30
+ // });
31
+ }
32
+
33
+ /**
34
+ * 预加载仪表盘数据
35
+ * 在登录成功后或应用初始化时调用
36
+ */
37
+ export async function prefetchDashboardData() {
38
+ await Promise.all([
39
+ // prefetchUserInfo(),
40
+ // Add other prefetch functions as needed
41
+ ]);
42
+ }
43
+
44
+ // ============================================================================
45
+ // 预加载 Hooks
46
+ // ============================================================================
47
+
48
+ /**
49
+ * 获取预加载函数的 Hook
50
+ * 返回可以在事件(如 hover)中调用的预加载函数
51
+ */
52
+ export function usePrefetch() {
53
+ const createHoverHandler = useCallback(
54
+ (prefetchFn: () => Promise<unknown>) => {
55
+ let prefetched = false;
56
+ return () => {
57
+ if (!prefetched) {
58
+ prefetched = true;
59
+ prefetchFn();
60
+ }
61
+ };
62
+ },
63
+ [],
64
+ );
65
+
66
+ return {
67
+ createHoverHandler,
68
+ prefetchUserInfo,
69
+ prefetchDashboardData,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * 导航项预加载映射
75
+ * 根据路由路径返回对应的预加载函数
76
+ */
77
+ export function useNavPrefetch() {
78
+ const { createHoverHandler } = usePrefetch();
79
+
80
+ const navPrefetchMap = useMemo(
81
+ () => ({
82
+ '/dashboard': createHoverHandler(prefetchDashboardData),
83
+ // Add more route prefetch mappings as needed
84
+ }),
85
+ [createHoverHandler],
86
+ );
87
+
88
+ return navPrefetchMap;
89
+ }
90
+
91
+ /**
92
+ * 路由预加载映射
93
+ * 根据当前路由路径预加载对应页面数据
94
+ */
95
+ const routePrefetchMap: Record<string, () => Promise<unknown>> = {
96
+ '/dashboard': prefetchDashboardData,
97
+ // Add more route prefetch mappings as needed
98
+ };
99
+
100
+ /**
101
+ * 路由变化时自动预加载 Hook
102
+ * 在组件中使用,当路由变化时自动预加载目标页面数据
103
+ */
104
+ export function useRoutePrefetch() {
105
+ const pathname = usePathname();
106
+ const previousPathname = useRef<string | null>(null);
107
+
108
+ useEffect(() => {
109
+ const currentPath = pathname || '/';
110
+
111
+ if (previousPathname.current === currentPath) {
112
+ return;
113
+ }
114
+
115
+ previousPathname.current = currentPath;
116
+
117
+ const prefetchFn = routePrefetchMap[currentPath];
118
+
119
+ if (prefetchFn) {
120
+ prefetchFn().catch((error) => {
121
+ console.warn(
122
+ `Failed to prefetch data for route ${currentPath}:`,
123
+ error,
124
+ );
125
+ });
126
+ }
127
+ }, [pathname]);
128
+ }
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Query Client 单例
5
+ * 用于在 hooks 外部访问 queryClient(如预加载)
6
+ */
7
+
8
+ import { QueryClient } from '@tanstack/react-query';
9
+ import { cacheTime, gcTime } from './cache-config';
10
+
11
+ /**
12
+ * 创建 QueryClient 实例
13
+ */
14
+ function createQueryClient() {
15
+ return new QueryClient({
16
+ defaultOptions: {
17
+ queries: {
18
+ staleTime: cacheTime.medium,
19
+ gcTime: gcTime.medium,
20
+ retry: 1,
21
+ refetchOnWindowFocus: false,
22
+ },
23
+ },
24
+ });
25
+ }
26
+
27
+ // 单例实例
28
+ let queryClientInstance: QueryClient | undefined;
29
+
30
+ /**
31
+ * 获取 QueryClient 实例
32
+ * 在组件外使用(如预加载)
33
+ */
34
+ export const queryClient =
35
+ typeof window !== 'undefined'
36
+ ? (queryClientInstance ??= createQueryClient())
37
+ : createQueryClient();
@@ -0,0 +1,48 @@
1
+ /**
2
+ * 运行时翻译注册表:供 fetch 层、非 React 模块在客户端使用 next-intl 文案。
3
+ * 由 RuntimeI18nBridge 在挂载时注入各 namespace 的 t 函数。
4
+ */
5
+
6
+ export type NamespaceTranslate = (
7
+ key: string,
8
+ values?: Record<string, string | number>,
9
+ ) => string;
10
+
11
+ const registry = new Map<string, NamespaceTranslate>();
12
+
13
+ export function registerIntlNamespace(
14
+ namespace: string,
15
+ translate: NamespaceTranslate | null,
16
+ ): void {
17
+ if (translate) {
18
+ registry.set(namespace, translate);
19
+ } else {
20
+ registry.delete(namespace);
21
+ }
22
+ }
23
+
24
+ export function intlNs(
25
+ namespace: string,
26
+ key: string,
27
+ values?: Record<string, string | number>,
28
+ ): string {
29
+ const t = registry.get(namespace);
30
+ if (!t) {
31
+ return key;
32
+ }
33
+ try {
34
+ return values && Object.keys(values).length > 0 ? t(key, values) : t(key);
35
+ } catch {
36
+ return key;
37
+ }
38
+ }
39
+
40
+ /** 带 locale 前缀的登录路径,供 401 跳转 */
41
+ export function getLocaleLoginPath(): string {
42
+ if (typeof window === 'undefined') {
43
+ return '/login';
44
+ }
45
+ const pathname = window.location.pathname;
46
+ const m = pathname.match(/^\/(en|zh-CN)(?=\/|$)/);
47
+ return m ? `${m[0]}/login` : '/login';
48
+ }
@@ -1,4 +1,4 @@
1
- import { API_CONFIG } from './config';
1
+ import { API_CONFIG } from '@/config';
2
2
 
3
3
  function urlEncode(params: Record<string, unknown>) {
4
4
  if (params && typeof params === 'object' && !Array.isArray(params)) {
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ReactNode } from 'react';
9
9
  import { createContext, useContext } from 'react';
10
- import { BRAND_CONFIG } from '@/lib/config';
10
+ import { BRAND_CONFIG } from '@/config';
11
11
 
12
12
  interface AppContextType {
13
13
  title: string;
@@ -0,0 +1,228 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Auth Context Provider
5
+ *
6
+ * Provides authentication state and methods throughout the application.
7
+ * Manages user info, tokens, login/logout functionality.
8
+ */
9
+ import {
10
+ createContext,
11
+ useContext,
12
+ useState,
13
+ useEffect,
14
+ useCallback,
15
+ type ReactNode,
16
+ } from 'react';
17
+ import { useRouter } from '@/i18n/navigation';
18
+ import type { UserInfo, LoginSuccess } from '@repo/contracts';
19
+ import {
20
+ getUser,
21
+ setUser,
22
+ getTokens,
23
+ setTokens,
24
+ clearAll,
25
+ isTokenExpired,
26
+ } from '@/lib/storage';
27
+ import { signClient } from '@/lib/api/contracts/client';
28
+ import { prefetchDashboardData } from '@/lib/api/prefetch';
29
+
30
+ // ============================================================================
31
+ // Type Definitions
32
+ // ============================================================================
33
+
34
+ export interface AuthContextType {
35
+ // State
36
+ user: UserInfo | null;
37
+ isLoading: boolean;
38
+ isAuthenticated: boolean;
39
+ isAdmin: boolean;
40
+
41
+ // Actions
42
+ login: (data: LoginSuccess) => void;
43
+ logout: () => Promise<void>;
44
+ refreshUser: () => Promise<void>;
45
+ }
46
+
47
+ // ============================================================================
48
+ // Context Creation
49
+ // ============================================================================
50
+
51
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
52
+
53
+ // ============================================================================
54
+ // Provider Component
55
+ // ============================================================================
56
+
57
+ interface AuthProviderProps {
58
+ children: ReactNode;
59
+ }
60
+
61
+ export function AuthProvider({ children }: AuthProviderProps) {
62
+ const [user, setUserState] = useState<UserInfo | null>(null);
63
+ const [isLoading, setIsLoading] = useState(true);
64
+ const router = useRouter();
65
+
66
+ // Initialize user from localStorage on mount and prefetch dashboard data
67
+ useEffect(() => {
68
+ const storedUser = getUser();
69
+ if (storedUser) {
70
+ setUserState(storedUser);
71
+ // Prefetch dashboard data for returning authenticated users
72
+ prefetchDashboardData().catch((error) => {
73
+ console.warn('Failed to prefetch dashboard data on init:', error);
74
+ });
75
+ }
76
+ setIsLoading(false);
77
+
78
+ // Listen for user info updates from other components
79
+ const handleUserUpdate = () => {
80
+ const updatedUser = getUser();
81
+ setUserState(updatedUser);
82
+ };
83
+
84
+ window.addEventListener('userInfoUpdated', handleUserUpdate);
85
+ return () => {
86
+ window.removeEventListener('userInfoUpdated', handleUserUpdate);
87
+ };
88
+ }, []);
89
+
90
+ // Login: store user and tokens, then prefetch dashboard data
91
+ const login = useCallback((data: LoginSuccess) => {
92
+ // Store user (merges with existing data if any)
93
+ setUser(data.user);
94
+ setTokens({
95
+ access: data.access,
96
+ refresh: data.refresh,
97
+ accessExpire: data.accessExpire,
98
+ expire: data.expire,
99
+ });
100
+ // Get the merged user from storage for state
101
+ const mergedUser = getUser();
102
+ setUserState(mergedUser);
103
+ // Prefetch dashboard data after successful login
104
+ prefetchDashboardData().catch((error) => {
105
+ console.warn('Failed to prefetch dashboard data:', error);
106
+ });
107
+ }, []);
108
+
109
+ // Logout: clear storage and redirect
110
+ const logout = useCallback(async () => {
111
+ try {
112
+ // Call sign out endpoint
113
+ await signClient.signOut({
114
+ body: {},
115
+ });
116
+ } catch (error) {
117
+ // Ignore errors during logout
118
+ console.warn('Sign out API call failed:', error);
119
+ } finally {
120
+ // Clear local storage
121
+ clearAll();
122
+ setUserState(null);
123
+ // Redirect to login page
124
+ router.push('/login');
125
+ }
126
+ }, [router]);
127
+
128
+ // Refresh user info from server
129
+ const refreshUser = useCallback(async () => {
130
+ const tokens = getTokens();
131
+ if (!tokens?.refresh) {
132
+ return;
133
+ }
134
+
135
+ try {
136
+ const response = await signClient.refreshToken({
137
+ query: { refresh: tokens.refresh },
138
+ });
139
+
140
+ if (response.status === 200 && response.body.data) {
141
+ const data = response.body.data;
142
+ // Store user (merges with existing data)
143
+ setUser(data.user);
144
+ setTokens({
145
+ access: data.access,
146
+ refresh: data.refresh,
147
+ accessExpire: data.accessExpire,
148
+ expire: data.expire,
149
+ });
150
+ // Get the merged user from storage for state
151
+ const mergedUser = getUser();
152
+ setUserState(mergedUser);
153
+ }
154
+ } catch (error) {
155
+ console.error('Failed to refresh user:', error);
156
+ }
157
+ }, []);
158
+
159
+ // Auto-refresh token before expiry
160
+ useEffect(() => {
161
+ if (!user) return;
162
+
163
+ const checkAndRefresh = () => {
164
+ if (isTokenExpired()) {
165
+ refreshUser();
166
+ }
167
+ };
168
+
169
+ const interval = setInterval(checkAndRefresh, 60000);
170
+ return () => clearInterval(interval);
171
+ }, [user, refreshUser]);
172
+
173
+ const value: AuthContextType = {
174
+ user,
175
+ isLoading,
176
+ isAuthenticated: !!user && !isTokenExpired(),
177
+ isAdmin: user?.isAdmin ?? false,
178
+ login,
179
+ logout,
180
+ refreshUser,
181
+ };
182
+
183
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
184
+ }
185
+
186
+ // ============================================================================
187
+ // Hook
188
+ // ============================================================================
189
+
190
+ /**
191
+ * Hook to access auth context
192
+ * @throws Error if used outside AuthProvider
193
+ */
194
+ export function useAuth(): AuthContextType {
195
+ const context = useContext(AuthContext);
196
+ if (context === undefined) {
197
+ throw new Error('useAuth must be used within an AuthProvider');
198
+ }
199
+ return context;
200
+ }
201
+
202
+ // ============================================================================
203
+ // Utility Hooks
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Hook to check if user is authenticated
208
+ */
209
+ export function useIsAuthenticated(): boolean {
210
+ const { isAuthenticated } = useAuth();
211
+ return isAuthenticated;
212
+ }
213
+
214
+ /**
215
+ * Hook to get current user ID
216
+ */
217
+ export function useUserId(): string | null {
218
+ const { user } = useAuth();
219
+ return user?.id ?? null;
220
+ }
221
+
222
+ /**
223
+ * Hook to check if current user is admin
224
+ */
225
+ export function useIsAdmin(): boolean {
226
+ const { isAdmin } = useAuth();
227
+ return isAdmin;
228
+ }
@@ -11,35 +11,54 @@ import NextTopLoader from 'nextjs-toploader';
11
11
  import { QueryProvider } from './query-provider';
12
12
  import { ThemeProvider } from './theme-provider';
13
13
  import { AppProvider } from './app-provider';
14
+ import { AuthProvider } from './auth-provider';
15
+ import { RuntimeI18nBridge } from '@/components/runtime-i18n-bridge';
14
16
 
15
17
  export { QueryProvider } from './query-provider';
16
18
  export { ThemeProvider } from './theme-provider';
17
19
  export { AppProvider, useApp } from './app-provider';
20
+ export {
21
+ AuthProvider,
22
+ useAuth,
23
+ useIsAuthenticated,
24
+ useUserId,
25
+ useIsAdmin,
26
+ } from './auth-provider';
27
+ export { IntlClientProvider } from './intl-client-provider';
18
28
 
19
29
  interface ProvidersProps {
20
30
  children: ReactNode;
21
31
  }
22
32
 
23
33
  /**
24
- * Root Providers Component
34
+ * Root providers Component
25
35
  * 根 Providers 组件
26
36
  *
27
37
  * Combines all providers in the correct order:
28
- * 1. AppProvider - Application context
29
- * 2. ThemeProvider - Theme management
30
- * 3. QueryProvider - React Query
31
- * 4. UI components (Toaster, TopLoader)
38
+ * 1. AppProvider - Application context (brand, version)
39
+ * 2. ThemeProvider - Theme management (dark/light mode)
40
+ * 3. AuthProvider - Authentication state and methods
41
+ * 4. QueryProvider - React Query
42
+ * 5. UI components (Toaster, TopLoader)
32
43
  */
33
44
  export function Providers({ children }: ProvidersProps) {
34
45
  return (
35
46
  <AppProvider>
36
47
  <ThemeProvider>
37
- <NextTopLoader color="transparent" />
38
- <Toaster position="top-center" richColors />
39
- <QueryProvider>{children}</QueryProvider>
48
+ <AuthProvider>
49
+ <NextTopLoader color="#000" showSpinner={false} height={2} />
50
+ <Toaster
51
+ position="top-center"
52
+ richColors
53
+ closeButton
54
+ duration={4000}
55
+ />
56
+ <RuntimeI18nBridge />
57
+ <QueryProvider>{children}</QueryProvider>
58
+ </AuthProvider>
40
59
  </ThemeProvider>
41
60
  </AppProvider>
42
61
  );
43
62
  }
44
63
 
45
- export default Providers;
64
+ export default Providers;
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import type { AbstractIntlMessages } from 'next-intl';
5
+ import { NextIntlClientProvider, IntlErrorCode } from 'next-intl';
6
+ import type { Locale } from '@/i18n/config';
7
+
8
+ interface IntlClientProviderProps {
9
+ locale: Locale;
10
+ messages: AbstractIntlMessages;
11
+ /** Set from server layout using NODE_ENV so the client bundle avoids env reads. */
12
+ enableMissingMessageLogging?: boolean;
13
+ children: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * Wraps next-intl client provider. When enableMissingMessageLogging is true,
18
+ * logs missing message keys to the console to catch JSON drift early.
19
+ */
20
+ export function IntlClientProvider({
21
+ locale,
22
+ messages,
23
+ enableMissingMessageLogging = false,
24
+ children,
25
+ }: IntlClientProviderProps) {
26
+ return (
27
+ <NextIntlClientProvider
28
+ locale={locale}
29
+ messages={messages}
30
+ onError={
31
+ enableMissingMessageLogging
32
+ ? (error) => {
33
+ if (error.code === IntlErrorCode.MISSING_MESSAGE) {
34
+ console.warn('[next-intl] Missing message:', error.message);
35
+ }
36
+ }
37
+ : undefined
38
+ }
39
+ >
40
+ {children}
41
+ </NextIntlClientProvider>
42
+ );
43
+ }
@@ -2,12 +2,16 @@
2
2
  * Vitest Configuration for Next.js Web Application
3
3
  *
4
4
  * @see https://vitest.dev/config/
5
+ *
6
+ * Note: Vitest 4.x uses Vite 7 internally while @vitejs/plugin-react expects
7
+ * Vite 6 types. This type mismatch is known and doesn't affect functionality.
5
8
  */
6
9
  import { defineConfig } from 'vitest/config';
7
10
  import react from '@vitejs/plugin-react';
8
11
  import path from 'path';
9
12
 
10
13
  export default defineConfig({
14
+ // @ts-expect-error Vitest 4.x/Vite 7 type mismatch with @vitejs/plugin-react
11
15
  plugins: [react()],
12
16
  test: {
13
17
  // Test environment