@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.
Files changed (125) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/index.cjs +3 -3
  3. package/dist/index.js +3 -3
  4. package/dist/templates/next-app/.env.template +8 -4
  5. package/dist/templates/next-app/config/IOCIdentifier.ts +4 -1
  6. package/dist/templates/next-app/config/Identifier/api.ts +34 -0
  7. package/dist/templates/next-app/config/Identifier/common.ts +7 -0
  8. package/dist/templates/next-app/config/Identifier/index.ts +2 -0
  9. package/dist/templates/next-app/config/Identifier/page.login.ts +2 -2
  10. package/dist/templates/next-app/config/Identifier/page.register.ts +43 -22
  11. package/dist/templates/next-app/config/Identifier/validator.ts +34 -0
  12. package/dist/templates/next-app/config/i18n/index.ts +1 -0
  13. package/dist/templates/next-app/config/i18n/register18n.ts +44 -0
  14. package/dist/templates/next-app/eslint.config.mjs +17 -0
  15. package/dist/templates/next-app/migrations/schema/UserSchema.ts +24 -0
  16. package/dist/templates/next-app/migrations/sql/1694244000000.sql +10 -0
  17. package/dist/templates/next-app/next.config.ts +1 -0
  18. package/dist/templates/next-app/package.json +12 -2
  19. package/dist/templates/next-app/public/locales/en.json +19 -2
  20. package/dist/templates/next-app/public/locales/zh.json +19 -2
  21. package/dist/templates/next-app/src/app/[locale]/admin/layout.tsx +18 -0
  22. package/dist/templates/next-app/src/app/[locale]/admin/page.tsx +22 -0
  23. package/dist/templates/next-app/src/app/[locale]/admin/users/page.tsx +62 -0
  24. package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -1
  25. package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +26 -6
  26. package/dist/templates/next-app/src/app/[locale]/login/page.tsx +7 -5
  27. package/dist/templates/next-app/src/app/[locale]/page.tsx +5 -5
  28. package/dist/templates/next-app/src/app/[locale]/register/RegisterForm.tsx +176 -0
  29. package/dist/templates/next-app/src/app/[locale]/register/page.tsx +79 -0
  30. package/dist/templates/next-app/src/app/api/admin/users/route.ts +39 -0
  31. package/dist/templates/next-app/src/app/api/user/login/route.ts +50 -0
  32. package/dist/templates/next-app/src/app/api/user/logout/route.ts +27 -0
  33. package/dist/templates/next-app/src/app/api/user/register/route.ts +50 -0
  34. package/dist/templates/next-app/src/base/cases/AdminPageManager.ts +40 -0
  35. package/dist/templates/next-app/src/base/cases/AppConfig.ts +19 -0
  36. package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +46 -0
  37. package/dist/templates/next-app/src/base/cases/PageParams.ts +1 -1
  38. package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +70 -0
  39. package/dist/templates/next-app/src/base/cases/RequestState.ts +20 -0
  40. package/dist/templates/next-app/src/base/cases/RouterService.ts +4 -0
  41. package/dist/templates/next-app/src/base/cases/StringEncryptor.ts +67 -0
  42. package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +48 -0
  43. package/dist/templates/next-app/src/base/port/AdminLayoutInterface.ts +26 -0
  44. package/dist/templates/next-app/src/base/port/AdminPageInterface.ts +87 -0
  45. package/dist/templates/next-app/src/base/port/AppApiInterface.ts +14 -0
  46. package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +15 -0
  47. package/dist/templates/next-app/src/base/port/AsyncStateInterface.ts +7 -0
  48. package/dist/templates/next-app/src/base/port/DBBridgeInterface.ts +21 -0
  49. package/dist/templates/next-app/src/base/port/DBMigrationInterface.ts +92 -0
  50. package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +3 -2
  51. package/dist/templates/next-app/src/base/port/MigrationApiInterface.ts +3 -0
  52. package/dist/templates/next-app/src/base/port/PaginationInterface.ts +6 -0
  53. package/dist/templates/next-app/src/base/port/ServerApiResponseInterface.ts +6 -0
  54. package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +3 -2
  55. package/dist/templates/next-app/src/base/services/AdminUserService.ts +45 -0
  56. package/dist/templates/next-app/src/base/services/I18nService.ts +9 -45
  57. package/dist/templates/next-app/src/base/services/UserService.ts +9 -8
  58. package/dist/templates/next-app/src/base/services/adminApi/AdminApiRequester.ts +21 -0
  59. package/dist/templates/next-app/src/base/services/adminApi/AdminUserApi.ts +34 -0
  60. package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +63 -0
  61. package/dist/templates/next-app/src/base/services/appApi/AppApiRequester.ts +56 -0
  62. package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +71 -0
  63. package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +49 -0
  64. package/dist/templates/next-app/src/base/services/migrations/MigrationsApi.ts +43 -0
  65. package/dist/templates/next-app/src/core/bootstraps/BootstrapClient.ts +1 -1
  66. package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +26 -12
  67. package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +5 -4
  68. package/dist/templates/next-app/src/core/bootstraps/PrintBootstrap.ts +1 -1
  69. package/dist/templates/next-app/src/core/clientIoc/ClientIOC.ts +1 -1
  70. package/dist/templates/next-app/src/core/clientIoc/ClientIOCRegister.ts +1 -1
  71. package/dist/templates/next-app/src/core/globals.ts +1 -1
  72. package/dist/templates/next-app/src/core/serverIoc/ServerIOC.ts +1 -1
  73. package/dist/templates/next-app/src/core/serverIoc/ServerIOCRegister.ts +1 -1
  74. package/dist/templates/next-app/src/server/AppErrorApi.ts +10 -0
  75. package/dist/templates/next-app/src/server/AppSuccessApi.ts +7 -0
  76. package/dist/templates/next-app/src/server/PasswordEncrypt.ts +12 -0
  77. package/dist/templates/next-app/src/server/ServerAuth.ts +60 -0
  78. package/dist/templates/next-app/src/server/SupabaseBridge.ts +159 -0
  79. package/dist/templates/next-app/src/server/UserCredentialToken.ts +49 -0
  80. package/dist/templates/next-app/src/server/port/CrentialTokenInterface.ts +5 -0
  81. package/dist/templates/next-app/src/server/port/DBTableInterface.ts +10 -0
  82. package/dist/templates/next-app/src/server/port/ServerAuthInterface.ts +11 -0
  83. package/dist/templates/next-app/src/server/port/ServerInterface.ts +22 -0
  84. package/dist/templates/next-app/src/server/port/UserRepositoryInterface.ts +15 -0
  85. package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +8 -0
  86. package/dist/templates/next-app/src/server/port/ValidatorInterface.ts +23 -0
  87. package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +94 -0
  88. package/dist/templates/next-app/src/server/services/AdminAuthPlugin.ts +19 -0
  89. package/dist/templates/next-app/src/server/services/ApiUserService.ts +26 -0
  90. package/dist/templates/next-app/src/server/services/UserService.ts +105 -0
  91. package/dist/templates/next-app/src/server/validators/LoginValidator.ts +79 -0
  92. package/dist/templates/next-app/src/server/validators/PaginationValidator.ts +48 -0
  93. package/dist/templates/next-app/src/styles/css/antd-themes/{_default.css → _common/_default.css} +74 -1
  94. package/dist/templates/next-app/src/styles/css/antd-themes/{dark.css → _common/dark.css} +73 -0
  95. package/dist/templates/next-app/src/styles/css/antd-themes/_common/index.css +3 -0
  96. package/dist/templates/next-app/src/styles/css/antd-themes/{pink.css → _common/pink.css} +70 -0
  97. package/dist/templates/next-app/src/styles/css/antd-themes/index.css +4 -3
  98. package/dist/templates/next-app/src/styles/css/antd-themes/menu/_default.css +108 -0
  99. package/dist/templates/next-app/src/styles/css/antd-themes/menu/dark.css +67 -0
  100. package/dist/templates/next-app/src/styles/css/antd-themes/menu/index.css +3 -0
  101. package/dist/templates/next-app/src/styles/css/antd-themes/menu/pink.css +67 -0
  102. package/dist/templates/next-app/src/styles/css/antd-themes/pagination/_default.css +33 -0
  103. package/dist/templates/next-app/src/styles/css/antd-themes/pagination/dark.css +32 -0
  104. package/dist/templates/next-app/src/styles/css/antd-themes/pagination/index.css +3 -0
  105. package/dist/templates/next-app/src/styles/css/antd-themes/pagination/pink.css +35 -0
  106. package/dist/templates/next-app/src/styles/css/antd-themes/table/_default.css +44 -0
  107. package/dist/templates/next-app/src/styles/css/antd-themes/table/dark.css +43 -0
  108. package/dist/templates/next-app/src/styles/css/antd-themes/table/index.css +3 -0
  109. package/dist/templates/next-app/src/styles/css/antd-themes/table/pink.css +43 -0
  110. package/dist/templates/next-app/src/uikit/components/AdminLayout.tsx +106 -0
  111. package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +68 -17
  112. package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +6 -1
  113. package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +9 -2
  114. package/dist/templates/next-app/src/uikit/components/ComboProvider.tsx +11 -3
  115. package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +1 -1
  116. package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +21 -11
  117. package/dist/templates/next-app/src/uikit/hook/useIOC.ts +1 -1
  118. package/dist/templates/next-app/src/uikit/hook/useMountedClient.ts +7 -1
  119. package/dist/templates/next-app/tsconfig.json +3 -1
  120. package/package.json +2 -2
  121. package/dist/templates/next-app/src/base/cases/ServerAuth.ts +0 -17
  122. package/dist/templates/next-app/src/base/cases/ServerErrorHandler.ts +0 -27
  123. package/dist/templates/next-app/src/base/port/ServerAuthInterface.ts +0 -3
  124. package/dist/templates/next-app/src/base/port/ServerInterface.ts +0 -12
  125. /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,5 @@
1
+ export interface CrentialTokenInterface<T> {
2
+ generateToken(data: T, options?: { expiresIn?: string }): Promise<string>;
3
+
4
+ parseToken(token: string): Promise<T>;
5
+ }
@@ -0,0 +1,10 @@
1
+ import type { PaginationInterface } from '@/base/port/PaginationInterface';
2
+
3
+ export interface DBTableInterface {
4
+ readonly name: string;
5
+
6
+ pagination<T = unknown>(params: {
7
+ page: number;
8
+ pageSize: number;
9
+ }): Promise<PaginationInterface<T>>;
10
+ }
@@ -0,0 +1,11 @@
1
+ export interface ServerAuthInterface {
2
+ setAuth(credential_token: string): Promise<void>;
3
+
4
+ getAuth(): Promise<string>;
5
+
6
+ clear(): Promise<void>;
7
+
8
+ hasAuth(): Promise<boolean>;
9
+
10
+ throwIfNotAuth(): Promise<void>;
11
+ }
@@ -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
+ }