@qlover/create-app 0.7.8 → 0.7.10

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 (89) hide show
  1. package/CHANGELOG.md +140 -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 +20 -0
  7. package/dist/templates/next-app/config/Identifier/index.ts +2 -0
  8. package/dist/templates/next-app/config/Identifier/page.home.ts +7 -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/HomeI18n .ts +22 -0
  13. package/dist/templates/next-app/config/i18n/index.ts +1 -0
  14. package/dist/templates/next-app/config/i18n/register18n.ts +44 -0
  15. package/dist/templates/next-app/config/theme.ts +1 -0
  16. package/dist/templates/next-app/migrations/schema/UserSchema.ts +13 -0
  17. package/dist/templates/next-app/migrations/sql/1694244000000.sql +10 -0
  18. package/dist/templates/next-app/package.json +15 -6
  19. package/dist/templates/next-app/public/locales/en.json +17 -2
  20. package/dist/templates/next-app/public/locales/zh.json +17 -2
  21. package/dist/templates/next-app/src/app/[locale]/admin/layout.tsx +21 -0
  22. package/dist/templates/next-app/src/app/[locale]/admin/page.tsx +10 -0
  23. package/dist/templates/next-app/src/app/[locale]/layout.tsx +1 -7
  24. package/dist/templates/next-app/src/app/[locale]/login/LoginForm.tsx +28 -16
  25. package/dist/templates/next-app/src/app/[locale]/login/page.tsx +10 -17
  26. package/dist/templates/next-app/src/app/[locale]/page.tsx +94 -102
  27. package/dist/templates/next-app/src/app/[locale]/register/RegisterForm.tsx +176 -0
  28. package/dist/templates/next-app/src/app/[locale]/register/page.tsx +79 -0
  29. package/dist/templates/next-app/src/app/api/user/login/route.ts +50 -0
  30. package/dist/templates/next-app/src/app/api/user/logout/route.ts +27 -0
  31. package/dist/templates/next-app/src/app/api/user/register/route.ts +50 -0
  32. package/dist/templates/next-app/src/base/cases/AppConfig.ts +19 -0
  33. package/dist/templates/next-app/src/base/cases/DialogErrorPlugin.ts +35 -0
  34. package/dist/templates/next-app/src/base/cases/RequestEncryptPlugin.ts +70 -0
  35. package/dist/templates/next-app/src/base/cases/RouterService.ts +4 -0
  36. package/dist/templates/next-app/src/base/cases/StringEncryptor.ts +67 -0
  37. package/dist/templates/next-app/src/base/cases/UserServiceApi.ts +48 -0
  38. package/dist/templates/next-app/src/base/port/AppApiInterface.ts +14 -0
  39. package/dist/templates/next-app/src/base/port/AppUserApiInterface.ts +15 -0
  40. package/dist/templates/next-app/src/base/port/DBBridgeInterface.ts +18 -0
  41. package/dist/templates/next-app/src/base/port/DBMigrationInterface.ts +92 -0
  42. package/dist/templates/next-app/src/base/port/DBTableInterface.ts +3 -0
  43. package/dist/templates/next-app/src/base/port/I18nServiceInterface.ts +3 -2
  44. package/dist/templates/next-app/src/base/port/MigrationApiInterface.ts +3 -0
  45. package/dist/templates/next-app/src/base/port/ServerApiResponseInterface.ts +6 -0
  46. package/dist/templates/next-app/src/base/port/UserServiceInterface.ts +3 -2
  47. package/dist/templates/next-app/src/base/services/I18nService.ts +9 -45
  48. package/dist/templates/next-app/src/base/services/UserService.ts +9 -8
  49. package/dist/templates/next-app/src/base/services/appApi/AppApiPlugin.ts +63 -0
  50. package/dist/templates/next-app/src/base/services/appApi/AppUserApi.ts +72 -0
  51. package/dist/templates/next-app/src/base/services/appApi/AppUserApiBootstrap.ts +48 -0
  52. package/dist/templates/next-app/src/base/services/appApi/AppUserType.ts +51 -0
  53. package/dist/templates/next-app/src/base/services/migrations/MigrationsApi.ts +43 -0
  54. package/dist/templates/next-app/src/core/bootstraps/BootstrapServer.ts +30 -5
  55. package/dist/templates/next-app/src/core/bootstraps/BootstrapsRegistry.ts +4 -3
  56. package/dist/templates/next-app/src/server/AppErrorApi.ts +10 -0
  57. package/dist/templates/next-app/src/server/AppSuccessApi.ts +7 -0
  58. package/dist/templates/next-app/src/server/PasswordEncrypt.ts +12 -0
  59. package/dist/templates/next-app/src/server/ServerAuth.ts +50 -0
  60. package/dist/templates/next-app/src/server/SupabaseBridge.ts +124 -0
  61. package/dist/templates/next-app/src/server/UserCredentialToken.ts +49 -0
  62. package/dist/templates/next-app/src/server/port/CrentialTokenInterface.ts +5 -0
  63. package/dist/templates/next-app/src/server/port/ServerInterface.ts +22 -0
  64. package/dist/templates/next-app/src/server/port/UserAuthInterface.ts +9 -0
  65. package/dist/templates/next-app/src/server/port/UserRepositoryInterface.ts +15 -0
  66. package/dist/templates/next-app/src/server/port/UserServiceInterface.ts +8 -0
  67. package/dist/templates/next-app/src/server/port/ValidatorInterface.ts +23 -0
  68. package/dist/templates/next-app/src/server/repositorys/UserRepository.ts +63 -0
  69. package/dist/templates/next-app/src/server/services/UserService.ts +105 -0
  70. package/dist/templates/next-app/src/server/validators/LoginValidator.ts +79 -0
  71. package/dist/templates/next-app/src/styles/css/antd-themes/_default.css +12 -0
  72. package/dist/templates/next-app/src/styles/css/antd-themes/dark.css +26 -0
  73. package/dist/templates/next-app/src/styles/css/antd-themes/pink.css +16 -0
  74. package/dist/templates/next-app/src/styles/css/page.css +4 -3
  75. package/dist/templates/next-app/src/styles/css/themes/_default.css +1 -0
  76. package/dist/templates/next-app/src/styles/css/themes/dark.css +1 -0
  77. package/dist/templates/next-app/src/styles/css/themes/pink.css +1 -0
  78. package/dist/templates/next-app/src/uikit/components/BaseHeader.tsx +6 -11
  79. package/dist/templates/next-app/src/uikit/components/BaseLayout.tsx +27 -0
  80. package/dist/templates/next-app/src/uikit/components/BootstrapsProvider.tsx +8 -1
  81. package/dist/templates/next-app/src/uikit/components/LanguageSwitcher.tsx +49 -21
  82. package/dist/templates/next-app/src/uikit/components/LogoutButton.tsx +20 -10
  83. package/dist/templates/next-app/src/uikit/components/ThemeSwitcher.tsx +92 -48
  84. package/dist/templates/next-app/tsconfig.json +3 -1
  85. package/package.json +2 -2
  86. package/dist/templates/next-app/src/base/cases/ServerAuth.ts +0 -17
  87. package/dist/templates/next-app/src/base/port/ServerAuthInterface.ts +0 -3
  88. package/dist/templates/next-app/src/base/port/ServerInterface.ts +0 -12
  89. /package/dist/templates/next-app/src/{app/[locale]/login → uikit/components}/FeatureItem.tsx +0 -0
@@ -1,119 +1,111 @@
1
- import Image from 'next/image';
2
- import { PageParams } from '@/base/cases/PageParams';
3
- import { ServerAuth } from '@/base/cases/ServerAuth';
1
+ import { Button } from 'antd';
2
+ import { i18nConfig } from '@config/i18n';
3
+ import { homeI18n } from '@config/i18n/HomeI18n ';
4
+ import { PageParams, type PageParamsType } from '@/base/cases/PageParams';
4
5
  import type { PageParamsProps } from '@/base/types/PageProps';
5
6
  import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
6
7
  import { redirect } from '@/i18n/routing';
8
+ import { ServerAuth } from '@/server/ServerAuth';
9
+ import { BaseLayout } from '@/uikit/components/BaseLayout';
10
+ import type { Metadata } from 'next';
11
+
12
+ // const navigationItems = [
13
+ // {
14
+ // href: '/identifier',
15
+ // titleKey: 'HOME_IDENTIFIER',
16
+ // descriptionKey: 'HOME_IDENTIFIER_DESCRIPTION'
17
+ // }
18
+ // ];
19
+
20
+ // Generate static params for all supported locales (used for SSG)
21
+ export async function generateStaticParams() {
22
+ // Return one entry for each supported locale
23
+ return i18nConfig.supportedLngs.map((locale) => ({ locale }));
24
+ }
25
+
26
+ // Allow Next.js to statically generate this page if possible (default behavior)
27
+ export const dynamic = 'auto'; // Enable static generation when possible, fallback to dynamic if needed
28
+
29
+ // Optional: Use revalidate if you want ISR (Incremental Static Regeneration)
30
+ // export const revalidate = 3600; // Rebuild every hour (optional)
31
+
32
+ // Generate localized SEO metadata per locale (Next.js 15+ best practice)
33
+ export async function generateMetadata({
34
+ params
35
+ }: {
36
+ params: Promise<PageParamsType>;
37
+ }): Promise<Metadata> {
38
+ const pageParams = new PageParams(await params);
39
+ return await pageParams.getI18nInterface(homeI18n);
40
+ }
7
41
 
8
42
  export default async function Home({ params }: PageParamsProps) {
9
43
  const server = new BootstrapServer();
10
44
  const pageParams = new PageParams(await params!);
11
45
  const locale = pageParams.getLocale();
46
+ const tt = await pageParams.getI18nInterface(homeI18n);
12
47
 
13
- if (!(await new ServerAuth(server).hasAuth())) {
48
+ if (!(await server.getIOC(ServerAuth).hasAuth())) {
14
49
  return redirect({ href: '/login', locale });
15
50
  }
16
51
 
17
52
  return (
18
- <div
19
- data-testid="Home"
20
- className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20"
21
- >
22
- <main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
23
- <Image
24
- className="dark:invert"
25
- src="/next.svg"
26
- alt="Next.js logo"
27
- width={180}
28
- height={38}
29
- priority
30
- />
31
- <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
32
- <li className="mb-2 tracking-[-.01em]">
33
- Get started by editing{' '}
34
- <code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
35
- src/app/page.tsx
36
- </code>
37
- .
38
- </li>
39
- <li className="tracking-[-.01em]">
40
- Save and see your changes instantly.
41
- </li>
42
- </ol>
53
+ <BaseLayout data-testid="HomePage" showLogoutButton>
54
+ {/* Hero Section */}
55
+ <section className="py-16 px-4">
56
+ <div className="max-w-4xl mx-auto text-center">
57
+ <h1 className="text-4xl md:text-5xl font-bold mb-6 text-text">
58
+ {tt.welcome}
59
+ </h1>
60
+ <p className="text-xl text-text-secondary mb-8">{tt.description}</p>
61
+ </div>
62
+ </section>
63
+
64
+ {/* Navigation Grid */}
65
+ <section className="max-w-6xl mx-auto px-4 py-12">
66
+ <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
67
+ {/* {navigationItems.map((item) => (
68
+ <LocaleLink
69
+ data-testid={`HomePage-navigation-${item.href}`}
70
+ key={item.href}
71
+ title={item.titleKey}
72
+ className={clsx(
73
+ href={item.href}
74
+ 'block rounded-lg p-6',
75
+ 'bg-secondary',
76
+ 'border border-border',
77
+ 'hover:bg-elevated',
78
+ 'transition-colors duration-200'
79
+ )}
80
+ >
81
+ <h3 className={`text-xl font-semibold mb-3 text-text`}>
82
+ {t(item.titleKey)}
83
+ </h3>
84
+ <p className="text-text-secondary mb-4">
85
+ {t(item.descriptionKey)}
86
+ </p>
87
+ <Button type="primary" className="w-full">
88
+ {t(i18nKeys.HOME_EXPLORE)}
89
+ </Button>
90
+ </LocaleLink>
91
+ ))} */}
92
+ </div>
93
+ </section>
43
94
 
44
- <div className="flex gap-4 items-center flex-col sm:flex-row">
45
- <a
46
- className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
47
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
48
- target="_blank"
49
- rel="noopener noreferrer"
50
- >
51
- <Image
52
- className="dark:invert"
53
- src="/vercel.svg"
54
- alt="Vercel logomark"
55
- width={20}
56
- height={20}
57
- />
58
- Deploy now
59
- </a>
60
- <a
61
- className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
62
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
63
- target="_blank"
64
- rel="noopener noreferrer"
65
- >
66
- Read our docs
67
- </a>
95
+ {/* Call to Action Section */}
96
+ <section className="py-16 px-4 bg-elevated">
97
+ <div className="max-w-4xl mx-auto text-center">
98
+ <h2 className="text-3xl font-bold mb-4 text-text">
99
+ {tt.getStartedTitle}
100
+ </h2>
101
+ <p className="text-lg text-text-secondary mb-8">
102
+ {tt.getStartedDescription}
103
+ </p>
104
+ <Button type="primary" size="large" className="px-8">
105
+ {tt.getStartedButton}
106
+ </Button>
68
107
  </div>
69
- </main>
70
- <footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
71
- <a
72
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
73
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
74
- target="_blank"
75
- rel="noopener noreferrer"
76
- >
77
- <Image
78
- aria-hidden
79
- src="/file.svg"
80
- alt="File icon"
81
- width={16}
82
- height={16}
83
- />
84
- Learn
85
- </a>
86
- <a
87
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
88
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
89
- target="_blank"
90
- rel="noopener noreferrer"
91
- >
92
- <Image
93
- aria-hidden
94
- src="/window.svg"
95
- alt="Window icon"
96
- width={16}
97
- height={16}
98
- />
99
- Examples
100
- </a>
101
- <a
102
- className="flex items-center gap-2 hover:underline hover:underline-offset-4"
103
- href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
104
- target="_blank"
105
- rel="noopener noreferrer"
106
- >
107
- <Image
108
- aria-hidden
109
- src="/globe.svg"
110
- alt="Globe icon"
111
- width={16}
112
- height={16}
113
- />
114
- Go to nextjs.org →
115
- </a>
116
- </footer>
117
- </div>
108
+ </section>
109
+ </BaseLayout>
118
110
  );
119
111
  }
@@ -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 { I } from '@config/IOCIdentifier';
7
+ import { useIOC } from '@/uikit/hook/useIOC';
8
+ import type { RegisterI18nInterface } from '@config/i18n';
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 { i18nConfig, register18n } from '@config/i18n';
3
+ import { PageParams, type PageParamsType } from '@/base/cases/PageParams';
4
+ import type { PageParamsProps } from '@/base/types/PageProps';
5
+ import { BootstrapServer } from '@/core/bootstraps/BootstrapServer';
6
+ import { redirect } from '@/i18n/routing';
7
+ import { ServerAuth } from '@/server/ServerAuth';
8
+ import { BaseLayout } from '@/uikit/components/BaseLayout';
9
+ import { FeatureItem } from '@/uikit/components/FeatureItem';
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,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
+ }
@@ -1,3 +1,4 @@
1
+ import type { StringValue } from 'ms';
1
2
  import { name, version } from '../../../package.json';
2
3
  import type { EnvConfigInterface } from '@qlover/corekit-bridge';
3
4
 
@@ -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,35 @@
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
+ const message = this.i18nService.t(handleError.id);
32
+ this.dialogHandler.error(message);
33
+ }
34
+ }
35
+ }