@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.
Files changed (241) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +56 -0
  3. package/dist/cli.js +300 -0
  4. package/dist/index.cjs +303 -0
  5. package/dist/index.d.cts +26 -0
  6. package/dist/index.d.ts +26 -0
  7. package/dist/index.js +265 -0
  8. package/package.json +72 -0
  9. package/templates/.husky/pre-commit +56 -0
  10. package/templates/.nvmrc +1 -0
  11. package/templates/.prettierignore +7 -0
  12. package/templates/.prettierrc +7 -0
  13. package/templates/.yarnrc.yml +7 -0
  14. package/templates/apps/api/.env.example +19 -0
  15. package/templates/apps/api/eslint.config.mjs +23 -0
  16. package/templates/apps/api/package.json +20 -0
  17. package/templates/apps/api/project.json +76 -0
  18. package/templates/apps/api/src/app/abilities/__tests__/ability.guard.unit.test.ts +49 -0
  19. package/templates/apps/api/src/app/abilities/abilities.module.ts +10 -0
  20. package/templates/apps/api/src/app/abilities/ability.factory.ts +13 -0
  21. package/templates/apps/api/src/app/abilities/ability.guard.ts +29 -0
  22. package/templates/apps/api/src/app/abilities/check-ability.decorator.ts +12 -0
  23. package/templates/apps/api/src/app/app.module.ts +19 -0
  24. package/templates/apps/api/src/app/auth/__tests__/auth.guard.unit.test.ts +66 -0
  25. package/templates/apps/api/src/app/auth/auth.controller.ts +62 -0
  26. package/templates/apps/api/src/app/auth/auth.guard.ts +42 -0
  27. package/templates/apps/api/src/app/auth/auth.module.ts +17 -0
  28. package/templates/apps/api/src/app/auth/public.decorator.ts +4 -0
  29. package/templates/apps/api/src/app/profile/profile.controller.ts +15 -0
  30. package/templates/apps/api/src/app/profile/profile.module.ts +5 -0
  31. package/templates/apps/api/src/app/storage/__tests__/assert-ownership.unit.test.ts +28 -0
  32. package/templates/apps/api/src/app/storage/assert-ownership.ts +8 -0
  33. package/templates/apps/api/src/app/storage/storage.controller.ts +108 -0
  34. package/templates/apps/api/src/app/storage/storage.module.ts +10 -0
  35. package/templates/apps/api/src/assets/.gitkeep +0 -0
  36. package/templates/apps/api/src/main.ts +43 -0
  37. package/templates/apps/api/tsconfig.app.json +13 -0
  38. package/templates/apps/api/tsconfig.json +16 -0
  39. package/templates/apps/api/tsconfig.spec.json +16 -0
  40. package/templates/apps/api/vitest.config.mts +21 -0
  41. package/templates/apps/api/webpack.config.js +25 -0
  42. package/templates/apps/microservices/auth/.env.example +38 -0
  43. package/templates/apps/microservices/auth/package.json +19 -0
  44. package/templates/apps/microservices/auth/project.json +65 -0
  45. package/templates/apps/microservices/auth/src/app/__tests__/auth.controller.firebase.integration.unit.test.ts +53 -0
  46. package/templates/apps/microservices/auth/src/app/__tests__/auth.controller.supabase.integration.unit.test.ts +47 -0
  47. package/templates/apps/microservices/auth/src/app/__tests__/auth.controller.unit.test.ts +87 -0
  48. package/templates/apps/microservices/auth/src/app/app.module.ts +66 -0
  49. package/templates/apps/microservices/auth/src/app/auth.controller.ts +60 -0
  50. package/templates/apps/microservices/auth/src/assets/.gitkeep +0 -0
  51. package/templates/apps/microservices/auth/src/main.ts +28 -0
  52. package/templates/apps/microservices/auth/tsconfig.app.json +13 -0
  53. package/templates/apps/microservices/auth/tsconfig.json +16 -0
  54. package/templates/apps/microservices/auth/tsconfig.spec.json +16 -0
  55. package/templates/apps/microservices/auth/vitest.config.mts +21 -0
  56. package/templates/apps/microservices/auth/webpack.config.js +25 -0
  57. package/templates/apps/microservices/upload/.env.example +30 -0
  58. package/templates/apps/microservices/upload/package.json +21 -0
  59. package/templates/apps/microservices/upload/project.json +65 -0
  60. package/templates/apps/microservices/upload/src/app/__tests__/storage.controller.unit.test.ts +49 -0
  61. package/templates/apps/microservices/upload/src/app/app.module.ts +117 -0
  62. package/templates/apps/microservices/upload/src/app/storage.controller.ts +51 -0
  63. package/templates/apps/microservices/upload/src/assets/.gitkeep +0 -0
  64. package/templates/apps/microservices/upload/src/main.ts +28 -0
  65. package/templates/apps/microservices/upload/tsconfig.app.json +13 -0
  66. package/templates/apps/microservices/upload/tsconfig.json +16 -0
  67. package/templates/apps/microservices/upload/tsconfig.spec.json +16 -0
  68. package/templates/apps/microservices/upload/vitest.config.mts +22 -0
  69. package/templates/apps/microservices/upload/webpack.config.js +25 -0
  70. package/templates/apps/templates/client-shadcn/.env.example +2 -0
  71. package/templates/apps/templates/client-shadcn/eslint.config.mjs +10 -0
  72. package/templates/apps/templates/client-shadcn/index.html +17 -0
  73. package/templates/apps/templates/client-shadcn/project.json +9 -0
  74. package/templates/apps/templates/client-shadcn/public/favicon.ico +0 -0
  75. package/templates/apps/templates/client-shadcn/src/app/app.module.css +1 -0
  76. package/templates/apps/templates/client-shadcn/src/app/app.spec.tsx +9 -0
  77. package/templates/apps/templates/client-shadcn/src/app/app.tsx +7 -0
  78. package/templates/apps/templates/client-shadcn/src/assets/.gitkeep +0 -0
  79. package/templates/apps/templates/client-shadcn/src/components/AccessDeniedPage.tsx +15 -0
  80. package/templates/apps/templates/client-shadcn/src/components/PageLayout.tsx +55 -0
  81. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutFooter.tsx +8 -0
  82. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutHeader.tsx +57 -0
  83. package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +44 -0
  84. package/templates/apps/templates/client-shadcn/src/components/ui/button.tsx +50 -0
  85. package/templates/apps/templates/client-shadcn/src/components/ui/card.tsx +63 -0
  86. package/templates/apps/templates/client-shadcn/src/components/ui/input.tsx +23 -0
  87. package/templates/apps/templates/client-shadcn/src/components/ui/label.tsx +18 -0
  88. package/templates/apps/templates/client-shadcn/src/globals.css +27 -0
  89. package/templates/apps/templates/client-shadcn/src/layouts/MainLayout.tsx +17 -0
  90. package/templates/apps/templates/client-shadcn/src/lib/notify.ts +15 -0
  91. package/templates/apps/templates/client-shadcn/src/lib/utils.ts +6 -0
  92. package/templates/apps/templates/client-shadcn/src/main.tsx +50 -0
  93. package/templates/apps/templates/client-shadcn/src/routeTree.gen.ts +136 -0
  94. package/templates/apps/templates/client-shadcn/src/routes/__root.tsx +5 -0
  95. package/templates/apps/templates/client-shadcn/src/routes/_dashboard/dashboard.tsx +33 -0
  96. package/templates/apps/templates/client-shadcn/src/routes/_dashboard/profile.tsx +88 -0
  97. package/templates/apps/templates/client-shadcn/src/routes/_dashboard.tsx +16 -0
  98. package/templates/apps/templates/client-shadcn/src/routes/index.tsx +33 -0
  99. package/templates/apps/templates/client-shadcn/src/routes/login.tsx +93 -0
  100. package/templates/apps/templates/client-shadcn/src/styles.css +1 -0
  101. package/templates/apps/templates/client-shadcn/tsconfig.app.json +27 -0
  102. package/templates/apps/templates/client-shadcn/tsconfig.json +21 -0
  103. package/templates/apps/templates/client-shadcn/tsconfig.spec.json +30 -0
  104. package/templates/apps/templates/client-shadcn/vite.config.mts +92 -0
  105. package/templates/apps/templates/client-shadcn-e2e/eslint.config.mjs +12 -0
  106. package/templates/apps/templates/client-shadcn-e2e/playwright.config.ts +69 -0
  107. package/templates/apps/templates/client-shadcn-e2e/project.json +10 -0
  108. package/templates/apps/templates/client-shadcn-e2e/src/icore.spec.ts +27 -0
  109. package/templates/apps/templates/client-shadcn-e2e/tsconfig.json +19 -0
  110. package/templates/eslint.config.mjs +20 -0
  111. package/templates/libs/auth-client/README.md +11 -0
  112. package/templates/libs/auth-client/eslint.config.mjs +22 -0
  113. package/templates/libs/auth-client/package.json +15 -0
  114. package/templates/libs/auth-client/project.json +19 -0
  115. package/templates/libs/auth-client/src/index.ts +2 -0
  116. package/templates/libs/auth-client/src/lib/auth-client.module.ts +25 -0
  117. package/templates/libs/auth-client/src/lib/auth-client.service.ts +30 -0
  118. package/templates/libs/auth-client/tsconfig.json +24 -0
  119. package/templates/libs/auth-client/tsconfig.lib.json +26 -0
  120. package/templates/libs/auth-client/tsconfig.spec.json +22 -0
  121. package/templates/libs/auth-client/vitest.config.mts +22 -0
  122. package/templates/libs/auth-strategies/firebase/README.md +11 -0
  123. package/templates/libs/auth-strategies/firebase/eslint.config.mjs +22 -0
  124. package/templates/libs/auth-strategies/firebase/package.json +15 -0
  125. package/templates/libs/auth-strategies/firebase/project.json +19 -0
  126. package/templates/libs/auth-strategies/firebase/src/index.ts +4 -0
  127. package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.contract.unit.test.ts +13 -0
  128. package/templates/libs/auth-strategies/firebase/src/lib/firebase-auth.strategy.ts +77 -0
  129. package/templates/libs/auth-strategies/firebase/src/lib/identity-toolkit.client.ts +72 -0
  130. package/templates/libs/auth-strategies/firebase/src/lib/testing/mock-admin-auth.ts +41 -0
  131. package/templates/libs/auth-strategies/firebase/src/lib/testing/mock-identity-toolkit.ts +76 -0
  132. package/templates/libs/auth-strategies/firebase/tsconfig.json +24 -0
  133. package/templates/libs/auth-strategies/firebase/tsconfig.lib.json +23 -0
  134. package/templates/libs/auth-strategies/firebase/tsconfig.spec.json +22 -0
  135. package/templates/libs/auth-strategies/firebase/vitest.config.mts +22 -0
  136. package/templates/libs/auth-strategies/supabase/README.md +11 -0
  137. package/templates/libs/auth-strategies/supabase/eslint.config.mjs +22 -0
  138. package/templates/libs/auth-strategies/supabase/package.json +16 -0
  139. package/templates/libs/auth-strategies/supabase/project.json +19 -0
  140. package/templates/libs/auth-strategies/supabase/src/index.ts +2 -0
  141. package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.contract.unit.test.ts +8 -0
  142. package/templates/libs/auth-strategies/supabase/src/lib/supabase-auth.strategy.ts +79 -0
  143. package/templates/libs/auth-strategies/supabase/src/lib/testing/mock-supabase.ts +107 -0
  144. package/templates/libs/auth-strategies/supabase/tsconfig.json +24 -0
  145. package/templates/libs/auth-strategies/supabase/tsconfig.lib.json +23 -0
  146. package/templates/libs/auth-strategies/supabase/tsconfig.spec.json +22 -0
  147. package/templates/libs/auth-strategies/supabase/vitest.config.mts +22 -0
  148. package/templates/libs/shared/README.md +11 -0
  149. package/templates/libs/shared/eslint.config.mjs +24 -0
  150. package/templates/libs/shared/package.json +14 -0
  151. package/templates/libs/shared/project.json +19 -0
  152. package/templates/libs/shared/src/__tests__/transport.unit.test.ts +58 -0
  153. package/templates/libs/shared/src/abilities/__tests__/ability.unit.test.ts +28 -0
  154. package/templates/libs/shared/src/abilities/ability.ts +21 -0
  155. package/templates/libs/shared/src/abilities/index.ts +2 -0
  156. package/templates/libs/shared/src/abilities/subjects.ts +2 -0
  157. package/templates/libs/shared/src/index.ts +3 -0
  158. package/templates/libs/shared/src/strategies/__tests__/fake-auth.contract.unit.test.ts +4 -0
  159. package/templates/libs/shared/src/strategies/__tests__/fake-storage.contract.unit.test.ts +4 -0
  160. package/templates/libs/shared/src/strategies/auth.ts +21 -0
  161. package/templates/libs/shared/src/strategies/contract/auth-contract.ts +66 -0
  162. package/templates/libs/shared/src/strategies/contract/storage-contract.ts +58 -0
  163. package/templates/libs/shared/src/strategies/fakes/fake-auth.ts +73 -0
  164. package/templates/libs/shared/src/strategies/fakes/fake-storage.ts +51 -0
  165. package/templates/libs/shared/src/strategies/fakes/index.ts +2 -0
  166. package/templates/libs/shared/src/strategies/index.ts +5 -0
  167. package/templates/libs/shared/src/strategies/storage.ts +17 -0
  168. package/templates/libs/shared/src/transport.ts +55 -0
  169. package/templates/libs/shared/tsconfig.json +24 -0
  170. package/templates/libs/shared/tsconfig.lib.json +23 -0
  171. package/templates/libs/shared/tsconfig.spec.json +22 -0
  172. package/templates/libs/shared/vitest.config.mts +21 -0
  173. package/templates/libs/storage-strategies/cloudinary/README.md +11 -0
  174. package/templates/libs/storage-strategies/cloudinary/eslint.config.mjs +23 -0
  175. package/templates/libs/storage-strategies/cloudinary/package.json +15 -0
  176. package/templates/libs/storage-strategies/cloudinary/project.json +19 -0
  177. package/templates/libs/storage-strategies/cloudinary/src/index.ts +2 -0
  178. package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.contract.unit.test.ts +8 -0
  179. package/templates/libs/storage-strategies/cloudinary/src/lib/cloudinary-storage.strategy.ts +75 -0
  180. package/templates/libs/storage-strategies/cloudinary/src/lib/testing/mock-cloudinary.ts +36 -0
  181. package/templates/libs/storage-strategies/cloudinary/tsconfig.json +24 -0
  182. package/templates/libs/storage-strategies/cloudinary/tsconfig.lib.json +23 -0
  183. package/templates/libs/storage-strategies/cloudinary/tsconfig.spec.json +22 -0
  184. package/templates/libs/storage-strategies/cloudinary/vitest.config.mts +22 -0
  185. package/templates/libs/storage-strategies/firebase/README.md +11 -0
  186. package/templates/libs/storage-strategies/firebase/eslint.config.mjs +23 -0
  187. package/templates/libs/storage-strategies/firebase/package.json +15 -0
  188. package/templates/libs/storage-strategies/firebase/project.json +19 -0
  189. package/templates/libs/storage-strategies/firebase/src/index.ts +2 -0
  190. package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.contract.unit.test.ts +8 -0
  191. package/templates/libs/storage-strategies/firebase/src/lib/firebase-storage.strategy.ts +63 -0
  192. package/templates/libs/storage-strategies/firebase/src/lib/testing/mock-firebase-storage.ts +43 -0
  193. package/templates/libs/storage-strategies/firebase/tsconfig.json +24 -0
  194. package/templates/libs/storage-strategies/firebase/tsconfig.lib.json +23 -0
  195. package/templates/libs/storage-strategies/firebase/tsconfig.spec.json +22 -0
  196. package/templates/libs/storage-strategies/firebase/vitest.config.mts +22 -0
  197. package/templates/libs/storage-strategies/supabase/README.md +11 -0
  198. package/templates/libs/storage-strategies/supabase/eslint.config.mjs +22 -0
  199. package/templates/libs/storage-strategies/supabase/package.json +16 -0
  200. package/templates/libs/storage-strategies/supabase/project.json +19 -0
  201. package/templates/libs/storage-strategies/supabase/src/index.ts +2 -0
  202. package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.contract.unit.test.ts +8 -0
  203. package/templates/libs/storage-strategies/supabase/src/lib/supabase-storage.strategy.ts +53 -0
  204. package/templates/libs/storage-strategies/supabase/src/lib/testing/mock-supabase-storage.ts +78 -0
  205. package/templates/libs/storage-strategies/supabase/tsconfig.json +24 -0
  206. package/templates/libs/storage-strategies/supabase/tsconfig.lib.json +23 -0
  207. package/templates/libs/storage-strategies/supabase/tsconfig.spec.json +22 -0
  208. package/templates/libs/storage-strategies/supabase/vitest.config.mts +22 -0
  209. package/templates/libs/template-shared/README.md +11 -0
  210. package/templates/libs/template-shared/eslint.config.mjs +23 -0
  211. package/templates/libs/template-shared/package.json +22 -0
  212. package/templates/libs/template-shared/project.json +19 -0
  213. package/templates/libs/template-shared/src/index.ts +9 -0
  214. package/templates/libs/template-shared/src/lib/abilities/ability-provider.tsx +19 -0
  215. package/templates/libs/template-shared/src/lib/api/create-api.ts +20 -0
  216. package/templates/libs/template-shared/src/lib/draft/index.ts +1 -0
  217. package/templates/libs/template-shared/src/lib/i18n/create-i18n.ts +42 -0
  218. package/templates/libs/template-shared/src/lib/i18n/keys.ts +30 -0
  219. package/templates/libs/template-shared/src/lib/landing/LandingPage.tsx +68 -0
  220. package/templates/libs/template-shared/src/lib/notify/use-notify.ts +26 -0
  221. package/templates/libs/template-shared/src/lib/stores/auth.store.ts +29 -0
  222. package/templates/libs/template-shared/src/lib/stores/loading.store.ts +13 -0
  223. package/templates/libs/template-shared/tsconfig.json +24 -0
  224. package/templates/libs/template-shared/tsconfig.lib.json +25 -0
  225. package/templates/libs/template-shared/tsconfig.spec.json +22 -0
  226. package/templates/libs/template-shared/vitest.config.mts +22 -0
  227. package/templates/libs/upload-client/README.md +11 -0
  228. package/templates/libs/upload-client/eslint.config.mjs +22 -0
  229. package/templates/libs/upload-client/package.json +15 -0
  230. package/templates/libs/upload-client/project.json +19 -0
  231. package/templates/libs/upload-client/src/index.ts +2 -0
  232. package/templates/libs/upload-client/src/lib/upload-client.module.ts +25 -0
  233. package/templates/libs/upload-client/src/lib/upload-client.service.ts +38 -0
  234. package/templates/libs/upload-client/tsconfig.json +24 -0
  235. package/templates/libs/upload-client/tsconfig.lib.json +26 -0
  236. package/templates/libs/upload-client/tsconfig.spec.json +22 -0
  237. package/templates/libs/upload-client/vitest.config.mts +22 -0
  238. package/templates/nx.json +113 -0
  239. package/templates/package.json +24 -0
  240. package/templates/tools/create-icore/_template-shell/package.json +24 -0
  241. package/templates/tsconfig.base.json +29 -0
@@ -0,0 +1,60 @@
1
+ import { Controller, Inject, Logger } from '@nestjs/common';
2
+ import { ConfigService } from '@nestjs/config';
3
+ import { MessagePattern, Payload } from '@nestjs/microservices';
4
+ import type { AuthSession, AuthStrategy, VerifiedToken } from '@icore/shared';
5
+
6
+ @Controller()
7
+ export class AuthController {
8
+ private readonly logger = new Logger(AuthController.name);
9
+
10
+ constructor(
11
+ @Inject('AuthStrategy') private readonly strategy: AuthStrategy,
12
+ private readonly cfg: ConfigService,
13
+ ) {}
14
+
15
+ @MessagePattern('auth.verify')
16
+ verify(@Payload() payload: { token: string }): Promise<VerifiedToken> {
17
+ return this.strategy.verifyToken(payload.token);
18
+ }
19
+
20
+ @MessagePattern('auth.login')
21
+ login(@Payload() payload: { email: string; password: string }): Promise<AuthSession> {
22
+ return this.strategy.signIn(payload.email, payload.password);
23
+ }
24
+
25
+ @MessagePattern('auth.signup')
26
+ async signup(@Payload() payload: { email: string; password: string }): Promise<AuthSession> {
27
+ const session = await this.strategy.signUp(payload.email, payload.password);
28
+ await this.assignInitialRole(session.user.id, session.user.email);
29
+ return session;
30
+ }
31
+
32
+ @MessagePattern('auth.refresh')
33
+ refresh(@Payload() payload: { refreshToken: string }): Promise<AuthSession> {
34
+ return this.strategy.refresh(payload.refreshToken);
35
+ }
36
+
37
+ @MessagePattern('auth.setRole')
38
+ setRole(@Payload() payload: { uid: string; role: string }): Promise<void> {
39
+ return this.strategy.setRole(payload.uid, payload.role);
40
+ }
41
+
42
+ // Idempotent: skips work when a role already exists. Admin emails come
43
+ // from ADMINS_LIST (comma-separated). Everyone else gets 'user'.
44
+ private async assignInitialRole(uid: string, email: string): Promise<void> {
45
+ const existing = await this.strategy.getRole(uid);
46
+ if (existing) {
47
+ this.logger.log(`Role already set for ${uid}: ${existing} — skipping`);
48
+ return;
49
+ }
50
+
51
+ const admins = (this.cfg.get<string>('ADMINS_LIST') ?? '')
52
+ .split(',')
53
+ .map((e) => e.trim().toLowerCase())
54
+ .filter((e) => e.length > 0);
55
+
56
+ const role = admins.includes(email.toLowerCase()) ? 'admin' : 'user';
57
+ await this.strategy.setRole(uid, role);
58
+ this.logger.log(`Assigned role '${role}' to ${uid} (${email})`);
59
+ }
60
+ }
@@ -0,0 +1,28 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import { NestFactory } from '@nestjs/core';
3
+ import { MicroserviceOptions } from '@nestjs/microservices';
4
+ import { buildTransportMS } from '@icore/shared';
5
+ import { AppModule } from './app/app.module';
6
+
7
+ async function bootstrap() {
8
+ const app = await NestFactory.createMicroservice<MicroserviceOptions>(
9
+ AppModule,
10
+ buildTransportMS('AUTH'),
11
+ );
12
+ await app.listen();
13
+ }
14
+
15
+ bootstrap()
16
+ .then(() => {
17
+ const logger = new Logger('Auth-Bootstrap');
18
+ logger.log(
19
+ `Auth MS Bootstrap completed: transport=${process.env.AUTH_TRANSPORT ?? 'tcp'} host=${process.env.AUTH_HOST ?? '127.0.0.1'} port=${process.env.AUTH_PORT ?? '4001'}`,
20
+ );
21
+ })
22
+ .catch((err) => {
23
+ new Logger('Auth-Bootstrap').error(
24
+ 'Auth MS bootstrap failed',
25
+ err instanceof Error ? err.stack : err,
26
+ );
27
+ process.exit(1);
28
+ });
@@ -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"],
8
+ "experimentalDecorators": true,
9
+ "emitDecoratorMetadata": true,
10
+ "target": "es2021"
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.app.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ],
13
+ "compilerOptions": {
14
+ "esModuleInterop": true
15
+ }
16
+ }
@@ -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/microservices/auth',
8
+ plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
9
+ test: {
10
+ name: 'auth',
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/microservices/auth',
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/microservices/auth'),
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,30 @@
1
+ # Transport (gateway ↔ this MS) — TCP by default; flip to redis or nats in prod
2
+ UPLOAD_TRANSPORT=tcp
3
+ UPLOAD_HOST=127.0.0.1
4
+ UPLOAD_PORT=4002
5
+ # UPLOAD_REDIS_URL=redis://localhost:6379
6
+ # UPLOAD_NATS_URL=nats://localhost:4222
7
+
8
+ # Which concrete StorageStrategy to instantiate
9
+ STORAGE_PROVIDER=supabase
10
+ # STORAGE_PROVIDER=firebase
11
+ # STORAGE_PROVIDER=cloudinary
12
+
13
+ # --- Supabase credentials (when STORAGE_PROVIDER=supabase) ---
14
+ SUPABASE_URL=https://<your-project-ref>.supabase.co
15
+ SUPABASE_SERVICE_ROLE_KEY=eyJ...
16
+ SUPABASE_STORAGE_BUCKET=icore-uploads
17
+
18
+ # --- Firebase Cloud Storage credentials (when STORAGE_PROVIDER=firebase) ---
19
+ FB_ADMIN_TYPE=service_account
20
+ FB_ADMIN_PROJECT_ID=<your-project-id>
21
+ FB_ADMIN_PRIVATE_KEY='-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----\n'
22
+ FB_ADMIN_CLIENT_EMAIL=firebase-adminsdk-<hash>@<project-id>.iam.gserviceaccount.com
23
+ FIREBASE_STORAGE_BUCKET=<project-id>.appspot.com
24
+
25
+ # --- Cloudinary credentials (when STORAGE_PROVIDER=cloudinary) ---
26
+ CLOUDINARY_CLOUD_NAME=<your-cloud-name>
27
+ CLOUDINARY_API_KEY=<key>
28
+ CLOUDINARY_API_SECRET=<secret>
29
+ # Synthetic StorageRef.bucket value (Cloudinary has no buckets). Optional, defaults to "cloudinary".
30
+ # CLOUDINARY_BUCKET_TAG=icore-uploads
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "upload",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "dependencies": {
6
+ "@icore/shared": "*",
7
+ "@icore/storage-cloudinary": "*",
8
+ "@icore/storage-firebase": "*",
9
+ "@icore/storage-supabase": "*",
10
+ "cloudinary": "^2.0.0",
11
+ "firebase-admin": "^13.0.0",
12
+ "@nestjs/common": "^11.1.24",
13
+ "@nestjs/config": "^4.0.4",
14
+ "@nestjs/core": "^11.1.24",
15
+ "@nestjs/microservices": "^11.1.24",
16
+ "@supabase/supabase-js": "^2.106.2",
17
+ "reflect-metadata": "^0.2.2",
18
+ "rxjs": "^7.8.2",
19
+ "tslib": "^2.3.0"
20
+ }
21
+ }
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "upload",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "apps/microservices/upload/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/upload"
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/upload/package.json",
26
+ "{workspaceRoot}/dist/apps/microservices/upload/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/upload/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": "upload:build",
52
+ "runBuildTargetDependencies": false
53
+ },
54
+ "configurations": {
55
+ "development": {
56
+ "buildTarget": "upload:build:development"
57
+ },
58
+ "production": {
59
+ "buildTarget": "upload:build:production"
60
+ }
61
+ }
62
+ }
63
+ },
64
+ "tags": []
65
+ }
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { FakeStorageStrategy } from '@icore/shared';
3
+ import { StorageController } from '../storage.controller';
4
+
5
+ const fixture = () => {
6
+ const strategy = new FakeStorageStrategy();
7
+ return { strategy, controller: new StorageController(strategy) };
8
+ };
9
+
10
+ const file = (filename = 'hello.txt') => ({
11
+ buffer: Buffer.from('hello world').toString('base64'),
12
+ filename,
13
+ mimeType: 'text/plain',
14
+ });
15
+
16
+ describe('StorageController', () => {
17
+ it('upload returns a StorageRef under the user prefix', async () => {
18
+ const { controller } = fixture();
19
+ const ref = await controller.upload({ userId: 'user-1', file: file() });
20
+ expect(ref.path.startsWith('user-1/')).toBe(true);
21
+ });
22
+
23
+ it('list returns previously uploaded files for the same user', async () => {
24
+ const { controller } = fixture();
25
+ await controller.upload({ userId: 'user-2', file: file() });
26
+ const refs = await controller.list({ userId: 'user-2' });
27
+ expect(refs.length).toBe(1);
28
+ });
29
+
30
+ it('signedUrl returns a non-empty string', async () => {
31
+ const { controller } = fixture();
32
+ const ref = await controller.upload({ userId: 'user-3', file: file() });
33
+ const url = await controller.signedUrl({ userId: 'user-3', ref, ttlSec: 60 });
34
+ expect(url.length).toBeGreaterThan(0);
35
+ });
36
+
37
+ it('remove deletes the file', async () => {
38
+ const { controller } = fixture();
39
+ const ref = await controller.upload({ userId: 'user-4', file: file() });
40
+ await controller.remove({ userId: 'user-4', ref });
41
+ expect(await controller.list({ userId: 'user-4' })).toEqual([]);
42
+ });
43
+
44
+ it('signedUrl for a foreign user throws', async () => {
45
+ const { controller } = fixture();
46
+ const ref = await controller.upload({ userId: 'owner', file: file() });
47
+ await expect(controller.signedUrl({ userId: 'attacker', ref })).rejects.toThrow();
48
+ });
49
+ });
@@ -0,0 +1,117 @@
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 { v2 as cloudinary } from 'cloudinary';
7
+ import { SupabaseStorageStrategy } from '@icore/storage-supabase';
8
+ import { FirebaseStorageStrategy } from '@icore/storage-firebase';
9
+ import { CloudinaryStorageStrategy, type CloudinaryApiLike } from '@icore/storage-cloudinary';
10
+ import type { StorageStrategy } from '@icore/shared';
11
+ import { StorageController } from './storage.controller';
12
+
13
+ function makeFirebaseStorage(cfg: ConfigService): StorageStrategy {
14
+ const bucketName = cfg.getOrThrow<string>('FIREBASE_STORAGE_BUCKET');
15
+ if (admin.apps.length === 0) {
16
+ admin.initializeApp({
17
+ credential: admin.credential.cert({
18
+ projectId: cfg.getOrThrow<string>('FB_ADMIN_PROJECT_ID'),
19
+ clientEmail: cfg.getOrThrow<string>('FB_ADMIN_CLIENT_EMAIL'),
20
+ privateKey: cfg.getOrThrow<string>('FB_ADMIN_PRIVATE_KEY').replace(/\\n/g, '\n'),
21
+ }),
22
+ });
23
+ }
24
+ return new FirebaseStorageStrategy({
25
+ bucket: admin
26
+ .storage()
27
+ .bucket(bucketName) as unknown as import('@icore/storage-firebase').FirebaseStorageBucketLike,
28
+ });
29
+ }
30
+
31
+ function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
32
+ cloudinary.config({
33
+ cloud_name: cfg.getOrThrow<string>('CLOUDINARY_CLOUD_NAME'),
34
+ api_key: cfg.getOrThrow<string>('CLOUDINARY_API_KEY'),
35
+ api_secret: cfg.getOrThrow<string>('CLOUDINARY_API_SECRET'),
36
+ secure: true,
37
+ });
38
+
39
+ const api: CloudinaryApiLike = {
40
+ async upload(buffer, opts) {
41
+ return new Promise((resolve, reject) => {
42
+ const stream = cloudinary.uploader.upload_stream(
43
+ { public_id: opts.public_id, resource_type: opts.resource_type ?? 'raw' },
44
+ (error, result) => {
45
+ if (error || !result) reject(error ?? new Error('upload_failed'));
46
+ else resolve({ public_id: result.public_id, secure_url: result.secure_url });
47
+ },
48
+ );
49
+ stream.end(buffer);
50
+ });
51
+ },
52
+ async destroy(publicId) {
53
+ await cloudinary.uploader.destroy(publicId);
54
+ },
55
+ privateDownloadUrl(publicId, format, opts) {
56
+ return cloudinary.utils.private_download_url(publicId, format ?? '', opts ?? {});
57
+ },
58
+ async resources(opts) {
59
+ const res = await cloudinary.api.resources({
60
+ prefix: opts.prefix,
61
+ type: opts.type ?? 'upload',
62
+ });
63
+ return {
64
+ resources: (res.resources ?? []).map((r: { public_id: string }) => ({
65
+ public_id: r.public_id,
66
+ })),
67
+ };
68
+ },
69
+ };
70
+
71
+ return new CloudinaryStorageStrategy({
72
+ api,
73
+ bucket: cfg.get<string>('CLOUDINARY_BUCKET_TAG') ?? 'cloudinary',
74
+ });
75
+ }
76
+
77
+ @Module({
78
+ imports: [
79
+ ConfigModule.forRoot({
80
+ isGlobal: true,
81
+ envFilePath: [
82
+ join(process.cwd(), 'apps/microservices/upload/.env'),
83
+ join(process.cwd(), '.env'),
84
+ ],
85
+ }),
86
+ ],
87
+ controllers: [StorageController],
88
+ providers: [
89
+ {
90
+ provide: 'StorageStrategy',
91
+ useFactory: (cfg: ConfigService): StorageStrategy => {
92
+ const provider = cfg.getOrThrow<string>('STORAGE_PROVIDER');
93
+ switch (provider) {
94
+ case 'supabase': {
95
+ const client = createClient(
96
+ cfg.getOrThrow<string>('SUPABASE_URL'),
97
+ cfg.getOrThrow<string>('SUPABASE_SERVICE_ROLE_KEY'),
98
+ { auth: { autoRefreshToken: false, persistSession: false } },
99
+ );
100
+ return new SupabaseStorageStrategy({
101
+ client,
102
+ bucket: cfg.getOrThrow<string>('SUPABASE_STORAGE_BUCKET'),
103
+ });
104
+ }
105
+ case 'firebase':
106
+ return makeFirebaseStorage(cfg);
107
+ case 'cloudinary':
108
+ return makeCloudinaryStorage(cfg);
109
+ default:
110
+ throw new Error(`Unsupported STORAGE_PROVIDER: ${provider}`);
111
+ }
112
+ },
113
+ inject: [ConfigService],
114
+ },
115
+ ],
116
+ })
117
+ export class AppModule {}
@@ -0,0 +1,51 @@
1
+ import { Controller, Inject } from '@nestjs/common';
2
+ import { MessagePattern, Payload } from '@nestjs/microservices';
3
+ import type { StorageRef, StorageStrategy } from '@icore/shared';
4
+
5
+ interface UploadPayload {
6
+ userId: string;
7
+ file: { buffer: string; filename: string; mimeType: string };
8
+ }
9
+
10
+ interface RefPayload {
11
+ userId: string;
12
+ ref: StorageRef;
13
+ }
14
+
15
+ interface SignedUrlPayload extends RefPayload {
16
+ ttlSec?: number;
17
+ }
18
+
19
+ interface ListPayload {
20
+ userId: string;
21
+ prefix?: string;
22
+ }
23
+
24
+ @Controller()
25
+ export class StorageController {
26
+ constructor(@Inject('StorageStrategy') private readonly strategy: StorageStrategy) {}
27
+
28
+ @MessagePattern('storage.upload')
29
+ upload(@Payload() payload: UploadPayload): Promise<StorageRef> {
30
+ return this.strategy.upload(payload.userId, {
31
+ buffer: Buffer.from(payload.file.buffer, 'base64'),
32
+ filename: payload.file.filename,
33
+ mimeType: payload.file.mimeType,
34
+ });
35
+ }
36
+
37
+ @MessagePattern('storage.remove')
38
+ remove(@Payload() payload: RefPayload): Promise<void> {
39
+ return this.strategy.remove(payload.userId, payload.ref);
40
+ }
41
+
42
+ @MessagePattern('storage.signedUrl')
43
+ signedUrl(@Payload() payload: SignedUrlPayload): Promise<string> {
44
+ return this.strategy.getSignedUrl(payload.userId, payload.ref, payload.ttlSec);
45
+ }
46
+
47
+ @MessagePattern('storage.list')
48
+ list(@Payload() payload: ListPayload): Promise<StorageRef[]> {
49
+ return this.strategy.list(payload.userId, payload.prefix);
50
+ }
51
+ }
@@ -0,0 +1,28 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import { NestFactory } from '@nestjs/core';
3
+ import { MicroserviceOptions } from '@nestjs/microservices';
4
+ import { buildTransportMS } from '@icore/shared';
5
+ import { AppModule } from './app/app.module';
6
+
7
+ async function bootstrap() {
8
+ const app = await NestFactory.createMicroservice<MicroserviceOptions>(
9
+ AppModule,
10
+ buildTransportMS('UPLOAD'),
11
+ );
12
+ await app.listen();
13
+ }
14
+
15
+ bootstrap()
16
+ .then(() => {
17
+ const logger = new Logger('Upload-Bootstrap');
18
+ logger.log(
19
+ `Upload MS Bootstrap completed: transport=${process.env.UPLOAD_TRANSPORT ?? 'tcp'} host=${process.env.UPLOAD_HOST ?? '127.0.0.1'} port=${process.env.UPLOAD_PORT ?? '4002'}`,
20
+ );
21
+ })
22
+ .catch((err) => {
23
+ new Logger('Upload-Bootstrap').error(
24
+ 'Upload MS bootstrap failed',
25
+ err instanceof Error ? err.stack : err,
26
+ );
27
+ process.exit(1);
28
+ });
@@ -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"],
8
+ "experimentalDecorators": true,
9
+ "emitDecoratorMetadata": true,
10
+ "target": "es2021"
11
+ },
12
+ "include": ["src/**/*.ts"]
13
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "files": [],
4
+ "include": [],
5
+ "references": [
6
+ {
7
+ "path": "./tsconfig.app.json"
8
+ },
9
+ {
10
+ "path": "./tsconfig.spec.json"
11
+ }
12
+ ],
13
+ "compilerOptions": {
14
+ "esModuleInterop": true
15
+ }
16
+ }
@@ -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,22 @@
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/microservices/upload',
8
+ plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
9
+ test: {
10
+ name: 'upload',
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
+ passWithNoTests: true,
17
+ coverage: {
18
+ reportsDirectory: '../../../coverage/apps/microservices/upload',
19
+ provider: 'v8' as const,
20
+ },
21
+ },
22
+ }));
@@ -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/microservices/upload'),
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,2 @@
1
+ # API base URL — dev server reverse-proxies /api to the gateway
2
+ VITE_API_URL=/api
@@ -0,0 +1,10 @@
1
+ import baseConfig from '../../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx'],
7
+ // Override or add rules here
8
+ rules: {},
9
+ },
10
+ ];