@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,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
MAX_FILES=50
|
|
4
|
+
DASHES="--------------------------------------------------------"
|
|
5
|
+
|
|
6
|
+
echo_print() {
|
|
7
|
+
local message="$1"
|
|
8
|
+
local color="${2:-\033[0m}"
|
|
9
|
+
printf "%s\n" "$DASHES"
|
|
10
|
+
printf "%b\n" "${color}${message}\033[0m"
|
|
11
|
+
printf "%s\n" "$DASHES"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
check_command() {
|
|
15
|
+
local command="$1"
|
|
16
|
+
local success_message="$2"
|
|
17
|
+
local failure_message="$3"
|
|
18
|
+
|
|
19
|
+
if eval "$command"; then
|
|
20
|
+
echo_print "✅ $success_message" "\033[32m"
|
|
21
|
+
else
|
|
22
|
+
echo_print "❌ $failure_message" "\033[31m"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
FILES_CHANGED=$(git diff --cached --name-only | wc -l)
|
|
28
|
+
NON_MD_FILES=$(git diff --cached --name-only | grep -vi '\.md$' | wc -l)
|
|
29
|
+
|
|
30
|
+
if [ "$FILES_CHANGED" -gt 0 ] && [ "$NON_MD_FILES" -eq 0 ]; then
|
|
31
|
+
echo_print "📝 Only Markdown files staged. Skipping lint-staged + nx checks." "\033[34m"
|
|
32
|
+
exit 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ "$FILES_CHANGED" -gt "$MAX_FILES" ]; then
|
|
36
|
+
echo "$DASHES"
|
|
37
|
+
echo "🔹 Max files per commit: $MAX_FILES"
|
|
38
|
+
echo "❌ You are trying to commit more than $MAX_FILES files."
|
|
39
|
+
echo "$DASHES"
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
echo_print "✅ Files staged: $FILES_CHANGED" "\033[32m"
|
|
44
|
+
|
|
45
|
+
check_command "yarn lint-staged --concurrent false --relative" \
|
|
46
|
+
"lint-staged passed." \
|
|
47
|
+
"lint-staged failed. Fix the issues before committing."
|
|
48
|
+
|
|
49
|
+
if [ -d libs ] || [ -d apps ]; then
|
|
50
|
+
check_command "yarn nx affected -t lint" \
|
|
51
|
+
"nx affected lint passed." \
|
|
52
|
+
"nx affected lint failed."
|
|
53
|
+
check_command "yarn nx affected -t test" \
|
|
54
|
+
"nx affected tests passed." \
|
|
55
|
+
"nx affected tests failed."
|
|
56
|
+
fi
|
package/templates/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
22
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
API_ORIGIN=http://localhost
|
|
2
|
+
API_PORT=3001
|
|
3
|
+
|
|
4
|
+
# Auth MS transport — must match apps/microservices/auth/.env
|
|
5
|
+
AUTH_TRANSPORT=tcp
|
|
6
|
+
AUTH_HOST=127.0.0.1
|
|
7
|
+
AUTH_PORT=4001
|
|
8
|
+
# AUTH_REDIS_URL=redis://localhost:6379
|
|
9
|
+
# AUTH_NATS_URL=nats://localhost:4222
|
|
10
|
+
|
|
11
|
+
# Upload MS transport — must match apps/microservices/upload/.env
|
|
12
|
+
UPLOAD_TRANSPORT=tcp
|
|
13
|
+
UPLOAD_HOST=127.0.0.1
|
|
14
|
+
UPLOAD_PORT=4002
|
|
15
|
+
# UPLOAD_REDIS_URL=redis://localhost:6379
|
|
16
|
+
# UPLOAD_NATS_URL=nats://localhost:4222
|
|
17
|
+
|
|
18
|
+
# Per-request multipart file size cap (KB). Default 5120 (5 MB) when unset.
|
|
19
|
+
MAX_FILE_SIZE_KB=5120
|
|
@@ -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
|
+
'{projectRoot}/webpack.config.{js,ts}',
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
languageOptions: {
|
|
20
|
+
parser: await import('jsonc-eslint-parser'),
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"devDependencies": {
|
|
6
|
+
"@types/multer": "*"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@icore/auth-client": "*",
|
|
10
|
+
"@icore/shared": "*",
|
|
11
|
+
"@icore/upload-client": "*",
|
|
12
|
+
"@nestjs/common": "^11.1.24",
|
|
13
|
+
"@nestjs/config": "^4.0.4",
|
|
14
|
+
"@nestjs/core": "^11.1.24",
|
|
15
|
+
"@nestjs/platform-express": "^11.1.24",
|
|
16
|
+
"@nestjs/swagger": "^11.4.4",
|
|
17
|
+
"@nestjs/throttler": "^6.5.0",
|
|
18
|
+
"express": "^4.22.2"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api",
|
|
3
|
+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
+
"sourceRoot": "apps/api/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/api"
|
|
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/api/package.json",
|
|
26
|
+
"{workspaceRoot}/dist/apps/api/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/api/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
|
+
"test": {
|
|
46
|
+
"executor": "@nx/vitest:test",
|
|
47
|
+
"outputs": ["{workspaceRoot}/coverage/apps/api"],
|
|
48
|
+
"options": {
|
|
49
|
+
"config": "apps/api/vitest.config.mts"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"lint": {
|
|
53
|
+
"executor": "@nx/eslint:lint",
|
|
54
|
+
"outputs": ["{options.outputFile}"]
|
|
55
|
+
},
|
|
56
|
+
"serve": {
|
|
57
|
+
"continuous": true,
|
|
58
|
+
"executor": "@nx/js:node",
|
|
59
|
+
"defaultConfiguration": "development",
|
|
60
|
+
"dependsOn": ["build"],
|
|
61
|
+
"options": {
|
|
62
|
+
"buildTarget": "api:build",
|
|
63
|
+
"runBuildTargetDependencies": false
|
|
64
|
+
},
|
|
65
|
+
"configurations": {
|
|
66
|
+
"development": {
|
|
67
|
+
"buildTarget": "api:build:development"
|
|
68
|
+
},
|
|
69
|
+
"production": {
|
|
70
|
+
"buildTarget": "api:build:production"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"tags": []
|
|
76
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { ForbiddenException, type ExecutionContext } from '@nestjs/common';
|
|
4
|
+
import { AbilityFactory } from '../ability.factory';
|
|
5
|
+
import { AbilityGuard } from '../ability.guard';
|
|
6
|
+
|
|
7
|
+
function ctx(user: { uid: string; role?: string } | undefined): ExecutionContext {
|
|
8
|
+
return {
|
|
9
|
+
getHandler: () => undefined,
|
|
10
|
+
getClass: () => undefined,
|
|
11
|
+
switchToHttp: () => ({ getRequest: () => ({ user }) }),
|
|
12
|
+
} as unknown as ExecutionContext;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('AbilityGuard', () => {
|
|
16
|
+
const factory = new AbilityFactory();
|
|
17
|
+
|
|
18
|
+
it('passes when no @CheckAbility metadata is present', () => {
|
|
19
|
+
const reflector = {
|
|
20
|
+
getAllAndOverride: vi.fn().mockReturnValue(undefined),
|
|
21
|
+
} as unknown as Reflector;
|
|
22
|
+
const guard = new AbilityGuard(reflector, factory);
|
|
23
|
+
expect(guard.canActivate(ctx({ uid: 'u', role: 'user' }))).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('admin passes manage/all', () => {
|
|
27
|
+
const reflector = {
|
|
28
|
+
getAllAndOverride: vi.fn().mockReturnValue({ action: 'manage', subject: 'all' }),
|
|
29
|
+
} as unknown as Reflector;
|
|
30
|
+
const guard = new AbilityGuard(reflector, factory);
|
|
31
|
+
expect(guard.canActivate(ctx({ uid: 'u', role: 'admin' }))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('regular user is denied manage/all', () => {
|
|
35
|
+
const reflector = {
|
|
36
|
+
getAllAndOverride: vi.fn().mockReturnValue({ action: 'manage', subject: 'all' }),
|
|
37
|
+
} as unknown as Reflector;
|
|
38
|
+
const guard = new AbilityGuard(reflector, factory);
|
|
39
|
+
expect(() => guard.canActivate(ctx({ uid: 'u', role: 'user' }))).toThrow(ForbiddenException);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('anonymous (no req.user) is denied manage/all', () => {
|
|
43
|
+
const reflector = {
|
|
44
|
+
getAllAndOverride: vi.fn().mockReturnValue({ action: 'manage', subject: 'all' }),
|
|
45
|
+
} as unknown as Reflector;
|
|
46
|
+
const guard = new AbilityGuard(reflector, factory);
|
|
47
|
+
expect(() => guard.canActivate(ctx(undefined))).toThrow(ForbiddenException);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
3
|
+
import { AbilityFactory } from './ability.factory';
|
|
4
|
+
import { AbilityGuard } from './ability.guard';
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
providers: [AbilityFactory, { provide: APP_GUARD, useClass: AbilityGuard }],
|
|
8
|
+
exports: [AbilityFactory],
|
|
9
|
+
})
|
|
10
|
+
export class AbilitiesModule {}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { defineAbilitiesFor, type AppAbility, type VerifiedToken } from '@icore/shared';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class AbilityFactory {
|
|
6
|
+
forUser(token: VerifiedToken | null | undefined): AppAbility {
|
|
7
|
+
if (!token) return defineAbilitiesFor(null);
|
|
8
|
+
return defineAbilitiesFor({
|
|
9
|
+
id: token.uid,
|
|
10
|
+
role: token.role === 'admin' ? 'admin' : 'user',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import type { Request } from 'express';
|
|
4
|
+
import type { VerifiedToken } from '@icore/shared';
|
|
5
|
+
import { AbilityFactory } from './ability.factory';
|
|
6
|
+
import { CHECK_ABILITY_KEY, type RequiredRule } from './check-ability.decorator';
|
|
7
|
+
|
|
8
|
+
@Injectable()
|
|
9
|
+
export class AbilityGuard implements CanActivate {
|
|
10
|
+
constructor(
|
|
11
|
+
private readonly reflector: Reflector,
|
|
12
|
+
private readonly factory: AbilityFactory,
|
|
13
|
+
) {}
|
|
14
|
+
|
|
15
|
+
canActivate(ctx: ExecutionContext): boolean {
|
|
16
|
+
const required = this.reflector.getAllAndOverride<RequiredRule | undefined>(CHECK_ABILITY_KEY, [
|
|
17
|
+
ctx.getHandler(),
|
|
18
|
+
ctx.getClass(),
|
|
19
|
+
]);
|
|
20
|
+
if (!required) return true;
|
|
21
|
+
|
|
22
|
+
const req = ctx.switchToHttp().getRequest<Request & { user?: VerifiedToken }>();
|
|
23
|
+
const ability = this.factory.forUser(req.user);
|
|
24
|
+
if (!ability.can(required.action, required.subject)) {
|
|
25
|
+
throw new ForbiddenException();
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { SetMetadata } from '@nestjs/common';
|
|
2
|
+
import type { AbilityAction, AbilitySubject } from '@icore/shared';
|
|
3
|
+
|
|
4
|
+
export const CHECK_ABILITY_KEY = 'checkAbility';
|
|
5
|
+
|
|
6
|
+
export interface RequiredRule {
|
|
7
|
+
action: AbilityAction;
|
|
8
|
+
subject: AbilitySubject;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const CheckAbility = (action: AbilityAction, subject: AbilitySubject) =>
|
|
12
|
+
SetMetadata(CHECK_ABILITY_KEY, { action, subject } satisfies RequiredRule);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { ThrottlerModule, seconds } from '@nestjs/throttler';
|
|
4
|
+
import { AuthModule } from './auth/auth.module';
|
|
5
|
+
import { ProfileModule } from './profile/profile.module';
|
|
6
|
+
import { AbilitiesModule } from './abilities/abilities.module';
|
|
7
|
+
import { StorageModule } from './storage/storage.module';
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [
|
|
11
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
12
|
+
ThrottlerModule.forRoot([{ name: 'auth-burst', ttl: seconds(60), limit: 10 }]),
|
|
13
|
+
AuthModule,
|
|
14
|
+
AbilitiesModule,
|
|
15
|
+
ProfileModule,
|
|
16
|
+
StorageModule,
|
|
17
|
+
],
|
|
18
|
+
})
|
|
19
|
+
export class AppModule {}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { Reflector } from '@nestjs/core';
|
|
3
|
+
import { UnauthorizedException, type ExecutionContext } from '@nestjs/common';
|
|
4
|
+
import { AuthGuard } from '../auth.guard';
|
|
5
|
+
|
|
6
|
+
interface MockReq {
|
|
7
|
+
headers: Record<string, string | undefined>;
|
|
8
|
+
user?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ctx(headers: Record<string, string | undefined>): ExecutionContext {
|
|
12
|
+
const req: MockReq = { headers };
|
|
13
|
+
return {
|
|
14
|
+
getHandler: () => undefined,
|
|
15
|
+
getClass: () => undefined,
|
|
16
|
+
switchToHttp: () => ({ getRequest: () => req }),
|
|
17
|
+
} as unknown as ExecutionContext;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('AuthGuard', () => {
|
|
21
|
+
const makeGuard = (overrides: { isPublic?: boolean; verify?: () => Promise<unknown> } = {}) => {
|
|
22
|
+
const reflector = {
|
|
23
|
+
getAllAndOverride: vi.fn().mockReturnValue(overrides.isPublic ?? false),
|
|
24
|
+
} as unknown as Reflector;
|
|
25
|
+
const client = {
|
|
26
|
+
verify: vi
|
|
27
|
+
.fn()
|
|
28
|
+
.mockImplementation(overrides.verify ?? (() => Promise.resolve({ uid: 'u1' }))),
|
|
29
|
+
};
|
|
30
|
+
return { guard: new AuthGuard(reflector, client as never), client };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
it('lets @Public routes through without checking the header', async () => {
|
|
34
|
+
const { guard } = makeGuard({ isPublic: true });
|
|
35
|
+
await expect(guard.canActivate(ctx({}))).resolves.toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects when Authorization header is missing', async () => {
|
|
39
|
+
const { guard } = makeGuard();
|
|
40
|
+
await expect(guard.canActivate(ctx({ authorization: undefined }))).rejects.toBeInstanceOf(
|
|
41
|
+
UnauthorizedException,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('rejects when scheme is not Bearer', async () => {
|
|
46
|
+
const { guard } = makeGuard();
|
|
47
|
+
await expect(guard.canActivate(ctx({ authorization: 'Basic abc' }))).rejects.toBeInstanceOf(
|
|
48
|
+
UnauthorizedException,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('verifies token and attaches user on success', async () => {
|
|
53
|
+
const { guard } = makeGuard({ verify: () => Promise.resolve({ uid: 'u1', role: 'user' }) });
|
|
54
|
+
const c = ctx({ authorization: 'Bearer abc' });
|
|
55
|
+
await expect(guard.canActivate(c)).resolves.toBe(true);
|
|
56
|
+
const req = c.switchToHttp().getRequest() as MockReq;
|
|
57
|
+
expect((req.user as { uid: string }).uid).toBe('u1');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('rejects when verify throws', async () => {
|
|
61
|
+
const { guard } = makeGuard({ verify: () => Promise.reject(new Error('bad')) });
|
|
62
|
+
await expect(guard.canActivate(ctx({ authorization: 'Bearer abc' }))).rejects.toBeInstanceOf(
|
|
63
|
+
UnauthorizedException,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Body, Controller, Post } from '@nestjs/common';
|
|
2
|
+
import { Throttle, seconds } from '@nestjs/throttler';
|
|
3
|
+
import { ApiBody, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
4
|
+
import { AuthClientService } from '@icore/auth-client';
|
|
5
|
+
import { Public } from './public.decorator';
|
|
6
|
+
|
|
7
|
+
// 10 auth-burst requests / 60s across register + login + refresh.
|
|
8
|
+
// Server-side gate against credential-stuffing; gateway only.
|
|
9
|
+
@ApiTags('auth')
|
|
10
|
+
@Controller('auth')
|
|
11
|
+
@Throttle({ 'auth-burst': { limit: 10, ttl: seconds(60) } })
|
|
12
|
+
export class AuthController {
|
|
13
|
+
constructor(private readonly authClient: AuthClientService) {}
|
|
14
|
+
|
|
15
|
+
@Public()
|
|
16
|
+
@Post('register')
|
|
17
|
+
@ApiOperation({ summary: 'Create a new user and return an auth session' })
|
|
18
|
+
@ApiBody({
|
|
19
|
+
schema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
required: ['email', 'password'],
|
|
22
|
+
properties: {
|
|
23
|
+
email: { type: 'string', format: 'email' },
|
|
24
|
+
password: { type: 'string', minLength: 8 },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
register(@Body() body: { email: string; password: string }) {
|
|
29
|
+
return this.authClient.signup(body.email, body.password);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Public()
|
|
33
|
+
@Post('login')
|
|
34
|
+
@ApiOperation({ summary: 'Exchange email + password for an auth session' })
|
|
35
|
+
@ApiBody({
|
|
36
|
+
schema: {
|
|
37
|
+
type: 'object',
|
|
38
|
+
required: ['email', 'password'],
|
|
39
|
+
properties: {
|
|
40
|
+
email: { type: 'string', format: 'email' },
|
|
41
|
+
password: { type: 'string' },
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
login(@Body() body: { email: string; password: string }) {
|
|
46
|
+
return this.authClient.login(body.email, body.password);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@Public()
|
|
50
|
+
@Post('refresh')
|
|
51
|
+
@ApiOperation({ summary: 'Exchange a refresh token for a fresh access token' })
|
|
52
|
+
@ApiBody({
|
|
53
|
+
schema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
required: ['refreshToken'],
|
|
56
|
+
properties: { refreshToken: { type: 'string' } },
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
refresh(@Body() body: { refreshToken: string }) {
|
|
60
|
+
return this.authClient.refresh(body.refreshToken);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CanActivate,
|
|
3
|
+
ExecutionContext,
|
|
4
|
+
Inject,
|
|
5
|
+
Injectable,
|
|
6
|
+
UnauthorizedException,
|
|
7
|
+
} from '@nestjs/common';
|
|
8
|
+
import { Reflector } from '@nestjs/core';
|
|
9
|
+
import { AuthClientService } from '@icore/auth-client';
|
|
10
|
+
import type { Request } from 'express';
|
|
11
|
+
import { IS_PUBLIC_KEY } from './public.decorator';
|
|
12
|
+
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class AuthGuard implements CanActivate {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly reflector: Reflector,
|
|
17
|
+
@Inject(AuthClientService) private readonly authClient: AuthClientService,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
async canActivate(ctx: ExecutionContext): Promise<boolean> {
|
|
21
|
+
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
|
22
|
+
ctx.getHandler(),
|
|
23
|
+
ctx.getClass(),
|
|
24
|
+
]);
|
|
25
|
+
if (isPublic) return true;
|
|
26
|
+
|
|
27
|
+
const req = ctx.switchToHttp().getRequest<Request & { user?: unknown }>();
|
|
28
|
+
const header = req.headers.authorization ?? '';
|
|
29
|
+
const [scheme, token] = header.split(' ');
|
|
30
|
+
if (scheme !== 'Bearer' || !token) {
|
|
31
|
+
throw new UnauthorizedException('missing_bearer');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const verified = await this.authClient.verify(token);
|
|
36
|
+
req.user = verified;
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
throw new UnauthorizedException('invalid_token');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
3
|
+
import { ThrottlerGuard } from '@nestjs/throttler';
|
|
4
|
+
import { AuthClientModule } from '@icore/auth-client';
|
|
5
|
+
import { AuthController } from './auth.controller';
|
|
6
|
+
import { AuthGuard } from './auth.guard';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
imports: [AuthClientModule.forRoot()],
|
|
10
|
+
controllers: [AuthController],
|
|
11
|
+
providers: [
|
|
12
|
+
{ provide: APP_GUARD, useClass: ThrottlerGuard },
|
|
13
|
+
{ provide: APP_GUARD, useClass: AuthGuard },
|
|
14
|
+
],
|
|
15
|
+
exports: [AuthClientModule],
|
|
16
|
+
})
|
|
17
|
+
export class AuthModule {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Controller, Get, Req } from '@nestjs/common';
|
|
2
|
+
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
3
|
+
import type { Request } from 'express';
|
|
4
|
+
import type { VerifiedToken } from '@icore/shared';
|
|
5
|
+
|
|
6
|
+
@ApiTags('profile')
|
|
7
|
+
@ApiBearerAuth()
|
|
8
|
+
@Controller('profile')
|
|
9
|
+
export class ProfileController {
|
|
10
|
+
@Get()
|
|
11
|
+
@ApiOperation({ summary: 'Return the authenticated user (uid / email / role)' })
|
|
12
|
+
me(@Req() req: Request & { user?: VerifiedToken }): VerifiedToken | undefined {
|
|
13
|
+
return req.user;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ForbiddenException } from '@nestjs/common';
|
|
3
|
+
import { assertOwnership } from '../assert-ownership';
|
|
4
|
+
|
|
5
|
+
describe('assertOwnership', () => {
|
|
6
|
+
it('passes when the path starts with userId/', () => {
|
|
7
|
+
expect(() => assertOwnership({ bucket: 'b', path: 'user-1/foo.txt' }, 'user-1')).not.toThrow();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('throws ForbiddenException on a foreign prefix', () => {
|
|
11
|
+
expect(() => assertOwnership({ bucket: 'b', path: 'attacker/foo.txt' }, 'user-1')).toThrow(
|
|
12
|
+
ForbiddenException,
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('throws when path has no prefix at all', () => {
|
|
17
|
+
expect(() => assertOwnership({ bucket: 'b', path: 'foo.txt' }, 'user-1')).toThrow(
|
|
18
|
+
ForbiddenException,
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('treats userId-substring-but-not-prefix as foreign', () => {
|
|
23
|
+
// "user-12/x" must NOT match "user-1" — prefix must terminate at `/`
|
|
24
|
+
expect(() => assertOwnership({ bucket: 'b', path: 'user-12/x' }, 'user-1')).toThrow(
|
|
25
|
+
ForbiddenException,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ForbiddenException } from '@nestjs/common';
|
|
2
|
+
import type { StorageRef } from '@icore/shared';
|
|
3
|
+
|
|
4
|
+
export function assertOwnership(ref: StorageRef, userId: string): void {
|
|
5
|
+
if (!ref.path.startsWith(`${userId}/`)) {
|
|
6
|
+
throw new ForbiddenException('foreign_storage_ref');
|
|
7
|
+
}
|
|
8
|
+
}
|