@qlover/create-app 1.0.1 → 1.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.cjs +9 -9
  3. package/dist/index.js +9 -9
  4. package/dist/templates/next-app/config/Identifier/api.ts +7 -0
  5. package/dist/templates/next-app/config/Identifier/pages/page.register.ts +8 -0
  6. package/dist/templates/next-app/config/common.ts +1 -1
  7. package/dist/templates/next-app/config/i18n/HomeI18n.ts +2 -0
  8. package/dist/templates/next-app/config/i18n/register18n.ts +2 -1
  9. package/dist/templates/next-app/config/route.ts +9 -0
  10. package/dist/templates/next-app/migrations/schema/UserSchema.ts +1 -1
  11. package/dist/templates/next-app/next.config.ts +5 -4
  12. package/dist/templates/next-app/package.json +7 -8
  13. package/dist/templates/next-app/public/locales/en.json +4 -1
  14. package/dist/templates/next-app/public/locales/zh.json +4 -1
  15. package/dist/templates/next-app/src/app/[locale]/auth/layout.tsx +18 -0
  16. package/dist/templates/next-app/src/app/[locale]/{login → auth/login}/LoginForm.tsx +2 -1
  17. package/dist/templates/next-app/src/app/[locale]/{login → auth/login}/page.tsx +4 -5
  18. package/dist/templates/next-app/src/app/[locale]/auth/page.tsx +8 -0
  19. package/dist/templates/next-app/src/app/[locale]/{register → auth/register}/RegisterForm.tsx +24 -3
  20. package/dist/templates/next-app/src/app/[locale]/{register → auth/register}/page.tsx +4 -5
  21. package/dist/templates/next-app/src/app/[locale]/page.tsx +18 -45
  22. package/dist/templates/next-app/src/app/api/ai/completions/route.ts +13 -13
  23. package/dist/templates/next-app/src/app/api/auth/callback/route.ts +11 -0
  24. package/dist/templates/next-app/src/app/api/callback/route.ts +49 -0
  25. package/dist/templates/next-app/src/base/cases/AppConfig.ts +2 -0
  26. package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +3 -6
  27. package/dist/templates/next-app/src/base/cases/DialogHandler.ts +0 -1
  28. package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +13 -15
  29. package/dist/templates/next-app/src/base/cases/RouterService.ts +2 -7
  30. package/dist/templates/next-app/src/base/cases/StringEncryptor.ts +0 -6
  31. package/dist/templates/next-app/src/base/cases/TranslateI18nUtil.ts +53 -0
  32. package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +0 -10
  33. package/dist/templates/next-app/src/base/port/AdminLayoutInterface.ts +0 -3
  34. package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +1 -1
  35. package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +0 -18
  36. package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +10 -5
  37. package/dist/templates/next-app/src/base/services/{appApi/AppApiRequester.ts → AppApiRequester.ts} +16 -11
  38. package/dist/templates/next-app/src/base/services/AppUserGateway.ts +110 -0
  39. package/dist/templates/next-app/src/base/services/I18nService.ts +1 -7
  40. package/dist/templates/next-app/src/base/services/ResourceService.ts +1 -4
  41. package/dist/templates/next-app/src/base/services/UserService.ts +28 -17
  42. package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +5 -7
  43. package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +4 -3
  44. package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +24 -16
  45. package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +2 -5
  46. package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +15 -18
  47. package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +0 -5
  48. package/dist/templates/next-app/src/core/globals.ts +1 -0
  49. package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +2 -8
  50. package/dist/templates/next-app/src/i18n/routing.ts +8 -3
  51. package/dist/templates/next-app/src/lib/supabase/client.ts +8 -0
  52. package/dist/templates/next-app/src/lib/supabase/conts.ts +2 -0
  53. package/dist/templates/next-app/src/lib/supabase/proxy.ts +84 -0
  54. package/dist/templates/next-app/src/lib/supabase/server.ts +38 -0
  55. package/dist/templates/next-app/src/proxy.ts +8 -1
  56. package/dist/templates/next-app/src/server/AppPageRouteParams.ts +5 -2
  57. package/dist/templates/next-app/src/server/NextApiServer.ts +2 -9
  58. package/dist/templates/next-app/src/server/PagesRouteParams.ts +3 -4
  59. package/dist/templates/next-app/src/server/ServerAuth.ts +18 -12
  60. package/dist/templates/next-app/src/server/SupabaseBridge.ts +66 -59
  61. package/dist/templates/next-app/src/server/controllers/UserController.ts +7 -2
  62. package/dist/templates/next-app/src/server/port/ServerAuthInterface.ts +4 -0
  63. package/dist/templates/next-app/src/server/port/ServerInterface.ts +2 -1
  64. package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +7 -1
  65. package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +0 -3
  66. package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +0 -3
  67. package/dist/templates/next-app/src/server/services/UserService.ts +71 -51
  68. package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +0 -3
  69. package/dist/templates/next-app/src/server/validators/LoginValidator.ts +0 -6
  70. package/dist/templates/next-app/src/server/validators/SignupVerifyValidator.ts +68 -0
  71. package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -3
  72. package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +0 -6
  73. package/dist/templates/next-app/src/uikit/components-app/AdminButton.tsx +29 -0
  74. package/dist/templates/next-app/src/uikit/components-app/AppRoutePage.tsx +21 -28
  75. package/dist/templates/next-app/src/uikit/components-app/AuthButton.tsx +20 -0
  76. package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +2 -2
  77. package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +3 -17
  78. package/dist/templates/next-app/src/uikit/utils/getHashParams.ts +8 -0
  79. package/dist/templates/next-app/src/uikit/utils/getHashVerifyEmailParams.ts +42 -0
  80. package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +2 -0
  81. package/package.json +2 -2
  82. package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +0 -25
  83. package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +0 -78
  84. package/dist/templates/next-app/src/base/services/adminApi/AdminApiRequester.ts +0 -25
  85. package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +0 -78
  86. package/dist/templates/next-app/src/server/port/UserControllerInerface.ts +0 -8
@@ -1,7 +1,13 @@
1
1
  import type { UserSchema } from '@migrations/schema/UserSchema';
2
2
 
3
+ export type UserServiceRegisterParams = {
4
+ username?: string;
5
+ email: string;
6
+ password: string;
7
+ };
8
+
3
9
  export interface UserServiceInterface {
4
- register(params: { email: string; password: string }): Promise<UserSchema>;
10
+ register(params: UserServiceRegisterParams): Promise<UserSchema>;
5
11
  login(params: { email: string; password: string }): Promise<UserSchema>;
6
12
 
7
13
  logout(): Promise<void>;
@@ -27,9 +27,6 @@ export class LocalesRepository implements LocalesRepositoryInterface {
27
27
  @inject(Datetime) protected datetime: Datetime
28
28
  ) {}
29
29
 
30
- /**
31
- * @override
32
- */
33
30
  public async getAll(): Promise<LocalesSchema[]> {
34
31
  const result = await this.dbBridge.get({
35
32
  table: this.name,
@@ -25,9 +25,6 @@ export class UserRepository implements UserRepositoryInterface {
25
25
  @inject(I.DBBridgeInterface) protected dbBridge: DBBridgeInterface
26
26
  ) {}
27
27
 
28
- /**
29
- * @override
30
- */
31
28
  public getAll(): Promise<unknown> {
32
29
  return this.dbBridge.get({ table: this.name });
33
30
  }
@@ -1,13 +1,15 @@
1
+ import { ExecutorError, type EncryptorInterface } from '@qlover/fe-corekit';
2
+ import { Session, User } from '@supabase/supabase-js';
1
3
  import { inject, injectable } from 'inversify';
2
- import { isEmpty, last, omit } from 'lodash';
4
+ import { isString } from 'lodash';
5
+ import { AppConfig } from '@/base/cases/AppConfig';
3
6
  import type { UserSchema } from '@migrations/schema/UserSchema';
4
- import {
5
- API_USER_NOT_FOUND,
6
- API_USER_ALREADY_EXISTS
7
- } from '@config/Identifier/api';
7
+ import { API_USER_NOT_FOUND } from '@config/Identifier/api';
8
+ import { I } from '@config/IOCIdentifier';
8
9
  import { PasswordEncrypt } from '../PasswordEncrypt';
9
10
  import { UserRepository } from '../repositorys/UserRepository';
10
11
  import { ServerAuth } from '../ServerAuth';
12
+ import { SupabaseBridge } from '../SupabaseBridge';
11
13
  import {
12
14
  UserCredentialToken,
13
15
  type UserCredentialTokenValue
@@ -15,11 +17,20 @@ import {
15
17
  import type { CrentialTokenInterface } from '../port/CrentialTokenInterface';
16
18
  import type { ServerAuthInterface } from '../port/ServerAuthInterface';
17
19
  import type { UserRepositoryInterface } from '../port/UserRepositoryInterface';
18
- import type { UserServiceInterface } from '../port/UserServiceInterface';
19
- import type { EncryptorInterface } from '@qlover/fe-corekit';
20
+ import type {
21
+ UserServiceInterface,
22
+ UserServiceRegisterParams
23
+ } from '../port/UserServiceInterface';
24
+ import type { LoggerInterface } from '@qlover/logger';
20
25
 
21
26
  @injectable()
22
27
  export class UserService implements UserServiceInterface {
28
+ @inject(I.Logger)
29
+ protected logger!: LoggerInterface;
30
+
31
+ @inject(I.AppConfig)
32
+ protected appConfig!: AppConfig;
33
+
23
34
  constructor(
24
35
  @inject(UserRepository)
25
36
  protected userRepository: UserRepositoryInterface,
@@ -28,33 +39,39 @@ export class UserService implements UserServiceInterface {
28
39
  @inject(PasswordEncrypt)
29
40
  protected encryptor: EncryptorInterface<string, string>,
30
41
  @inject(UserCredentialToken)
31
- protected credentialToken: CrentialTokenInterface<UserCredentialTokenValue>
42
+ protected credentialToken: CrentialTokenInterface<UserCredentialTokenValue>,
43
+ @inject(SupabaseBridge) protected supabaseBridge: SupabaseBridge
32
44
  ) {}
33
45
 
34
46
  /**
35
47
  * @override
36
48
  */
37
- public async register(params: {
38
- email: string;
39
- password: string;
40
- }): Promise<UserSchema> {
41
- const user = await this.userRepository.getUserByEmail(params.email);
42
-
43
- if (!isEmpty(user)) {
44
- throw new Error(API_USER_ALREADY_EXISTS);
45
- }
46
-
47
- const result = await this.userRepository.add({
49
+ public async register(
50
+ params: UserServiceRegisterParams
51
+ ): Promise<UserSchema> {
52
+ const supabase = await this.supabaseBridge.getSupabase();
53
+
54
+ // TODO: 检查 username, 是否重复
55
+ // const user = await this.userRepository.getUserByEmail(params.email);
56
+ // if (!isEmpty(user)) {
57
+ // throw new Error(API_USER_ALREADY_EXISTS);
58
+ // }
59
+
60
+ const result = await supabase.auth.signUp({
48
61
  email: params.email,
49
- password: this.encryptor.encrypt(params.password)
62
+ password: params.password
63
+
64
+ // options: {
65
+ // emailRedirectTo: 'http://localhost:3100/callback'
66
+ // }
50
67
  });
68
+ this.supabaseBridge.throwIfError(result);
51
69
 
52
- const target = last(result);
53
- if (!target) {
70
+ if (!result.data.user) {
54
71
  throw new Error(API_USER_NOT_FOUND);
55
72
  }
56
73
 
57
- return omit(target, 'password') as UserSchema;
74
+ return this.supabaseBridge.toUserSchema(result.data.user);
58
75
  }
59
76
 
60
77
  /**
@@ -63,52 +80,55 @@ export class UserService implements UserServiceInterface {
63
80
  public async login(params: {
64
81
  email: string;
65
82
  password: string;
83
+ authCode?: string;
66
84
  }): Promise<UserSchema> {
67
- const user = await this.userRepository.getUserByEmail(params.email);
68
-
69
- if (!user) {
70
- throw new Error(API_USER_NOT_FOUND);
71
- }
72
-
73
- const encryptedPassword = this.encryptor.encrypt(params.password);
85
+ const supabase = await this.supabaseBridge.getSupabase();
74
86
 
75
- if (encryptedPassword !== user.password) {
76
- throw new Error(API_USER_NOT_FOUND);
87
+ if (params.authCode) {
88
+ const ares = await supabase.auth.exchangeCodeForSession(params.authCode);
89
+ this.supabaseBridge.throwIfError(ares);
77
90
  }
78
91
 
79
- const credentialToken = await this.credentialToken.generateToken(user);
80
-
81
- await this.userRepository.updateById(user.id, {
82
- credential_token: credentialToken
92
+ const result = await supabase.auth.signInWithPassword({
93
+ email: params.email,
94
+ password: params.password
83
95
  });
96
+ this.supabaseBridge.throwIfError(result);
84
97
 
85
- return Object.assign(omit(user, 'password') as UserSchema, {
86
- credential_token: credentialToken
87
- });
98
+ this.logger.info('supbase login succees', result.data);
99
+
100
+ return this.supabaseBridge.toUserSchema(result.data.user!);
88
101
  }
89
102
 
90
103
  /**
91
104
  * @override
92
105
  */
93
106
  public async logout(): Promise<void> {
94
- const auth = await this.userAuth.getAuth();
107
+ const supabase = await this.supabaseBridge.getSupabase();
108
+
109
+ const response = await supabase.auth.signOut();
110
+
111
+ this.supabaseBridge.throwIfError(response);
112
+ }
95
113
 
96
- if (!auth) {
97
- return;
114
+ public async exchangeSessionForCode(code: string): Promise<{
115
+ user: User;
116
+ session: Session;
117
+ }> {
118
+ if (code == null || !isString(code)) {
119
+ throw new ExecutorError('code is required');
98
120
  }
99
121
 
100
- try {
101
- const user = await this.credentialToken.parseToken(auth);
122
+ const supabase = await this.supabaseBridge.getSupabase();
123
+ const response = await supabase.auth.exchangeCodeForSession(code);
124
+ this.supabaseBridge.throwIfError(response);
102
125
 
103
- console.log('user', user);
126
+ this.logger.debug('exchangeSessionForCode', response.data);
104
127
 
105
- await this.userRepository.updateById(user.id, {
106
- credential_token: ''
107
- });
108
- } catch {
109
- return;
128
+ if (!response.data.user) {
129
+ throw new ExecutorError(API_USER_NOT_FOUND);
110
130
  }
111
131
 
112
- await this.userAuth.clear();
132
+ return response.data;
113
133
  }
114
134
  }
@@ -53,9 +53,6 @@ export class LocalesValidator implements ValidatorInterface<
53
53
  }
54
54
 
55
55
  export class LocalesImportValidator implements ValidatorInterface<ImportLocalesData> {
56
- /**
57
- * @override
58
- */
59
56
  public getHasAnyFilesLocale(
60
57
  values: FormData
61
58
  ): { language: LocaleType; value: FormDataEntryValue }[] {
@@ -27,9 +27,6 @@ const passwordSchema = z
27
27
  .regex(/^\S+$/, { message: V_PASSWORD_SPECIAL_CHARS });
28
28
 
29
29
  export class LoginValidator implements ValidatorInterface<LoginValidatorData> {
30
- /**
31
- * @override
32
- */
33
30
  public validateEmail(data: unknown): void | ValidationFaildResult {
34
31
  const emailResult = emailSchema.safeParse(data);
35
32
  if (!emailResult.success) {
@@ -37,9 +34,6 @@ export class LoginValidator implements ValidatorInterface<LoginValidatorData> {
37
34
  }
38
35
  }
39
36
 
40
- /**
41
- * @override
42
- */
43
37
  public validatePassword(data: unknown): void | ValidationFaildResult {
44
38
  const passwordResult = passwordSchema.safeParse(data);
45
39
  if (!passwordResult.success) {
@@ -0,0 +1,68 @@
1
+ import { ExecutorError } from '@qlover/fe-corekit';
2
+ import { isPlainObject, isString } from 'lodash';
3
+ import type { ExtendedExecutorError } from './ExtendedExecutorError';
4
+ import type {
5
+ ValidationFaildResult,
6
+ ValidatorInterface
7
+ } from '../port/ValidatorInterface';
8
+ import type { EmailOtpType } from '@supabase/supabase-js';
9
+
10
+ export type SignupVerifyParamType = {
11
+ access_token: string;
12
+ expires_at: string;
13
+ expires_in: string;
14
+ refresh_token: string;
15
+ token_type: string;
16
+ type: EmailOtpType;
17
+ };
18
+
19
+ export const emailVerifyParamKeys = [
20
+ 'access_token',
21
+ 'expires_at',
22
+ 'expires_in',
23
+ 'refresh_token',
24
+ 'token_type',
25
+ 'type'
26
+ ] as const;
27
+
28
+ export class SignupVerifyValidator implements ValidatorInterface<SignupVerifyParamType> {
29
+ /**
30
+ * @override
31
+ */
32
+ public validate(data: unknown): void | ValidationFaildResult {
33
+ if (!isPlainObject(data)) {
34
+ return {
35
+ path: ['form'],
36
+ message: 'Invalid Signup verify params'
37
+ };
38
+ }
39
+
40
+ for (const key of emailVerifyParamKeys) {
41
+ if (
42
+ !(
43
+ isString((data as SignupVerifyParamType)[key]) &&
44
+ (data as SignupVerifyParamType)[key]
45
+ )
46
+ ) {
47
+ return {
48
+ path: [key],
49
+ message: `Invalid Signup verify ${key} params`
50
+ };
51
+ }
52
+ }
53
+ }
54
+ /**
55
+ * @override
56
+ */
57
+ public getThrow(data: unknown): SignupVerifyParamType {
58
+ const result = this.validate(data);
59
+
60
+ if (result == null) {
61
+ return data as SignupVerifyParamType;
62
+ }
63
+
64
+ const error: ExtendedExecutorError = new ExecutorError(result.message);
65
+ error.issues = [result];
66
+ throw error;
67
+ }
68
+ }
@@ -1,14 +1,14 @@
1
1
  'use client';
2
2
  import '@ant-design/v5-patch-for-react-19';
3
3
 
4
- import { useEffect, useState } from 'react';
5
4
  import { useLocale } from 'next-intl';
5
+ import { useEffect, useState } from 'react';
6
+ import type { I18nServiceLocale } from '@/base/port/I18nServiceInterface';
6
7
  import { BootstrapClient } from '@/core/bootstraps/BootstrapClient';
8
+ import { I } from '@config/IOCIdentifier';
7
9
  import { useIOC } from '../hook/useIOC';
8
10
  import { useStrictEffect } from '../hook/useStrictEffect';
9
11
  import { useWarnTranslations } from '../hook/useWarnTranslations';
10
- import { I } from '@config/IOCIdentifier';
11
- import type { I18nServiceLocale } from '@/base/port/I18nServiceInterface';
12
12
 
13
13
  export function BootstrapsProvider(props: { children: React.ReactNode }) {
14
14
  const IOC = useIOC();
@@ -10,18 +10,12 @@ export class LocalesImportEvent extends StoreInterface<LocalesImportEventState>
10
10
  super(() => new LocalesImportEventState());
11
11
  }
12
12
 
13
- /**
14
- * @override
15
- */
16
13
  protected validate(file: File): void {
17
14
  if (file.type !== 'application/json') {
18
15
  throw new Error('File must be a JSON file');
19
16
  }
20
17
  }
21
18
 
22
- /**
23
- * @override
24
- */
25
19
  public async onImport(type: LocaleType, file: File): Promise<void> {
26
20
  try {
27
21
  this.validate(file);
@@ -0,0 +1,29 @@
1
+ import { TeamOutlined } from '@ant-design/icons';
2
+ import { bootstrapServer } from '@/core/bootstraps/BootstrapServer';
3
+ import { ServerAuth } from '@/server/ServerAuth';
4
+ import { LocaleLink } from '../components/LocaleLink';
5
+
6
+ export async function AdminButton(props: {
7
+ adminTitle: string;
8
+ locale?: string;
9
+ }) {
10
+ const { adminTitle, locale } = props;
11
+ const hasAuth = await bootstrapServer.getIOC(ServerAuth).hasAuth();
12
+
13
+ if (!hasAuth) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <LocaleLink
19
+ data-testid="AdminButton"
20
+ key="admin-button"
21
+ href="/admin"
22
+ title={adminTitle}
23
+ locale={locale}
24
+ className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
25
+ >
26
+ <TeamOutlined className="text-lg text-text" />
27
+ </LocaleLink>
28
+ );
29
+ }
@@ -1,10 +1,10 @@
1
- import { TeamOutlined } from '@ant-design/icons';
2
1
  import { clsx } from 'clsx';
3
2
  import { useLocale } from 'next-intl';
4
- import { useMemo, type HTMLAttributes } from 'react';
3
+ import { Suspense, type HTMLAttributes } from 'react';
4
+ import { AdminButton } from './AdminButton';
5
5
  import { AppBridge } from './AppBridge';
6
+ import { AuthButton } from './AuthButton';
6
7
  import { LanguageSwitcher } from './LanguageSwitcher';
7
- import { LogoutButton } from './LogoutButton';
8
8
  import { ThemeSwitcher } from './ThemeSwitcher';
9
9
  import { LocaleLink } from '../components/LocaleLink';
10
10
 
@@ -14,9 +14,9 @@ export interface AppRoutePageTT {
14
14
  }
15
15
 
16
16
  export interface AppRoutePageProps extends HTMLAttributes<HTMLDivElement> {
17
- showLogoutButton?: boolean;
18
17
  showAdminButton?: boolean;
19
18
  mainProps?: HTMLAttributes<HTMLElement>;
19
+ showAuthButton?: boolean;
20
20
  headerClassName?: string;
21
21
  headerHref?: string;
22
22
  tt: AppRoutePageTT;
@@ -34,10 +34,10 @@ export interface AppRoutePageProps extends HTMLAttributes<HTMLDivElement> {
34
34
  */
35
35
  export function AppRoutePage({
36
36
  children,
37
- showLogoutButton,
38
37
  showAdminButton,
39
38
  mainProps,
40
39
  headerClassName,
40
+ showAuthButton,
41
41
  tt,
42
42
  headerHref = '/',
43
43
  ...props
@@ -45,28 +45,6 @@ export function AppRoutePage({
45
45
  const locale = useLocale();
46
46
  const adminTitle = tt.adminTitle;
47
47
 
48
- const actions = useMemo(() => {
49
- return [
50
- showAdminButton && (
51
- <LocaleLink
52
- key="admin-button"
53
- href="/admin"
54
- title={adminTitle}
55
- locale={locale}
56
- className="text-text hover:text-text-hover cursor-pointer text-lg transition-colors"
57
- >
58
- <TeamOutlined className="text-lg text-text" />
59
- </LocaleLink>
60
- ),
61
-
62
- <LanguageSwitcher key="language-switcher" />,
63
-
64
- <ThemeSwitcher key="theme-switcher" />,
65
-
66
- showLogoutButton && <LogoutButton key="logout-button" />
67
- ].filter(Boolean);
68
- }, [adminTitle, showAdminButton, showLogoutButton, locale]);
69
-
70
48
  return (
71
49
  <div
72
50
  data-testid="AppRoutePage"
@@ -100,7 +78,22 @@ export function AppRoutePage({
100
78
  </span>
101
79
  </LocaleLink>
102
80
  </div>
103
- <div className="flex items-center gap-2 md:gap-4">{actions}</div>
81
+ <div className="flex items-center gap-2 md:gap-4">
82
+ {showAdminButton && (
83
+ <Suspense>
84
+ <AdminButton adminTitle={adminTitle} locale={locale} />
85
+ </Suspense>
86
+ )}
87
+
88
+ <LanguageSwitcher key="language-switcher" />
89
+ <ThemeSwitcher key="theme-switcher" />
90
+
91
+ {showAuthButton && (
92
+ <Suspense>
93
+ <AuthButton />
94
+ </Suspense>
95
+ )}
96
+ </div>
104
97
  </div>
105
98
  </header>
106
99
 
@@ -0,0 +1,20 @@
1
+ import { bootstrapServer } from '@/core/bootstraps/BootstrapServer';
2
+ import { Link } from '@/i18n/routing';
3
+ import { ServerAuth } from '@/server/ServerAuth';
4
+ import { ROUTE_LOGIN, ROUTE_REGISTER } from '@config/route';
5
+ import { LogoutButton } from './LogoutButton';
6
+
7
+ export async function AuthButton() {
8
+ const hasAuth = await bootstrapServer.getIOC(ServerAuth).hasAuth();
9
+
10
+ if (hasAuth) {
11
+ return <LogoutButton data-testid="logout-button" />;
12
+ }
13
+
14
+ return (
15
+ <div data-testid="AuthButton" className="flex gap-2" data-auth={hasAuth}>
16
+ <Link href={ROUTE_LOGIN}>Sign in</Link>
17
+ <Link href={ROUTE_REGISTER}>Sign up</Link>
18
+ </div>
19
+ );
20
+ }
@@ -1,5 +1,5 @@
1
1
  import { useMemo } from 'react';
2
- import { TranslateI18nInterface } from '@/base/cases/TranslateI18nInterface';
2
+ import { TranslateI18nUtil } from '@/base/cases/TranslateI18nUtil';
3
3
  import { useWarnTranslations } from './useWarnTranslations';
4
4
 
5
5
  /**
@@ -14,7 +14,7 @@ export function useI18nInterface<T extends Record<string, string>>(
14
14
  const t = useWarnTranslations();
15
15
 
16
16
  const i18n = useMemo(
17
- () => TranslateI18nInterface.translate(i18nInterface, t),
17
+ () => TranslateI18nUtil.translate(i18nInterface, t),
18
18
  [i18nInterface, t]
19
19
  );
20
20
 
@@ -1,25 +1,11 @@
1
1
  import { useTranslations as useNextTranslations } from 'next-intl';
2
- import { useCallback } from 'react';
3
- import { i18nWarnMissingTranslation } from '@config/common';
2
+ import { useMemo } from 'react';
3
+ import { TranslateI18nUtil } from '@/base/cases/TranslateI18nUtil';
4
4
 
5
5
  export function useWarnTranslations() {
6
6
  const t = useNextTranslations();
7
7
 
8
- const overrideT = useCallback(
9
- (key: string) => {
10
- if (!i18nWarnMissingTranslation) {
11
- return t(key);
12
- }
13
-
14
- if (t.has(key)) {
15
- return t(key);
16
- }
17
-
18
- console.warn(`[i18n] Missing translation: ${key}`);
19
- return key;
20
- },
21
- [t]
22
- );
8
+ const overrideT = useMemo(() => TranslateI18nUtil.overrideTranslateT(t), [t]);
23
9
 
24
10
  return Object.assign(overrideT, t);
25
11
  }
@@ -0,0 +1,8 @@
1
+ export function getHashParams(hash: string): Record<string, string | null> {
2
+ const sp = new URLSearchParams(hash.startsWith('#') ? hash.slice(1) : hash);
3
+ const params = {};
4
+ sp.forEach((value, key) => {
5
+ Object.assign(params, { [key]: value });
6
+ });
7
+ return params;
8
+ }
@@ -0,0 +1,42 @@
1
+ import { isString, pick } from 'lodash';
2
+ import type { SignupVerifyParamType } from '@/server/validators/SignupVerifyValidator';
3
+
4
+ export const emailVerifyParamKeys = [
5
+ 'access_token',
6
+ 'expires_at',
7
+ 'expires_in',
8
+ 'refresh_token',
9
+ 'token_type',
10
+ 'type'
11
+ ] as const;
12
+
13
+ export function getHashParams(hash: string): Record<string, string | null> {
14
+ const sp = new URLSearchParams(hash.startsWith('#') ? hash.slice(1) : hash);
15
+ const params = {};
16
+ sp.forEach((value, key) => {
17
+ Object.assign(params, { [key]: value });
18
+ });
19
+ return params;
20
+ }
21
+
22
+ export function isEmailVerifyParam(
23
+ value: unknown
24
+ ): value is SignupVerifyParamType {
25
+ return emailVerifyParamKeys.every(
26
+ (key) =>
27
+ emailVerifyParamKeys.includes(key) &&
28
+ (value as SignupVerifyParamType)[key] != null &&
29
+ isString((value as SignupVerifyParamType)[key])
30
+ );
31
+ }
32
+
33
+ export function getHashVerifyEmailParams(
34
+ hash: string
35
+ ): SignupVerifyParamType | null {
36
+ if (!hash) {
37
+ return null;
38
+ }
39
+
40
+ const hashParams = pick(getHashParams(hash), emailVerifyParamKeys);
41
+ return isEmailVerifyParam(hashParams) ? hashParams : null;
42
+ }
@@ -48,4 +48,6 @@ export interface PageI18nInterface {
48
48
  author?: string;
49
49
  publishedTime?: string;
50
50
  modifiedTime?: string;
51
+
52
+ [key: string]: unknown;
51
53
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qlover/create-app",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Create a new app with a single command",
5
5
  "private": false,
6
6
  "type": "module",
@@ -38,7 +38,7 @@
38
38
  "dependencies": {
39
39
  "commander": "^13.1.0",
40
40
  "inquirer": "^12.3.2",
41
- "@qlover/scripts-context": "2.0.0"
41
+ "@qlover/scripts-context": "2.1.0"
42
42
  },
43
43
  "scripts": {
44
44
  "lint": "eslint src --fix",
@@ -1,25 +0,0 @@
1
- import type { PageI18nInterface } from '@config/i18n';
2
- import type { useTranslations } from 'next-intl';
3
-
4
- /**
5
- * Translate I18n Interface tools class
6
- *
7
- * @param i18nInterface - The i18n interface to translate
8
- * @param t - The translations function
9
- * @returns The translated i18n interface
10
- */
11
- export class TranslateI18nInterface {
12
- public static translate<T extends PageI18nInterface | Record<string, string>>(
13
- source: T,
14
- t: ReturnType<typeof useTranslations>
15
- ): T {
16
- return Object.fromEntries(
17
- Object.entries(source).map(([key, value]) => {
18
- if (typeof value === 'string') {
19
- return [key, t(value)];
20
- }
21
- return [key, value];
22
- })
23
- ) as T;
24
- }
25
- }