@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.
- package/CHANGELOG.md +30 -0
- package/dist/index.cjs +9 -9
- package/dist/index.js +9 -9
- package/dist/templates/next-app/config/Identifier/api.ts +7 -0
- package/dist/templates/next-app/config/Identifier/pages/page.register.ts +8 -0
- package/dist/templates/next-app/config/common.ts +1 -1
- package/dist/templates/next-app/config/i18n/HomeI18n.ts +2 -0
- package/dist/templates/next-app/config/i18n/register18n.ts +2 -1
- package/dist/templates/next-app/config/route.ts +9 -0
- package/dist/templates/next-app/migrations/schema/UserSchema.ts +1 -1
- package/dist/templates/next-app/next.config.ts +5 -4
- package/dist/templates/next-app/package.json +7 -8
- package/dist/templates/next-app/public/locales/en.json +4 -1
- package/dist/templates/next-app/public/locales/zh.json +4 -1
- package/dist/templates/next-app/src/app/[locale]/auth/layout.tsx +18 -0
- package/dist/templates/next-app/src/app/[locale]/{login → auth/login}/LoginForm.tsx +2 -1
- package/dist/templates/next-app/src/app/[locale]/{login → auth/login}/page.tsx +4 -5
- package/dist/templates/next-app/src/app/[locale]/auth/page.tsx +8 -0
- package/dist/templates/next-app/src/app/[locale]/{register → auth/register}/RegisterForm.tsx +24 -3
- package/dist/templates/next-app/src/app/[locale]/{register → auth/register}/page.tsx +4 -5
- package/dist/templates/next-app/src/app/[locale]/page.tsx +18 -45
- package/dist/templates/next-app/src/app/api/ai/completions/route.ts +13 -13
- package/dist/templates/next-app/src/app/api/auth/callback/route.ts +11 -0
- package/dist/templates/next-app/src/app/api/callback/route.ts +49 -0
- package/dist/templates/next-app/src/base/cases/AppConfig.ts +2 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +3 -6
- package/dist/templates/next-app/src/base/cases/DialogHandler.ts +0 -1
- package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +13 -15
- package/dist/templates/next-app/src/base/cases/RouterService.ts +2 -7
- package/dist/templates/next-app/src/base/cases/StringEncryptor.ts +0 -6
- package/dist/templates/next-app/src/base/cases/TranslateI18nUtil.ts +53 -0
- package/dist/templates/next-app/src/base/cases/ZodColumnBuilder.ts +0 -10
- package/dist/templates/next-app/src/base/port/AdminLayoutInterface.ts +0 -3
- package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +1 -1
- package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +0 -18
- package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +10 -5
- package/dist/templates/next-app/src/base/services/{appApi/AppApiRequester.ts → AppApiRequester.ts} +16 -11
- package/dist/templates/next-app/src/base/services/AppUserGateway.ts +110 -0
- package/dist/templates/next-app/src/base/services/I18nService.ts +1 -7
- package/dist/templates/next-app/src/base/services/ResourceService.ts +1 -4
- package/dist/templates/next-app/src/base/services/UserService.ts +28 -17
- package/dist/templates/next-app/src/base/services/adminApi/AdminLocalesApi.ts +5 -7
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +4 -3
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +24 -16
- package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +2 -5
- package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +15 -18
- package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +0 -5
- package/dist/templates/next-app/src/core/globals.ts +1 -0
- package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +2 -8
- package/dist/templates/next-app/src/i18n/routing.ts +8 -3
- package/dist/templates/next-app/src/lib/supabase/client.ts +8 -0
- package/dist/templates/next-app/src/lib/supabase/conts.ts +2 -0
- package/dist/templates/next-app/src/lib/supabase/proxy.ts +84 -0
- package/dist/templates/next-app/src/lib/supabase/server.ts +38 -0
- package/dist/templates/next-app/src/proxy.ts +8 -1
- package/dist/templates/next-app/src/server/AppPageRouteParams.ts +5 -2
- package/dist/templates/next-app/src/server/NextApiServer.ts +2 -9
- package/dist/templates/next-app/src/server/PagesRouteParams.ts +3 -4
- package/dist/templates/next-app/src/server/ServerAuth.ts +18 -12
- package/dist/templates/next-app/src/server/SupabaseBridge.ts +66 -59
- package/dist/templates/next-app/src/server/controllers/UserController.ts +7 -2
- package/dist/templates/next-app/src/server/port/ServerAuthInterface.ts +4 -0
- package/dist/templates/next-app/src/server/port/ServerInterface.ts +2 -1
- package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +7 -1
- package/dist/templates/next-app/src/server/repositorys/LocalesRepository.ts +0 -3
- package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +0 -3
- package/dist/templates/next-app/src/server/services/UserService.ts +71 -51
- package/dist/templates/next-app/src/server/validators/LocalesValidator.ts +0 -3
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +0 -6
- package/dist/templates/next-app/src/server/validators/SignupVerifyValidator.ts +68 -0
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +3 -3
- package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts +0 -6
- package/dist/templates/next-app/src/uikit/components-app/AdminButton.tsx +29 -0
- package/dist/templates/next-app/src/uikit/components-app/AppRoutePage.tsx +21 -28
- package/dist/templates/next-app/src/uikit/components-app/AuthButton.tsx +20 -0
- package/dist/templates/next-app/src/uikit/hook/useI18nInterface.ts +2 -2
- package/dist/templates/next-app/src/uikit/hook/useWarnTranslations.ts +3 -17
- package/dist/templates/next-app/src/uikit/utils/getHashParams.ts +8 -0
- package/dist/templates/next-app/src/uikit/utils/getHashVerifyEmailParams.ts +42 -0
- package/dist/templates/react-app/config/i18n/PageI18nInterface.ts +2 -0
- package/package.json +2 -2
- package/dist/templates/next-app/src/base/cases/TranslateI18nInterface.ts +0 -25
- package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +0 -78
- package/dist/templates/next-app/src/base/services/adminApi/AdminApiRequester.ts +0 -25
- package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +0 -78
- 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:
|
|
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 {
|
|
4
|
+
import { isString } from 'lodash';
|
|
5
|
+
import { AppConfig } from '@/base/cases/AppConfig';
|
|
3
6
|
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
4
|
-
import {
|
|
5
|
-
|
|
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 {
|
|
19
|
-
|
|
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(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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:
|
|
62
|
+
password: params.password
|
|
63
|
+
|
|
64
|
+
// options: {
|
|
65
|
+
// emailRedirectTo: 'http://localhost:3100/callback'
|
|
66
|
+
// }
|
|
50
67
|
});
|
|
68
|
+
this.supabaseBridge.throwIfError(result);
|
|
51
69
|
|
|
52
|
-
|
|
53
|
-
if (!target) {
|
|
70
|
+
if (!result.data.user) {
|
|
54
71
|
throw new Error(API_USER_NOT_FOUND);
|
|
55
72
|
}
|
|
56
73
|
|
|
57
|
-
return
|
|
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
|
|
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 (
|
|
76
|
-
|
|
87
|
+
if (params.authCode) {
|
|
88
|
+
const ares = await supabase.auth.exchangeCodeForSession(params.authCode);
|
|
89
|
+
this.supabaseBridge.throwIfError(ares);
|
|
77
90
|
}
|
|
78
91
|
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
|
|
122
|
+
const supabase = await this.supabaseBridge.getSupabase();
|
|
123
|
+
const response = await supabase.auth.exchangeCodeForSession(code);
|
|
124
|
+
this.supabaseBridge.throwIfError(response);
|
|
102
125
|
|
|
103
|
-
|
|
126
|
+
this.logger.debug('exchangeSessionForCode', response.data);
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
});
|
|
108
|
-
} catch {
|
|
109
|
-
return;
|
|
128
|
+
if (!response.data.user) {
|
|
129
|
+
throw new ExecutorError(API_USER_NOT_FOUND);
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
|
|
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();
|
package/dist/templates/next-app/src/uikit/components/localesImportButton/LocalesImportEvent.ts
CHANGED
|
@@ -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 {
|
|
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">
|
|
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 {
|
|
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
|
-
() =>
|
|
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 {
|
|
3
|
-
import {
|
|
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 =
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@qlover/create-app",
|
|
3
|
-
"version": "1.0
|
|
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.
|
|
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
|
-
}
|