@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Box, Button, Stack, TextField, Typography } from '@mui/material';
|
|
3
|
+
import { SyntheticEvent } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { useNotify } from '@icore/template-shared';
|
|
6
|
+
import { api } from '@/main';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
onSuccess: (email: string) => void;
|
|
10
|
+
onSwitchLogin: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function RegisterForm({ onSuccess, onSwitchLogin }: Props) {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const notify = useNotify();
|
|
16
|
+
|
|
17
|
+
const [email, setEmail] = useState('');
|
|
18
|
+
const [password, setPassword] = useState('');
|
|
19
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
20
|
+
const [mismatch, setMismatch] = useState(false);
|
|
21
|
+
const [submitting, setSubmitting] = useState(false);
|
|
22
|
+
|
|
23
|
+
async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
if (password !== confirmPassword) {
|
|
26
|
+
setMismatch(true);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setMismatch(false);
|
|
30
|
+
setSubmitting(true);
|
|
31
|
+
try {
|
|
32
|
+
await api('/auth/register', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ email, password }),
|
|
36
|
+
});
|
|
37
|
+
onSuccess(email);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
40
|
+
} finally {
|
|
41
|
+
setSubmitting(false);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Stack spacing={2}>
|
|
47
|
+
<Stack spacing={0.5}>
|
|
48
|
+
<Typography variant="h5" fontWeight={600}>
|
|
49
|
+
{t('auth.registerTitle')}
|
|
50
|
+
</Typography>
|
|
51
|
+
<Typography variant="body2" color="text.secondary">
|
|
52
|
+
{t('auth.registerSubtitle')}
|
|
53
|
+
</Typography>
|
|
54
|
+
</Stack>
|
|
55
|
+
|
|
56
|
+
<Box component="form" onSubmit={handleSubmit} autoComplete="on">
|
|
57
|
+
<TextField
|
|
58
|
+
label={t('auth.email')}
|
|
59
|
+
type="email"
|
|
60
|
+
autoComplete="email"
|
|
61
|
+
required
|
|
62
|
+
fullWidth
|
|
63
|
+
margin="normal"
|
|
64
|
+
value={email}
|
|
65
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
66
|
+
/>
|
|
67
|
+
<TextField
|
|
68
|
+
label={t('auth.password')}
|
|
69
|
+
type="password"
|
|
70
|
+
autoComplete="new-password"
|
|
71
|
+
required
|
|
72
|
+
fullWidth
|
|
73
|
+
margin="normal"
|
|
74
|
+
value={password}
|
|
75
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
76
|
+
/>
|
|
77
|
+
<TextField
|
|
78
|
+
label={t('auth.confirmPassword')}
|
|
79
|
+
type="password"
|
|
80
|
+
autoComplete="new-password"
|
|
81
|
+
required
|
|
82
|
+
fullWidth
|
|
83
|
+
margin="normal"
|
|
84
|
+
value={confirmPassword}
|
|
85
|
+
onChange={(e) => {
|
|
86
|
+
setConfirmPassword(e.target.value);
|
|
87
|
+
setMismatch(false);
|
|
88
|
+
}}
|
|
89
|
+
error={mismatch}
|
|
90
|
+
helperText={mismatch ? t('auth.passwordMismatch') : undefined}
|
|
91
|
+
/>
|
|
92
|
+
<Button type="submit" variant="contained" fullWidth disabled={submitting} sx={{ mt: 2 }}>
|
|
93
|
+
{t('auth.register')}
|
|
94
|
+
</Button>
|
|
95
|
+
</Box>
|
|
96
|
+
|
|
97
|
+
<Typography variant="body2" color="text.secondary" textAlign="center">
|
|
98
|
+
{t('auth.switchToLogin')}{' '}
|
|
99
|
+
<Box
|
|
100
|
+
component="span"
|
|
101
|
+
onClick={onSwitchLogin}
|
|
102
|
+
sx={{
|
|
103
|
+
color: 'primary.main',
|
|
104
|
+
cursor: 'pointer',
|
|
105
|
+
'&:hover': { textDecoration: 'underline' },
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{t('auth.switchToLoginLink')}
|
|
109
|
+
</Box>
|
|
110
|
+
</Typography>
|
|
111
|
+
</Stack>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -39,7 +39,7 @@ wireMuiNotifier();
|
|
|
39
39
|
function Root() {
|
|
40
40
|
const mode = useThemeStore((s) => s.mode);
|
|
41
41
|
const theme = useMemo(
|
|
42
|
-
() => createTheme({ palette: { mode, primary: { main: '#
|
|
42
|
+
() => createTheme({ palette: { mode, primary: { main: '#22c55e' } } }),
|
|
43
43
|
[mode],
|
|
44
44
|
);
|
|
45
45
|
return (
|
|
@@ -1,200 +1,64 @@
|
|
|
1
1
|
import { useState } from 'react';
|
|
2
|
-
import { createFileRoute
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Stack,
|
|
10
|
-
Tab,
|
|
11
|
-
Tabs,
|
|
12
|
-
TextField,
|
|
13
|
-
Typography,
|
|
14
|
-
} from '@mui/material';
|
|
15
|
-
import GoogleIcon from '@mui/icons-material/Google';
|
|
16
|
-
import GitHubIcon from '@mui/icons-material/GitHub';
|
|
17
|
-
import { useTranslation } from 'react-i18next';
|
|
18
|
-
import { useAuthStore, useNotify } from '@icore/template-shared';
|
|
19
|
-
import { api } from '../main';
|
|
2
|
+
import { createFileRoute } from '@tanstack/react-router';
|
|
3
|
+
import { Box, useMediaQuery, useTheme } from '@mui/material';
|
|
4
|
+
import { AuthBrandPanel } from '../components/auth/AuthBrandPanel';
|
|
5
|
+
import { CheckEmailScreen } from '../components/auth/CheckEmailScreen';
|
|
6
|
+
import { LoginForm } from '../components/auth/LoginForm';
|
|
7
|
+
import { MagicLinkForm } from '../components/auth/MagicLinkForm';
|
|
8
|
+
import { RegisterForm } from '../components/auth/RegisterForm';
|
|
20
9
|
|
|
21
|
-
type Mode = '
|
|
10
|
+
type Mode = 'login' | 'register' | 'magicLink' | 'checkEmail';
|
|
22
11
|
|
|
23
12
|
function LoginPage() {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
const
|
|
13
|
+
const [mode, setMode] = useState<Mode>('login');
|
|
14
|
+
const [checkEmail, setCheckEmail] = useState('');
|
|
15
|
+
const theme = useTheme();
|
|
16
|
+
const isLg = useMediaQuery(theme.breakpoints.up('lg'));
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const [sentEmail, setSentEmail] = useState('');
|
|
33
|
-
const [submitting, setSubmitting] = useState(false);
|
|
34
|
-
|
|
35
|
-
async function handlePasswordSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
|
36
|
-
e.preventDefault();
|
|
37
|
-
setSubmitting(true);
|
|
38
|
-
try {
|
|
39
|
-
const session = await api<{
|
|
40
|
-
accessToken: string;
|
|
41
|
-
refreshToken: string;
|
|
42
|
-
user: { id: string; email: string; role?: string };
|
|
43
|
-
}>('/auth/login', {
|
|
44
|
-
method: 'POST',
|
|
45
|
-
headers: { 'Content-Type': 'application/json' },
|
|
46
|
-
body: JSON.stringify({ email, password }),
|
|
47
|
-
});
|
|
48
|
-
setAuth(session);
|
|
49
|
-
notify.success(t('auth.login'));
|
|
50
|
-
await navigate({ to: '/dashboard' });
|
|
51
|
-
} catch (err) {
|
|
52
|
-
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
53
|
-
} finally {
|
|
54
|
-
setSubmitting(false);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function handleMagicLinkSubmit(e: React.SyntheticEvent<HTMLFormElement>) {
|
|
59
|
-
e.preventDefault();
|
|
60
|
-
setSubmitting(true);
|
|
61
|
-
try {
|
|
62
|
-
await api('/auth/magic-link', {
|
|
63
|
-
method: 'POST',
|
|
64
|
-
headers: { 'Content-Type': 'application/json' },
|
|
65
|
-
body: JSON.stringify({ email }),
|
|
66
|
-
});
|
|
67
|
-
setSentEmail(email);
|
|
68
|
-
setMode('magicLinkSent');
|
|
69
|
-
} catch (err) {
|
|
70
|
-
notify.error(err instanceof Error ? err.message : t('error.unknown'));
|
|
71
|
-
} finally {
|
|
72
|
-
setSubmitting(false);
|
|
73
|
-
}
|
|
18
|
+
function handleRegisterSuccess(email: string) {
|
|
19
|
+
setCheckEmail(email);
|
|
20
|
+
setMode('checkEmail');
|
|
74
21
|
}
|
|
75
22
|
|
|
76
23
|
return (
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{mode !== 'magicLinkSent' && (
|
|
87
|
-
<>
|
|
88
|
-
<Tabs
|
|
89
|
-
value={mode}
|
|
90
|
-
onChange={(_, v: Mode) => setMode(v)}
|
|
91
|
-
variant="fullWidth"
|
|
92
|
-
sx={{ mb: 2 }}
|
|
93
|
-
>
|
|
94
|
-
<Tab label={t('auth.withPassword')} value="password" />
|
|
95
|
-
<Tab label={t('auth.withMagicLink')} value="magicLinkRequest" />
|
|
96
|
-
</Tabs>
|
|
97
|
-
<Stack spacing={1} sx={{ mb: 2 }}>
|
|
98
|
-
<Button
|
|
99
|
-
variant="outlined"
|
|
100
|
-
fullWidth
|
|
101
|
-
startIcon={<GoogleIcon />}
|
|
102
|
-
onClick={() => window.location.assign('/api/auth/oauth/google')}
|
|
103
|
-
>
|
|
104
|
-
{t('auth.continueWithGoogle')}
|
|
105
|
-
</Button>
|
|
106
|
-
<Button
|
|
107
|
-
variant="outlined"
|
|
108
|
-
fullWidth
|
|
109
|
-
startIcon={<GitHubIcon />}
|
|
110
|
-
onClick={() => window.location.assign('/api/auth/oauth/github')}
|
|
111
|
-
>
|
|
112
|
-
{t('auth.continueWithGithub')}
|
|
113
|
-
</Button>
|
|
114
|
-
</Stack>
|
|
115
|
-
<Divider sx={{ mb: 2 }} />
|
|
116
|
-
</>
|
|
117
|
-
)}
|
|
24
|
+
<Box
|
|
25
|
+
sx={{
|
|
26
|
+
minHeight: '100vh',
|
|
27
|
+
display: 'flex',
|
|
28
|
+
bgcolor: '#020617',
|
|
29
|
+
}}
|
|
30
|
+
>
|
|
31
|
+
{isLg && <AuthBrandPanel />}
|
|
118
32
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
33
|
+
<Box
|
|
34
|
+
sx={{
|
|
35
|
+
flex: 1,
|
|
36
|
+
display: 'flex',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
p: '40px 24px',
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Box sx={{ width: '100%', maxWidth: 400 }}>
|
|
43
|
+
{mode === 'login' && (
|
|
44
|
+
<LoginForm
|
|
45
|
+
onSwitchRegister={() => setMode('register')}
|
|
46
|
+
onSwitchMagicLink={() => setMode('magicLink')}
|
|
130
47
|
/>
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
fullWidth
|
|
137
|
-
margin="normal"
|
|
138
|
-
value={password}
|
|
139
|
-
onChange={(e) => setPassword(e.target.value)}
|
|
48
|
+
)}
|
|
49
|
+
{mode === 'register' && (
|
|
50
|
+
<RegisterForm
|
|
51
|
+
onSuccess={handleRegisterSuccess}
|
|
52
|
+
onSwitchLogin={() => setMode('login')}
|
|
140
53
|
/>
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</Button>
|
|
150
|
-
</Box>
|
|
151
|
-
)}
|
|
152
|
-
|
|
153
|
-
{mode === 'magicLinkRequest' && (
|
|
154
|
-
<Box component="form" onSubmit={handleMagicLinkSubmit} autoComplete="on">
|
|
155
|
-
<TextField
|
|
156
|
-
label={t('auth.email')}
|
|
157
|
-
type="email"
|
|
158
|
-
autoComplete="email"
|
|
159
|
-
required
|
|
160
|
-
fullWidth
|
|
161
|
-
margin="normal"
|
|
162
|
-
value={email}
|
|
163
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
164
|
-
/>
|
|
165
|
-
<Button
|
|
166
|
-
type="submit"
|
|
167
|
-
variant="contained"
|
|
168
|
-
fullWidth
|
|
169
|
-
disabled={submitting}
|
|
170
|
-
sx={{ mt: 2 }}
|
|
171
|
-
>
|
|
172
|
-
{t('auth.sendMagicLink')}
|
|
173
|
-
</Button>
|
|
174
|
-
</Box>
|
|
175
|
-
)}
|
|
176
|
-
|
|
177
|
-
{mode === 'magicLinkSent' && (
|
|
178
|
-
<Stack spacing={2} alignItems="center" textAlign="center">
|
|
179
|
-
<Typography variant="h6">{t('auth.magicLinkSent')}</Typography>
|
|
180
|
-
<Typography variant="body2" color="text.secondary">
|
|
181
|
-
{t('auth.magicLinkSentDescription', { email: sentEmail })}
|
|
182
|
-
</Typography>
|
|
183
|
-
<Button
|
|
184
|
-
variant="outlined"
|
|
185
|
-
fullWidth
|
|
186
|
-
onClick={() => {
|
|
187
|
-
setEmail('');
|
|
188
|
-
setSentEmail('');
|
|
189
|
-
setMode('magicLinkRequest');
|
|
190
|
-
}}
|
|
191
|
-
>
|
|
192
|
-
{t('auth.magicLinkUseDifferentEmail')}
|
|
193
|
-
</Button>
|
|
194
|
-
</Stack>
|
|
195
|
-
)}
|
|
196
|
-
</Paper>
|
|
197
|
-
</Container>
|
|
54
|
+
)}
|
|
55
|
+
{mode === 'magicLink' && <MagicLinkForm onSwitchLogin={() => setMode('login')} />}
|
|
56
|
+
{mode === 'checkEmail' && (
|
|
57
|
+
<CheckEmailScreen email={checkEmail} onBack={() => setMode('login')} />
|
|
58
|
+
)}
|
|
59
|
+
</Box>
|
|
60
|
+
</Box>
|
|
61
|
+
</Box>
|
|
198
62
|
);
|
|
199
63
|
}
|
|
200
64
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ShieldCheck, Zap, Globe } from 'lucide-react';
|
|
2
|
+
|
|
3
|
+
const FEATURES = [
|
|
4
|
+
{ icon: ShieldCheck, text: 'Auth, storage & payments — wired out of the box' },
|
|
5
|
+
{ icon: Zap, text: 'Strategy pattern — swap any provider via env' },
|
|
6
|
+
{ icon: Globe, text: 'Multi-language, dark mode, CASL authorization' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function AuthBrandPanel() {
|
|
10
|
+
return (
|
|
11
|
+
<div className="relative hidden lg:flex lg:w-2/5 flex-col justify-between p-12 bg-[--color-card] overflow-hidden">
|
|
12
|
+
{/* Green ambient glow */}
|
|
13
|
+
<div
|
|
14
|
+
aria-hidden
|
|
15
|
+
className="pointer-events-none absolute -top-32 -left-32 h-96 w-96 rounded-full bg-[--color-primary]/10 blur-3xl"
|
|
16
|
+
/>
|
|
17
|
+
<div
|
|
18
|
+
aria-hidden
|
|
19
|
+
className="pointer-events-none absolute bottom-0 right-0 h-64 w-64 rounded-full bg-[--color-primary]/5 blur-2xl"
|
|
20
|
+
/>
|
|
21
|
+
|
|
22
|
+
<div className="relative z-10">
|
|
23
|
+
<div className="flex items-center gap-2 mb-12">
|
|
24
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-[--color-primary]">
|
|
25
|
+
<span className="text-sm font-bold text-[--color-primary-foreground]">i</span>
|
|
26
|
+
</div>
|
|
27
|
+
<span className="text-lg font-semibold tracking-tight">iCore</span>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<h2 className="text-3xl font-bold leading-tight mb-4">
|
|
31
|
+
Enterprise-grade
|
|
32
|
+
<br />
|
|
33
|
+
<span className="text-[--color-primary]">full-stack scaffold</span>
|
|
34
|
+
</h2>
|
|
35
|
+
<p className="text-[--color-muted-foreground] text-sm leading-relaxed max-w-xs">
|
|
36
|
+
Nx monorepo · NestJS microservices · React 19. Production-ready from day one.
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<ul className="relative z-10 space-y-4">
|
|
41
|
+
{FEATURES.map(({ icon: Icon, text }) => (
|
|
42
|
+
<li key={text} className="flex items-start gap-3">
|
|
43
|
+
<div className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-[--color-primary]/15">
|
|
44
|
+
<Icon size={11} className="text-[--color-primary]" />
|
|
45
|
+
</div>
|
|
46
|
+
<span className="text-sm text-[--color-muted-foreground]">{text}</span>
|
|
47
|
+
</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MailCheck } from 'lucide-react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '../ui/button';
|
|
4
|
+
|
|
5
|
+
interface CheckEmailScreenProps {
|
|
6
|
+
email: string;
|
|
7
|
+
onBack: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CheckEmailScreen({ email, onBack }: CheckEmailScreenProps) {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<div className="flex flex-col items-center text-center space-y-4 py-4">
|
|
15
|
+
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-[--color-primary]/15">
|
|
16
|
+
<MailCheck size={24} className="text-[--color-primary]" />
|
|
17
|
+
</div>
|
|
18
|
+
<div className="space-y-1">
|
|
19
|
+
<h3 className="text-lg font-semibold">{t('auth.checkEmail')}</h3>
|
|
20
|
+
<p className="text-sm text-[--color-muted-foreground] max-w-xs">
|
|
21
|
+
{t('auth.checkEmailDescription', { email })}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
<Button type="button" variant="outline" className="w-full cursor-pointer" onClick={onBack}>
|
|
25
|
+
{t('auth.backToLogin')}
|
|
26
|
+
</Button>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { SyntheticEvent, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Loader2 } from 'lucide-react';
|
|
4
|
+
import { Button } from '../ui/button';
|
|
5
|
+
import { Input } from '../ui/input';
|
|
6
|
+
import { Label } from '../ui/label';
|
|
7
|
+
|
|
8
|
+
interface LoginFormProps {
|
|
9
|
+
onSuccess: (session: {
|
|
10
|
+
accessToken: string;
|
|
11
|
+
refreshToken: string;
|
|
12
|
+
user: { id: string; email: string; role?: string };
|
|
13
|
+
}) => void;
|
|
14
|
+
onError: (msg: string) => void;
|
|
15
|
+
onSwitchToRegister: () => void;
|
|
16
|
+
onSwitchToMagicLink: () => void;
|
|
17
|
+
api: <T>(path: string, init?: RequestInit) => Promise<T>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function LoginForm({
|
|
21
|
+
onSuccess,
|
|
22
|
+
onError,
|
|
23
|
+
onSwitchToRegister,
|
|
24
|
+
onSwitchToMagicLink,
|
|
25
|
+
api,
|
|
26
|
+
}: LoginFormProps) {
|
|
27
|
+
const { t } = useTranslation();
|
|
28
|
+
const [email, setEmail] = useState('');
|
|
29
|
+
const [password, setPassword] = useState('');
|
|
30
|
+
const [submitting, setSubmitting] = useState(false);
|
|
31
|
+
|
|
32
|
+
async function handleSubmit(e: SyntheticEvent<HTMLFormElement>) {
|
|
33
|
+
e.preventDefault();
|
|
34
|
+
setSubmitting(true);
|
|
35
|
+
try {
|
|
36
|
+
const session = await api<{
|
|
37
|
+
accessToken: string;
|
|
38
|
+
refreshToken: string;
|
|
39
|
+
user: { id: string; email: string; role?: string };
|
|
40
|
+
}>('/auth/login', {
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: { 'Content-Type': 'application/json' },
|
|
43
|
+
body: JSON.stringify({ email, password }),
|
|
44
|
+
});
|
|
45
|
+
onSuccess(session);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
onError(err instanceof Error ? err.message : t('error.unknown'));
|
|
48
|
+
} finally {
|
|
49
|
+
setSubmitting(false);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-5">
|
|
55
|
+
<div className="space-y-1">
|
|
56
|
+
<h1 className="text-2xl font-bold">{t('auth.loginTitle')}</h1>
|
|
57
|
+
<p className="text-sm text-[--color-muted-foreground]">{t('auth.loginSubtitle')}</p>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
61
|
+
<div className="space-y-2">
|
|
62
|
+
<Label htmlFor="login-email">{t('auth.email')}</Label>
|
|
63
|
+
<Input
|
|
64
|
+
id="login-email"
|
|
65
|
+
type="email"
|
|
66
|
+
value={email}
|
|
67
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
68
|
+
placeholder="you@example.com"
|
|
69
|
+
required
|
|
70
|
+
autoComplete="email"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="space-y-2">
|
|
74
|
+
<Label htmlFor="login-password">{t('auth.password')}</Label>
|
|
75
|
+
<Input
|
|
76
|
+
id="login-password"
|
|
77
|
+
type="password"
|
|
78
|
+
value={password}
|
|
79
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
80
|
+
required
|
|
81
|
+
autoComplete="current-password"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
<Button type="submit" className="w-full cursor-pointer" disabled={submitting}>
|
|
85
|
+
{submitting ? <Loader2 size={16} className="animate-spin" /> : t('auth.login')}
|
|
86
|
+
</Button>
|
|
87
|
+
</form>
|
|
88
|
+
|
|
89
|
+
<div className="relative">
|
|
90
|
+
<div className="absolute inset-0 flex items-center">
|
|
91
|
+
<div className="w-full border-t border-[--color-border]" />
|
|
92
|
+
</div>
|
|
93
|
+
<div className="relative flex justify-center text-xs">
|
|
94
|
+
<span className="bg-[--color-card] px-2 text-[--color-muted-foreground]">
|
|
95
|
+
{t('auth.orContinueWith')}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="grid grid-cols-2 gap-3">
|
|
101
|
+
<Button
|
|
102
|
+
type="button"
|
|
103
|
+
variant="outline"
|
|
104
|
+
className="cursor-pointer"
|
|
105
|
+
onClick={() => window.location.assign('/api/auth/oauth/google')}
|
|
106
|
+
>
|
|
107
|
+
<svg viewBox="0 0 24 24" className="size-4 shrink-0" aria-hidden>
|
|
108
|
+
<path
|
|
109
|
+
fill="#4285F4"
|
|
110
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
111
|
+
/>
|
|
112
|
+
<path
|
|
113
|
+
fill="#34A853"
|
|
114
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
115
|
+
/>
|
|
116
|
+
<path
|
|
117
|
+
fill="#FBBC05"
|
|
118
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"
|
|
119
|
+
/>
|
|
120
|
+
<path
|
|
121
|
+
fill="#EA4335"
|
|
122
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
123
|
+
/>
|
|
124
|
+
</svg>
|
|
125
|
+
Google
|
|
126
|
+
</Button>
|
|
127
|
+
<Button
|
|
128
|
+
type="button"
|
|
129
|
+
variant="outline"
|
|
130
|
+
className="cursor-pointer"
|
|
131
|
+
onClick={() => window.location.assign('/api/auth/oauth/github')}
|
|
132
|
+
>
|
|
133
|
+
<svg viewBox="0 0 24 24" className="size-4 shrink-0" aria-hidden fill="currentColor">
|
|
134
|
+
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z" />
|
|
135
|
+
</svg>
|
|
136
|
+
GitHub
|
|
137
|
+
</Button>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div className="text-center text-sm text-[--color-muted-foreground] space-y-1">
|
|
141
|
+
<p>
|
|
142
|
+
{t('auth.switchToRegister')}{' '}
|
|
143
|
+
<button
|
|
144
|
+
type="button"
|
|
145
|
+
onClick={onSwitchToRegister}
|
|
146
|
+
className="text-[--color-primary] font-medium hover:underline cursor-pointer"
|
|
147
|
+
>
|
|
148
|
+
{t('auth.switchToRegisterLink')}
|
|
149
|
+
</button>
|
|
150
|
+
</p>
|
|
151
|
+
<button
|
|
152
|
+
type="button"
|
|
153
|
+
onClick={onSwitchToMagicLink}
|
|
154
|
+
className="text-xs hover:underline cursor-pointer"
|
|
155
|
+
>
|
|
156
|
+
{t('auth.withMagicLink')}
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
);
|
|
161
|
+
}
|