@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
package/templates/.yarnrc.yml
CHANGED
|
@@ -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
|
+
}
|