@idevconn/create-icore 0.1.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/LICENSE +201 -0
- package/README.md +56 -0
- package/dist/cli.js +300 -0
- package/dist/index.cjs +303 -0
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +265 -0
- package/package.json +72 -0
- package/templates/.husky/pre-commit +56 -0
- package/templates/.nvmrc +1 -0
- package/templates/.prettierignore +7 -0
- package/templates/.prettierrc +7 -0
- package/templates/.yarnrc.yml +7 -0
- package/templates/apps/api/.env.example +19 -0
- package/templates/apps/api/eslint.config.mjs +23 -0
- package/templates/apps/api/package.json +20 -0
- package/templates/apps/api/project.json +76 -0
- package/templates/apps/api/src/app/abilities/__tests__/ability.guard.unit.test.ts +49 -0
- package/templates/apps/api/src/app/abilities/abilities.module.ts +10 -0
- package/templates/apps/api/src/app/abilities/ability.factory.ts +13 -0
- package/templates/apps/api/src/app/abilities/ability.guard.ts +29 -0
- package/templates/apps/api/src/app/abilities/check-ability.decorator.ts +12 -0
- package/templates/apps/api/src/app/app.module.ts +19 -0
- package/templates/apps/api/src/app/auth/__tests__/auth.guard.unit.test.ts +66 -0
- package/templates/apps/api/src/app/auth/auth.controller.ts +62 -0
- package/templates/apps/api/src/app/auth/auth.guard.ts +42 -0
- package/templates/apps/api/src/app/auth/auth.module.ts +17 -0
- package/templates/apps/api/src/app/auth/public.decorator.ts +4 -0
- package/templates/apps/api/src/app/profile/profile.controller.ts +15 -0
- package/templates/apps/api/src/app/profile/profile.module.ts +5 -0
- package/templates/apps/api/src/app/storage/__tests__/assert-ownership.unit.test.ts +28 -0
- package/templates/apps/api/src/app/storage/assert-ownership.ts +8 -0
- package/templates/apps/api/src/app/storage/storage.controller.ts +108 -0
- package/templates/apps/api/src/app/storage/storage.module.ts +10 -0
- package/templates/apps/api/src/assets/.gitkeep +0 -0
- package/templates/apps/api/src/main.ts +43 -0
- package/templates/apps/api/tsconfig.app.json +13 -0
- package/templates/apps/api/tsconfig.json +16 -0
- package/templates/apps/api/tsconfig.spec.json +16 -0
- package/templates/apps/api/vitest.config.mts +21 -0
- package/templates/apps/api/webpack.config.js +25 -0
- package/templates/apps/microservices/auth/.env.example +38 -0
- package/templates/apps/microservices/auth/package.json +19 -0
- package/templates/apps/microservices/auth/project.json +65 -0
- package/templates/apps/microservices/auth/src/app/__tests__/auth.controller.firebase.integration.unit.test.ts +53 -0
- package/templates/apps/microservices/auth/src/app/__tests__/auth.controller.supabase.integration.unit.test.ts +47 -0
- package/templates/apps/microservices/auth/src/app/__tests__/auth.controller.unit.test.ts +87 -0
- package/templates/apps/microservices/auth/src/app/app.module.ts +66 -0
- package/templates/apps/microservices/auth/src/app/auth.controller.ts +60 -0
- package/templates/apps/microservices/auth/src/assets/.gitkeep +0 -0
- package/templates/apps/microservices/auth/src/main.ts +28 -0
- package/templates/apps/microservices/auth/tsconfig.app.json +13 -0
- package/templates/apps/microservices/auth/tsconfig.json +16 -0
- package/templates/apps/microservices/auth/tsconfig.spec.json +16 -0
- package/templates/apps/microservices/auth/vitest.config.mts +21 -0
- package/templates/apps/microservices/auth/webpack.config.js +25 -0
- package/templates/apps/microservices/upload/.env.example +30 -0
- package/templates/apps/microservices/upload/package.json +21 -0
- package/templates/apps/microservices/upload/project.json +65 -0
- package/templates/apps/microservices/upload/src/app/__tests__/storage.controller.unit.test.ts +49 -0
- package/templates/apps/microservices/upload/src/app/app.module.ts +117 -0
- package/templates/apps/microservices/upload/src/app/storage.controller.ts +51 -0
- package/templates/apps/microservices/upload/src/assets/.gitkeep +0 -0
- package/templates/apps/microservices/upload/src/main.ts +28 -0
- package/templates/apps/microservices/upload/tsconfig.app.json +13 -0
- package/templates/apps/microservices/upload/tsconfig.json +16 -0
- package/templates/apps/microservices/upload/tsconfig.spec.json +16 -0
- package/templates/apps/microservices/upload/vitest.config.mts +22 -0
- package/templates/apps/microservices/upload/webpack.config.js +25 -0
- package/templates/apps/templates/client-shadcn/.env.example +2 -0
- package/templates/apps/templates/client-shadcn/eslint.config.mjs +10 -0
- package/templates/apps/templates/client-shadcn/index.html +17 -0
- package/templates/apps/templates/client-shadcn/project.json +9 -0
- package/templates/apps/templates/client-shadcn/public/favicon.ico +0 -0
- package/templates/apps/templates/client-shadcn/src/app/app.module.css +1 -0
- package/templates/apps/templates/client-shadcn/src/app/app.spec.tsx +9 -0
- package/templates/apps/templates/client-shadcn/src/app/app.tsx +7 -0
- package/templates/apps/templates/client-shadcn/src/assets/.gitkeep +0 -0
- package/templates/apps/templates/client-shadcn/src/components/AccessDeniedPage.tsx +15 -0
- package/templates/apps/templates/client-shadcn/src/components/PageLayout.tsx +55 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutFooter.tsx +8 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutHeader.tsx +57 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +44 -0
- package/templates/apps/templates/client-shadcn/src/components/ui/button.tsx +50 -0
- package/templates/apps/templates/client-shadcn/src/components/ui/card.tsx +63 -0
- package/templates/apps/templates/client-shadcn/src/components/ui/input.tsx +23 -0
- package/templates/apps/templates/client-shadcn/src/components/ui/label.tsx +18 -0
- package/templates/apps/templates/client-shadcn/src/globals.css +27 -0
- package/templates/apps/templates/client-shadcn/src/layouts/MainLayout.tsx +17 -0
- package/templates/apps/templates/client-shadcn/src/lib/notify.ts +15 -0
- package/templates/apps/templates/client-shadcn/src/lib/utils.ts +6 -0
- package/templates/apps/templates/client-shadcn/src/main.tsx +50 -0
- package/templates/apps/templates/client-shadcn/src/routeTree.gen.ts +136 -0
- package/templates/apps/templates/client-shadcn/src/routes/__root.tsx +5 -0
- package/templates/apps/templates/client-shadcn/src/routes/_dashboard/dashboard.tsx +33 -0
- package/templates/apps/templates/client-shadcn/src/routes/_dashboard/profile.tsx +88 -0
- package/templates/apps/templates/client-shadcn/src/routes/_dashboard.tsx +16 -0
- package/templates/apps/templates/client-shadcn/src/routes/index.tsx +33 -0
- package/templates/apps/templates/client-shadcn/src/routes/login.tsx +93 -0
- package/templates/apps/templates/client-shadcn/src/styles.css +1 -0
- package/templates/apps/templates/client-shadcn/tsconfig.app.json +27 -0
- package/templates/apps/templates/client-shadcn/tsconfig.json +21 -0
- package/templates/apps/templates/client-shadcn/tsconfig.spec.json +30 -0
- package/templates/apps/templates/client-shadcn/vite.config.mts +92 -0
- package/templates/apps/templates/client-shadcn-e2e/eslint.config.mjs +12 -0
- package/templates/apps/templates/client-shadcn-e2e/playwright.config.ts +69 -0
- package/templates/apps/templates/client-shadcn-e2e/project.json +10 -0
- package/templates/apps/templates/client-shadcn-e2e/src/icore.spec.ts +27 -0
- package/templates/apps/templates/client-shadcn-e2e/tsconfig.json +19 -0
- package/templates/eslint.config.mjs +20 -0
- package/templates/libs/auth-client/README.md +11 -0
- package/templates/libs/auth-client/eslint.config.mjs +22 -0
- package/templates/libs/auth-client/package.json +15 -0
- package/templates/libs/auth-client/project.json +19 -0
- package/templates/libs/auth-client/src/index.ts +2 -0
- package/templates/libs/auth-client/src/lib/auth-client.module.ts +25 -0
- package/templates/libs/auth-client/src/lib/auth-client.service.ts +30 -0
- package/templates/libs/auth-client/tsconfig.json +24 -0
- package/templates/libs/auth-client/tsconfig.lib.json +26 -0
- package/templates/libs/auth-client/tsconfig.spec.json +22 -0
- package/templates/libs/auth-client/vitest.config.mts +22 -0
- package/templates/libs/auth-strategies/firebase/README.md +11 -0
- package/templates/libs/auth-strategies/firebase/eslint.config.mjs +22 -0
- package/templates/libs/auth-strategies/firebase/package.json +15 -0
- package/templates/libs/auth-strategies/firebase/project.json +19 -0
- package/templates/libs/auth-strategies/firebase/src/index.ts +4 -0
- package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.contract.unit.test.ts +13 -0
- package/templates/libs/auth-strategies/firebase/src/lib/firebase-auth.strategy.ts +77 -0
- package/templates/libs/auth-strategies/firebase/src/lib/identity-toolkit.client.ts +72 -0
- package/templates/libs/auth-strategies/firebase/src/lib/testing/mock-admin-auth.ts +41 -0
- package/templates/libs/auth-strategies/firebase/src/lib/testing/mock-identity-toolkit.ts +76 -0
- package/templates/libs/auth-strategies/firebase/tsconfig.json +24 -0
- package/templates/libs/auth-strategies/firebase/tsconfig.lib.json +23 -0
- package/templates/libs/auth-strategies/firebase/tsconfig.spec.json +22 -0
- package/templates/libs/auth-strategies/firebase/vitest.config.mts +22 -0
- package/templates/libs/auth-strategies/supabase/README.md +11 -0
- package/templates/libs/auth-strategies/supabase/eslint.config.mjs +22 -0
- package/templates/libs/auth-strategies/supabase/package.json +16 -0
- package/templates/libs/auth-strategies/supabase/project.json +19 -0
- package/templates/libs/auth-strategies/supabase/src/index.ts +2 -0
- package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.contract.unit.test.ts +8 -0
- package/templates/libs/auth-strategies/supabase/src/lib/supabase-auth.strategy.ts +79 -0
- package/templates/libs/auth-strategies/supabase/src/lib/testing/mock-supabase.ts +107 -0
- package/templates/libs/auth-strategies/supabase/tsconfig.json +24 -0
- package/templates/libs/auth-strategies/supabase/tsconfig.lib.json +23 -0
- package/templates/libs/auth-strategies/supabase/tsconfig.spec.json +22 -0
- package/templates/libs/auth-strategies/supabase/vitest.config.mts +22 -0
- package/templates/libs/shared/README.md +11 -0
- package/templates/libs/shared/eslint.config.mjs +24 -0
- package/templates/libs/shared/package.json +14 -0
- package/templates/libs/shared/project.json +19 -0
- package/templates/libs/shared/src/__tests__/transport.unit.test.ts +58 -0
- package/templates/libs/shared/src/abilities/__tests__/ability.unit.test.ts +28 -0
- package/templates/libs/shared/src/abilities/ability.ts +21 -0
- package/templates/libs/shared/src/abilities/index.ts +2 -0
- package/templates/libs/shared/src/abilities/subjects.ts +2 -0
- package/templates/libs/shared/src/index.ts +3 -0
- package/templates/libs/shared/src/strategies/__tests__/fake-auth.contract.unit.test.ts +4 -0
- package/templates/libs/shared/src/strategies/__tests__/fake-storage.contract.unit.test.ts +4 -0
- package/templates/libs/shared/src/strategies/auth.ts +21 -0
- package/templates/libs/shared/src/strategies/contract/auth-contract.ts +66 -0
- package/templates/libs/shared/src/strategies/contract/storage-contract.ts +58 -0
- package/templates/libs/shared/src/strategies/fakes/fake-auth.ts +73 -0
- package/templates/libs/shared/src/strategies/fakes/fake-storage.ts +51 -0
- package/templates/libs/shared/src/strategies/fakes/index.ts +2 -0
- package/templates/libs/shared/src/strategies/index.ts +5 -0
- package/templates/libs/shared/src/strategies/storage.ts +17 -0
- package/templates/libs/shared/src/transport.ts +55 -0
- package/templates/libs/shared/tsconfig.json +24 -0
- package/templates/libs/shared/tsconfig.lib.json +23 -0
- package/templates/libs/shared/tsconfig.spec.json +22 -0
- package/templates/libs/shared/vitest.config.mts +21 -0
- package/templates/libs/storage-strategies/cloudinary/README.md +11 -0
- package/templates/libs/storage-strategies/cloudinary/eslint.config.mjs +23 -0
- package/templates/libs/storage-strategies/cloudinary/package.json +15 -0
- package/templates/libs/storage-strategies/cloudinary/project.json +19 -0
- package/templates/libs/storage-strategies/cloudinary/src/index.ts +2 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.contract.unit.test.ts +8 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/cloudinary-storage.strategy.ts +75 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/testing/mock-cloudinary.ts +36 -0
- package/templates/libs/storage-strategies/cloudinary/tsconfig.json +24 -0
- package/templates/libs/storage-strategies/cloudinary/tsconfig.lib.json +23 -0
- package/templates/libs/storage-strategies/cloudinary/tsconfig.spec.json +22 -0
- package/templates/libs/storage-strategies/cloudinary/vitest.config.mts +22 -0
- package/templates/libs/storage-strategies/firebase/README.md +11 -0
- package/templates/libs/storage-strategies/firebase/eslint.config.mjs +23 -0
- package/templates/libs/storage-strategies/firebase/package.json +15 -0
- package/templates/libs/storage-strategies/firebase/project.json +19 -0
- package/templates/libs/storage-strategies/firebase/src/index.ts +2 -0
- package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.contract.unit.test.ts +8 -0
- package/templates/libs/storage-strategies/firebase/src/lib/firebase-storage.strategy.ts +63 -0
- package/templates/libs/storage-strategies/firebase/src/lib/testing/mock-firebase-storage.ts +43 -0
- package/templates/libs/storage-strategies/firebase/tsconfig.json +24 -0
- package/templates/libs/storage-strategies/firebase/tsconfig.lib.json +23 -0
- package/templates/libs/storage-strategies/firebase/tsconfig.spec.json +22 -0
- package/templates/libs/storage-strategies/firebase/vitest.config.mts +22 -0
- package/templates/libs/storage-strategies/supabase/README.md +11 -0
- package/templates/libs/storage-strategies/supabase/eslint.config.mjs +22 -0
- package/templates/libs/storage-strategies/supabase/package.json +16 -0
- package/templates/libs/storage-strategies/supabase/project.json +19 -0
- package/templates/libs/storage-strategies/supabase/src/index.ts +2 -0
- package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.contract.unit.test.ts +8 -0
- package/templates/libs/storage-strategies/supabase/src/lib/supabase-storage.strategy.ts +53 -0
- package/templates/libs/storage-strategies/supabase/src/lib/testing/mock-supabase-storage.ts +78 -0
- package/templates/libs/storage-strategies/supabase/tsconfig.json +24 -0
- package/templates/libs/storage-strategies/supabase/tsconfig.lib.json +23 -0
- package/templates/libs/storage-strategies/supabase/tsconfig.spec.json +22 -0
- package/templates/libs/storage-strategies/supabase/vitest.config.mts +22 -0
- package/templates/libs/template-shared/README.md +11 -0
- package/templates/libs/template-shared/eslint.config.mjs +23 -0
- package/templates/libs/template-shared/package.json +22 -0
- package/templates/libs/template-shared/project.json +19 -0
- package/templates/libs/template-shared/src/index.ts +9 -0
- package/templates/libs/template-shared/src/lib/abilities/ability-provider.tsx +19 -0
- package/templates/libs/template-shared/src/lib/api/create-api.ts +20 -0
- package/templates/libs/template-shared/src/lib/draft/index.ts +1 -0
- package/templates/libs/template-shared/src/lib/i18n/create-i18n.ts +42 -0
- package/templates/libs/template-shared/src/lib/i18n/keys.ts +30 -0
- package/templates/libs/template-shared/src/lib/landing/LandingPage.tsx +68 -0
- package/templates/libs/template-shared/src/lib/notify/use-notify.ts +26 -0
- package/templates/libs/template-shared/src/lib/stores/auth.store.ts +29 -0
- package/templates/libs/template-shared/src/lib/stores/loading.store.ts +13 -0
- package/templates/libs/template-shared/tsconfig.json +24 -0
- package/templates/libs/template-shared/tsconfig.lib.json +25 -0
- package/templates/libs/template-shared/tsconfig.spec.json +22 -0
- package/templates/libs/template-shared/vitest.config.mts +22 -0
- package/templates/libs/upload-client/README.md +11 -0
- package/templates/libs/upload-client/eslint.config.mjs +22 -0
- package/templates/libs/upload-client/package.json +15 -0
- package/templates/libs/upload-client/project.json +19 -0
- package/templates/libs/upload-client/src/index.ts +2 -0
- package/templates/libs/upload-client/src/lib/upload-client.module.ts +25 -0
- package/templates/libs/upload-client/src/lib/upload-client.service.ts +38 -0
- package/templates/libs/upload-client/tsconfig.json +24 -0
- package/templates/libs/upload-client/tsconfig.lib.json +26 -0
- package/templates/libs/upload-client/tsconfig.spec.json +22 -0
- package/templates/libs/upload-client/vitest.config.mts +22 -0
- package/templates/nx.json +113 -0
- package/templates/package.json +24 -0
- package/templates/tools/create-icore/_template-shell/package.json +24 -0
- package/templates/tsconfig.base.json +29 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BadRequestException,
|
|
3
|
+
Body,
|
|
4
|
+
Controller,
|
|
5
|
+
Delete,
|
|
6
|
+
Get,
|
|
7
|
+
PayloadTooLargeException,
|
|
8
|
+
Post,
|
|
9
|
+
Query,
|
|
10
|
+
Req,
|
|
11
|
+
UploadedFile,
|
|
12
|
+
UseInterceptors,
|
|
13
|
+
} from '@nestjs/common';
|
|
14
|
+
import { ConfigService } from '@nestjs/config';
|
|
15
|
+
import { FileInterceptor } from '@nestjs/platform-express';
|
|
16
|
+
import {
|
|
17
|
+
ApiBearerAuth,
|
|
18
|
+
ApiBody,
|
|
19
|
+
ApiConsumes,
|
|
20
|
+
ApiOperation,
|
|
21
|
+
ApiQuery,
|
|
22
|
+
ApiTags,
|
|
23
|
+
} from '@nestjs/swagger';
|
|
24
|
+
import { UploadClientService } from '@icore/upload-client';
|
|
25
|
+
import type { StorageRef, VerifiedToken } from '@icore/shared';
|
|
26
|
+
import type { Request } from 'express';
|
|
27
|
+
import { assertOwnership } from './assert-ownership';
|
|
28
|
+
|
|
29
|
+
interface AuthedReq extends Request {
|
|
30
|
+
user?: VerifiedToken;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const DEFAULT_MAX_KB = 5120;
|
|
34
|
+
|
|
35
|
+
@ApiTags('storage')
|
|
36
|
+
@ApiBearerAuth()
|
|
37
|
+
@Controller('storage')
|
|
38
|
+
export class StorageController {
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly uploadClient: UploadClientService,
|
|
41
|
+
private readonly cfg: ConfigService,
|
|
42
|
+
) {}
|
|
43
|
+
|
|
44
|
+
@Post('upload')
|
|
45
|
+
@UseInterceptors(FileInterceptor('file'))
|
|
46
|
+
@ApiOperation({ summary: 'Upload a file and return its StorageRef' })
|
|
47
|
+
@ApiConsumes('multipart/form-data')
|
|
48
|
+
@ApiBody({
|
|
49
|
+
schema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: { file: { type: 'string', format: 'binary' } },
|
|
52
|
+
required: ['file'],
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
async upload(
|
|
56
|
+
@UploadedFile() file: Express.Multer.File | undefined,
|
|
57
|
+
@Req() req: AuthedReq,
|
|
58
|
+
): Promise<StorageRef> {
|
|
59
|
+
if (!file) throw new BadRequestException('missing_file');
|
|
60
|
+
const maxKb = Number(this.cfg.get<string>('MAX_FILE_SIZE_KB') ?? DEFAULT_MAX_KB);
|
|
61
|
+
if (file.size > maxKb * 1024) {
|
|
62
|
+
throw new PayloadTooLargeException(`file exceeds ${maxKb} KB`);
|
|
63
|
+
}
|
|
64
|
+
return this.uploadClient.upload(req.user!.uid, {
|
|
65
|
+
buffer: file.buffer,
|
|
66
|
+
filename: file.originalname,
|
|
67
|
+
mimeType: file.mimetype,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@Get('signed-url')
|
|
72
|
+
@ApiOperation({ summary: 'Sign a StorageRef for short-lived download' })
|
|
73
|
+
@ApiQuery({ name: 'bucket', type: String })
|
|
74
|
+
@ApiQuery({ name: 'path', type: String })
|
|
75
|
+
@ApiQuery({ name: 'ttlSec', type: Number, required: false })
|
|
76
|
+
signedUrl(
|
|
77
|
+
@Query('bucket') bucket: string,
|
|
78
|
+
@Query('path') path: string,
|
|
79
|
+
@Query('ttlSec') ttlSec: string | undefined,
|
|
80
|
+
@Req() req: AuthedReq,
|
|
81
|
+
): Promise<string> {
|
|
82
|
+
const ref: StorageRef = { bucket, path };
|
|
83
|
+
assertOwnership(ref, req.user!.uid);
|
|
84
|
+
return this.uploadClient.signedUrl(req.user!.uid, ref, ttlSec ? Number(ttlSec) : undefined);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@Delete('remove')
|
|
88
|
+
@ApiOperation({ summary: 'Delete a file the caller owns' })
|
|
89
|
+
@ApiBody({
|
|
90
|
+
schema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
required: ['bucket', 'path'],
|
|
93
|
+
properties: { bucket: { type: 'string' }, path: { type: 'string' } },
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
remove(@Body() body: { bucket: string; path: string }, @Req() req: AuthedReq): Promise<void> {
|
|
97
|
+
const ref: StorageRef = { bucket: body.bucket, path: body.path };
|
|
98
|
+
assertOwnership(ref, req.user!.uid);
|
|
99
|
+
return this.uploadClient.remove(req.user!.uid, ref);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@Get('list')
|
|
103
|
+
@ApiOperation({ summary: "List the caller's stored files" })
|
|
104
|
+
@ApiQuery({ name: 'prefix', type: String, required: false })
|
|
105
|
+
list(@Query('prefix') prefix: string | undefined, @Req() req: AuthedReq): Promise<StorageRef[]> {
|
|
106
|
+
return this.uploadClient.list(req.user!.uid, prefix);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { UploadClientModule } from '@icore/upload-client';
|
|
3
|
+
import { StorageController } from './storage.controller';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
imports: [UploadClientModule.forRoot()],
|
|
7
|
+
controllers: [StorageController],
|
|
8
|
+
exports: [UploadClientModule],
|
|
9
|
+
})
|
|
10
|
+
export class StorageModule {}
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
import { NestFactory } from '@nestjs/core';
|
|
3
|
+
import { NestExpressApplication } from '@nestjs/platform-express';
|
|
4
|
+
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
5
|
+
import { AppModule } from './app/app.module';
|
|
6
|
+
import pkg from '@icore/package.json';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PORT = 3001;
|
|
9
|
+
|
|
10
|
+
async function bootstrap() {
|
|
11
|
+
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
|
12
|
+
app.setGlobalPrefix('api');
|
|
13
|
+
|
|
14
|
+
const swaggerConfig = new DocumentBuilder()
|
|
15
|
+
.setTitle('iCore API')
|
|
16
|
+
.setDescription('iCore Gateway HTTP surface')
|
|
17
|
+
.setVersion(pkg.version)
|
|
18
|
+
.addBearerAuth()
|
|
19
|
+
.build();
|
|
20
|
+
const document = SwaggerModule.createDocument(app, swaggerConfig);
|
|
21
|
+
SwaggerModule.setup('api/docs', app, document);
|
|
22
|
+
|
|
23
|
+
const port = Number(process.env.API_PORT ?? DEFAULT_PORT);
|
|
24
|
+
await app.listen(port);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
bootstrap()
|
|
28
|
+
.then(() => {
|
|
29
|
+
const logger = new Logger('API-Bootstrap');
|
|
30
|
+
logger.log(
|
|
31
|
+
`API Bootstrap completed successfully: ${process.env.API_ORIGIN ?? 'http://localhost'}:${process.env.API_PORT ?? DEFAULT_PORT}/api`,
|
|
32
|
+
);
|
|
33
|
+
logger.log(
|
|
34
|
+
`Swagger UI: ${process.env.API_ORIGIN ?? 'http://localhost'}:${process.env.API_PORT ?? DEFAULT_PORT}/api/docs`,
|
|
35
|
+
);
|
|
36
|
+
})
|
|
37
|
+
.catch((err) => {
|
|
38
|
+
new Logger('API-Bootstrap').error(
|
|
39
|
+
'Gateway bootstrap failed',
|
|
40
|
+
err instanceof Error ? err.stack : err,
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"module": "node16",
|
|
6
|
+
"moduleResolution": "node16",
|
|
7
|
+
"types": ["node", "multer"],
|
|
8
|
+
"experimentalDecorators": true,
|
|
9
|
+
"emitDecoratorMetadata": true,
|
|
10
|
+
"target": "es2021"
|
|
11
|
+
},
|
|
12
|
+
"include": ["src/**/*.ts"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "../../dist/out-tsc",
|
|
5
|
+
"types": ["vitest/globals", "vitest/importMeta", "vite/client", "node", "vitest"]
|
|
6
|
+
},
|
|
7
|
+
"include": [
|
|
8
|
+
"vitest.config.ts",
|
|
9
|
+
"vitest.config.mts",
|
|
10
|
+
"src/**/*.test.ts",
|
|
11
|
+
"src/**/*.spec.ts",
|
|
12
|
+
"src/**/*.test.tsx",
|
|
13
|
+
"src/**/*.spec.tsx",
|
|
14
|
+
"src/**/*.d.ts"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
|
3
|
+
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
|
4
|
+
|
|
5
|
+
export default defineConfig(() => ({
|
|
6
|
+
root: __dirname,
|
|
7
|
+
cacheDir: '../../node_modules/.vite/apps/api',
|
|
8
|
+
plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
|
9
|
+
test: {
|
|
10
|
+
name: 'api',
|
|
11
|
+
watch: false,
|
|
12
|
+
globals: true,
|
|
13
|
+
environment: 'node',
|
|
14
|
+
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
|
15
|
+
reporters: ['default'],
|
|
16
|
+
coverage: {
|
|
17
|
+
reportsDirectory: '../../coverage/apps/api',
|
|
18
|
+
provider: 'v8' as const,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
output: {
|
|
6
|
+
path: join(__dirname, '../../dist/apps/api'),
|
|
7
|
+
clean: true,
|
|
8
|
+
...(process.env.NODE_ENV !== 'production' && {
|
|
9
|
+
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
|
|
10
|
+
}),
|
|
11
|
+
},
|
|
12
|
+
plugins: [
|
|
13
|
+
new NxAppWebpackPlugin({
|
|
14
|
+
target: 'node',
|
|
15
|
+
compiler: 'tsc',
|
|
16
|
+
main: './src/main.ts',
|
|
17
|
+
tsConfig: './tsconfig.app.json',
|
|
18
|
+
assets: ['./src/assets'],
|
|
19
|
+
optimization: false,
|
|
20
|
+
outputHashing: 'none',
|
|
21
|
+
generatePackageJson: true,
|
|
22
|
+
sourceMap: true,
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Transport (gateway ↔ this MS) — TCP by default; flip to redis or nats in prod
|
|
2
|
+
AUTH_TRANSPORT=tcp
|
|
3
|
+
AUTH_HOST=127.0.0.1
|
|
4
|
+
AUTH_PORT=4001
|
|
5
|
+
# AUTH_REDIS_URL=redis://localhost:6379
|
|
6
|
+
# AUTH_NATS_URL=nats://localhost:4222
|
|
7
|
+
|
|
8
|
+
# Which concrete AuthStrategy to instantiate
|
|
9
|
+
AUTH_PROVIDER=supabase
|
|
10
|
+
# AUTH_PROVIDER=firebase
|
|
11
|
+
|
|
12
|
+
# Comma-separated emails granted admin role on signup. Case-insensitive.
|
|
13
|
+
# Empty / unset = everyone gets 'user'. Existing roles are never overwritten.
|
|
14
|
+
ADMINS_LIST=
|
|
15
|
+
|
|
16
|
+
# --- Supabase credentials (when AUTH_PROVIDER=supabase) ---
|
|
17
|
+
SUPABASE_URL=https://<your-project-ref>.supabase.co
|
|
18
|
+
SUPABASE_SERVICE_ROLE_KEY=eyJ...
|
|
19
|
+
|
|
20
|
+
# --- Firebase credentials (when AUTH_PROVIDER=firebase) ---
|
|
21
|
+
# Get these from Firebase console → Project Settings → Service accounts →
|
|
22
|
+
# Generate new private key. Paste each field below. The private key must
|
|
23
|
+
# preserve its \n newline escapes — quote it with single quotes.
|
|
24
|
+
FB_ADMIN_TYPE=service_account
|
|
25
|
+
FB_ADMIN_PROJECT_ID=<your-project-id>
|
|
26
|
+
FB_ADMIN_PRIVATE_KEY_ID=<id>
|
|
27
|
+
FB_ADMIN_PRIVATE_KEY='-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n'
|
|
28
|
+
FB_ADMIN_CLIENT_EMAIL=firebase-adminsdk-<hash>@<project-id>.iam.gserviceaccount.com
|
|
29
|
+
FB_ADMIN_CLIENT_ID=<id>
|
|
30
|
+
FB_ADMIN_AUTH_URI=https://accounts.google.com/o/oauth2/auth
|
|
31
|
+
FB_ADMIN_TOKEN_URI=https://oauth2.googleapis.com/token
|
|
32
|
+
FB_ADMIN_AUTH_PROVIDER_X509_CERT_URL=https://www.googleapis.com/oauth2/v1/certs
|
|
33
|
+
FB_ADMIN_CLIENT_X509_CERT_URL=https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-<hash>%40<project-id>.iam.gserviceaccount.com
|
|
34
|
+
FB_ADMIN_UNIVERSE_DOMAIN=googleapis.com
|
|
35
|
+
|
|
36
|
+
# Firebase Web API key (Project Settings → General → Your apps → SDK config → apiKey).
|
|
37
|
+
# Required for Identity Toolkit REST calls (signUp / signIn / refresh).
|
|
38
|
+
FIREBASE_WEB_API_KEY=<your-web-api-key>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@icore/auth-firebase": "*",
|
|
7
|
+
"@icore/auth-supabase": "*",
|
|
8
|
+
"@icore/shared": "*",
|
|
9
|
+
"firebase-admin": "^13.0.0",
|
|
10
|
+
"@nestjs/common": "^11.1.24",
|
|
11
|
+
"@nestjs/config": "^4.0.4",
|
|
12
|
+
"@nestjs/core": "^11.1.24",
|
|
13
|
+
"@nestjs/microservices": "^11.1.24",
|
|
14
|
+
"@supabase/supabase-js": "^2.106.2",
|
|
15
|
+
"reflect-metadata": "^0.2.2",
|
|
16
|
+
"rxjs": "^7.8.2",
|
|
17
|
+
"tslib": "^2.8.1"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "apps/microservices/auth/src",
|
|
5
|
+
"projectType": "application",
|
|
6
|
+
"targets": {
|
|
7
|
+
"build": {
|
|
8
|
+
"executor": "nx:run-commands",
|
|
9
|
+
"options": {
|
|
10
|
+
"command": "webpack-cli build",
|
|
11
|
+
"args": ["--node-env=production"],
|
|
12
|
+
"cwd": "apps/microservices/auth"
|
|
13
|
+
},
|
|
14
|
+
"configurations": {
|
|
15
|
+
"development": {
|
|
16
|
+
"args": ["--node-env=development"]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"prune-lockfile": {
|
|
21
|
+
"dependsOn": ["build"],
|
|
22
|
+
"cache": true,
|
|
23
|
+
"executor": "@nx/js:prune-lockfile",
|
|
24
|
+
"outputs": [
|
|
25
|
+
"{workspaceRoot}/dist/apps/microservices/auth/package.json",
|
|
26
|
+
"{workspaceRoot}/dist/apps/microservices/auth/yarn.lock"
|
|
27
|
+
],
|
|
28
|
+
"options": {
|
|
29
|
+
"buildTarget": "build"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"copy-workspace-modules": {
|
|
33
|
+
"dependsOn": ["build"],
|
|
34
|
+
"cache": true,
|
|
35
|
+
"outputs": ["{workspaceRoot}/dist/apps/microservices/auth/workspace_modules"],
|
|
36
|
+
"executor": "@nx/js:copy-workspace-modules",
|
|
37
|
+
"options": {
|
|
38
|
+
"buildTarget": "build"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"prune": {
|
|
42
|
+
"dependsOn": ["prune-lockfile", "copy-workspace-modules"],
|
|
43
|
+
"executor": "nx:noop"
|
|
44
|
+
},
|
|
45
|
+
"serve": {
|
|
46
|
+
"continuous": true,
|
|
47
|
+
"executor": "@nx/js:node",
|
|
48
|
+
"defaultConfiguration": "development",
|
|
49
|
+
"dependsOn": ["build"],
|
|
50
|
+
"options": {
|
|
51
|
+
"buildTarget": "auth:build",
|
|
52
|
+
"runBuildTargetDependencies": false
|
|
53
|
+
},
|
|
54
|
+
"configurations": {
|
|
55
|
+
"development": {
|
|
56
|
+
"buildTarget": "auth:build:development"
|
|
57
|
+
},
|
|
58
|
+
"production": {
|
|
59
|
+
"buildTarget": "auth:build:production"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"tags": []
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import {
|
|
4
|
+
FirebaseAuthStrategy,
|
|
5
|
+
createMockIdentityToolkit,
|
|
6
|
+
createMockAdminAuth,
|
|
7
|
+
} from '@icore/auth-firebase';
|
|
8
|
+
import { AuthController } from '../auth.controller';
|
|
9
|
+
|
|
10
|
+
function makeConfig(env: Record<string, string | undefined>): ConfigService {
|
|
11
|
+
return { get: (key: string) => env[key] } as unknown as ConfigService;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('AuthController × FirebaseAuthStrategy × ADMINS_LIST', () => {
|
|
15
|
+
const fixture = (env: Record<string, string | undefined> = {}) => {
|
|
16
|
+
const toolkit = createMockIdentityToolkit();
|
|
17
|
+
const adminAuth = createMockAdminAuth({ identityToolkit: toolkit });
|
|
18
|
+
const strategy = new FirebaseAuthStrategy({ identityToolkit: toolkit.client, adminAuth });
|
|
19
|
+
return { strategy, controller: new AuthController(strategy, makeConfig(env)) };
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
it('signup auto-assigns admin role when email is in ADMINS_LIST', async () => {
|
|
23
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com, owner@x.com' });
|
|
24
|
+
const session = await controller.signup({ email: 'boss@x.com', password: 'pw12345!' });
|
|
25
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('signup auto-assigns user role for non-admin email', async () => {
|
|
29
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com' });
|
|
30
|
+
const session = await controller.signup({ email: 'normal@x.com', password: 'pw12345!' });
|
|
31
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('signup auto-assigns user role when ADMINS_LIST is unset', async () => {
|
|
35
|
+
const { strategy, controller } = fixture({});
|
|
36
|
+
const session = await controller.signup({ email: 'a@x.com', password: 'pw12345!' });
|
|
37
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('does not overwrite an existing role on later setRole calls', async () => {
|
|
41
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com' });
|
|
42
|
+
const session = await controller.signup({ email: 'boss@x.com', password: 'pw12345!' });
|
|
43
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
44
|
+
await controller.setRole({ uid: session.user.id, role: 'user' });
|
|
45
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('ADMINS_LIST is case-insensitive on email match', async () => {
|
|
49
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'BOSS@x.COM' });
|
|
50
|
+
const session = await controller.signup({ email: 'boss@X.com', password: 'pw12345!' });
|
|
51
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { SupabaseAuthStrategy, createMockSupabaseClient } from '@icore/auth-supabase';
|
|
4
|
+
import { AuthController } from '../auth.controller';
|
|
5
|
+
|
|
6
|
+
function makeConfig(env: Record<string, string | undefined>): ConfigService {
|
|
7
|
+
return { get: (key: string) => env[key] } as unknown as ConfigService;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('AuthController × SupabaseAuthStrategy × ADMINS_LIST', () => {
|
|
11
|
+
const fixture = (env: Record<string, string | undefined> = {}) => {
|
|
12
|
+
const strategy = new SupabaseAuthStrategy({ client: createMockSupabaseClient() });
|
|
13
|
+
return { strategy, controller: new AuthController(strategy, makeConfig(env)) };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
it('signup auto-assigns admin role when email is in ADMINS_LIST', async () => {
|
|
17
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com, owner@x.com' });
|
|
18
|
+
const session = await controller.signup({ email: 'boss@x.com', password: 'pw12345!' });
|
|
19
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('signup auto-assigns user role for non-admin email', async () => {
|
|
23
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com' });
|
|
24
|
+
const session = await controller.signup({ email: 'normal@x.com', password: 'pw12345!' });
|
|
25
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('signup auto-assigns user role when ADMINS_LIST is unset', async () => {
|
|
29
|
+
const { strategy, controller } = fixture({});
|
|
30
|
+
const session = await controller.signup({ email: 'a@x.com', password: 'pw12345!' });
|
|
31
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('does not overwrite an existing role on later setRole calls', async () => {
|
|
35
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com' });
|
|
36
|
+
const session = await controller.signup({ email: 'boss@x.com', password: 'pw12345!' });
|
|
37
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
38
|
+
await controller.setRole({ uid: session.user.id, role: 'user' });
|
|
39
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('ADMINS_LIST is case-insensitive on email match', async () => {
|
|
43
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'BOSS@x.COM' });
|
|
44
|
+
const session = await controller.signup({ email: 'boss@X.com', password: 'pw12345!' });
|
|
45
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ConfigService } from '@nestjs/config';
|
|
3
|
+
import { FakeAuthStrategy } from '@icore/shared';
|
|
4
|
+
import { AuthController } from '../auth.controller';
|
|
5
|
+
|
|
6
|
+
function makeConfig(env: Record<string, string | undefined>): ConfigService {
|
|
7
|
+
return {
|
|
8
|
+
get: (key: string) => env[key],
|
|
9
|
+
} as unknown as ConfigService;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('AuthController', () => {
|
|
13
|
+
const fixture = (env: Record<string, string | undefined> = {}) => {
|
|
14
|
+
const strategy = new FakeAuthStrategy();
|
|
15
|
+
return { strategy, controller: new AuthController(strategy, makeConfig(env)) };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
it('signup → verify round-trip resolves the new uid', async () => {
|
|
19
|
+
const { controller } = fixture();
|
|
20
|
+
const session = await controller.signup({ email: 't@x.com', password: 'pw12345!' });
|
|
21
|
+
expect(session.accessToken).toBeTruthy();
|
|
22
|
+
const verified = await controller.verify({ token: session.accessToken });
|
|
23
|
+
expect(verified.uid).toBe(session.user.id);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('login after signup issues a new session for the same user', async () => {
|
|
27
|
+
const { controller } = fixture();
|
|
28
|
+
const signup = await controller.signup({ email: 'l@x.com', password: 'pw12345!' });
|
|
29
|
+
const login = await controller.login({ email: 'l@x.com', password: 'pw12345!' });
|
|
30
|
+
expect(login.user.id).toBe(signup.user.id);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('refresh issues a new session and invalidates the used token', async () => {
|
|
34
|
+
const { controller } = fixture();
|
|
35
|
+
const first = await controller.signup({ email: 'r@x.com', password: 'pw12345!' });
|
|
36
|
+
const next = await controller.refresh({ refreshToken: first.refreshToken });
|
|
37
|
+
expect(next.user.id).toBe(first.user.id);
|
|
38
|
+
await expect(controller.refresh({ refreshToken: first.refreshToken })).rejects.toThrow();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('setRole writes a role visible on verify after re-login', async () => {
|
|
42
|
+
const { controller } = fixture();
|
|
43
|
+
const session = await controller.signup({ email: 's@x.com', password: 'pw12345!' });
|
|
44
|
+
await controller.setRole({ uid: session.user.id, role: 'admin' });
|
|
45
|
+
const re = await controller.login({ email: 's@x.com', password: 'pw12345!' });
|
|
46
|
+
const verified = await controller.verify({ token: re.accessToken });
|
|
47
|
+
expect(verified.role).toBe('admin');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('signup auto-assigns admin role when email is in ADMINS_LIST', async () => {
|
|
51
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com, owner@x.com' });
|
|
52
|
+
const session = await controller.signup({ email: 'boss@x.com', password: 'pw12345!' });
|
|
53
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('signup auto-assigns user role when email is NOT in ADMINS_LIST', async () => {
|
|
57
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com' });
|
|
58
|
+
const session = await controller.signup({ email: 'normal@x.com', password: 'pw12345!' });
|
|
59
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('signup auto-assigns user role when ADMINS_LIST is unset', async () => {
|
|
63
|
+
const { strategy, controller } = fixture({});
|
|
64
|
+
const session = await controller.signup({ email: 'a@x.com', password: 'pw12345!' });
|
|
65
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('ADMINS_LIST is case-insensitive on email match', async () => {
|
|
69
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'BOSS@x.COM' });
|
|
70
|
+
const session = await controller.signup({ email: 'boss@X.com', password: 'pw12345!' });
|
|
71
|
+
expect(await strategy.getRole(session.user.id)).toBe('admin');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('does not overwrite an existing role on re-signup attempts', async () => {
|
|
75
|
+
// The fake throws on duplicate signup, but a manual sequence simulates
|
|
76
|
+
// the idempotency path: set the role first, then call the private hook
|
|
77
|
+
// again via setRole-after-signup. We use a separate signup path so the
|
|
78
|
+
// strategy's state mirrors a returning consumer.
|
|
79
|
+
const { strategy, controller } = fixture({ ADMINS_LIST: 'boss@x.com' });
|
|
80
|
+
const session = await controller.signup({ email: 'boss@x.com', password: 'pw12345!' });
|
|
81
|
+
// Manually demote, then re-invoke setRole(admin) to prove the hook is
|
|
82
|
+
// not re-run on subsequent calls. The controller's hook only runs
|
|
83
|
+
// inside signup(), so demoting after the fact must persist.
|
|
84
|
+
await controller.setRole({ uid: session.user.id, role: 'user' });
|
|
85
|
+
expect(await strategy.getRole(session.user.id)).toBe('user');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import { ConfigModule, ConfigService } from '@nestjs/config';
|
|
4
|
+
import { createClient } from '@supabase/supabase-js';
|
|
5
|
+
import * as admin from 'firebase-admin';
|
|
6
|
+
import { SupabaseAuthStrategy } from '@icore/auth-supabase';
|
|
7
|
+
import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
|
|
8
|
+
import type { AuthStrategy } from '@icore/shared';
|
|
9
|
+
import { AuthController } from './auth.controller';
|
|
10
|
+
|
|
11
|
+
function makeFirebaseStrategy(cfg: ConfigService): AuthStrategy {
|
|
12
|
+
const projectId = cfg.getOrThrow<string>('FB_ADMIN_PROJECT_ID');
|
|
13
|
+
if (admin.apps.length === 0) {
|
|
14
|
+
admin.initializeApp({
|
|
15
|
+
credential: admin.credential.cert({
|
|
16
|
+
projectId,
|
|
17
|
+
clientEmail: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_EMAIL'),
|
|
18
|
+
privateKey: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
const identityToolkit = new HttpIdentityToolkitClient(
|
|
23
|
+
cfg.getOrThrow<string>('FIREBASE_WEB_API_KEY'),
|
|
24
|
+
);
|
|
25
|
+
return new FirebaseAuthStrategy({
|
|
26
|
+
identityToolkit,
|
|
27
|
+
adminAuth: admin.auth(),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@Module({
|
|
32
|
+
imports: [
|
|
33
|
+
ConfigModule.forRoot({
|
|
34
|
+
isGlobal: true,
|
|
35
|
+
envFilePath: [
|
|
36
|
+
join(process.cwd(), 'apps/microservices/auth/.env'),
|
|
37
|
+
join(process.cwd(), '.env'),
|
|
38
|
+
],
|
|
39
|
+
}),
|
|
40
|
+
],
|
|
41
|
+
controllers: [AuthController],
|
|
42
|
+
providers: [
|
|
43
|
+
{
|
|
44
|
+
provide: 'AuthStrategy',
|
|
45
|
+
useFactory: (cfg: ConfigService): AuthStrategy => {
|
|
46
|
+
const provider = cfg.getOrThrow<string>('AUTH_PROVIDER');
|
|
47
|
+
switch (provider) {
|
|
48
|
+
case 'supabase': {
|
|
49
|
+
const client = createClient(
|
|
50
|
+
cfg.getOrThrow<string>('SUPABASE_URL'),
|
|
51
|
+
cfg.getOrThrow<string>('SUPABASE_SERVICE_ROLE_KEY'),
|
|
52
|
+
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
53
|
+
);
|
|
54
|
+
return new SupabaseAuthStrategy({ client });
|
|
55
|
+
}
|
|
56
|
+
case 'firebase':
|
|
57
|
+
return makeFirebaseStrategy(cfg);
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported AUTH_PROVIDER: ${provider}`);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
inject: [ConfigService],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
})
|
|
66
|
+
export class AppModule {}
|