@idevconn/create-icore 0.6.3 → 0.7.1

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 (87) hide show
  1. package/dist/cli.js +384 -276
  2. package/dist/index.cjs +385 -277
  3. package/dist/index.d.cts +3 -3
  4. package/dist/index.d.ts +3 -3
  5. package/dist/index.js +382 -274
  6. package/package.json +1 -1
  7. package/templates/.yarn/releases/yarn-4.16.0.cjs +944 -0
  8. package/templates/.yarnrc.yml +1 -1
  9. package/templates/apps/api/src/app/storage/storage.controller.ts +28 -0
  10. package/templates/apps/microservices/auth/src/app/app.module.ts +20 -2
  11. package/templates/apps/microservices/notes/src/app/app.module.ts +17 -2
  12. package/templates/apps/microservices/upload/src/app/app.module.ts +17 -2
  13. package/templates/apps/microservices/upload/src/app/storage.controller.ts +7 -0
  14. package/templates/apps/templates/client-antd/src/components/auth/AuthBrandPanel.tsx +59 -0
  15. package/templates/apps/templates/client-antd/src/components/auth/CheckEmailScreen.tsx +28 -0
  16. package/templates/apps/templates/client-antd/src/components/auth/LoginForm.tsx +116 -0
  17. package/templates/apps/templates/client-antd/src/components/auth/MagicLinkForm.tsx +95 -0
  18. package/templates/apps/templates/client-antd/src/components/auth/RegisterForm.tsx +98 -0
  19. package/templates/apps/templates/client-antd/src/globals.less +6 -0
  20. package/templates/apps/templates/client-antd/src/main.tsx +1 -1
  21. package/templates/apps/templates/client-antd/src/routes/login.tsx +45 -181
  22. package/templates/apps/templates/client-mui/src/components/auth/AuthBrandPanel.tsx +59 -0
  23. package/templates/apps/templates/client-mui/src/components/auth/CheckEmailScreen.tsx +28 -0
  24. package/templates/apps/templates/client-mui/src/components/auth/LoginForm.tsx +141 -0
  25. package/templates/apps/templates/client-mui/src/components/auth/MagicLinkForm.tsx +106 -0
  26. package/templates/apps/templates/client-mui/src/components/auth/RegisterForm.tsx +113 -0
  27. package/templates/apps/templates/client-mui/src/main.tsx +1 -1
  28. package/templates/apps/templates/client-mui/src/routes/login.tsx +50 -186
  29. package/templates/apps/templates/client-shadcn/src/components/auth/AuthBrandPanel.tsx +52 -0
  30. package/templates/apps/templates/client-shadcn/src/components/auth/CheckEmailScreen.tsx +29 -0
  31. package/templates/apps/templates/client-shadcn/src/components/auth/LoginForm.tsx +161 -0
  32. package/templates/apps/templates/client-shadcn/src/components/auth/MagicLinkForm.tsx +110 -0
  33. package/templates/apps/templates/client-shadcn/src/components/auth/RegisterForm.tsx +107 -0
  34. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutHeader.tsx +31 -10
  35. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +22 -27
  36. package/templates/apps/templates/client-shadcn/src/components/ui/card.tsx +1 -1
  37. package/templates/apps/templates/client-shadcn/src/globals.css +39 -13
  38. package/templates/apps/templates/client-shadcn/src/routes/auth.callback.tsx +1 -1
  39. package/templates/apps/templates/client-shadcn/src/routes/login.tsx +55 -165
  40. package/templates/libs/auth-strategies/mongodb/CHANGELOG.md +8 -0
  41. package/templates/libs/auth-strategies/mongodb/README.md +11 -0
  42. package/templates/libs/auth-strategies/mongodb/eslint.config.mjs +19 -0
  43. package/templates/libs/auth-strategies/mongodb/jest.config.cts +10 -0
  44. package/templates/libs/auth-strategies/mongodb/package.json +16 -0
  45. package/templates/libs/auth-strategies/mongodb/project.json +19 -0
  46. package/templates/libs/auth-strategies/mongodb/src/index.ts +1 -0
  47. package/templates/libs/auth-strategies/mongodb/src/lib/__tests__/mongodb-auth.strategy.unit.test.ts +42 -0
  48. package/templates/libs/auth-strategies/mongodb/src/lib/auth-mongodb.spec.ts +7 -0
  49. package/templates/libs/auth-strategies/mongodb/src/lib/auth-mongodb.ts +3 -0
  50. package/templates/libs/auth-strategies/mongodb/src/lib/mongodb-auth.strategy.ts +188 -0
  51. package/templates/libs/auth-strategies/mongodb/tsconfig.json +23 -0
  52. package/templates/libs/auth-strategies/mongodb/tsconfig.lib.json +10 -0
  53. package/templates/libs/auth-strategies/mongodb/tsconfig.spec.json +16 -0
  54. package/templates/libs/db-strategies/mongodb/CHANGELOG.md +7 -0
  55. package/templates/libs/db-strategies/mongodb/README.md +11 -0
  56. package/templates/libs/db-strategies/mongodb/eslint.config.mjs +19 -0
  57. package/templates/libs/db-strategies/mongodb/jest.config.cts +10 -0
  58. package/templates/libs/db-strategies/mongodb/package.json +14 -0
  59. package/templates/libs/db-strategies/mongodb/project.json +19 -0
  60. package/templates/libs/db-strategies/mongodb/src/index.ts +1 -0
  61. package/templates/libs/db-strategies/mongodb/src/lib/__tests__/mongodb-db.strategy.unit.test.ts +38 -0
  62. package/templates/libs/db-strategies/mongodb/src/lib/mongodb-db.strategy.ts +108 -0
  63. package/templates/libs/db-strategies/mongodb/src/lib/mongodb.spec.ts +7 -0
  64. package/templates/libs/db-strategies/mongodb/src/lib/mongodb.ts +3 -0
  65. package/templates/libs/db-strategies/mongodb/tsconfig.json +23 -0
  66. package/templates/libs/db-strategies/mongodb/tsconfig.lib.json +10 -0
  67. package/templates/libs/db-strategies/mongodb/tsconfig.spec.json +16 -0
  68. package/templates/libs/shared/src/strategies/storage.ts +3 -0
  69. package/templates/libs/storage-strategies/mongodb/CHANGELOG.md +8 -0
  70. package/templates/libs/storage-strategies/mongodb/README.md +11 -0
  71. package/templates/libs/storage-strategies/mongodb/eslint.config.mjs +19 -0
  72. package/templates/libs/storage-strategies/mongodb/jest.config.cts +10 -0
  73. package/templates/libs/storage-strategies/mongodb/package.json +14 -0
  74. package/templates/libs/storage-strategies/mongodb/project.json +19 -0
  75. package/templates/libs/storage-strategies/mongodb/src/index.ts +1 -0
  76. package/templates/libs/storage-strategies/mongodb/src/lib/__tests__/mongodb-storage.strategy.unit.test.ts +38 -0
  77. package/templates/libs/storage-strategies/mongodb/src/lib/mongodb-storage.strategy.ts +93 -0
  78. package/templates/libs/storage-strategies/mongodb/src/lib/storage-mongodb.spec.ts +7 -0
  79. package/templates/libs/storage-strategies/mongodb/src/lib/storage-mongodb.ts +3 -0
  80. package/templates/libs/storage-strategies/mongodb/tsconfig.json +23 -0
  81. package/templates/libs/storage-strategies/mongodb/tsconfig.lib.json +10 -0
  82. package/templates/libs/storage-strategies/mongodb/tsconfig.spec.json +16 -0
  83. package/templates/libs/template-shared/src/lib/i18n/keys.ts +216 -56
  84. package/templates/libs/template-shared/src/lib/stores/theme.store.ts +1 -6
  85. package/templates/libs/upload-client/src/lib/upload-client.service.ts +7 -0
  86. package/templates/tsconfig.base.json +4 -1
  87. package/templates/.yarn/releases/yarn-4.15.0.cjs +0 -940
@@ -9,4 +9,4 @@ nodeLinker: node-modules
9
9
 
10
10
  npmMinimalAgeGate: 0
11
11
 
12
- yarnPath: .yarn/releases/yarn-4.15.0.cjs
12
+ yarnPath: .yarn/releases/yarn-4.16.0.cjs
@@ -4,10 +4,12 @@ import {
4
4
  Controller,
5
5
  Delete,
6
6
  Get,
7
+ NotFoundException,
7
8
  PayloadTooLargeException,
8
9
  Post,
9
10
  Query,
10
11
  Req,
12
+ StreamableFile,
11
13
  UploadedFile,
12
14
  UseInterceptors,
13
15
  } from '@nestjs/common';
@@ -19,6 +21,7 @@ import {
19
21
  ApiConsumes,
20
22
  ApiOperation,
21
23
  ApiQuery,
24
+ ApiResponse,
22
25
  ApiTags,
23
26
  } from '@nestjs/swagger';
24
27
  import { UploadClientService } from '@icore/upload-client';
@@ -105,4 +108,29 @@ export class StorageController {
105
108
  list(@Query('prefix') prefix: string | undefined, @Req() req: AuthedReq): Promise<StorageRef[]> {
106
109
  return this.uploadClient.list(req.user!.uid, prefix);
107
110
  }
111
+
112
+ @Get('file')
113
+ @ApiOperation({ summary: 'Download a file proxied through the gateway (GridFS providers)' })
114
+ @ApiQuery({ name: 'bucket', type: String })
115
+ @ApiQuery({ name: 'path', type: String })
116
+ @ApiResponse({
117
+ status: 200,
118
+ description: 'Raw file bytes',
119
+ content: { 'application/octet-stream': {} },
120
+ })
121
+ async downloadFile(
122
+ @Query('bucket') bucket: string,
123
+ @Query('path') path: string,
124
+ @Req() req: AuthedReq,
125
+ ): Promise<StreamableFile> {
126
+ const ref: StorageRef = { bucket, path };
127
+ assertOwnership(ref, req.user!.uid);
128
+ const buffer = await this.uploadClient.downloadBuffer(req.user!.uid, ref);
129
+ if (!buffer) throw new NotFoundException('storage_provider_does_not_support_direct_download');
130
+ const filename = path.split('/').pop() ?? 'file';
131
+ return new StreamableFile(buffer, {
132
+ type: 'application/octet-stream',
133
+ disposition: `inline; filename="${filename}"`,
134
+ });
135
+ }
108
136
  }
@@ -3,8 +3,11 @@ import { Module, Logger } from '@nestjs/common';
3
3
  import { ConfigModule, ConfigService } from '@nestjs/config';
4
4
  import { createClient } from '@supabase/supabase-js';
5
5
  import { SupabaseAuthStrategy } from '@icore/auth-supabase';
6
+ import { MongoDbAuthStrategy } from '@icore/auth-mongodb';
6
7
  import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
7
8
  import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
9
+ import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
10
+ import { Connection } from 'mongoose';
8
11
  import { FakeAuthStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
9
12
  import type { AuthStrategy } from '@icore/shared';
10
13
  import { AuthController } from './auth.controller';
@@ -15,6 +18,7 @@ const ENV_PATH = 'apps/microservices/auth/.env';
15
18
  const REQUIRED_ENV: Record<string, string[]> = {
16
19
  supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
17
20
  firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_WEB_API_KEY'],
21
+ mongodb: ['MONGODB_URI', 'JWT_SECRET'],
18
22
  };
19
23
 
20
24
  function requireEnv(cfg: ConfigService, key: string): string {
@@ -39,6 +43,13 @@ function makeFirebaseAuth(cfg: ConfigService): AuthStrategy {
39
43
  });
40
44
  }
41
45
 
46
+ function makeMongoDbAuth(connection: Connection, cfg: ConfigService): AuthStrategy {
47
+ return new MongoDbAuthStrategy({
48
+ connection,
49
+ jwtSecret: requireEnv(cfg, 'JWT_SECRET'),
50
+ });
51
+ }
52
+
42
53
  @Module({
43
54
  imports: [
44
55
  ConfigModule.forRoot({
@@ -48,12 +59,18 @@ function makeFirebaseAuth(cfg: ConfigService): AuthStrategy {
48
59
  join(process.cwd(), '.env'),
49
60
  ],
50
61
  }),
62
+ MongooseModule.forRootAsync({
63
+ useFactory: (cfg: ConfigService) => ({
64
+ uri: cfg.get<string>('MONGODB_URI'),
65
+ }),
66
+ inject: [ConfigService],
67
+ }),
51
68
  ],
52
69
  controllers: [AuthController],
53
70
  providers: [
54
71
  {
55
72
  provide: 'AuthStrategy',
56
- useFactory: (cfg: ConfigService): AuthStrategy => {
73
+ useFactory: (cfg: ConfigService, connection: Connection): AuthStrategy => {
57
74
  const logger = new Logger('AuthStrategy');
58
75
  const provider = cfg.get<string>('AUTH_PROVIDER')?.trim();
59
76
  const keys = provider ? REQUIRED_ENV[provider] : undefined;
@@ -78,13 +95,14 @@ function makeFirebaseAuth(cfg: ConfigService): AuthStrategy {
78
95
 
79
96
  try {
80
97
  if (provider === 'supabase') return makeSupabaseAuth(cfg);
98
+ if (provider === 'mongodb') return makeMongoDbAuth(connection, cfg);
81
99
  return makeFirebaseAuth(cfg);
82
100
  } catch (err) {
83
101
  // Vars present but invalid (e.g. placeholder URL the SDK rejects).
84
102
  return fallback(err instanceof Error ? err.message : String(err));
85
103
  }
86
104
  },
87
- inject: [ConfigService],
105
+ inject: [ConfigService, getConnectionToken()],
88
106
  },
89
107
  ],
90
108
  })
@@ -3,8 +3,11 @@ import { Module, Logger } from '@nestjs/common';
3
3
  import { ConfigModule, ConfigService } from '@nestjs/config';
4
4
  import { createClient } from '@supabase/supabase-js';
5
5
  import { SupabaseDBStrategy } from '@icore/db-supabase';
6
+ import { MongoDbDBStrategy } from '@icore/db-mongodb';
6
7
  import { FirestoreDBStrategy } from '@icore/db-firestore';
7
8
  import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
9
+ import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
10
+ import { Connection } from 'mongoose';
8
11
  import { FakeDBStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
9
12
  import type { DBStrategy } from '@icore/shared';
10
13
  import { NotesController } from './notes.controller';
@@ -16,6 +19,7 @@ const REQUIRED_ENV: Record<string, string[]> = {
16
19
  supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
17
20
  firestore: [...FIREBASE_ADMIN_REQUIRED_ENV],
18
21
  firebase: [...FIREBASE_ADMIN_REQUIRED_ENV],
22
+ mongodb: ['MONGODB_URI'],
19
23
  };
20
24
 
21
25
  function requireEnv(cfg: ConfigService, key: string): string {
@@ -38,6 +42,10 @@ function makeFirestoreDB(cfg: ConfigService): DBStrategy {
38
42
  });
39
43
  }
40
44
 
45
+ function makeMongoDb(connection: Connection): DBStrategy {
46
+ return new MongoDbDBStrategy({ connection });
47
+ }
48
+
41
49
  @Module({
42
50
  imports: [
43
51
  ConfigModule.forRoot({
@@ -47,12 +55,18 @@ function makeFirestoreDB(cfg: ConfigService): DBStrategy {
47
55
  join(process.cwd(), '.env'),
48
56
  ],
49
57
  }),
58
+ MongooseModule.forRootAsync({
59
+ useFactory: (cfg: ConfigService) => ({
60
+ uri: cfg.get<string>('MONGODB_URI'),
61
+ }),
62
+ inject: [ConfigService],
63
+ }),
50
64
  ],
51
65
  controllers: [NotesController],
52
66
  providers: [
53
67
  {
54
68
  provide: 'DBStrategy',
55
- useFactory: (cfg: ConfigService): DBStrategy => {
69
+ useFactory: (cfg: ConfigService, connection: Connection): DBStrategy => {
56
70
  const logger = new Logger('DBStrategy');
57
71
  const provider = cfg.get<string>('DB_PROVIDER')?.trim();
58
72
  const keys = provider ? REQUIRED_ENV[provider] : undefined;
@@ -75,12 +89,13 @@ function makeFirestoreDB(cfg: ConfigService): DBStrategy {
75
89
 
76
90
  try {
77
91
  if (provider === 'supabase') return makeSupabaseDB(cfg);
92
+ if (provider === 'mongodb') return makeMongoDb(connection);
78
93
  return makeFirestoreDB(cfg);
79
94
  } catch (err) {
80
95
  return fallback(err instanceof Error ? err.message : String(err));
81
96
  }
82
97
  },
83
- inject: [ConfigService],
98
+ inject: [ConfigService, getConnectionToken()],
84
99
  },
85
100
  ],
86
101
  })
@@ -4,9 +4,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
4
4
  import { createClient } from '@supabase/supabase-js';
5
5
  import { v2 as cloudinary } from 'cloudinary';
6
6
  import { SupabaseStorageStrategy } from '@icore/storage-supabase';
7
+ import { MongoDbStorageStrategy } from '@icore/storage-mongodb';
7
8
  import { FirebaseStorageStrategy } from '@icore/storage-firebase';
8
9
  import { CloudinaryStorageStrategy, type CloudinaryApiLike } from '@icore/storage-cloudinary';
9
10
  import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
11
+ import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
12
+ import { Connection } from 'mongoose';
10
13
  import { FakeStorageStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
11
14
  import type { StorageStrategy } from '@icore/shared';
12
15
  import { StorageController } from './storage.controller';
@@ -17,6 +20,7 @@ const REQUIRED_ENV: Record<string, string[]> = {
17
20
  supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_STORAGE_BUCKET'],
18
21
  firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_STORAGE_BUCKET'],
19
22
  cloudinary: ['CLOUDINARY_CLOUD_NAME', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET'],
23
+ mongodb: ['MONGODB_URI'],
20
24
  };
21
25
 
22
26
  function requireEnv(cfg: ConfigService, key: string): string {
@@ -91,6 +95,10 @@ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
91
95
  });
92
96
  }
93
97
 
98
+ function makeMongoDbStorage(connection: Connection): StorageStrategy {
99
+ return new MongoDbStorageStrategy({ connection });
100
+ }
101
+
94
102
  @Module({
95
103
  imports: [
96
104
  ConfigModule.forRoot({
@@ -100,12 +108,18 @@ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
100
108
  join(process.cwd(), '.env'),
101
109
  ],
102
110
  }),
111
+ MongooseModule.forRootAsync({
112
+ useFactory: (cfg: ConfigService) => ({
113
+ uri: cfg.get<string>('MONGODB_URI'),
114
+ }),
115
+ inject: [ConfigService],
116
+ }),
103
117
  ],
104
118
  controllers: [StorageController],
105
119
  providers: [
106
120
  {
107
121
  provide: 'StorageStrategy',
108
- useFactory: (cfg: ConfigService): StorageStrategy => {
122
+ useFactory: (cfg: ConfigService, connection: Connection): StorageStrategy => {
109
123
  const logger = new Logger('StorageStrategy');
110
124
  const provider = cfg.get<string>('STORAGE_PROVIDER')?.trim();
111
125
  const keys = provider ? REQUIRED_ENV[provider] : undefined;
@@ -129,12 +143,13 @@ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
129
143
  try {
130
144
  if (provider === 'supabase') return makeSupabaseStorage(cfg);
131
145
  if (provider === 'firebase') return makeFirebaseStorage(cfg);
146
+ if (provider === 'mongodb') return makeMongoDbStorage(connection);
132
147
  return makeCloudinaryStorage(cfg);
133
148
  } catch (err) {
134
149
  return fallback(err instanceof Error ? err.message : String(err));
135
150
  }
136
151
  },
137
- inject: [ConfigService],
152
+ inject: [ConfigService, getConnectionToken()],
138
153
  },
139
154
  ],
140
155
  })
@@ -48,4 +48,11 @@ export class StorageController {
48
48
  list(@Payload() payload: ListPayload): Promise<StorageRef[]> {
49
49
  return this.strategy.list(payload.userId, payload.prefix);
50
50
  }
51
+
52
+ @MessagePattern('storage.downloadBuffer')
53
+ async downloadBuffer(@Payload() payload: RefPayload): Promise<string | null> {
54
+ if (!this.strategy.downloadBuffer) return null;
55
+ const buf = await this.strategy.downloadBuffer(payload.userId, payload.ref);
56
+ return buf.toString('base64');
57
+ }
51
58
  }
@@ -0,0 +1,59 @@
1
+ import { Space, Typography } from 'antd';
2
+ import { CheckCircleOutlined } from '@ant-design/icons';
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
+ <div
13
+ style={{
14
+ flex: 1,
15
+ background: 'linear-gradient(135deg, #0d1117 0%, #0f1e0f 100%)',
16
+ display: 'flex',
17
+ flexDirection: 'column',
18
+ justifyContent: 'center',
19
+ padding: '48px 56px',
20
+ position: 'relative',
21
+ overflow: 'hidden',
22
+ }}
23
+ >
24
+ <div
25
+ style={{
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
+ <Space direction="vertical" size={32} style={{ position: 'relative', zIndex: 1 }}>
37
+ <Space direction="vertical" size={4}>
38
+ <Typography.Title level={2} style={{ color: '#22c55e', margin: 0, fontSize: 28 }}>
39
+ iCore
40
+ </Typography.Title>
41
+ <Typography.Text style={{ color: 'rgba(255,255,255,0.5)', fontSize: 13 }}>
42
+ Enterprise scaffold for NestJS + React
43
+ </Typography.Text>
44
+ </Space>
45
+
46
+ <Space direction="vertical" size={8}>
47
+ {FEATURES.map((f) => (
48
+ <Space key={f} size={8} align="center">
49
+ <CheckCircleOutlined style={{ color: '#22c55e', fontSize: 14 }} />
50
+ <Typography.Text style={{ color: 'rgba(255,255,255,0.75)', fontSize: 14 }}>
51
+ {f}
52
+ </Typography.Text>
53
+ </Space>
54
+ ))}
55
+ </Space>
56
+ </Space>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,28 @@
1
+ import { Button, Result, Space, Typography } from 'antd';
2
+ import { MailOutlined } from '@ant-design/icons';
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
+ <Space direction="vertical" size={0} style={{ width: '100%', textAlign: 'center' }}>
14
+ <Result
15
+ icon={<MailOutlined style={{ fontSize: 48, color: '#22c55e' }} />}
16
+ title={t('auth.checkEmail')}
17
+ subTitle={
18
+ <Typography.Text type="secondary">
19
+ {t('auth.checkEmailDescription', { email })}
20
+ </Typography.Text>
21
+ }
22
+ />
23
+ <Button block onClick={onBack}>
24
+ {t('auth.backToLogin')}
25
+ </Button>
26
+ </Space>
27
+ );
28
+ }
@@ -0,0 +1,116 @@
1
+ import { Button, Divider, Form, Input, Space, Typography } from 'antd';
2
+ import { GithubOutlined, GoogleOutlined } from '@ant-design/icons';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { useNavigate } from '@tanstack/react-router';
5
+ import { useAuthStore, useNotify } from '@icore/template-shared';
6
+ import { api } from '@/main';
7
+
8
+ interface FormValues {
9
+ email: string;
10
+ password: string;
11
+ }
12
+
13
+ interface Props {
14
+ onSwitchRegister: () => void;
15
+ onSwitchMagicLink: () => void;
16
+ }
17
+
18
+ export function LoginForm({ onSwitchRegister, onSwitchMagicLink }: Props) {
19
+ const { t } = useTranslation();
20
+ const navigate = useNavigate();
21
+ const notify = useNotify();
22
+ const setAuth = useAuthStore((s) => s.setAuth);
23
+ const [form] = Form.useForm<FormValues>();
24
+
25
+ async function handleFinish(values: FormValues) {
26
+ try {
27
+ const session = await api<{
28
+ accessToken: string;
29
+ refreshToken: string;
30
+ user: { id: string; email: string; role?: string };
31
+ }>('/auth/login', {
32
+ method: 'POST',
33
+ headers: { 'Content-Type': 'application/json' },
34
+ body: JSON.stringify({ email: values.email, password: values.password }),
35
+ });
36
+ setAuth(session);
37
+ notify.success(t('auth.login'));
38
+ await navigate({ to: '/dashboard' });
39
+ } catch (err) {
40
+ notify.error(err instanceof Error ? err.message : t('error.unknown'));
41
+ }
42
+ }
43
+
44
+ return (
45
+ <Space direction="vertical" size={16} style={{ width: '100%' }}>
46
+ <Space direction="vertical" size={4}>
47
+ <Typography.Title level={3} style={{ margin: 0 }}>
48
+ {t('auth.loginTitle')}
49
+ </Typography.Title>
50
+ <Typography.Text type="secondary">{t('auth.loginSubtitle')}</Typography.Text>
51
+ </Space>
52
+
53
+ <Space direction="vertical" size={8} style={{ width: '100%' }}>
54
+ <Button
55
+ block
56
+ icon={<GoogleOutlined />}
57
+ onClick={() => window.location.assign('/api/auth/oauth/google')}
58
+ >
59
+ {t('auth.continueWithGoogle')}
60
+ </Button>
61
+ <Button
62
+ block
63
+ icon={<GithubOutlined />}
64
+ onClick={() => window.location.assign('/api/auth/oauth/github')}
65
+ >
66
+ {t('auth.continueWithGithub')}
67
+ </Button>
68
+ </Space>
69
+
70
+ <Divider plain>
71
+ <Typography.Text type="secondary" style={{ fontSize: 12 }}>
72
+ {t('auth.orContinueWith')}
73
+ </Typography.Text>
74
+ </Divider>
75
+
76
+ <Form form={form} layout="vertical" onFinish={handleFinish} autoComplete="on">
77
+ <Form.Item
78
+ name="email"
79
+ label={t('auth.email')}
80
+ rules={[
81
+ { required: true, message: `${t('auth.email')} is required` },
82
+ { type: 'email', message: 'Please enter a valid email' },
83
+ ]}
84
+ >
85
+ <Input autoComplete="email" size="large" />
86
+ </Form.Item>
87
+
88
+ <Form.Item
89
+ name="password"
90
+ label={t('auth.password')}
91
+ rules={[{ required: true, message: `${t('auth.password')} is required` }]}
92
+ >
93
+ <Input.Password autoComplete="current-password" size="large" />
94
+ </Form.Item>
95
+
96
+ <Form.Item style={{ marginBottom: 8 }}>
97
+ <Button type="primary" htmlType="submit" block size="large">
98
+ {t('auth.login')}
99
+ </Button>
100
+ </Form.Item>
101
+ </Form>
102
+
103
+ <Space direction="vertical" size={4} style={{ width: '100%', textAlign: 'center' }}>
104
+ <Typography.Text type="secondary" style={{ fontSize: 13 }}>
105
+ {t('auth.switchToRegister')}{' '}
106
+ <Typography.Link onClick={onSwitchRegister}>
107
+ {t('auth.switchToRegisterLink')}
108
+ </Typography.Link>
109
+ </Typography.Text>
110
+ <Typography.Link onClick={onSwitchMagicLink} style={{ fontSize: 13 }}>
111
+ {t('auth.withMagicLink')}
112
+ </Typography.Link>
113
+ </Space>
114
+ </Space>
115
+ );
116
+ }
@@ -0,0 +1,95 @@
1
+ import { Button, Form, Input, Result, Space, Typography } from 'antd';
2
+ import { MailOutlined } from '@ant-design/icons';
3
+ import { useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { useNotify } from '@icore/template-shared';
6
+ import { api } from '@/main';
7
+
8
+ interface FormValues {
9
+ email: string;
10
+ }
11
+
12
+ interface Props {
13
+ onSwitchLogin: () => void;
14
+ }
15
+
16
+ export function MagicLinkForm({ onSwitchLogin }: Props) {
17
+ const { t } = useTranslation();
18
+ const notify = useNotify();
19
+ const [form] = Form.useForm<FormValues>();
20
+ const [sentEmail, setSentEmail] = useState('');
21
+
22
+ async function handleFinish(values: FormValues) {
23
+ try {
24
+ await api('/auth/magic-link', {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify({ email: values.email }),
28
+ });
29
+ setSentEmail(values.email);
30
+ } catch (err) {
31
+ notify.error(err instanceof Error ? err.message : t('error.unknown'));
32
+ }
33
+ }
34
+
35
+ if (sentEmail) {
36
+ return (
37
+ <Space direction="vertical" size={0} style={{ width: '100%', textAlign: 'center' }}>
38
+ <Result
39
+ icon={<MailOutlined style={{ fontSize: 48, color: '#22c55e' }} />}
40
+ title={t('auth.magicLinkSent')}
41
+ subTitle={
42
+ <Typography.Text type="secondary">
43
+ {t('auth.magicLinkSentDescription', { email: sentEmail })}
44
+ </Typography.Text>
45
+ }
46
+ />
47
+ <Button
48
+ block
49
+ onClick={() => {
50
+ setSentEmail('');
51
+ form.resetFields();
52
+ }}
53
+ >
54
+ {t('auth.magicLinkUseDifferentEmail')}
55
+ </Button>
56
+ </Space>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <Space direction="vertical" size={16} style={{ width: '100%' }}>
62
+ <Space direction="vertical" size={4}>
63
+ <Typography.Title level={3} style={{ margin: 0 }}>
64
+ {t('auth.withMagicLink')}
65
+ </Typography.Title>
66
+ <Typography.Text type="secondary">{t('auth.loginSubtitle')}</Typography.Text>
67
+ </Space>
68
+
69
+ <Form form={form} layout="vertical" onFinish={handleFinish} autoComplete="on">
70
+ <Form.Item
71
+ name="email"
72
+ label={t('auth.email')}
73
+ rules={[
74
+ { required: true, message: `${t('auth.email')} is required` },
75
+ { type: 'email', message: 'Please enter a valid email' },
76
+ ]}
77
+ >
78
+ <Input autoComplete="email" size="large" />
79
+ </Form.Item>
80
+
81
+ <Form.Item style={{ marginBottom: 8 }}>
82
+ <Button type="primary" htmlType="submit" block size="large">
83
+ {t('auth.sendMagicLink')}
84
+ </Button>
85
+ </Form.Item>
86
+ </Form>
87
+
88
+ <div style={{ textAlign: 'center' }}>
89
+ <Typography.Link onClick={onSwitchLogin} style={{ fontSize: 13 }}>
90
+ {t('auth.backToLogin')}
91
+ </Typography.Link>
92
+ </div>
93
+ </Space>
94
+ );
95
+ }
@@ -0,0 +1,98 @@
1
+ import { Button, Form, Input, Space, Typography } from 'antd';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { useNotify } from '@icore/template-shared';
4
+ import { api } from '@/main';
5
+
6
+ interface FormValues {
7
+ email: string;
8
+ password: string;
9
+ confirmPassword: string;
10
+ }
11
+
12
+ interface Props {
13
+ onSuccess: (email: string) => void;
14
+ onSwitchLogin: () => void;
15
+ }
16
+
17
+ export function RegisterForm({ onSuccess, onSwitchLogin }: Props) {
18
+ const { t } = useTranslation();
19
+ const notify = useNotify();
20
+ const [form] = Form.useForm<FormValues>();
21
+
22
+ async function handleFinish(values: FormValues) {
23
+ try {
24
+ await api('/auth/register', {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify({ email: values.email, password: values.password }),
28
+ });
29
+ onSuccess(values.email);
30
+ } catch (err) {
31
+ notify.error(err instanceof Error ? err.message : t('error.unknown'));
32
+ }
33
+ }
34
+
35
+ return (
36
+ <Space direction="vertical" size={16} style={{ width: '100%' }}>
37
+ <Space direction="vertical" size={4}>
38
+ <Typography.Title level={3} style={{ margin: 0 }}>
39
+ {t('auth.registerTitle')}
40
+ </Typography.Title>
41
+ <Typography.Text type="secondary">{t('auth.registerSubtitle')}</Typography.Text>
42
+ </Space>
43
+
44
+ <Form form={form} layout="vertical" onFinish={handleFinish} autoComplete="on">
45
+ <Form.Item
46
+ name="email"
47
+ label={t('auth.email')}
48
+ rules={[
49
+ { required: true, message: `${t('auth.email')} is required` },
50
+ { type: 'email', message: 'Please enter a valid email' },
51
+ ]}
52
+ >
53
+ <Input autoComplete="email" size="large" />
54
+ </Form.Item>
55
+
56
+ <Form.Item
57
+ name="password"
58
+ label={t('auth.password')}
59
+ rules={[{ required: true, message: `${t('auth.password')} is required` }]}
60
+ >
61
+ <Input.Password autoComplete="new-password" size="large" />
62
+ </Form.Item>
63
+
64
+ <Form.Item
65
+ name="confirmPassword"
66
+ label={t('auth.confirmPassword')}
67
+ dependencies={['password']}
68
+ rules={[
69
+ { required: true, message: `${t('auth.confirmPassword')} is required` },
70
+ ({ getFieldValue }) => ({
71
+ validator(_, value) {
72
+ if (!value || getFieldValue('password') === value) {
73
+ return Promise.resolve();
74
+ }
75
+ return Promise.reject(new Error(t('auth.passwordMismatch')));
76
+ },
77
+ }),
78
+ ]}
79
+ >
80
+ <Input.Password autoComplete="new-password" size="large" />
81
+ </Form.Item>
82
+
83
+ <Form.Item style={{ marginBottom: 8 }}>
84
+ <Button type="primary" htmlType="submit" block size="large">
85
+ {t('auth.register')}
86
+ </Button>
87
+ </Form.Item>
88
+ </Form>
89
+
90
+ <div style={{ textAlign: 'center' }}>
91
+ <Typography.Text type="secondary" style={{ fontSize: 13 }}>
92
+ {t('auth.switchToLogin')}{' '}
93
+ <Typography.Link onClick={onSwitchLogin}>{t('auth.switchToLoginLink')}</Typography.Link>
94
+ </Typography.Text>
95
+ </div>
96
+ </Space>
97
+ );
98
+ }
@@ -9,3 +9,9 @@ body {
9
9
  -webkit-font-smoothing: antialiased;
10
10
  -moz-osx-font-smoothing: grayscale;
11
11
  }
12
+
13
+ @media (min-width: 1024px) {
14
+ .auth-brand-lg {
15
+ display: flex !important;
16
+ }
17
+ }