@qlover/create-app 0.7.9 → 0.7.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.
- package/CHANGELOG.md +184 -0
- package/dist/index.cjs +3 -3
- package/dist/index.js +3 -3
- package/dist/templates/next-app/.env.template +8 -4
- package/dist/templates/next-app/config/IOCIdentifier.ts +4 -1
- package/dist/templates/next-app/config/Identifier/api.ts +34 -0
- package/dist/templates/next-app/config/Identifier/common.ts +7 -0
- package/dist/templates/next-app/config/Identifier/index.ts +2 -0
- package/dist/templates/next-app/config/Identifier/page.login.ts +2 -2
- package/dist/templates/next-app/config/Identifier/page.register.ts +43 -22
- package/dist/templates/next-app/config/Identifier/validator.ts +34 -0
- package/dist/templates/next-app/config/i18n/index.ts +1 -0
- package/dist/templates/next-app/config/i18n/register18n.ts +44 -0
- package/dist/templates/next-app/eslint.config.mjs +17 -0
- package/dist/templates/next-app/migrations/schema/UserSchema.ts +24 -0
- package/dist/templates/next-app/migrations/sql/1694244000000.sql +10 -0
- package/dist/templates/next-app/next.config.ts +1 -0
- package/dist/templates/next-app/package.json +12 -2
- package/dist/templates/next-app/public/locales/en.json +19 -2
- package/dist/templates/next-app/public/locales/zh.json +19 -2
- package/dist/templates/next-app/src/app/[locale]/admin/layout.tsx +18 -0
- package/dist/templates/next-app/src/app/[locale]/admin/page.tsx +22 -0
- package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +62 -0
- package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -1
- package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +26 -6
- package/dist/templates/next-app/src/app/[locale]/login/page.tsx +7 -5
- package/dist/templates/next-app/src/app/[locale]/page.tsx +5 -5
- package/dist/templates/next-app/src/app/[locale]/register/RegisterForm.tsx +176 -0
- package/dist/templates/next-app/src/app/[locale]/register/page.tsx +79 -0
- package/dist/templates/next-app/src/app/api/admin/users/route.ts +39 -0
- package/dist/templates/next-app/src/app/api/user/login/route.ts +50 -0
- package/dist/templates/next-app/src/app/api/user/logout/route.ts +27 -0
- package/dist/templates/next-app/src/app/api/user/register/route.ts +50 -0
- package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +40 -0
- package/dist/templates/next-app/src/base/cases/AppConfig.ts +19 -0
- package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +46 -0
- package/dist/templates/next-app/src/base/cases/PageParams.ts +1 -1
- package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +70 -0
- package/dist/templates/next-app/src/base/cases/RequestState.ts +20 -0
- package/dist/templates/next-app/src/base/cases/RouterService.ts +4 -0
- package/dist/templates/next-app/src/base/cases/StringEncryptor.ts +67 -0
- package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +48 -0
- package/dist/templates/next-app/src/base/port/AdminLayoutInterface.ts +26 -0
- package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +87 -0
- package/dist/templates/next-app/src/base/port/AppApiInterface.ts +14 -0
- package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +15 -0
- package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +7 -0
- package/dist/templates/next-app/src/base/port/DBBridgeInterface.ts +21 -0
- package/dist/templates/next-app/src/base/port/DBMigrationInterface.ts +92 -0
- package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +3 -2
- package/dist/templates/next-app/src/base/port/MigrationApiInterface.ts +3 -0
- package/dist/templates/next-app/src/base/port/PaginationInterface.ts +6 -0
- package/dist/templates/next-app/src/base/port/ServerApiResponseInterface.ts +6 -0
- package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +3 -2
- package/dist/templates/next-app/src/base/services/AdminUserService.ts +45 -0
- package/dist/templates/next-app/src/base/services/I18nService.ts +9 -45
- package/dist/templates/next-app/src/base/services/UserService.ts +9 -8
- package/dist/templates/next-app/src/base/services/adminApi/AdminApiRequester.ts +21 -0
- package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +34 -0
- package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +63 -0
- package/dist/templates/next-app/src/base/services/appApi/AppApiRequester.ts +56 -0
- package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +71 -0
- package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +49 -0
- package/dist/templates/next-app/src/base/services/migrations/MigrationsApi.ts +43 -0
- package/dist/templates/next-app/src/core/bootstraps/BootstrapClient.ts +1 -1
- package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +26 -12
- package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +5 -4
- package/dist/templates/next-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
- package/dist/templates/next-app/src/core/clientIoc/ClientIOC.ts +1 -1
- package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +1 -1
- package/dist/templates/next-app/src/core/globals.ts +1 -1
- package/dist/templates/next-app/src/core/serverIoc/ServerIOC.ts +1 -1
- package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +1 -1
- package/dist/templates/next-app/src/server/AppErrorApi.ts +10 -0
- package/dist/templates/next-app/src/server/AppSuccessApi.ts +7 -0
- package/dist/templates/next-app/src/server/PasswordEncrypt.ts +12 -0
- package/dist/templates/next-app/src/server/ServerAuth.ts +60 -0
- package/dist/templates/next-app/src/server/SupabaseBridge.ts +159 -0
- package/dist/templates/next-app/src/server/UserCredentialToken.ts +49 -0
- package/dist/templates/next-app/src/server/port/CrentialTokenInterface.ts +5 -0
- package/dist/templates/next-app/src/server/port/DBTableInterface.ts +10 -0
- package/dist/templates/next-app/src/server/port/ServerAuthInterface.ts +11 -0
- package/dist/templates/next-app/src/server/port/ServerInterface.ts +22 -0
- package/dist/templates/next-app/src/server/port/UserRepositoryInterface.ts +15 -0
- package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +8 -0
- package/dist/templates/next-app/src/server/port/ValidatorInterface.ts +23 -0
- package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +94 -0
- package/dist/templates/next-app/src/server/services/AdminAuthPlugin.ts +19 -0
- package/dist/templates/next-app/src/server/services/ApiUserService.ts +26 -0
- package/dist/templates/next-app/src/server/services/UserService.ts +105 -0
- package/dist/templates/next-app/src/server/validators/LoginValidator.ts +79 -0
- package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +48 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +74 -1
- package/dist/templates/next-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +73 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/_common/index.css +3 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +70 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/index.css +4 -3
- package/dist/templates/next-app/src/styles/css/antd-themes/menu/_default.css +108 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/menu/dark.css +67 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/menu/index.css +3 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/menu/pink.css +67 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +33 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +32 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/index.css +3 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +35 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/table/_default.css +44 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/table/dark.css +43 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/table/index.css +3 -0
- package/dist/templates/next-app/src/styles/css/antd-themes/table/pink.css +43 -0
- package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +106 -0
- package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +68 -17
- package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +6 -1
- package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +9 -2
- package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +11 -3
- package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +1 -1
- package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +21 -11
- package/dist/templates/next-app/src/uikit/hook/useIOC.ts +1 -1
- package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +7 -1
- package/dist/templates/next-app/tsconfig.json +3 -1
- package/package.json +2 -2
- package/dist/templates/next-app/src/base/cases/ServerAuth.ts +0 -17
- package/dist/templates/next-app/src/base/cases/ServerErrorHandler.ts +0 -27
- package/dist/templates/next-app/src/base/port/ServerAuthInterface.ts +0 -3
- package/dist/templates/next-app/src/base/port/ServerInterface.ts +0 -12
- /package/dist/templates/next-app/src/{app/[locale]/login → uikit/components}/FeatureItem.tsx +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createClient,
|
|
3
|
+
type PostgrestSingleResponse,
|
|
4
|
+
type SupabaseClient,
|
|
5
|
+
type PostgrestResponse
|
|
6
|
+
} from '@supabase/supabase-js';
|
|
7
|
+
import { injectable, inject } from 'inversify';
|
|
8
|
+
import type { AppConfig } from '@/base/cases/AppConfig';
|
|
9
|
+
import type {
|
|
10
|
+
BridgeEvent,
|
|
11
|
+
DBBridgeInterface,
|
|
12
|
+
Where
|
|
13
|
+
} from '@/base/port/DBBridgeInterface';
|
|
14
|
+
import { I } from '@config/IOCIdentifier';
|
|
15
|
+
import type { LoggerInterface } from '@qlover/logger';
|
|
16
|
+
import type { PostgrestFilterBuilder } from '@supabase/postgrest-js';
|
|
17
|
+
|
|
18
|
+
const whereHandlerMaps = {
|
|
19
|
+
'=': 'eq',
|
|
20
|
+
'!=': 'neq',
|
|
21
|
+
'>': 'gt',
|
|
22
|
+
'<': 'lt',
|
|
23
|
+
'>=': 'gte',
|
|
24
|
+
'<=': 'lte'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
@injectable()
|
|
28
|
+
export class SupabaseBridge implements DBBridgeInterface {
|
|
29
|
+
protected supabase: SupabaseClient;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
@inject(I.AppConfig) appConfig: AppConfig,
|
|
33
|
+
@inject(I.Logger) protected logger: LoggerInterface
|
|
34
|
+
) {
|
|
35
|
+
this.supabase = createClient(
|
|
36
|
+
appConfig.supabaseUrl,
|
|
37
|
+
appConfig.supabaseAnonKey
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getSupabase(): SupabaseClient {
|
|
42
|
+
return this.supabase;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async execSql(sql: string): Promise<PostgrestSingleResponse<unknown>> {
|
|
46
|
+
const res = await this.supabase.rpc('exec_sql', { sql });
|
|
47
|
+
return this.catch(res);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected async catch(
|
|
51
|
+
result: PostgrestSingleResponse<unknown>
|
|
52
|
+
): Promise<PostgrestSingleResponse<unknown>> {
|
|
53
|
+
if (result.error) {
|
|
54
|
+
this.logger.info(result);
|
|
55
|
+
throw new Error(result.error.message);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected handleWhere(
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
handler: PostgrestFilterBuilder<any, any, any, any, string, unknown, any>,
|
|
63
|
+
wheres: Where[]
|
|
64
|
+
): void {
|
|
65
|
+
for (const where of wheres) {
|
|
66
|
+
const [key, operator, value] = where;
|
|
67
|
+
const opr = whereHandlerMaps[operator];
|
|
68
|
+
if (!opr) {
|
|
69
|
+
throw new Error(`Unsupported where operation: ${value}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
if (typeof (handler as any)[opr] !== 'function') {
|
|
74
|
+
throw new Error(`Unsupported where operation: ${value}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
+
(handler as any)[opr](key, value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async add(event: BridgeEvent): Promise<PostgrestSingleResponse<unknown>> {
|
|
83
|
+
const { table, data } = event;
|
|
84
|
+
if (!data) {
|
|
85
|
+
throw new Error('Data is required for add operation');
|
|
86
|
+
}
|
|
87
|
+
const res = await this.supabase
|
|
88
|
+
.from(table)
|
|
89
|
+
.insert(Array.isArray(data) ? data : [data])
|
|
90
|
+
.select();
|
|
91
|
+
return this.catch(res);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async update(event: BridgeEvent): Promise<PostgrestSingleResponse<unknown>> {
|
|
95
|
+
const { table, data, where } = event;
|
|
96
|
+
if (!data) {
|
|
97
|
+
throw new Error('Data is required for update operation');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const handler = this.supabase.from(table).update(data);
|
|
101
|
+
|
|
102
|
+
this.handleWhere(handler, where ?? []);
|
|
103
|
+
|
|
104
|
+
return this.catch(await handler);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async delete(event: BridgeEvent): Promise<PostgrestSingleResponse<unknown>> {
|
|
108
|
+
const { table, where } = event;
|
|
109
|
+
const handler = this.supabase.from(table).delete();
|
|
110
|
+
|
|
111
|
+
this.handleWhere(handler, where ?? []);
|
|
112
|
+
|
|
113
|
+
return this.catch(await handler);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async get(event: BridgeEvent): Promise<PostgrestSingleResponse<unknown>> {
|
|
117
|
+
const { table, fields = '*', where } = event;
|
|
118
|
+
const selectFields = Array.isArray(fields) ? fields.join(',') : fields;
|
|
119
|
+
const handler = this.supabase.from(table).select(selectFields);
|
|
120
|
+
|
|
121
|
+
this.handleWhere(handler, where ?? []);
|
|
122
|
+
|
|
123
|
+
return this.catch(await handler);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async pagination(event: BridgeEvent): Promise<PostgrestResponse<unknown>> {
|
|
127
|
+
const { table, fields = '*', where, page = 1, pageSize = 10 } = event;
|
|
128
|
+
const selectFields = Array.isArray(fields) ? fields.join(',') : fields;
|
|
129
|
+
|
|
130
|
+
// 获取总数
|
|
131
|
+
const countHandler = this.supabase
|
|
132
|
+
.from(table)
|
|
133
|
+
.select('*', { count: 'exact', head: true });
|
|
134
|
+
|
|
135
|
+
this.handleWhere(countHandler, where ?? []);
|
|
136
|
+
const countResult = await this.catch(await countHandler);
|
|
137
|
+
|
|
138
|
+
// 获取分页数据
|
|
139
|
+
const handler = this.supabase
|
|
140
|
+
.from(table)
|
|
141
|
+
.select(selectFields)
|
|
142
|
+
.range((page - 1) * pageSize, page * pageSize - 1);
|
|
143
|
+
|
|
144
|
+
this.handleWhere(handler, where ?? []);
|
|
145
|
+
const result = await this.catch(await handler);
|
|
146
|
+
|
|
147
|
+
if (result.error) {
|
|
148
|
+
return result as PostgrestResponse<unknown>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
data: Array.isArray(result.data) ? result.data : [],
|
|
153
|
+
error: null,
|
|
154
|
+
count: countResult.count,
|
|
155
|
+
status: result.status,
|
|
156
|
+
statusText: result.statusText
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { inject, injectable } from 'inversify';
|
|
2
|
+
import jwt from 'jsonwebtoken';
|
|
3
|
+
import { AppConfig } from '@/base/cases/AppConfig';
|
|
4
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
5
|
+
import type { CrentialTokenInterface } from './port/CrentialTokenInterface';
|
|
6
|
+
|
|
7
|
+
export type UserCredentialTokenValue = Pick<UserSchema, 'id' | 'email'>;
|
|
8
|
+
|
|
9
|
+
@injectable()
|
|
10
|
+
export class UserCredentialToken
|
|
11
|
+
implements CrentialTokenInterface<UserCredentialTokenValue>
|
|
12
|
+
{
|
|
13
|
+
protected jwtSecret: string;
|
|
14
|
+
protected jwtExpiresIn: string;
|
|
15
|
+
|
|
16
|
+
constructor(@inject(AppConfig) protected config: AppConfig) {
|
|
17
|
+
this.jwtSecret = config.jwtSecret;
|
|
18
|
+
this.jwtExpiresIn = config.jwtExpiresIn;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async generateToken(
|
|
22
|
+
data: UserCredentialTokenValue,
|
|
23
|
+
options: { expiresIn?: string } = {}
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
const { expiresIn = '30 days' } = options;
|
|
26
|
+
|
|
27
|
+
return jwt.sign({ i: data.id, e: data.email }, this.jwtSecret, {
|
|
28
|
+
expiresIn: expiresIn as jwt.SignOptions['expiresIn'],
|
|
29
|
+
algorithm: 'HS256',
|
|
30
|
+
noTimestamp: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async parseToken(token: string): Promise<UserCredentialTokenValue> {
|
|
35
|
+
try {
|
|
36
|
+
const decoded = jwt.verify(token, this.jwtSecret) as {
|
|
37
|
+
i: UserSchema['id'];
|
|
38
|
+
e: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
id: decoded.i,
|
|
43
|
+
email: decoded.e
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error('Invalid token');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ExecutorError, type PromiseTask } from '@qlover/fe-corekit';
|
|
2
|
+
import { type IOCIdentifierMapServer } from '@config/IOCIdentifier';
|
|
3
|
+
import type {
|
|
4
|
+
ServiceIdentifier,
|
|
5
|
+
IOCContainerInterface,
|
|
6
|
+
IOCFunctionInterface,
|
|
7
|
+
LoggerInterface
|
|
8
|
+
} from '@qlover/corekit-bridge';
|
|
9
|
+
|
|
10
|
+
export interface ServerInterface {
|
|
11
|
+
readonly logger: LoggerInterface;
|
|
12
|
+
|
|
13
|
+
getIOC(): IOCFunctionInterface<IOCIdentifierMapServer, IOCContainerInterface>;
|
|
14
|
+
getIOC<T extends keyof IOCIdentifierMapServer>(
|
|
15
|
+
identifier: T
|
|
16
|
+
): IOCIdentifierMapServer[T];
|
|
17
|
+
getIOC<T>(serviceIdentifier: ServiceIdentifier<T>): T;
|
|
18
|
+
|
|
19
|
+
execNoError<Result>(
|
|
20
|
+
task?: PromiseTask<Result, unknown>
|
|
21
|
+
): Promise<Result | ExecutorError>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DBTableInterface } from '@/server/port/DBTableInterface';
|
|
2
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
3
|
+
|
|
4
|
+
export interface UserRepositoryInterface extends DBTableInterface {
|
|
5
|
+
getUserByEmail(email: string): Promise<UserSchema | null>;
|
|
6
|
+
add(params: {
|
|
7
|
+
email: string;
|
|
8
|
+
password: string;
|
|
9
|
+
}): Promise<UserSchema[] | null>;
|
|
10
|
+
|
|
11
|
+
updateById(
|
|
12
|
+
id: number,
|
|
13
|
+
params: Partial<Omit<UserSchema, 'id' | 'created_at'>>
|
|
14
|
+
): Promise<void>;
|
|
15
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
2
|
+
|
|
3
|
+
export interface UserServiceInterface {
|
|
4
|
+
register(params: { email: string; password: string }): Promise<UserSchema>;
|
|
5
|
+
login(params: { email: string; password: string }): Promise<unknown>;
|
|
6
|
+
|
|
7
|
+
logout(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ValidationFaildResult {
|
|
2
|
+
path: PropertyKey[];
|
|
3
|
+
message: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface ValidatorInterface {
|
|
7
|
+
/**
|
|
8
|
+
* Validate the data and return validation result
|
|
9
|
+
* @param data - The data to validate
|
|
10
|
+
* @returns true if validation passes, or ValidationError if validation fails
|
|
11
|
+
*/
|
|
12
|
+
validate(
|
|
13
|
+
data: unknown
|
|
14
|
+
): Promise<void | ValidationFaildResult> | void | ValidationFaildResult;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the data if it is valid, otherwise throw an error with validation details
|
|
18
|
+
* @param data - The data to validate
|
|
19
|
+
* @returns The data if it is valid
|
|
20
|
+
* @throws {import('@qlover/fe-corekit').ExecutorError} if the data is invalid, with validation errors
|
|
21
|
+
*/
|
|
22
|
+
getThrow(data: unknown): unknown;
|
|
23
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { inject, injectable } from 'inversify';
|
|
2
|
+
import { isEmpty, last } from 'lodash';
|
|
3
|
+
import type { DBBridgeInterface } from '@/base/port/DBBridgeInterface';
|
|
4
|
+
import type { PaginationInterface } from '@/base/port/PaginationInterface';
|
|
5
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
6
|
+
import { SupabaseBridge } from '../SupabaseBridge';
|
|
7
|
+
import type { UserRepositoryInterface } from '../port/UserRepositoryInterface';
|
|
8
|
+
|
|
9
|
+
@injectable()
|
|
10
|
+
export class UserRepository implements UserRepositoryInterface {
|
|
11
|
+
readonly name = 'fe_users';
|
|
12
|
+
|
|
13
|
+
protected safeFields = [
|
|
14
|
+
'created_at',
|
|
15
|
+
// 'credential_token',
|
|
16
|
+
'email',
|
|
17
|
+
'email_confirmed_at',
|
|
18
|
+
'id',
|
|
19
|
+
// 'password',
|
|
20
|
+
'role',
|
|
21
|
+
'updated_at'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
constructor(@inject(SupabaseBridge) protected dbBridge: DBBridgeInterface) {}
|
|
25
|
+
|
|
26
|
+
getAll(): Promise<unknown> {
|
|
27
|
+
return this.dbBridge.get({ table: this.name });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @override
|
|
32
|
+
*/
|
|
33
|
+
async getUserByEmail(email: string): Promise<UserSchema | null> {
|
|
34
|
+
const result = await this.dbBridge.get({
|
|
35
|
+
table: this.name,
|
|
36
|
+
where: [['email', '=', email]]
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (isEmpty(result.data)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return last(result.data as UserSchema[]) ?? null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @override
|
|
48
|
+
*/
|
|
49
|
+
async add(params: {
|
|
50
|
+
email: string;
|
|
51
|
+
password: string;
|
|
52
|
+
}): Promise<UserSchema[] | null> {
|
|
53
|
+
const result = await this.dbBridge.add({
|
|
54
|
+
table: this.name,
|
|
55
|
+
data: params
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (isEmpty(result.data)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return result.data as UserSchema[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async updateById(
|
|
66
|
+
id: number,
|
|
67
|
+
params: Partial<Omit<UserSchema, 'id' | 'created_at'>>
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
await this.dbBridge.update({
|
|
70
|
+
table: this.name,
|
|
71
|
+
data: params,
|
|
72
|
+
where: [['id', '=', id]]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async pagination<UserSchema>(params: {
|
|
77
|
+
page: number;
|
|
78
|
+
pageSize: number;
|
|
79
|
+
}): Promise<PaginationInterface<UserSchema>> {
|
|
80
|
+
const result = await this.dbBridge.pagination({
|
|
81
|
+
table: this.name,
|
|
82
|
+
page: params.page,
|
|
83
|
+
pageSize: params.pageSize,
|
|
84
|
+
fields: this.safeFields
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
list: result.data as UserSchema[],
|
|
89
|
+
total: result.count ?? 0,
|
|
90
|
+
page: params.page,
|
|
91
|
+
pageSize: params.pageSize
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { BootstrapServerContextValue } from '@/core/bootstraps/BootstrapServer';
|
|
2
|
+
import { ServerAuth } from '../ServerAuth';
|
|
3
|
+
import type { ServerAuthInterface } from '../port/ServerAuthInterface';
|
|
4
|
+
import type { BootstrapExecutorPlugin } from '@qlover/corekit-bridge';
|
|
5
|
+
import type { ExecutorContext } from '@qlover/fe-corekit';
|
|
6
|
+
|
|
7
|
+
export class AdminAuthPlugin implements BootstrapExecutorPlugin {
|
|
8
|
+
pluginName = 'AdminAuthPlugin';
|
|
9
|
+
|
|
10
|
+
async onBefore(
|
|
11
|
+
context: ExecutorContext<BootstrapServerContextValue>
|
|
12
|
+
): Promise<void> {
|
|
13
|
+
const { IOC } = context.parameters;
|
|
14
|
+
|
|
15
|
+
const serverAuth: ServerAuthInterface = IOC(ServerAuth);
|
|
16
|
+
|
|
17
|
+
await serverAuth.throwIfNotAuth();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { inject, injectable } from 'inversify';
|
|
2
|
+
import type { PaginationInterface } from '@/base/port/PaginationInterface';
|
|
3
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
4
|
+
import { UserRepository } from '../repositorys/UserRepository';
|
|
5
|
+
import { PaginationValidator } from '../validators/PaginationValidator';
|
|
6
|
+
import type { UserRepositoryInterface } from '../port/UserRepositoryInterface';
|
|
7
|
+
import type { ValidatorInterface } from '../port/ValidatorInterface';
|
|
8
|
+
|
|
9
|
+
@injectable()
|
|
10
|
+
export class ApiUserService {
|
|
11
|
+
constructor(
|
|
12
|
+
@inject(UserRepository)
|
|
13
|
+
protected userRepository: UserRepositoryInterface,
|
|
14
|
+
@inject(PaginationValidator)
|
|
15
|
+
protected paginationValidator: ValidatorInterface
|
|
16
|
+
) {}
|
|
17
|
+
|
|
18
|
+
async getUsers(params: {
|
|
19
|
+
page: number;
|
|
20
|
+
pageSize: number;
|
|
21
|
+
}): Promise<PaginationInterface<UserSchema>> {
|
|
22
|
+
const result = await this.userRepository.pagination(params);
|
|
23
|
+
|
|
24
|
+
return result as PaginationInterface<UserSchema>;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { inject, injectable } from 'inversify';
|
|
2
|
+
import { isEmpty, last, omit } from 'lodash';
|
|
3
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
4
|
+
import {
|
|
5
|
+
API_USER_NOT_FOUND,
|
|
6
|
+
API_USER_ALREADY_EXISTS
|
|
7
|
+
} from '@config/Identifier/api';
|
|
8
|
+
import { PasswordEncrypt } from '../PasswordEncrypt';
|
|
9
|
+
import { UserRepository } from '../repositorys/UserRepository';
|
|
10
|
+
import { ServerAuth } from '../ServerAuth';
|
|
11
|
+
import {
|
|
12
|
+
UserCredentialToken,
|
|
13
|
+
type UserCredentialTokenValue
|
|
14
|
+
} from '../UserCredentialToken';
|
|
15
|
+
import type { CrentialTokenInterface } from '../port/CrentialTokenInterface';
|
|
16
|
+
import type { ServerAuthInterface } from '../port/ServerAuthInterface';
|
|
17
|
+
import type { UserRepositoryInterface } from '../port/UserRepositoryInterface';
|
|
18
|
+
import type { UserServiceInterface } from '../port/UserServiceInterface';
|
|
19
|
+
import type { Encryptor } from '@qlover/fe-corekit';
|
|
20
|
+
|
|
21
|
+
@injectable()
|
|
22
|
+
export class UserService implements UserServiceInterface {
|
|
23
|
+
constructor(
|
|
24
|
+
@inject(UserRepository)
|
|
25
|
+
protected userRepository: UserRepositoryInterface,
|
|
26
|
+
@inject(ServerAuth)
|
|
27
|
+
protected userAuth: ServerAuthInterface,
|
|
28
|
+
@inject(PasswordEncrypt)
|
|
29
|
+
protected encryptor: Encryptor<string, string>,
|
|
30
|
+
@inject(UserCredentialToken)
|
|
31
|
+
protected credentialToken: CrentialTokenInterface<UserCredentialTokenValue>
|
|
32
|
+
) {}
|
|
33
|
+
|
|
34
|
+
async register(params: {
|
|
35
|
+
email: string;
|
|
36
|
+
password: string;
|
|
37
|
+
}): Promise<UserSchema> {
|
|
38
|
+
const user = await this.userRepository.getUserByEmail(params.email);
|
|
39
|
+
|
|
40
|
+
if (!isEmpty(user)) {
|
|
41
|
+
throw new Error(API_USER_ALREADY_EXISTS);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const result = await this.userRepository.add({
|
|
45
|
+
email: params.email,
|
|
46
|
+
password: this.encryptor.encrypt(params.password)
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const target = last(result);
|
|
50
|
+
if (!target) {
|
|
51
|
+
throw new Error(API_USER_NOT_FOUND);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return omit(target, 'password') as UserSchema;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async login(params: {
|
|
58
|
+
email: string;
|
|
59
|
+
password: string;
|
|
60
|
+
}): Promise<UserSchema> {
|
|
61
|
+
const user = await this.userRepository.getUserByEmail(params.email);
|
|
62
|
+
|
|
63
|
+
if (!user) {
|
|
64
|
+
throw new Error(API_USER_NOT_FOUND);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const encryptedPassword = this.encryptor.encrypt(params.password);
|
|
68
|
+
|
|
69
|
+
if (encryptedPassword !== user.password) {
|
|
70
|
+
throw new Error(API_USER_NOT_FOUND);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const credentialToken = await this.credentialToken.generateToken(user);
|
|
74
|
+
|
|
75
|
+
await this.userRepository.updateById(user.id, {
|
|
76
|
+
credential_token: credentialToken
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return Object.assign(omit(user, 'password') as UserSchema, {
|
|
80
|
+
credential_token: credentialToken
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async logout(): Promise<void> {
|
|
85
|
+
const auth = await this.userAuth.getAuth();
|
|
86
|
+
|
|
87
|
+
if (!auth) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const user = await this.credentialToken.parseToken(auth);
|
|
93
|
+
|
|
94
|
+
console.log('user', user);
|
|
95
|
+
|
|
96
|
+
await this.userRepository.updateById(user.id, {
|
|
97
|
+
credential_token: ''
|
|
98
|
+
});
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await this.userAuth.clear();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { ExecutorError } from '@qlover/fe-corekit';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
V_LOGIN_PARAMS_REQUIRED,
|
|
5
|
+
V_EMAIL_INVALID,
|
|
6
|
+
V_PASSWORD_MIN_LENGTH,
|
|
7
|
+
V_PASSWORD_MAX_LENGTH,
|
|
8
|
+
V_PASSWORD_SPECIAL_CHARS
|
|
9
|
+
} from '@config/Identifier/validator';
|
|
10
|
+
import type {
|
|
11
|
+
ValidatorInterface,
|
|
12
|
+
ValidationFaildResult
|
|
13
|
+
} from '../port/ValidatorInterface';
|
|
14
|
+
|
|
15
|
+
export interface LoginValidatorData {
|
|
16
|
+
email: string;
|
|
17
|
+
password: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const emailSchema = z.email({ error: V_EMAIL_INVALID });
|
|
21
|
+
|
|
22
|
+
const passwordSchema = z
|
|
23
|
+
.string()
|
|
24
|
+
.min(6, { error: V_PASSWORD_MIN_LENGTH })
|
|
25
|
+
.max(50, { error: V_PASSWORD_MAX_LENGTH })
|
|
26
|
+
.regex(/^\S+$/, { error: V_PASSWORD_SPECIAL_CHARS });
|
|
27
|
+
|
|
28
|
+
interface ExtendedExecutorError extends ExecutorError {
|
|
29
|
+
issues?: ValidationFaildResult[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class LoginValidator implements ValidatorInterface {
|
|
33
|
+
validateEmail(data: unknown): void | ValidationFaildResult {
|
|
34
|
+
const emailResult = emailSchema.safeParse(data);
|
|
35
|
+
if (!emailResult.success) {
|
|
36
|
+
return emailResult.error.issues[0];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
validatePassword(data: unknown): void | ValidationFaildResult {
|
|
41
|
+
const passwordResult = passwordSchema.safeParse(data);
|
|
42
|
+
if (!passwordResult.success) {
|
|
43
|
+
return passwordResult.error.issues[0];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
validate(data: unknown): void | ValidationFaildResult {
|
|
48
|
+
if (typeof data !== 'object' || data === null) {
|
|
49
|
+
return {
|
|
50
|
+
path: ['form'],
|
|
51
|
+
message: V_LOGIN_PARAMS_REQUIRED
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const { email, password } = data as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
let validateResult = this.validateEmail(email);
|
|
58
|
+
if (validateResult != null) {
|
|
59
|
+
return validateResult;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
validateResult = this.validatePassword(password);
|
|
63
|
+
if (validateResult != null) {
|
|
64
|
+
return validateResult;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
getThrow(data: unknown): LoginValidatorData {
|
|
69
|
+
const result = this.validate(data);
|
|
70
|
+
|
|
71
|
+
if (result == null) {
|
|
72
|
+
return data as LoginValidatorData;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const error: ExtendedExecutorError = new ExecutorError(result.message);
|
|
76
|
+
error.issues = [result];
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { PaginationInterface } from '@/base/port/PaginationInterface';
|
|
3
|
+
import { API_PAGE_INVALID } from '@config/Identifier';
|
|
4
|
+
import {
|
|
5
|
+
type ValidationFaildResult,
|
|
6
|
+
type ValidatorInterface
|
|
7
|
+
} from '../port/ValidatorInterface';
|
|
8
|
+
|
|
9
|
+
const pageSchema = z
|
|
10
|
+
.number()
|
|
11
|
+
.or(z.string())
|
|
12
|
+
.transform((val) => Number(val))
|
|
13
|
+
.refine((val) => val > 0, {
|
|
14
|
+
message: API_PAGE_INVALID
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export class PaginationValidator implements ValidatorInterface {
|
|
18
|
+
protected defaultPageSize = 10;
|
|
19
|
+
|
|
20
|
+
validatePageSize(value: unknown): void | ValidationFaildResult {
|
|
21
|
+
const result = pageSchema.safeParse(value);
|
|
22
|
+
if (!result.success) {
|
|
23
|
+
return result.error.issues[0];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
validate(data: unknown): void | ValidationFaildResult {
|
|
28
|
+
if (typeof data !== 'object' || data === null) {
|
|
29
|
+
return {
|
|
30
|
+
path: ['form'],
|
|
31
|
+
message: 'form is required'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return this.validatePageSize((data as Record<string, unknown>).page);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getThrow(
|
|
39
|
+
data: unknown
|
|
40
|
+
): Pick<PaginationInterface<unknown>, 'page' | 'pageSize'> {
|
|
41
|
+
const result = this.validate(data);
|
|
42
|
+
if (result) {
|
|
43
|
+
throw new Error(result.message);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { page: 1, pageSize: 10 };
|
|
47
|
+
}
|
|
48
|
+
}
|