@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
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Button } from 'antd';
|
|
2
|
-
import { i18nConfig } from '@config/i18n';
|
|
3
|
-
import { homeI18n } from '@config/i18n/HomeI18n ';
|
|
4
2
|
import { PageParams, type PageParamsType } from '@/base/cases/PageParams';
|
|
5
|
-
import { ServerAuth } from '@/base/cases/ServerAuth';
|
|
6
3
|
import type { PageParamsProps } from '@/base/types/PageProps';
|
|
7
4
|
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
8
5
|
import { redirect } from '@/i18n/routing';
|
|
6
|
+
import { ServerAuth } from '@/server/ServerAuth';
|
|
9
7
|
import { BaseLayout } from '@/uikit/components/BaseLayout';
|
|
8
|
+
import { i18nConfig } from '@config/i18n';
|
|
9
|
+
import { homeI18n } from '@config/i18n/HomeI18n ';
|
|
10
10
|
import type { Metadata } from 'next';
|
|
11
11
|
|
|
12
12
|
// const navigationItems = [
|
|
@@ -45,12 +45,12 @@ export default async function Home({ params }: PageParamsProps) {
|
|
|
45
45
|
const locale = pageParams.getLocale();
|
|
46
46
|
const tt = await pageParams.getI18nInterface(homeI18n);
|
|
47
47
|
|
|
48
|
-
if (!(await
|
|
48
|
+
if (!(await server.getIOC(ServerAuth).hasAuth())) {
|
|
49
49
|
return redirect({ href: '/login', locale });
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
return (
|
|
53
|
-
<BaseLayout data-testid="HomePage">
|
|
53
|
+
<BaseLayout data-testid="HomePage" showLogoutButton showAdminButton>
|
|
54
54
|
{/* Hero Section */}
|
|
55
55
|
<section className="py-16 px-4">
|
|
56
56
|
<div className="max-w-4xl mx-auto text-center">
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons';
|
|
4
|
+
import { Button, Checkbox, Form, Input } from 'antd';
|
|
5
|
+
import { useState } from 'react';
|
|
6
|
+
import { useIOC } from '@/uikit/hook/useIOC';
|
|
7
|
+
import type { RegisterI18nInterface } from '@config/i18n';
|
|
8
|
+
import { I } from '@config/IOCIdentifier';
|
|
9
|
+
|
|
10
|
+
export function RegisterForm(props: { tt: RegisterI18nInterface }) {
|
|
11
|
+
const { tt } = props;
|
|
12
|
+
const userService = useIOC(I.UserServiceInterface);
|
|
13
|
+
const logger = useIOC(I.Logger);
|
|
14
|
+
const routerService = useIOC(I.RouterServiceInterface);
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
|
|
17
|
+
const handleRegister = async (values: unknown) => {
|
|
18
|
+
try {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
await userService.register(values);
|
|
21
|
+
routerService.gotoLogin();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
logger.error(error);
|
|
24
|
+
} finally {
|
|
25
|
+
setLoading(false);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleLoginClick = (e: React.MouseEvent) => {
|
|
30
|
+
e.preventDefault();
|
|
31
|
+
routerService.gotoLogin();
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Form
|
|
36
|
+
data-testid="RegisterForm"
|
|
37
|
+
name="register"
|
|
38
|
+
onFinish={handleRegister}
|
|
39
|
+
layout="vertical"
|
|
40
|
+
className="space-y-4"
|
|
41
|
+
validateTrigger="onSubmit"
|
|
42
|
+
>
|
|
43
|
+
<Form.Item
|
|
44
|
+
name="username"
|
|
45
|
+
rules={[
|
|
46
|
+
{
|
|
47
|
+
required: true,
|
|
48
|
+
message: tt.username_required
|
|
49
|
+
}
|
|
50
|
+
]}
|
|
51
|
+
>
|
|
52
|
+
<Input
|
|
53
|
+
prefix={<UserOutlined className="text-text-tertiary" />}
|
|
54
|
+
placeholder={tt.username}
|
|
55
|
+
className="h-12 text-base bg-secondary border-border"
|
|
56
|
+
/>
|
|
57
|
+
</Form.Item>
|
|
58
|
+
|
|
59
|
+
<Form.Item
|
|
60
|
+
name="email"
|
|
61
|
+
rules={[
|
|
62
|
+
{
|
|
63
|
+
required: true,
|
|
64
|
+
type: 'email',
|
|
65
|
+
message: tt.email_required
|
|
66
|
+
}
|
|
67
|
+
]}
|
|
68
|
+
>
|
|
69
|
+
<Input
|
|
70
|
+
prefix={<MailOutlined className="text-text-tertiary" />}
|
|
71
|
+
placeholder={tt.email}
|
|
72
|
+
className="h-12 text-base bg-secondary border-border"
|
|
73
|
+
/>
|
|
74
|
+
</Form.Item>
|
|
75
|
+
|
|
76
|
+
<Form.Item
|
|
77
|
+
name="password"
|
|
78
|
+
rules={[
|
|
79
|
+
{
|
|
80
|
+
required: true,
|
|
81
|
+
message: tt.password_required
|
|
82
|
+
}
|
|
83
|
+
]}
|
|
84
|
+
>
|
|
85
|
+
<Input.Password
|
|
86
|
+
prefix={<LockOutlined />}
|
|
87
|
+
placeholder={tt.password}
|
|
88
|
+
className="h-12 text-base"
|
|
89
|
+
/>
|
|
90
|
+
</Form.Item>
|
|
91
|
+
|
|
92
|
+
<Form.Item
|
|
93
|
+
name="confirmPassword"
|
|
94
|
+
dependencies={['password']}
|
|
95
|
+
rules={[
|
|
96
|
+
{
|
|
97
|
+
required: true,
|
|
98
|
+
message: tt.confirm_password_required
|
|
99
|
+
},
|
|
100
|
+
({ getFieldValue }) => ({
|
|
101
|
+
validator(_, value) {
|
|
102
|
+
if (!value || getFieldValue('password') === value) {
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
return Promise.reject(tt.password_mismatch);
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
]}
|
|
109
|
+
>
|
|
110
|
+
<Input.Password
|
|
111
|
+
prefix={<LockOutlined />}
|
|
112
|
+
placeholder={tt.confirm_password}
|
|
113
|
+
className="h-12 text-base"
|
|
114
|
+
/>
|
|
115
|
+
</Form.Item>
|
|
116
|
+
|
|
117
|
+
<Form.Item
|
|
118
|
+
name="agreeToTerms"
|
|
119
|
+
valuePropName="checked"
|
|
120
|
+
rules={[
|
|
121
|
+
{
|
|
122
|
+
validator: (_, value) =>
|
|
123
|
+
value
|
|
124
|
+
? Promise.resolve()
|
|
125
|
+
: Promise.reject(new Error(tt.terms_required))
|
|
126
|
+
}
|
|
127
|
+
]}
|
|
128
|
+
>
|
|
129
|
+
<Checkbox>
|
|
130
|
+
<span className="text-text-secondary">
|
|
131
|
+
{tt.terms_prefix}{' '}
|
|
132
|
+
<a
|
|
133
|
+
href="#"
|
|
134
|
+
className="text-brand hover:text-brand-hover"
|
|
135
|
+
target="_blank"
|
|
136
|
+
rel="noopener noreferrer"
|
|
137
|
+
>
|
|
138
|
+
{tt.terms_link}
|
|
139
|
+
</a>{' '}
|
|
140
|
+
{tt.terms_and}{' '}
|
|
141
|
+
<a
|
|
142
|
+
href="#"
|
|
143
|
+
className="text-brand hover:text-brand-hover"
|
|
144
|
+
target="_blank"
|
|
145
|
+
rel="noopener noreferrer"
|
|
146
|
+
>
|
|
147
|
+
{tt.privacy_link}
|
|
148
|
+
</a>
|
|
149
|
+
</span>
|
|
150
|
+
</Checkbox>
|
|
151
|
+
</Form.Item>
|
|
152
|
+
|
|
153
|
+
<Form.Item>
|
|
154
|
+
<Button
|
|
155
|
+
type="primary"
|
|
156
|
+
htmlType="submit"
|
|
157
|
+
loading={loading}
|
|
158
|
+
className="w-full h-12 text-base"
|
|
159
|
+
>
|
|
160
|
+
{tt.button}
|
|
161
|
+
</Button>
|
|
162
|
+
</Form.Item>
|
|
163
|
+
|
|
164
|
+
<div className="text-center mt-6">
|
|
165
|
+
<span className="text-text-tertiary">{tt.have_account} </span>
|
|
166
|
+
<a
|
|
167
|
+
href="#"
|
|
168
|
+
className="text-brand hover:text-brand-hover"
|
|
169
|
+
onClick={handleLoginClick}
|
|
170
|
+
>
|
|
171
|
+
{tt.login_link}
|
|
172
|
+
</a>
|
|
173
|
+
</div>
|
|
174
|
+
</Form>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation';
|
|
2
|
+
import { PageParams, type PageParamsType } from '@/base/cases/PageParams';
|
|
3
|
+
import type { PageParamsProps } from '@/base/types/PageProps';
|
|
4
|
+
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
5
|
+
import { redirect } from '@/i18n/routing';
|
|
6
|
+
import { ServerAuth } from '@/server/ServerAuth';
|
|
7
|
+
import { BaseLayout } from '@/uikit/components/BaseLayout';
|
|
8
|
+
import { FeatureItem } from '@/uikit/components/FeatureItem';
|
|
9
|
+
import { i18nConfig, register18n } from '@config/i18n';
|
|
10
|
+
import { RegisterForm } from './RegisterForm';
|
|
11
|
+
import type { Metadata } from 'next';
|
|
12
|
+
|
|
13
|
+
// Generate static params for all supported locales (used for SSG)
|
|
14
|
+
export async function generateStaticParams() {
|
|
15
|
+
// Return one entry for each supported locale
|
|
16
|
+
return i18nConfig.supportedLngs.map((locale) => ({ locale }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Allow Next.js to statically generate this page if possible (default behavior)
|
|
20
|
+
export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
|
|
21
|
+
|
|
22
|
+
// Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
|
|
23
|
+
// export const revalidate = 3600; // Rebuild every hour (optional)
|
|
24
|
+
|
|
25
|
+
// Generate localized SEO metadata per locale (Next.js 15+ best practice)
|
|
26
|
+
export async function generateMetadata({
|
|
27
|
+
params
|
|
28
|
+
}: {
|
|
29
|
+
params: Promise<PageParamsType>;
|
|
30
|
+
}): Promise<Metadata> {
|
|
31
|
+
const pageParams = new PageParams(await params);
|
|
32
|
+
|
|
33
|
+
return await pageParams.getI18nInterface(register18n);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default async function LoginPage(props: PageParamsProps) {
|
|
37
|
+
if (!props.params) {
|
|
38
|
+
return notFound();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const params = await props.params;
|
|
42
|
+
const pageParams = new PageParams(params);
|
|
43
|
+
|
|
44
|
+
const server = new BootstrapServer();
|
|
45
|
+
|
|
46
|
+
if (await server.getIOC(ServerAuth).hasAuth()) {
|
|
47
|
+
return redirect({ href: '/', locale: params.locale! });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const tt = await pageParams.getI18nInterface(register18n);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<BaseLayout
|
|
54
|
+
data-testid="RegisterPage"
|
|
55
|
+
mainProps={{
|
|
56
|
+
className: 'text-xs1 bg-primary flex min-h-screen'
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
<div className="hidden lg:flex bg-secondary lg:w-1/2 p-12 flex-col">
|
|
60
|
+
<h1 className="text-4xl font-bold text-text mb-4">{tt.welcome}</h1>
|
|
61
|
+
<p className="text-text-secondary text-lg mb-8">{tt.subtitle}</p>
|
|
62
|
+
<div className="space-y-4">
|
|
63
|
+
<FeatureItem icon="🎯" text={tt.feature_ai_paths} />
|
|
64
|
+
<FeatureItem icon="🎯" text={tt.feature_smart_recommendations} />
|
|
65
|
+
<FeatureItem icon="📊" text={tt.feature_progress_tracking} />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="w-full lg:w-1/2 p-8 sm:p-12 flex items-center justify-center">
|
|
70
|
+
<div className="w-full max-w-[420px]">
|
|
71
|
+
<h2 className="text-2xl font-semibold mb-2 text-text">{tt.title}</h2>
|
|
72
|
+
<p className="text-text-secondary mb-8">{tt.subtitle}</p>
|
|
73
|
+
|
|
74
|
+
<RegisterForm tt={tt} />
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</BaseLayout>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ExecutorError } from '@qlover/fe-corekit';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
4
|
+
import { AppErrorApi } from '@/server/AppErrorApi';
|
|
5
|
+
import { AppSuccessApi } from '@/server/AppSuccessApi';
|
|
6
|
+
import { AdminAuthPlugin } from '@/server/services/AdminAuthPlugin';
|
|
7
|
+
import { ApiUserService } from '@/server/services/ApiUserService';
|
|
8
|
+
import { PaginationValidator } from '@/server/validators/PaginationValidator';
|
|
9
|
+
import type { NextRequest } from 'next/server';
|
|
10
|
+
|
|
11
|
+
export async function GET(req: NextRequest) {
|
|
12
|
+
const server = new BootstrapServer();
|
|
13
|
+
|
|
14
|
+
const result = await server
|
|
15
|
+
.use(new AdminAuthPlugin())
|
|
16
|
+
.execNoError(async ({ parameters: { IOC } }) => {
|
|
17
|
+
const searchParams = Object.fromEntries(
|
|
18
|
+
req.nextUrl.searchParams.entries()
|
|
19
|
+
);
|
|
20
|
+
const paginationParams = IOC(PaginationValidator).getThrow(searchParams);
|
|
21
|
+
|
|
22
|
+
const apiUserService = IOC(ApiUserService);
|
|
23
|
+
|
|
24
|
+
const result = await apiUserService.getUsers({
|
|
25
|
+
page: paginationParams.page,
|
|
26
|
+
pageSize: paginationParams.pageSize
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return result;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (result instanceof ExecutorError) {
|
|
33
|
+
return NextResponse.json(new AppErrorApi(result.id, result.message), {
|
|
34
|
+
status: 400
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return NextResponse.json(new AppSuccessApi(result));
|
|
39
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ExecutorError } from '@qlover/fe-corekit';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { StringEncryptor } from '@/base/cases/StringEncryptor';
|
|
4
|
+
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
5
|
+
import { AppErrorApi } from '@/server/AppErrorApi';
|
|
6
|
+
import { AppSuccessApi } from '@/server/AppSuccessApi';
|
|
7
|
+
import type { UserServiceInterface } from '@/server/port/UserServiceInterface';
|
|
8
|
+
import { ServerAuth } from '@/server/ServerAuth';
|
|
9
|
+
import { UserService } from '@/server/services/UserService';
|
|
10
|
+
import { LoginValidator } from '@/server/validators/LoginValidator';
|
|
11
|
+
import type { UserSchema } from '@migrations/schema/UserSchema';
|
|
12
|
+
import type { NextRequest } from 'next/server';
|
|
13
|
+
|
|
14
|
+
export async function POST(req: NextRequest) {
|
|
15
|
+
const server = new BootstrapServer();
|
|
16
|
+
|
|
17
|
+
const result = await server.execNoError(async ({ parameters: { IOC } }) => {
|
|
18
|
+
const requestBody = await req.json();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
if (requestBody.password) {
|
|
22
|
+
requestBody.password = IOC(StringEncryptor).decrypt(
|
|
23
|
+
requestBody.password
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
throw new ExecutorError(
|
|
28
|
+
'encrypt_password_failed',
|
|
29
|
+
'Encrypt password failed'
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const body = IOC(LoginValidator).getThrow(requestBody);
|
|
33
|
+
|
|
34
|
+
const userService: UserServiceInterface = IOC(UserService);
|
|
35
|
+
|
|
36
|
+
const user = (await userService.login(body)) as UserSchema;
|
|
37
|
+
|
|
38
|
+
await IOC(ServerAuth).setAuth(user.credential_token);
|
|
39
|
+
|
|
40
|
+
return user;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result instanceof ExecutorError) {
|
|
44
|
+
return NextResponse.json(new AppErrorApi(result.id, result.message), {
|
|
45
|
+
status: 400
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return NextResponse.json(new AppSuccessApi(result));
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ExecutorError } from '@qlover/fe-corekit';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
4
|
+
import { AppErrorApi } from '@/server/AppErrorApi';
|
|
5
|
+
import { AppSuccessApi } from '@/server/AppSuccessApi';
|
|
6
|
+
import type { UserServiceInterface } from '@/server/port/UserServiceInterface';
|
|
7
|
+
import { UserService } from '@/server/services/UserService';
|
|
8
|
+
|
|
9
|
+
export async function POST() {
|
|
10
|
+
const server = new BootstrapServer();
|
|
11
|
+
|
|
12
|
+
const result = await server.execNoError(async ({ parameters: { IOC } }) => {
|
|
13
|
+
const userService: UserServiceInterface = IOC(UserService);
|
|
14
|
+
|
|
15
|
+
await userService.logout();
|
|
16
|
+
|
|
17
|
+
return true;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (result instanceof ExecutorError) {
|
|
21
|
+
return NextResponse.json(new AppErrorApi(result.id, result.message), {
|
|
22
|
+
status: 400
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return NextResponse.json(new AppSuccessApi(result));
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ExecutorError } from '@qlover/fe-corekit';
|
|
2
|
+
import { NextResponse } from 'next/server';
|
|
3
|
+
import { StringEncryptor } from '@/base/cases/StringEncryptor';
|
|
4
|
+
import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
|
|
5
|
+
import { AppErrorApi } from '@/server/AppErrorApi';
|
|
6
|
+
import { AppSuccessApi } from '@/server/AppSuccessApi';
|
|
7
|
+
import type { UserServiceInterface } from '@/server/port/UserServiceInterface';
|
|
8
|
+
import { UserService } from '@/server/services/UserService';
|
|
9
|
+
import { LoginValidator } from '@/server/validators/LoginValidator';
|
|
10
|
+
import type { NextRequest } from 'next/server';
|
|
11
|
+
|
|
12
|
+
export async function POST(req: NextRequest) {
|
|
13
|
+
const server = new BootstrapServer();
|
|
14
|
+
|
|
15
|
+
const result = await server.execNoError(async ({ parameters: { IOC } }) => {
|
|
16
|
+
const requestBody = await req.json();
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
if (requestBody.password) {
|
|
20
|
+
requestBody.password = IOC(StringEncryptor).decrypt(
|
|
21
|
+
requestBody.password
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
throw new ExecutorError(
|
|
26
|
+
'encrypt_password_failed',
|
|
27
|
+
'Encrypt password failed'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const body = IOC(LoginValidator).getThrow(requestBody);
|
|
32
|
+
|
|
33
|
+
const userService: UserServiceInterface = IOC(UserService);
|
|
34
|
+
|
|
35
|
+
const user = await userService.register({
|
|
36
|
+
email: body.email,
|
|
37
|
+
password: body.password
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return user;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (result instanceof ExecutorError) {
|
|
44
|
+
return NextResponse.json(new AppErrorApi(result.id, result.message), {
|
|
45
|
+
status: 400
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return NextResponse.json(new AppSuccessApi(result));
|
|
50
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { injectable } from 'inversify';
|
|
2
|
+
import { AdminLayoutInterface } from '../port/AdminLayoutInterface';
|
|
3
|
+
import type {
|
|
4
|
+
NavItemInterface,
|
|
5
|
+
AdminPageState
|
|
6
|
+
} from '../port/AdminLayoutInterface';
|
|
7
|
+
|
|
8
|
+
const defaultNavItems: NavItemInterface[] = [
|
|
9
|
+
{
|
|
10
|
+
key: 'dashboard',
|
|
11
|
+
i18nKey: 'Dashboard',
|
|
12
|
+
pathname: '/admin'
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
key: 'users',
|
|
16
|
+
i18nKey: 'User Management',
|
|
17
|
+
pathname: '/admin/users'
|
|
18
|
+
}
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
class AdminPageManagerState implements AdminPageState {
|
|
22
|
+
collapsedSidebar = false;
|
|
23
|
+
|
|
24
|
+
navItems: NavItemInterface[] = defaultNavItems;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@injectable()
|
|
28
|
+
export class AdminPageManager extends AdminLayoutInterface {
|
|
29
|
+
constructor() {
|
|
30
|
+
super(() => new AdminPageManagerState());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
override toggleSidebar(): void {
|
|
34
|
+
this.emit(
|
|
35
|
+
this.cloneState({
|
|
36
|
+
collapsedSidebar: !this.state.collapsedSidebar
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { name, version } from '../../../package.json';
|
|
2
2
|
import type { EnvConfigInterface } from '@qlover/corekit-bridge';
|
|
3
|
+
import type { StringValue } from 'ms';
|
|
3
4
|
|
|
4
5
|
export class AppConfig implements EnvConfigInterface {
|
|
5
6
|
/**
|
|
@@ -12,4 +13,22 @@ export class AppConfig implements EnvConfigInterface {
|
|
|
12
13
|
readonly appVersion: string = version;
|
|
13
14
|
|
|
14
15
|
readonly userTokenKey: string = '_user_token';
|
|
16
|
+
|
|
17
|
+
readonly testLoginEmail: string = process.env.NEXT_PUBLIC_LOGIN_USER!;
|
|
18
|
+
readonly testLoginPassword: string = process.env.NEXT_PUBLIC_LOGIN_PASSWORD!;
|
|
19
|
+
|
|
20
|
+
readonly supabaseUrl: string = process.env.SUPABASE_URL!;
|
|
21
|
+
readonly supabaseAnonKey: string = process.env.SUPABASE_ANON_KEY!;
|
|
22
|
+
|
|
23
|
+
readonly stringEncryptorKey: string =
|
|
24
|
+
process.env.NEXT_PUBLIC_STRING_ENCRYPT_KEY!;
|
|
25
|
+
|
|
26
|
+
readonly jwtSecret: string = process.env.JWT_SECRET!;
|
|
27
|
+
/**
|
|
28
|
+
* login user token expires in
|
|
29
|
+
*
|
|
30
|
+
* @example '30 days'
|
|
31
|
+
* @example '1 year'
|
|
32
|
+
*/
|
|
33
|
+
readonly jwtExpiresIn: StringValue = '30 days';
|
|
15
34
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExecutorError,
|
|
3
|
+
type ExecutorContext,
|
|
4
|
+
type ExecutorPlugin
|
|
5
|
+
} from '@qlover/fe-corekit';
|
|
6
|
+
import { inject, injectable } from 'inversify';
|
|
7
|
+
import { I } from '@config/IOCIdentifier';
|
|
8
|
+
import type { DialogHandlerOptions } from './DialogHandler';
|
|
9
|
+
import type { I18nServiceInterface } from '../port/I18nServiceInterface';
|
|
10
|
+
import type { UIDialogInterface } from '@qlover/corekit-bridge';
|
|
11
|
+
|
|
12
|
+
@injectable()
|
|
13
|
+
export class DialogErrorPlugin implements ExecutorPlugin {
|
|
14
|
+
readonly pluginName = 'DialogErrorPlugin';
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
@inject(I.DialogHandler)
|
|
18
|
+
protected dialogHandler: UIDialogInterface<DialogHandlerOptions>,
|
|
19
|
+
@inject(I.I18nServiceInterface) protected i18nService: I18nServiceInterface
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
onError(context: ExecutorContext<unknown>): void | Promise<void> {
|
|
23
|
+
const { error, hooksRuntimes } = context;
|
|
24
|
+
const runtimesError = hooksRuntimes.returnValue;
|
|
25
|
+
|
|
26
|
+
// 优先使用 runtime 的错误, 他可能在运行时被修改
|
|
27
|
+
// 比如 RequestError 会被 AppApiPlugin 修改为 ExecutorError
|
|
28
|
+
const handleError = runtimesError || error;
|
|
29
|
+
|
|
30
|
+
if (handleError instanceof ExecutorError) {
|
|
31
|
+
if (this.isI18nMessage(handleError.message)) {
|
|
32
|
+
const message = this.i18nService.t(handleError.id);
|
|
33
|
+
this.dialogHandler.error(message);
|
|
34
|
+
} else {
|
|
35
|
+
this.dialogHandler.error(handleError.message);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
protected isI18nMessage(message: string): boolean {
|
|
41
|
+
// Check if message follows the pattern of underscore-separated format
|
|
42
|
+
// e.g., "namespace_key" or "namespace__key"
|
|
43
|
+
const pattern = /^[a-zA-Z]+(?:_[a-zA-Z]+)+$|^[a-zA-Z]+(?:__[a-zA-Z]+)+$/;
|
|
44
|
+
return pattern.test(message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { notFound } from 'next/navigation';
|
|
2
2
|
import { getMessages, getTranslations } from 'next-intl/server';
|
|
3
3
|
import { i18nConfig } from '@config/i18n';
|
|
4
|
-
import type { ParamsHandlerInterface as ParamsHandlerInterface } from '../port/ParamsHandlerInterface';
|
|
5
4
|
import type { LocaleType, PageI18nInterface } from '@config/i18n';
|
|
5
|
+
import type { ParamsHandlerInterface as ParamsHandlerInterface } from '../port/ParamsHandlerInterface';
|
|
6
6
|
|
|
7
7
|
export interface PageWithParams {
|
|
8
8
|
params?: Promise<PageParamsType>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { clone, isObject } from 'lodash';
|
|
2
|
+
import type {
|
|
3
|
+
Encryptor,
|
|
4
|
+
ExecutorContext,
|
|
5
|
+
ExecutorPlugin,
|
|
6
|
+
RequestAdapterConfig
|
|
7
|
+
} from '@qlover/fe-corekit';
|
|
8
|
+
|
|
9
|
+
export interface RequestEncryptPluginProps<Request = unknown>
|
|
10
|
+
extends RequestAdapterConfig<Request> {
|
|
11
|
+
/**
|
|
12
|
+
* 加密密码在 HTTP 请求中
|
|
13
|
+
*
|
|
14
|
+
* - 如果为空,则不加密密码
|
|
15
|
+
* - 如果为字符串,则加密密码
|
|
16
|
+
* - 如果为数组,则加密密码
|
|
17
|
+
*/
|
|
18
|
+
encryptProps?: string[] | string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class RequestEncryptPlugin
|
|
22
|
+
implements ExecutorPlugin<RequestEncryptPluginProps>
|
|
23
|
+
{
|
|
24
|
+
readonly pluginName = 'RequestEncryptPlugin';
|
|
25
|
+
|
|
26
|
+
constructor(protected encryptor: Encryptor<string, string>) {}
|
|
27
|
+
|
|
28
|
+
onBefore(
|
|
29
|
+
context: ExecutorContext<RequestEncryptPluginProps>
|
|
30
|
+
): void | Promise<void> {
|
|
31
|
+
const { responseType, encryptProps } = context.parameters;
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
responseType === 'json' &&
|
|
35
|
+
isObject(context.parameters.data) &&
|
|
36
|
+
encryptProps
|
|
37
|
+
) {
|
|
38
|
+
context.parameters.data = this.encryptData(
|
|
39
|
+
clone(context.parameters.data),
|
|
40
|
+
encryptProps
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected encryptData<T extends object>(
|
|
46
|
+
data: T,
|
|
47
|
+
encryptProps?: string | string[]
|
|
48
|
+
): T {
|
|
49
|
+
if (typeof encryptProps === 'string') {
|
|
50
|
+
const targetValue = data[encryptProps as keyof T];
|
|
51
|
+
|
|
52
|
+
if (typeof targetValue === 'string') {
|
|
53
|
+
const newValue = this.encryptor.encrypt(targetValue);
|
|
54
|
+
Object.assign(data, { [encryptProps]: newValue });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(encryptProps)) {
|
|
59
|
+
encryptProps.forEach((prop) => {
|
|
60
|
+
const targetValue = data[prop as keyof T];
|
|
61
|
+
if (typeof targetValue === 'string') {
|
|
62
|
+
const newValue = this.encryptor.encrypt(targetValue);
|
|
63
|
+
Object.assign(data, { [prop]: newValue });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AsyncStateInterface } from '@/base/port/AsyncStateInterface';
|
|
2
|
+
|
|
3
|
+
export class RequestState<T = unknown> implements AsyncStateInterface<T> {
|
|
4
|
+
startTime: number;
|
|
5
|
+
endTime: number;
|
|
6
|
+
|
|
7
|
+
constructor(
|
|
8
|
+
public loading: boolean = false,
|
|
9
|
+
public result: T | null = null,
|
|
10
|
+
public error: unknown | null = null
|
|
11
|
+
) {
|
|
12
|
+
this.startTime = Date.now();
|
|
13
|
+
this.endTime = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
end(): this {
|
|
17
|
+
this.endTime = Date.now();
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
}
|