@idevconn/create-icore 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +384 -276
- package/dist/index.cjs +385 -277
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +382 -274
- package/package.json +1 -1
- package/templates/.yarn/releases/yarn-4.16.0.cjs +944 -0
- package/templates/.yarnrc.yml +1 -1
- package/templates/apps/api/src/app/storage/storage.controller.ts +28 -0
- package/templates/apps/microservices/auth/src/app/app.module.ts +20 -2
- package/templates/apps/microservices/notes/src/app/app.module.ts +17 -2
- package/templates/apps/microservices/upload/src/app/app.module.ts +17 -2
- package/templates/apps/microservices/upload/src/app/storage.controller.ts +7 -0
- package/templates/apps/templates/client-antd/src/components/auth/AuthBrandPanel.tsx +59 -0
- package/templates/apps/templates/client-antd/src/components/auth/CheckEmailScreen.tsx +28 -0
- package/templates/apps/templates/client-antd/src/components/auth/LoginForm.tsx +116 -0
- package/templates/apps/templates/client-antd/src/components/auth/MagicLinkForm.tsx +95 -0
- package/templates/apps/templates/client-antd/src/components/auth/RegisterForm.tsx +98 -0
- package/templates/apps/templates/client-antd/src/globals.less +6 -0
- package/templates/apps/templates/client-antd/src/main.tsx +1 -1
- package/templates/apps/templates/client-antd/src/routes/login.tsx +45 -181
- package/templates/apps/templates/client-mui/src/components/auth/AuthBrandPanel.tsx +59 -0
- package/templates/apps/templates/client-mui/src/components/auth/CheckEmailScreen.tsx +28 -0
- package/templates/apps/templates/client-mui/src/components/auth/LoginForm.tsx +141 -0
- package/templates/apps/templates/client-mui/src/components/auth/MagicLinkForm.tsx +106 -0
- package/templates/apps/templates/client-mui/src/components/auth/RegisterForm.tsx +113 -0
- package/templates/apps/templates/client-mui/src/main.tsx +1 -1
- package/templates/apps/templates/client-mui/src/routes/login.tsx +50 -186
- package/templates/apps/templates/client-shadcn/src/components/auth/AuthBrandPanel.tsx +52 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/CheckEmailScreen.tsx +29 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/LoginForm.tsx +161 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/MagicLinkForm.tsx +110 -0
- package/templates/apps/templates/client-shadcn/src/components/auth/RegisterForm.tsx +107 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutHeader.tsx +31 -10
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +22 -27
- package/templates/apps/templates/client-shadcn/src/components/ui/card.tsx +1 -1
- package/templates/apps/templates/client-shadcn/src/globals.css +39 -13
- package/templates/apps/templates/client-shadcn/src/routes/auth.callback.tsx +1 -1
- package/templates/apps/templates/client-shadcn/src/routes/login.tsx +55 -165
- package/templates/libs/auth-strategies/mongodb/CHANGELOG.md +8 -0
- package/templates/libs/auth-strategies/mongodb/README.md +11 -0
- package/templates/libs/auth-strategies/mongodb/eslint.config.mjs +19 -0
- package/templates/libs/auth-strategies/mongodb/jest.config.cts +10 -0
- package/templates/libs/auth-strategies/mongodb/package.json +16 -0
- package/templates/libs/auth-strategies/mongodb/project.json +19 -0
- package/templates/libs/auth-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/__tests__/mongodb-auth.strategy.unit.test.ts +42 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/auth-mongodb.spec.ts +7 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/auth-mongodb.ts +3 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/mongodb-auth.strategy.ts +188 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.json +23 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.lib.json +10 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.spec.json +16 -0
- package/templates/libs/db-strategies/mongodb/CHANGELOG.md +7 -0
- package/templates/libs/db-strategies/mongodb/README.md +11 -0
- package/templates/libs/db-strategies/mongodb/eslint.config.mjs +19 -0
- package/templates/libs/db-strategies/mongodb/jest.config.cts +10 -0
- package/templates/libs/db-strategies/mongodb/package.json +14 -0
- package/templates/libs/db-strategies/mongodb/project.json +19 -0
- package/templates/libs/db-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/db-strategies/mongodb/src/lib/__tests__/mongodb-db.strategy.unit.test.ts +38 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb-db.strategy.ts +108 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb.spec.ts +7 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb.ts +3 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.json +23 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.lib.json +10 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.spec.json +16 -0
- package/templates/libs/shared/src/strategies/storage.ts +3 -0
- package/templates/libs/storage-strategies/mongodb/CHANGELOG.md +8 -0
- package/templates/libs/storage-strategies/mongodb/README.md +11 -0
- package/templates/libs/storage-strategies/mongodb/eslint.config.mjs +19 -0
- package/templates/libs/storage-strategies/mongodb/jest.config.cts +10 -0
- package/templates/libs/storage-strategies/mongodb/package.json +14 -0
- package/templates/libs/storage-strategies/mongodb/project.json +19 -0
- package/templates/libs/storage-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/__tests__/mongodb-storage.strategy.unit.test.ts +38 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/mongodb-storage.strategy.ts +93 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/storage-mongodb.spec.ts +7 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/storage-mongodb.ts +3 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.json +23 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.lib.json +10 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.spec.json +16 -0
- package/templates/libs/template-shared/src/lib/i18n/keys.ts +216 -56
- package/templates/libs/template-shared/src/lib/stores/theme.store.ts +1 -6
- package/templates/libs/upload-client/src/lib/upload-client.service.ts +7 -0
- package/templates/tsconfig.base.json +4 -1
- package/templates/.yarn/releases/yarn-4.15.0.cjs +0 -940
|
@@ -38,7 +38,7 @@ function Root() {
|
|
|
38
38
|
const mode = useThemeStore((s) => s.mode);
|
|
39
39
|
const algorithm = mode === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
|
40
40
|
return (
|
|
41
|
-
<ConfigProvider theme={{ algorithm }}>
|
|
41
|
+
<ConfigProvider theme={{ algorithm, token: { colorPrimary: '#22c55e', colorLink: '#22c55e' } }}>
|
|
42
42
|
<AntApp>
|
|
43
43
|
<QueryClientProvider client={queryClient}>
|
|
44
44
|
<AbilityProvider>
|
|
@@ -1,193 +1,57 @@
|
|
|
1
|
-
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
|
2
|
-
import { Button, Card, Form, Input, Result, Segmented, Space } from 'antd';
|
|
3
|
-
import { GithubOutlined, GoogleOutlined } from '@ant-design/icons';
|
|
4
1
|
import { useState } from 'react';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
3
|
+
import { AuthBrandPanel } from '../components/auth/AuthBrandPanel';
|
|
4
|
+
import { CheckEmailScreen } from '../components/auth/CheckEmailScreen';
|
|
5
|
+
import { LoginForm } from '../components/auth/LoginForm';
|
|
6
|
+
import { MagicLinkForm } from '../components/auth/MagicLinkForm';
|
|
7
|
+
import { RegisterForm } from '../components/auth/RegisterForm';
|
|
8
8
|
|
|
9
|
-
type Mode = '
|
|
10
|
-
|
|
11
|
-
interface PasswordFormValues {
|
|
12
|
-
email: string;
|
|
13
|
-
password: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface MagicLinkFormValues {
|
|
17
|
-
email: string;
|
|
18
|
-
}
|
|
9
|
+
type Mode = 'login' | 'register' | 'magicLink' | 'checkEmail';
|
|
19
10
|
|
|
20
11
|
function LoginPage() {
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const notify = useNotify();
|
|
24
|
-
const setAuth = useAuthStore((s) => s.setAuth);
|
|
12
|
+
const [mode, setMode] = useState<Mode>('login');
|
|
13
|
+
const [checkEmail, setCheckEmail] = useState('');
|
|
25
14
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const [magicLinkForm] = Form.useForm<MagicLinkFormValues>();
|
|
30
|
-
|
|
31
|
-
async function handlePasswordFinish(values: PasswordFormValues) {
|
|
32
|
-
try {
|
|
33
|
-
const session = await api<{
|
|
34
|
-
accessToken: string;
|
|
35
|
-
refreshToken: string;
|
|
36
|
-
user: { id: string; email: string; role?: string };
|
|
37
|
-
}>('/auth/login', {
|
|
38
|
-
method: 'POST',
|
|
39
|
-
headers: { 'Content-Type': 'application/json' },
|
|
40
|
-
body: JSON.stringify({ email: values.email, password: values.password }),
|
|
41
|
-
});
|
|
42
|
-
setAuth(session);
|
|
43
|
-
notify.success(t('auth.login'));
|
|
44
|
-
await navigate({ to: '/dashboard' });
|
|
45
|
-
} catch (err) {
|
|
46
|
-
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function handleMagicLinkFinish(values: MagicLinkFormValues) {
|
|
51
|
-
try {
|
|
52
|
-
await api('/auth/magic-link', {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
headers: { 'Content-Type': 'application/json' },
|
|
55
|
-
body: JSON.stringify({ email: values.email }),
|
|
56
|
-
});
|
|
57
|
-
setSentEmail(values.email);
|
|
58
|
-
setMode('magicLinkSent');
|
|
59
|
-
} catch (err) {
|
|
60
|
-
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
61
|
-
}
|
|
15
|
+
function handleRegisterSuccess(email: string) {
|
|
16
|
+
setCheckEmail(email);
|
|
17
|
+
setMode('checkEmail');
|
|
62
18
|
}
|
|
63
19
|
|
|
64
20
|
return (
|
|
65
|
-
<
|
|
66
|
-
style={{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
value={mode}
|
|
85
|
-
onChange={(v) => setMode(v as Mode)}
|
|
86
|
-
options={[
|
|
87
|
-
{ label: t('auth.withPassword'), value: 'password' },
|
|
88
|
-
{ label: t('auth.withMagicLink'), value: 'magicLinkRequest' },
|
|
89
|
-
]}
|
|
90
|
-
style={{ marginBottom: 16 }}
|
|
21
|
+
<div style={{ minHeight: '100vh', display: 'flex', background: '#020617' }}>
|
|
22
|
+
<div style={{ flex: 1, display: 'none' }} className="auth-brand-lg">
|
|
23
|
+
<AuthBrandPanel />
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div
|
|
27
|
+
style={{
|
|
28
|
+
flex: 1,
|
|
29
|
+
display: 'flex',
|
|
30
|
+
alignItems: 'center',
|
|
31
|
+
justifyContent: 'center',
|
|
32
|
+
padding: '40px 24px',
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<div style={{ width: '100%', maxWidth: 400 }}>
|
|
36
|
+
{mode === 'login' && (
|
|
37
|
+
<LoginForm
|
|
38
|
+
onSwitchRegister={() => setMode('register')}
|
|
39
|
+
onSwitchMagicLink={() => setMode('magicLink')}
|
|
91
40
|
/>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</Button>
|
|
107
|
-
</Space>
|
|
108
|
-
</>
|
|
109
|
-
)}
|
|
110
|
-
|
|
111
|
-
{mode === 'password' && (
|
|
112
|
-
<Form
|
|
113
|
-
form={passwordForm}
|
|
114
|
-
layout="vertical"
|
|
115
|
-
onFinish={handlePasswordFinish}
|
|
116
|
-
autoComplete="on"
|
|
117
|
-
>
|
|
118
|
-
<Form.Item
|
|
119
|
-
name="email"
|
|
120
|
-
label={t('auth.email')}
|
|
121
|
-
rules={[
|
|
122
|
-
{ required: true, message: `${t('auth.email')} is required` },
|
|
123
|
-
{ type: 'email', message: 'Please enter a valid email address' },
|
|
124
|
-
]}
|
|
125
|
-
>
|
|
126
|
-
<Input autoComplete="email" size="large" />
|
|
127
|
-
</Form.Item>
|
|
128
|
-
|
|
129
|
-
<Form.Item
|
|
130
|
-
name="password"
|
|
131
|
-
label={t('auth.password')}
|
|
132
|
-
rules={[{ required: true, message: `${t('auth.password')} is required` }]}
|
|
133
|
-
>
|
|
134
|
-
<Input.Password autoComplete="current-password" size="large" />
|
|
135
|
-
</Form.Item>
|
|
136
|
-
|
|
137
|
-
<Form.Item style={{ marginBottom: 0 }}>
|
|
138
|
-
<Button type="primary" htmlType="submit" block size="large">
|
|
139
|
-
{t('auth.login')}
|
|
140
|
-
</Button>
|
|
141
|
-
</Form.Item>
|
|
142
|
-
</Form>
|
|
143
|
-
)}
|
|
144
|
-
|
|
145
|
-
{mode === 'magicLinkRequest' && (
|
|
146
|
-
<Form
|
|
147
|
-
form={magicLinkForm}
|
|
148
|
-
layout="vertical"
|
|
149
|
-
onFinish={handleMagicLinkFinish}
|
|
150
|
-
autoComplete="on"
|
|
151
|
-
>
|
|
152
|
-
<Form.Item
|
|
153
|
-
name="email"
|
|
154
|
-
label={t('auth.email')}
|
|
155
|
-
rules={[
|
|
156
|
-
{ required: true, message: `${t('auth.email')} is required` },
|
|
157
|
-
{ type: 'email', message: 'Please enter a valid email address' },
|
|
158
|
-
]}
|
|
159
|
-
>
|
|
160
|
-
<Input autoComplete="email" size="large" />
|
|
161
|
-
</Form.Item>
|
|
162
|
-
|
|
163
|
-
<Form.Item style={{ marginBottom: 0 }}>
|
|
164
|
-
<Button type="primary" htmlType="submit" block size="large">
|
|
165
|
-
{t('auth.sendMagicLink')}
|
|
166
|
-
</Button>
|
|
167
|
-
</Form.Item>
|
|
168
|
-
</Form>
|
|
169
|
-
)}
|
|
170
|
-
|
|
171
|
-
{mode === 'magicLinkSent' && (
|
|
172
|
-
<Result
|
|
173
|
-
status="success"
|
|
174
|
-
title={t('auth.magicLinkSent')}
|
|
175
|
-
subTitle={t('auth.magicLinkSentDescription', { email: sentEmail })}
|
|
176
|
-
extra={
|
|
177
|
-
<Button
|
|
178
|
-
onClick={() => {
|
|
179
|
-
setSentEmail('');
|
|
180
|
-
magicLinkForm.resetFields();
|
|
181
|
-
setMode('magicLinkRequest');
|
|
182
|
-
}}
|
|
183
|
-
>
|
|
184
|
-
{t('auth.magicLinkUseDifferentEmail')}
|
|
185
|
-
</Button>
|
|
186
|
-
}
|
|
187
|
-
/>
|
|
188
|
-
)}
|
|
189
|
-
</Card>
|
|
190
|
-
</main>
|
|
41
|
+
)}
|
|
42
|
+
{mode === 'register' && (
|
|
43
|
+
<RegisterForm
|
|
44
|
+
onSuccess={handleRegisterSuccess}
|
|
45
|
+
onSwitchLogin={() => setMode('login')}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
{mode === 'magicLink' && <MagicLinkForm onSwitchLogin={() => setMode('login')} />}
|
|
49
|
+
{mode === 'checkEmail' && (
|
|
50
|
+
<CheckEmailScreen email={checkEmail} onBack={() => setMode('login')} />
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
191
55
|
);
|
|
192
56
|
}
|
|
193
57
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Box, Stack, Typography } from '@mui/material';
|
|
2
|
+
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
|
|
3
|
+
|
|
4
|
+
const FEATURES = [
|
|
5
|
+
'Strategy-pattern auth & storage',
|
|
6
|
+
'Multi-provider: password, magic link, OAuth',
|
|
7
|
+
'CASL role-based access control',
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
export function AuthBrandPanel() {
|
|
11
|
+
return (
|
|
12
|
+
<Box
|
|
13
|
+
sx={{
|
|
14
|
+
flex: 1,
|
|
15
|
+
background: 'linear-gradient(135deg, #0d1117 0%, #0f1e0f 100%)',
|
|
16
|
+
display: 'flex',
|
|
17
|
+
flexDirection: 'column',
|
|
18
|
+
justifyContent: 'center',
|
|
19
|
+
p: '48px 56px',
|
|
20
|
+
position: 'relative',
|
|
21
|
+
overflow: 'hidden',
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<Box
|
|
25
|
+
sx={{
|
|
26
|
+
position: 'absolute',
|
|
27
|
+
top: '20%',
|
|
28
|
+
left: '10%',
|
|
29
|
+
width: 320,
|
|
30
|
+
height: 320,
|
|
31
|
+
borderRadius: '50%',
|
|
32
|
+
background: 'radial-gradient(circle, rgba(34,197,94,0.15) 0%, transparent 70%)',
|
|
33
|
+
pointerEvents: 'none',
|
|
34
|
+
}}
|
|
35
|
+
/>
|
|
36
|
+
<Stack spacing={4} sx={{ position: 'relative', zIndex: 1 }}>
|
|
37
|
+
<Stack spacing={0.5}>
|
|
38
|
+
<Typography variant="h4" fontWeight={700} sx={{ color: '#22c55e' }}>
|
|
39
|
+
iCore
|
|
40
|
+
</Typography>
|
|
41
|
+
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.5)' }}>
|
|
42
|
+
Enterprise scaffold for NestJS + React
|
|
43
|
+
</Typography>
|
|
44
|
+
</Stack>
|
|
45
|
+
|
|
46
|
+
<Stack spacing={1}>
|
|
47
|
+
{FEATURES.map((f) => (
|
|
48
|
+
<Stack key={f} direction="row" spacing={1} alignItems="center">
|
|
49
|
+
<CheckCircleOutlineIcon sx={{ color: '#22c55e', fontSize: 16 }} />
|
|
50
|
+
<Typography variant="body2" sx={{ color: 'rgba(255,255,255,0.75)' }}>
|
|
51
|
+
{f}
|
|
52
|
+
</Typography>
|
|
53
|
+
</Stack>
|
|
54
|
+
))}
|
|
55
|
+
</Stack>
|
|
56
|
+
</Stack>
|
|
57
|
+
</Box>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Button, Stack, Typography } from '@mui/material';
|
|
2
|
+
import MarkEmailReadOutlinedIcon from '@mui/icons-material/MarkEmailReadOutlined';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
email: string;
|
|
7
|
+
onBack: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CheckEmailScreen({ email, onBack }: Props) {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
return (
|
|
13
|
+
<Stack spacing={3} alignItems="center" textAlign="center">
|
|
14
|
+
<MarkEmailReadOutlinedIcon sx={{ fontSize: 56, color: 'primary.main' }} />
|
|
15
|
+
<Stack spacing={1}>
|
|
16
|
+
<Typography variant="h5" fontWeight={600}>
|
|
17
|
+
{t('auth.checkEmail')}
|
|
18
|
+
</Typography>
|
|
19
|
+
<Typography variant="body2" color="text.secondary">
|
|
20
|
+
{t('auth.checkEmailDescription', { email })}
|
|
21
|
+
</Typography>
|
|
22
|
+
</Stack>
|
|
23
|
+
<Button variant="outlined" fullWidth onClick={onBack}>
|
|
24
|
+
{t('auth.backToLogin')}
|
|
25
|
+
</Button>
|
|
26
|
+
</Stack>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Box, Button, Divider, Stack, TextField, Typography } from '@mui/material';
|
|
3
|
+
import GoogleIcon from '@mui/icons-material/Google';
|
|
4
|
+
import GitHubIcon from '@mui/icons-material/GitHub';
|
|
5
|
+
import { SyntheticEvent } from 'react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { useNavigate } from '@tanstack/react-router';
|
|
8
|
+
import { useAuthStore, useNotify } from '@icore/template-shared';
|
|
9
|
+
import { api } from '@/main';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
onSwitchRegister: () => void;
|
|
13
|
+
onSwitchMagicLink: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function LoginForm({ onSwitchRegister, onSwitchMagicLink }: Props) {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const navigate = useNavigate();
|
|
19
|
+
const notify = useNotify();
|
|
20
|
+
const setAuth = useAuthStore((s) => s.setAuth);
|
|
21
|
+
|
|
22
|
+
const [email, setEmail] = useState('');
|
|
23
|
+
const [password, setPassword] = useState('');
|
|
24
|
+
const [submitting, setSubmitting] = useState(false);
|
|
25
|
+
|
|
26
|
+
async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
setSubmitting(true);
|
|
29
|
+
try {
|
|
30
|
+
const session = await api<{
|
|
31
|
+
accessToken: string;
|
|
32
|
+
refreshToken: string;
|
|
33
|
+
user: { id: string; email: string; role?: string };
|
|
34
|
+
}>('/auth/login', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ email, password }),
|
|
38
|
+
});
|
|
39
|
+
setAuth(session);
|
|
40
|
+
notify.success(t('auth.login'));
|
|
41
|
+
await navigate({ to: '/dashboard' });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
44
|
+
} finally {
|
|
45
|
+
setSubmitting(false);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Stack spacing={2}>
|
|
51
|
+
<Stack spacing={0.5}>
|
|
52
|
+
<Typography variant="h5" fontWeight={600}>
|
|
53
|
+
{t('auth.loginTitle')}
|
|
54
|
+
</Typography>
|
|
55
|
+
<Typography variant="body2" color="text.secondary">
|
|
56
|
+
{t('auth.loginSubtitle')}
|
|
57
|
+
</Typography>
|
|
58
|
+
</Stack>
|
|
59
|
+
|
|
60
|
+
<Stack spacing={1}>
|
|
61
|
+
<Button
|
|
62
|
+
variant="outlined"
|
|
63
|
+
fullWidth
|
|
64
|
+
startIcon={<GoogleIcon />}
|
|
65
|
+
onClick={() => window.location.assign('/api/auth/oauth/google')}
|
|
66
|
+
>
|
|
67
|
+
{t('auth.continueWithGoogle')}
|
|
68
|
+
</Button>
|
|
69
|
+
<Button
|
|
70
|
+
variant="outlined"
|
|
71
|
+
fullWidth
|
|
72
|
+
startIcon={<GitHubIcon />}
|
|
73
|
+
onClick={() => window.location.assign('/api/auth/oauth/github')}
|
|
74
|
+
>
|
|
75
|
+
{t('auth.continueWithGithub')}
|
|
76
|
+
</Button>
|
|
77
|
+
</Stack>
|
|
78
|
+
|
|
79
|
+
<Divider>
|
|
80
|
+
<Typography variant="caption" color="text.secondary">
|
|
81
|
+
{t('auth.orContinueWith')}
|
|
82
|
+
</Typography>
|
|
83
|
+
</Divider>
|
|
84
|
+
|
|
85
|
+
<Box component="form" onSubmit={handleSubmit} autoComplete="on">
|
|
86
|
+
<TextField
|
|
87
|
+
label={t('auth.email')}
|
|
88
|
+
type="email"
|
|
89
|
+
autoComplete="email"
|
|
90
|
+
required
|
|
91
|
+
fullWidth
|
|
92
|
+
margin="normal"
|
|
93
|
+
value={email}
|
|
94
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
95
|
+
/>
|
|
96
|
+
<TextField
|
|
97
|
+
label={t('auth.password')}
|
|
98
|
+
type="password"
|
|
99
|
+
autoComplete="current-password"
|
|
100
|
+
required
|
|
101
|
+
fullWidth
|
|
102
|
+
margin="normal"
|
|
103
|
+
value={password}
|
|
104
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
105
|
+
/>
|
|
106
|
+
<Button type="submit" variant="contained" fullWidth disabled={submitting} sx={{ mt: 2 }}>
|
|
107
|
+
{t('auth.login')}
|
|
108
|
+
</Button>
|
|
109
|
+
</Box>
|
|
110
|
+
|
|
111
|
+
<Stack spacing={0.5} alignItems="center">
|
|
112
|
+
<Typography variant="body2" color="text.secondary">
|
|
113
|
+
{t('auth.switchToRegister')}{' '}
|
|
114
|
+
<Box
|
|
115
|
+
component="span"
|
|
116
|
+
onClick={onSwitchRegister}
|
|
117
|
+
sx={{
|
|
118
|
+
color: 'primary.main',
|
|
119
|
+
cursor: 'pointer',
|
|
120
|
+
'&:hover': { textDecoration: 'underline' },
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
{t('auth.switchToRegisterLink')}
|
|
124
|
+
</Box>
|
|
125
|
+
</Typography>
|
|
126
|
+
<Box
|
|
127
|
+
component="span"
|
|
128
|
+
onClick={onSwitchMagicLink}
|
|
129
|
+
sx={{
|
|
130
|
+
fontSize: 13,
|
|
131
|
+
color: 'primary.main',
|
|
132
|
+
cursor: 'pointer',
|
|
133
|
+
'&:hover': { textDecoration: 'underline' },
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{t('auth.withMagicLink')}
|
|
137
|
+
</Box>
|
|
138
|
+
</Stack>
|
|
139
|
+
</Stack>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Box, Button, Stack, TextField, Typography } from '@mui/material';
|
|
3
|
+
import MailOutlineIcon from '@mui/icons-material/MailOutline';
|
|
4
|
+
import { SyntheticEvent } from 'react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { useNotify } from '@icore/template-shared';
|
|
7
|
+
import { api } from '@/main';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
onSwitchLogin: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function MagicLinkForm({ onSwitchLogin }: Props) {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const notify = useNotify();
|
|
16
|
+
|
|
17
|
+
const [email, setEmail] = useState('');
|
|
18
|
+
const [sentEmail, setSentEmail] = useState('');
|
|
19
|
+
const [submitting, setSubmitting] = useState(false);
|
|
20
|
+
|
|
21
|
+
async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
22
|
+
e.preventDefault();
|
|
23
|
+
setSubmitting(true);
|
|
24
|
+
try {
|
|
25
|
+
await api('/auth/magic-link', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'Content-Type': 'application/json' },
|
|
28
|
+
body: JSON.stringify({ email }),
|
|
29
|
+
});
|
|
30
|
+
setSentEmail(email);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
33
|
+
} finally {
|
|
34
|
+
setSubmitting(false);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sentEmail) {
|
|
39
|
+
return (
|
|
40
|
+
<Stack spacing={3} alignItems="center" textAlign="center">
|
|
41
|
+
<MailOutlineIcon sx={{ fontSize: 56, color: 'primary.main' }} />
|
|
42
|
+
<Stack spacing={1}>
|
|
43
|
+
<Typography variant="h5" fontWeight={600}>
|
|
44
|
+
{t('auth.magicLinkSent')}
|
|
45
|
+
</Typography>
|
|
46
|
+
<Typography variant="body2" color="text.secondary">
|
|
47
|
+
{t('auth.magicLinkSentDescription', { email: sentEmail })}
|
|
48
|
+
</Typography>
|
|
49
|
+
</Stack>
|
|
50
|
+
<Button
|
|
51
|
+
variant="outlined"
|
|
52
|
+
fullWidth
|
|
53
|
+
onClick={() => {
|
|
54
|
+
setEmail('');
|
|
55
|
+
setSentEmail('');
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{t('auth.magicLinkUseDifferentEmail')}
|
|
59
|
+
</Button>
|
|
60
|
+
</Stack>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Stack spacing={2}>
|
|
66
|
+
<Stack spacing={0.5}>
|
|
67
|
+
<Typography variant="h5" fontWeight={600}>
|
|
68
|
+
{t('auth.withMagicLink')}
|
|
69
|
+
</Typography>
|
|
70
|
+
<Typography variant="body2" color="text.secondary">
|
|
71
|
+
{t('auth.loginSubtitle')}
|
|
72
|
+
</Typography>
|
|
73
|
+
</Stack>
|
|
74
|
+
|
|
75
|
+
<Box component="form" onSubmit={handleSubmit} autoComplete="on">
|
|
76
|
+
<TextField
|
|
77
|
+
label={t('auth.email')}
|
|
78
|
+
type="email"
|
|
79
|
+
autoComplete="email"
|
|
80
|
+
required
|
|
81
|
+
fullWidth
|
|
82
|
+
margin="normal"
|
|
83
|
+
value={email}
|
|
84
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
85
|
+
/>
|
|
86
|
+
<Button type="submit" variant="contained" fullWidth disabled={submitting} sx={{ mt: 2 }}>
|
|
87
|
+
{t('auth.sendMagicLink')}
|
|
88
|
+
</Button>
|
|
89
|
+
</Box>
|
|
90
|
+
|
|
91
|
+
<Typography variant="body2" color="text.secondary" textAlign="center">
|
|
92
|
+
<Box
|
|
93
|
+
component="span"
|
|
94
|
+
onClick={onSwitchLogin}
|
|
95
|
+
sx={{
|
|
96
|
+
color: 'primary.main',
|
|
97
|
+
cursor: 'pointer',
|
|
98
|
+
'&:hover': { textDecoration: 'underline' },
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{t('auth.backToLogin')}
|
|
102
|
+
</Box>
|
|
103
|
+
</Typography>
|
|
104
|
+
</Stack>
|
|
105
|
+
);
|
|
106
|
+
}
|