@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,58 @@
1
+ import { describe, expect, it, beforeEach } from 'vitest';
2
+ import type { FileInput, StorageStrategy } from '../storage';
3
+
4
+ const fixture = (): FileInput => ({
5
+ buffer: Buffer.from('hello world'),
6
+ filename: 'hello.txt',
7
+ mimeType: 'text/plain',
8
+ });
9
+
10
+ export function runStorageContract(name: string, factory: () => StorageStrategy): void {
11
+ describe(`StorageStrategy contract: ${name}`, () => {
12
+ let strategy: StorageStrategy;
13
+
14
+ beforeEach(() => {
15
+ strategy = factory();
16
+ });
17
+
18
+ it('upload returns a StorageRef under the user prefix', async () => {
19
+ const ref = await strategy.upload('user-1', fixture());
20
+ expect(ref.path.startsWith('user-1/')).toBe(true);
21
+ expect(ref.bucket).toBeTruthy();
22
+ });
23
+
24
+ it('list returns previously uploaded files for the same user', async () => {
25
+ await strategy.upload('user-2', fixture());
26
+ const refs = await strategy.list('user-2');
27
+ expect(refs.length).toBe(1);
28
+ });
29
+
30
+ it('list isolates users', async () => {
31
+ await strategy.upload('user-a', fixture());
32
+ expect(await strategy.list('user-b')).toEqual([]);
33
+ });
34
+
35
+ it('getSignedUrl returns a non-empty string', async () => {
36
+ const ref = await strategy.upload('user-3', fixture());
37
+ const url = await strategy.getSignedUrl('user-3', ref, 60);
38
+ expect(typeof url).toBe('string');
39
+ expect(url.length).toBeGreaterThan(0);
40
+ });
41
+
42
+ it('remove deletes the file', async () => {
43
+ const ref = await strategy.upload('user-4', fixture());
44
+ await strategy.remove('user-4', ref);
45
+ expect(await strategy.list('user-4')).toEqual([]);
46
+ });
47
+
48
+ it('signed URL for a foreign user throws', async () => {
49
+ const ref = await strategy.upload('owner', fixture());
50
+ await expect(strategy.getSignedUrl('attacker', ref)).rejects.toThrow();
51
+ });
52
+
53
+ it('remove by a foreign user throws', async () => {
54
+ const ref = await strategy.upload('owner', fixture());
55
+ await expect(strategy.remove('attacker', ref)).rejects.toThrow();
56
+ });
57
+ });
58
+ }
@@ -0,0 +1,73 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { AuthSession, AuthStrategy, VerifiedToken } from '../auth';
3
+
4
+ interface StoredUser {
5
+ id: string;
6
+ email: string;
7
+ password: string;
8
+ role?: string;
9
+ }
10
+
11
+ export class FakeAuthStrategy implements AuthStrategy {
12
+ private readonly users = new Map<string, StoredUser>();
13
+ private readonly tokensToUid = new Map<string, string>();
14
+ private readonly refreshToUid = new Map<string, string>();
15
+
16
+ async signUp(email: string, password: string): Promise<AuthSession> {
17
+ if (this.users.has(email)) throw new Error('user_exists');
18
+ const user: StoredUser = { id: randomUUID(), email, password };
19
+ this.users.set(email, user);
20
+ return this.issueSession(user);
21
+ }
22
+
23
+ async signIn(email: string, password: string): Promise<AuthSession> {
24
+ const user = this.users.get(email);
25
+ if (!user || user.password !== password) throw new Error('invalid_credentials');
26
+ return this.issueSession(user);
27
+ }
28
+
29
+ async refresh(refreshToken: string): Promise<AuthSession> {
30
+ const uid = this.refreshToUid.get(refreshToken);
31
+ if (!uid) throw new Error('invalid_refresh_token');
32
+ this.refreshToUid.delete(refreshToken);
33
+ const user = this.findById(uid);
34
+ return this.issueSession(user);
35
+ }
36
+
37
+ async verifyToken(token: string): Promise<VerifiedToken> {
38
+ const uid = this.tokensToUid.get(token);
39
+ if (!uid) throw new Error('invalid_token');
40
+ const user = this.findById(uid);
41
+ return { uid: user.id, email: user.email, role: user.role };
42
+ }
43
+
44
+ async setRole(uid: string, role: string): Promise<void> {
45
+ const user = this.findById(uid);
46
+ user.role = role;
47
+ }
48
+
49
+ async getRole(uid: string): Promise<string | null> {
50
+ const user = this.findById(uid);
51
+ return user.role ?? null;
52
+ }
53
+
54
+ private findById(uid: string): StoredUser {
55
+ for (const user of this.users.values()) {
56
+ if (user.id === uid) return user;
57
+ }
58
+ throw new Error('user_missing');
59
+ }
60
+
61
+ private issueSession(user: StoredUser): AuthSession {
62
+ const accessToken = randomUUID();
63
+ const refreshToken = randomUUID();
64
+ this.tokensToUid.set(accessToken, user.id);
65
+ this.refreshToUid.set(refreshToken, user.id);
66
+ return {
67
+ accessToken,
68
+ refreshToken,
69
+ expiresIn: 3600,
70
+ user: { id: user.id, email: user.email },
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,51 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { FileInput, StorageRef, StorageStrategy } from '../storage';
3
+
4
+ interface StoredFile {
5
+ ownerId: string;
6
+ ref: StorageRef;
7
+ bytes: Buffer;
8
+ mimeType: string;
9
+ }
10
+
11
+ export class FakeStorageStrategy implements StorageStrategy {
12
+ private readonly bucket = 'fake-bucket';
13
+ private readonly files = new Map<string, StoredFile>();
14
+
15
+ async upload(userId: string, file: FileInput): Promise<StorageRef> {
16
+ const path = `${userId}/${randomUUID()}-${file.filename}`;
17
+ const ref: StorageRef = { bucket: this.bucket, path };
18
+ this.files.set(this.key(ref), {
19
+ ownerId: userId,
20
+ ref,
21
+ bytes: file.buffer,
22
+ mimeType: file.mimeType,
23
+ });
24
+ return ref;
25
+ }
26
+
27
+ async remove(userId: string, ref: StorageRef): Promise<void> {
28
+ const file = this.files.get(this.key(ref));
29
+ if (!file) throw new Error('not_found');
30
+ if (file.ownerId !== userId) throw new Error('forbidden');
31
+ this.files.delete(this.key(ref));
32
+ }
33
+
34
+ async getSignedUrl(userId: string, ref: StorageRef, ttlSec = 900): Promise<string> {
35
+ const file = this.files.get(this.key(ref));
36
+ if (!file) throw new Error('not_found');
37
+ if (file.ownerId !== userId) throw new Error('forbidden');
38
+ return `fake://${ref.bucket}/${ref.path}?ttl=${ttlSec}`;
39
+ }
40
+
41
+ async list(userId: string, prefix?: string): Promise<StorageRef[]> {
42
+ return [...this.files.values()]
43
+ .filter((f) => f.ownerId === userId)
44
+ .filter((f) => (prefix ? f.ref.path.startsWith(prefix) : true))
45
+ .map((f) => f.ref);
46
+ }
47
+
48
+ private key(ref: StorageRef): string {
49
+ return `${ref.bucket}::${ref.path}`;
50
+ }
51
+ }
@@ -0,0 +1,2 @@
1
+ export * from './fake-auth';
2
+ export * from './fake-storage';
@@ -0,0 +1,5 @@
1
+ export * from './auth';
2
+ export * from './storage';
3
+ export * from './contract/auth-contract';
4
+ export * from './contract/storage-contract';
5
+ export * from './fakes';
@@ -0,0 +1,17 @@
1
+ export interface StorageRef {
2
+ bucket: string;
3
+ path: string;
4
+ }
5
+
6
+ export interface FileInput {
7
+ buffer: Buffer;
8
+ filename: string;
9
+ mimeType: string;
10
+ }
11
+
12
+ export interface StorageStrategy {
13
+ upload(userId: string, file: FileInput): Promise<StorageRef>;
14
+ remove(userId: string, ref: StorageRef): Promise<void>;
15
+ getSignedUrl(userId: string, ref: StorageRef, ttlSec?: number): Promise<string>;
16
+ list(userId: string, prefix?: string): Promise<StorageRef[]>;
17
+ }
@@ -0,0 +1,55 @@
1
+ import { Transport, type ClientOptions, type MicroserviceOptions } from '@nestjs/microservices';
2
+
3
+ function required(name: string): string {
4
+ const value = process.env[name];
5
+ if (!value) throw new Error(`Missing required env var: ${name}`);
6
+ return value;
7
+ }
8
+
9
+ function requiredPort(name: string): number {
10
+ const raw = required(name);
11
+ const port = Number(raw);
12
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
13
+ throw new Error(`Invalid ${name}: expected integer 1-65535, got ${raw}`);
14
+ }
15
+ return port;
16
+ }
17
+
18
+ export function buildTransport(prefix: string): ClientOptions {
19
+ const kind = (process.env[`${prefix}_TRANSPORT`] ?? 'tcp').toLowerCase();
20
+ switch (kind) {
21
+ case 'tcp':
22
+ return {
23
+ transport: Transport.TCP,
24
+ options: {
25
+ host: required(`${prefix}_HOST`),
26
+ port: requiredPort(`${prefix}_PORT`),
27
+ },
28
+ };
29
+ case 'redis':
30
+ // ioredis accepts a connection URL string; the NestJS RedisOptions type
31
+ // exposes host/port fields but passes options directly to ioredis which
32
+ // also accepts a url field at runtime.
33
+ return {
34
+ transport: Transport.REDIS,
35
+ options: { url: required(`${prefix}_REDIS_URL`) },
36
+ } as unknown as ClientOptions;
37
+ case 'nats':
38
+ return {
39
+ transport: Transport.NATS,
40
+ options: { servers: required(`${prefix}_NATS_URL`).split(',') },
41
+ };
42
+ default:
43
+ throw new Error(`Unknown transport: ${kind}`);
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Same env contract as {@link buildTransport}, but typed for the server
49
+ * side: pass the result directly to `NestFactory.createMicroservice(...)`.
50
+ * Eliminates the `as unknown as MicroserviceOptions` cast at every MS
51
+ * bootstrap site.
52
+ */
53
+ export function buildTransportMS(prefix: string): MicroserviceOptions {
54
+ return buildTransport(prefix) as unknown as MicroserviceOptions;
55
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "importHelpers": true,
9
+ "noImplicitOverride": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "noPropertyAccessFromIndexSignature": true
13
+ },
14
+ "files": [],
15
+ "include": [],
16
+ "references": [
17
+ {
18
+ "path": "./tsconfig.lib.json"
19
+ },
20
+ {
21
+ "path": "./tsconfig.spec.json"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": [
10
+ "vite.config.ts",
11
+ "vite.config.mts",
12
+ "vitest.config.ts",
13
+ "vitest.config.mts",
14
+ "src/**/*.test.ts",
15
+ "src/**/*.spec.ts",
16
+ "src/**/*.test.tsx",
17
+ "src/**/*.spec.tsx",
18
+ "src/**/*.test.js",
19
+ "src/**/*.spec.js",
20
+ "src/**/*.test.jsx",
21
+ "src/**/*.spec.jsx"
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
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
+ "vite.config.ts",
9
+ "vite.config.mts",
10
+ "vitest.config.ts",
11
+ "vitest.config.mts",
12
+ "src/**/*.test.ts",
13
+ "src/**/*.spec.ts",
14
+ "src/**/*.test.tsx",
15
+ "src/**/*.spec.tsx",
16
+ "src/**/*.test.js",
17
+ "src/**/*.spec.js",
18
+ "src/**/*.test.jsx",
19
+ "src/**/*.spec.jsx",
20
+ "src/**/*.d.ts"
21
+ ]
22
+ }
@@ -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/libs/shared',
8
+ plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
9
+ test: {
10
+ name: 'shared',
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/libs/shared',
18
+ provider: 'v8' as const,
19
+ },
20
+ },
21
+ }));
@@ -0,0 +1,11 @@
1
+ # storage-cloudinary
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build storage-cloudinary` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test storage-cloudinary` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,23 @@
1
+ import baseConfig from '../../../eslint.config.mjs';
2
+
3
+ export default [
4
+ ...baseConfig,
5
+ {
6
+ files: ['**/*.json'],
7
+ rules: {
8
+ '@nx/dependency-checks': [
9
+ 'error',
10
+ {
11
+ ignoredFiles: [
12
+ '{projectRoot}/eslint.config.{js,cjs,mjs,ts,cts,mts}',
13
+ '{projectRoot}/vitest.config.{js,ts,mjs,mts}',
14
+ ],
15
+ ignoredDependencies: ['@icore/shared'],
16
+ },
17
+ ],
18
+ },
19
+ languageOptions: {
20
+ parser: await import('jsonc-eslint-parser'),
21
+ },
22
+ },
23
+ ];
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "@icore/storage-cloudinary",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "dependencies": {
9
+ "@icore/shared": "*",
10
+ "tslib": "^2.3.0"
11
+ },
12
+ "devDependencies": {
13
+ "vitest": "^4.0.0"
14
+ }
15
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "storage-cloudinary",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/storage-strategies/cloudinary/src",
5
+ "projectType": "library",
6
+ "tags": [],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/js:tsc",
10
+ "outputs": ["{options.outputPath}"],
11
+ "options": {
12
+ "outputPath": "dist/libs/storage-strategies/cloudinary",
13
+ "main": "libs/storage-strategies/cloudinary/src/index.ts",
14
+ "tsConfig": "libs/storage-strategies/cloudinary/tsconfig.lib.json",
15
+ "assets": ["libs/storage-strategies/cloudinary/*.md"]
16
+ }
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,2 @@
1
+ export * from './lib/cloudinary-storage.strategy.js';
2
+ export * from './lib/testing/mock-cloudinary.js';
@@ -0,0 +1,8 @@
1
+ import { runStorageContract } from '@icore/shared';
2
+ import { CloudinaryStorageStrategy } from '../cloudinary-storage.strategy.js';
3
+ import { createMockCloudinary } from '../testing/mock-cloudinary.js';
4
+
5
+ runStorageContract('CloudinaryStorageStrategy', () => {
6
+ const api = createMockCloudinary();
7
+ return new CloudinaryStorageStrategy({ api, bucket: 'icore-uploads' });
8
+ });
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { FileInput, StorageRef, StorageStrategy } from '@icore/shared';
3
+
4
+ export interface CloudinaryUploadResult {
5
+ public_id: string;
6
+ secure_url: string;
7
+ }
8
+
9
+ export interface CloudinaryApiLike {
10
+ upload(
11
+ buffer: Buffer,
12
+ opts: { public_id: string; resource_type?: 'image' | 'video' | 'raw' },
13
+ ): Promise<CloudinaryUploadResult>;
14
+ destroy(publicId: string): Promise<void>;
15
+ privateDownloadUrl(
16
+ publicId: string,
17
+ format: string | undefined,
18
+ opts?: { expires_at?: number },
19
+ ): string;
20
+ resources(opts: { prefix?: string; type?: string }): Promise<{
21
+ resources: Array<{ public_id: string }>;
22
+ }>;
23
+ }
24
+
25
+ export interface CloudinaryStorageStrategyOptions {
26
+ api: CloudinaryApiLike;
27
+ bucket: string; // Cloudinary doesn't have buckets; we synthesize the ref.bucket field
28
+ }
29
+
30
+ export class CloudinaryStorageStrategy implements StorageStrategy {
31
+ private readonly api: CloudinaryApiLike;
32
+ private readonly bucket: string;
33
+
34
+ constructor(opts: CloudinaryStorageStrategyOptions) {
35
+ this.api = opts.api;
36
+ this.bucket = opts.bucket;
37
+ }
38
+
39
+ async upload(userId: string, file: FileInput): Promise<StorageRef> {
40
+ const publicId = `${userId}/${randomUUID()}-${file.filename}`;
41
+ const result = await this.api.upload(file.buffer, {
42
+ public_id: publicId,
43
+ resource_type: this.detectResourceType(file.mimeType),
44
+ });
45
+ return { bucket: this.bucket, path: result.public_id };
46
+ }
47
+
48
+ async remove(userId: string, ref: StorageRef): Promise<void> {
49
+ this.assertOwner(userId, ref);
50
+ await this.api.destroy(ref.path);
51
+ }
52
+
53
+ async getSignedUrl(userId: string, ref: StorageRef, ttlSec = 900): Promise<string> {
54
+ this.assertOwner(userId, ref);
55
+ return this.api.privateDownloadUrl(ref.path, undefined, {
56
+ expires_at: Math.floor(Date.now() / 1000) + ttlSec,
57
+ });
58
+ }
59
+
60
+ async list(userId: string, prefix?: string): Promise<StorageRef[]> {
61
+ const folder = prefix ? `${userId}/${prefix}` : userId;
62
+ const { resources } = await this.api.resources({ prefix: `${folder}/` });
63
+ return resources.map((r) => ({ bucket: this.bucket, path: r.public_id }));
64
+ }
65
+
66
+ private assertOwner(userId: string, ref: StorageRef): void {
67
+ if (!ref.path.startsWith(`${userId}/`)) throw new Error('forbidden');
68
+ }
69
+
70
+ private detectResourceType(mimeType: string): 'image' | 'video' | 'raw' {
71
+ if (mimeType.startsWith('image/')) return 'image';
72
+ if (mimeType.startsWith('video/')) return 'video';
73
+ return 'raw';
74
+ }
75
+ }
@@ -0,0 +1,36 @@
1
+ import type { CloudinaryApiLike, CloudinaryUploadResult } from '../cloudinary-storage.strategy.js';
2
+
3
+ interface StoredObject {
4
+ bytes: Buffer;
5
+ resourceType: 'image' | 'video' | 'raw';
6
+ }
7
+
8
+ export function createMockCloudinary(): CloudinaryApiLike {
9
+ const objects = new Map<string, StoredObject>();
10
+
11
+ return {
12
+ async upload(buffer, opts) {
13
+ const publicId = opts.public_id;
14
+ if (objects.has(publicId)) throw new Error('exists');
15
+ objects.set(publicId, { bytes: buffer, resourceType: opts.resource_type ?? 'raw' });
16
+ return {
17
+ public_id: publicId,
18
+ secure_url: `https://mock.cloudinary/raw/${publicId}`,
19
+ } satisfies CloudinaryUploadResult;
20
+ },
21
+ async destroy(publicId) {
22
+ if (!objects.has(publicId)) throw new Error('not_found');
23
+ objects.delete(publicId);
24
+ },
25
+ privateDownloadUrl(publicId, _format, opts) {
26
+ if (!objects.has(publicId)) throw new Error('not_found');
27
+ return `https://mock.cloudinary/signed/${publicId}?ttl=${opts?.expires_at ?? 'na'}`;
28
+ },
29
+ async resources(opts) {
30
+ const matches = [...objects.keys()].filter((id) =>
31
+ opts.prefix ? id.startsWith(opts.prefix) : true,
32
+ );
33
+ return { resources: matches.map((public_id) => ({ public_id })) };
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "extends": "../../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "module": "node16",
5
+ "moduleResolution": "node16",
6
+ "forceConsistentCasingInFileNames": true,
7
+ "strict": true,
8
+ "importHelpers": true,
9
+ "noImplicitOverride": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "noPropertyAccessFromIndexSignature": true
13
+ },
14
+ "files": [],
15
+ "include": [],
16
+ "references": [
17
+ {
18
+ "path": "./tsconfig.lib.json"
19
+ },
20
+ {
21
+ "path": "./tsconfig.spec.json"
22
+ }
23
+ ]
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../../dist/out-tsc",
5
+ "declaration": true,
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": [
10
+ "vite.config.ts",
11
+ "vite.config.mts",
12
+ "vitest.config.ts",
13
+ "vitest.config.mts",
14
+ "src/**/*.test.ts",
15
+ "src/**/*.spec.ts",
16
+ "src/**/*.test.tsx",
17
+ "src/**/*.spec.tsx",
18
+ "src/**/*.test.js",
19
+ "src/**/*.spec.js",
20
+ "src/**/*.test.jsx",
21
+ "src/**/*.spec.jsx"
22
+ ]
23
+ }
@@ -0,0 +1,22 @@
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
+ "vite.config.ts",
9
+ "vite.config.mts",
10
+ "vitest.config.ts",
11
+ "vitest.config.mts",
12
+ "src/**/*.test.ts",
13
+ "src/**/*.spec.ts",
14
+ "src/**/*.test.tsx",
15
+ "src/**/*.spec.tsx",
16
+ "src/**/*.test.js",
17
+ "src/**/*.spec.js",
18
+ "src/**/*.test.jsx",
19
+ "src/**/*.spec.jsx",
20
+ "src/**/*.d.ts"
21
+ ]
22
+ }
@@ -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/libs/storage-strategies/cloudinary',
8
+ plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
9
+ test: {
10
+ name: 'storage-cloudinary',
11
+ watch: false,
12
+ globals: true,
13
+ environment: 'node',
14
+ include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
15
+ passWithNoTests: true,
16
+ reporters: ['default'],
17
+ coverage: {
18
+ reportsDirectory: '../../../coverage/libs/storage-strategies/cloudinary',
19
+ provider: 'v8' as const,
20
+ },
21
+ },
22
+ }));
@@ -0,0 +1,11 @@
1
+ # storage-firebase
2
+
3
+ This library was generated with [Nx](https://nx.dev).
4
+
5
+ ## Building
6
+
7
+ Run `nx build storage-firebase` to build the library.
8
+
9
+ ## Running unit tests
10
+
11
+ Run `nx test storage-firebase` to execute the unit tests via [Vitest](https://vitest.dev/).