@idevconn/create-icore 0.7.2 → 0.9.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/README.md +44 -9
- package/dist/cli.js +689 -363
- package/dist/index.cjs +806 -358
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +798 -351
- package/dist/manifest/audit.js +122 -0
- package/package.json +1 -1
- package/templates/apps/api/src/app/app.module.ts +2 -6
- package/templates/apps/api/src/app/features.module.ts +9 -0
- package/templates/apps/api/src/app/gateway-services.ts +7 -0
- package/templates/apps/api/src/main.ts +1 -5
- package/templates/apps/microservices/auth/src/app/app.module.ts +4 -93
- package/templates/apps/microservices/auth/src/app/auth.provider.ts +9 -0
- package/templates/apps/microservices/notes/src/app/app.module.ts +4 -86
- package/templates/apps/microservices/notes/src/app/db.provider.ts +9 -0
- package/templates/apps/microservices/upload/src/app/app.module.ts +4 -140
- package/templates/apps/microservices/upload/src/app/storage.provider.ts +9 -0
- package/templates/apps/templates/client-antd/src/components/layout/LayoutSider.tsx +15 -23
- package/templates/apps/templates/client-antd/src/nav.config.ts +17 -0
- package/templates/apps/templates/client-mui/src/components/layout/LayoutSider.tsx +19 -20
- package/templates/apps/templates/client-mui/src/nav.config.ts +17 -0
- package/templates/apps/templates/client-shadcn/src/components/layout/LayoutSider.tsx +20 -16
- package/templates/apps/templates/client-shadcn/src/nav.config.ts +17 -0
- package/templates/libs/auth-strategies/firebase/eslint.config.mjs +1 -0
- package/templates/libs/auth-strategies/firebase/package.json +4 -0
- package/templates/libs/auth-strategies/firebase/src/index.ts +1 -0
- package/templates/libs/auth-strategies/firebase/src/lib/__tests__/firebase-auth.module.unit.test.ts +49 -0
- package/templates/libs/auth-strategies/firebase/src/lib/firebase-auth.module.ts +41 -0
- package/templates/libs/auth-strategies/firebase/tsconfig.json +2 -0
- package/templates/libs/auth-strategies/mongodb/package.json +8 -1
- package/templates/libs/auth-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/__tests__/mongodb-auth.module.unit.test.ts +16 -0
- package/templates/libs/auth-strategies/mongodb/src/lib/mongodb-auth.module.ts +45 -0
- package/templates/libs/auth-strategies/mongodb/tsconfig.json +2 -0
- package/templates/libs/auth-strategies/supabase/eslint.config.mjs +1 -0
- package/templates/libs/auth-strategies/supabase/package.json +3 -0
- package/templates/libs/auth-strategies/supabase/src/index.ts +1 -0
- package/templates/libs/auth-strategies/supabase/src/lib/__tests__/supabase-auth.module.unit.test.ts +43 -0
- package/templates/libs/auth-strategies/supabase/src/lib/supabase-auth.module.ts +41 -0
- package/templates/libs/auth-strategies/supabase/tsconfig.json +2 -0
- package/templates/libs/db-strategies/firestore/eslint.config.mjs +1 -1
- package/templates/libs/db-strategies/firestore/package.json +4 -0
- package/templates/libs/db-strategies/firestore/src/index.ts +1 -0
- package/templates/libs/db-strategies/firestore/src/lib/__tests__/firestore-db.module.unit.test.ts +37 -0
- package/templates/libs/db-strategies/firestore/src/lib/firestore-db.module.ts +41 -0
- package/templates/libs/db-strategies/firestore/tsconfig.json +2 -0
- package/templates/libs/db-strategies/mongodb/package.json +4 -1
- package/templates/libs/db-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/db-strategies/mongodb/src/lib/__tests__/mongodb-db.module.unit.test.ts +14 -0
- package/templates/libs/db-strategies/mongodb/src/lib/mongodb-db.module.ts +41 -0
- package/templates/libs/db-strategies/mongodb/tsconfig.json +2 -0
- package/templates/libs/db-strategies/supabase/eslint.config.mjs +6 -1
- package/templates/libs/db-strategies/supabase/package.json +3 -0
- package/templates/libs/db-strategies/supabase/src/index.ts +1 -0
- package/templates/libs/db-strategies/supabase/src/lib/__tests__/supabase-db.module.unit.test.ts +32 -0
- package/templates/libs/db-strategies/supabase/src/lib/supabase-db.module.ts +41 -0
- package/templates/libs/db-strategies/supabase/tsconfig.json +2 -0
- package/templates/libs/shared/src/strategies/__tests__/provide-strategy.unit.test.ts +73 -0
- package/templates/libs/shared/src/strategies/index.ts +1 -0
- package/templates/libs/shared/src/strategies/provide-strategy.ts +44 -0
- package/templates/libs/storage-strategies/cloudinary/eslint.config.mjs +1 -1
- package/templates/libs/storage-strategies/cloudinary/package.json +4 -0
- package/templates/libs/storage-strategies/cloudinary/src/index.ts +1 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/__tests__/cloudinary-storage.module.unit.test.ts +40 -0
- package/templates/libs/storage-strategies/cloudinary/src/lib/cloudinary-storage.module.ts +85 -0
- package/templates/libs/storage-strategies/cloudinary/tsconfig.json +2 -0
- package/templates/libs/storage-strategies/firebase/eslint.config.mjs +1 -1
- package/templates/libs/storage-strategies/firebase/package.json +4 -0
- package/templates/libs/storage-strategies/firebase/src/index.ts +1 -0
- package/templates/libs/storage-strategies/firebase/src/lib/__tests__/firebase-storage.module.unit.test.ts +42 -0
- package/templates/libs/storage-strategies/firebase/src/lib/firebase-storage.module.ts +46 -0
- package/templates/libs/storage-strategies/firebase/tsconfig.json +2 -0
- package/templates/libs/storage-strategies/mongodb/package.json +4 -1
- package/templates/libs/storage-strategies/mongodb/src/index.ts +1 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/__tests__/mongodb-storage.module.unit.test.ts +14 -0
- package/templates/libs/storage-strategies/mongodb/src/lib/mongodb-storage.module.ts +41 -0
- package/templates/libs/storage-strategies/mongodb/tsconfig.json +2 -0
- package/templates/libs/storage-strategies/supabase/eslint.config.mjs +1 -0
- package/templates/libs/storage-strategies/supabase/package.json +3 -0
- package/templates/libs/storage-strategies/supabase/src/index.ts +1 -0
- package/templates/libs/storage-strategies/supabase/src/lib/__tests__/supabase-storage.module.unit.test.ts +46 -0
- package/templates/libs/storage-strategies/supabase/src/lib/supabase-storage.module.ts +46 -0
- package/templates/libs/storage-strategies/supabase/tsconfig.json +2 -0
- package/templates/package.json +1 -1
- package/templates/tools/create-icore/_template-shell/package.json +1 -1
- package/templates/tsconfig.base.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/manifest/audit.ts
|
|
2
|
+
import { readFile, readdir } from "fs/promises";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
var IGNORE_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".nx"]);
|
|
5
|
+
async function walk(dir, out = []) {
|
|
6
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
7
|
+
for (const e of entries) {
|
|
8
|
+
if (e.isDirectory()) {
|
|
9
|
+
if (!IGNORE_DIRS.has(e.name)) await walk(join(dir, e.name), out);
|
|
10
|
+
} else if (/\.(ts|tsx|mjs)$/.test(e.name)) {
|
|
11
|
+
out.push(join(dir, e.name));
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
async function tsconfigAliases(dir) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await readFile(join(dir, "tsconfig.base.json"), "utf8");
|
|
19
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const m of raw.matchAll(/"(@icore\/[a-z0-9.-]+)"\s*:/g)) {
|
|
21
|
+
if (m[1]) aliases.add(m[1]);
|
|
22
|
+
}
|
|
23
|
+
return aliases;
|
|
24
|
+
} catch {
|
|
25
|
+
return /* @__PURE__ */ new Set();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var PROVIDER_SDKS = {
|
|
29
|
+
supabase: ["@supabase/supabase-js"],
|
|
30
|
+
cloudinary: ["cloudinary"],
|
|
31
|
+
mongodb: ["mongoose"],
|
|
32
|
+
firebase: ["firebase-admin", "@icore/firebase-admin"]
|
|
33
|
+
};
|
|
34
|
+
async function readBlueprint(dir) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(await readFile(join(dir, "blueprint.json"), "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function forbiddenFromBlueprint(bp) {
|
|
42
|
+
const chosen = new Set(
|
|
43
|
+
[bp.authProvider, bp.dbProvider, bp.upload].filter((p) => Boolean(p))
|
|
44
|
+
);
|
|
45
|
+
const forbidden = [];
|
|
46
|
+
for (const [provider, sdks] of Object.entries(PROVIDER_SDKS)) {
|
|
47
|
+
if (!chosen.has(provider)) forbidden.push(...sdks);
|
|
48
|
+
}
|
|
49
|
+
return forbidden;
|
|
50
|
+
}
|
|
51
|
+
async function allPackageJsons(dir) {
|
|
52
|
+
const out = [];
|
|
53
|
+
const root = join(dir, "package.json");
|
|
54
|
+
out.push(root);
|
|
55
|
+
async function walk2(d) {
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = await readdir(d, { withFileTypes: true });
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
for (const e of entries) {
|
|
63
|
+
if (e.isDirectory()) {
|
|
64
|
+
if (!IGNORE_DIRS.has(e.name)) await walk2(join(d, e.name));
|
|
65
|
+
} else if (e.name === "package.json") {
|
|
66
|
+
out.push(join(d, e.name));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await walk2(join(dir, "apps"));
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
async function depKeys(pkgPath) {
|
|
74
|
+
try {
|
|
75
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
|
|
76
|
+
return /* @__PURE__ */ new Set([
|
|
77
|
+
...Object.keys(pkg.dependencies ?? {}),
|
|
78
|
+
...Object.keys(pkg.devDependencies ?? {})
|
|
79
|
+
]);
|
|
80
|
+
} catch {
|
|
81
|
+
return /* @__PURE__ */ new Set();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var ICORE_IMPORT = /(?:from|import\()\s*['"](@icore\/[a-z0-9.-]+)/g;
|
|
85
|
+
async function auditProject(dir, opts = {}) {
|
|
86
|
+
const violations = [];
|
|
87
|
+
const aliases = await tsconfigAliases(dir);
|
|
88
|
+
for (const file of await walk(dir)) {
|
|
89
|
+
const src = await readFile(file, "utf8");
|
|
90
|
+
for (const m of src.matchAll(ICORE_IMPORT)) {
|
|
91
|
+
const alias = m[1];
|
|
92
|
+
if (alias && !aliases.has(alias)) {
|
|
93
|
+
violations.push({
|
|
94
|
+
kind: "import-of-absent-lib",
|
|
95
|
+
detail: `${file} imports ${alias} (no tsconfig path \u2192 lib absent)`
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const bp = await readBlueprint(dir);
|
|
101
|
+
const forbidden = /* @__PURE__ */ new Set([
|
|
102
|
+
...opts.forbiddenDeps ?? [],
|
|
103
|
+
...bp ? forbiddenFromBlueprint(bp) : []
|
|
104
|
+
]);
|
|
105
|
+
if (forbidden.size > 0) {
|
|
106
|
+
for (const pkgPath of await allPackageJsons(dir)) {
|
|
107
|
+
const deps = await depKeys(pkgPath);
|
|
108
|
+
for (const f of forbidden) {
|
|
109
|
+
if (deps.has(f)) {
|
|
110
|
+
violations.push({
|
|
111
|
+
kind: "forbidden-dep",
|
|
112
|
+
detail: `${pkgPath} keeps forbidden dep ${f}`
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return violations;
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
auditProject
|
|
122
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idevconn/create-icore",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Bootstrap a new project from the iCore scaffold (Nx + NestJS + React + Vite + shadcn/Tailwind, swappable auth + storage providers).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "iDEVconn",
|
|
@@ -6,9 +6,7 @@ import { AuthModule } from './auth/auth.module';
|
|
|
6
6
|
import { ProfileModule } from './profile/profile.module';
|
|
7
7
|
import { AbilitiesModule } from './abilities/abilities.module';
|
|
8
8
|
import { StorageModule } from './storage/storage.module';
|
|
9
|
-
import {
|
|
10
|
-
import { NotesModule } from './notes/notes.module';
|
|
11
|
-
import { AdminModule } from './admin/admin.module';
|
|
9
|
+
import { FeaturesModule } from './features.module';
|
|
12
10
|
|
|
13
11
|
@Module({
|
|
14
12
|
imports: [
|
|
@@ -21,9 +19,7 @@ import { AdminModule } from './admin/admin.module';
|
|
|
21
19
|
AbilitiesModule,
|
|
22
20
|
ProfileModule,
|
|
23
21
|
StorageModule,
|
|
24
|
-
|
|
25
|
-
NotesModule,
|
|
26
|
-
AdminModule,
|
|
22
|
+
FeaturesModule,
|
|
27
23
|
],
|
|
28
24
|
})
|
|
29
25
|
export class AppModule {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { NotesModule } from './notes/notes.module';
|
|
3
|
+
import { PaymentModule } from './payment/payment.module';
|
|
4
|
+
import { AdminModule } from './admin/admin.module';
|
|
5
|
+
|
|
6
|
+
@Module({
|
|
7
|
+
imports: [NotesModule, PaymentModule, AdminModule],
|
|
8
|
+
})
|
|
9
|
+
export class FeaturesModule {}
|
|
@@ -5,13 +5,9 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
|
5
5
|
import cookieParser from 'cookie-parser';
|
|
6
6
|
import { formatGatewayBanner } from '@icore/shared';
|
|
7
7
|
import { AppModule } from './app/app.module';
|
|
8
|
+
import { GATEWAY_SERVICES } from './app/gateway-services';
|
|
8
9
|
import pkg from '@icore/package.json';
|
|
9
10
|
|
|
10
|
-
const GATEWAY_SERVICES = [
|
|
11
|
-
{ name: 'auth', prefix: 'AUTH' },
|
|
12
|
-
{ name: 'upload', prefix: 'UPLOAD' },
|
|
13
|
-
];
|
|
14
|
-
|
|
15
11
|
const DEFAULT_PORT = 3001;
|
|
16
12
|
|
|
17
13
|
async function bootstrap() {
|
|
@@ -1,54 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { Module
|
|
3
|
-
import { ConfigModule
|
|
4
|
-
import { createClient } from '@supabase/supabase-js';
|
|
5
|
-
import { SupabaseAuthStrategy } from '@icore/auth-supabase';
|
|
6
|
-
import { MongoDbAuthStrategy } from '@icore/auth-mongodb';
|
|
7
|
-
import { FirebaseAuthStrategy, HttpIdentityToolkitClient } from '@icore/auth-firebase';
|
|
8
|
-
import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
|
|
9
|
-
import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
|
|
10
|
-
import { Connection } from 'mongoose';
|
|
11
|
-
import { FakeAuthStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
|
|
12
|
-
import type { AuthStrategy } from '@icore/shared';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import { ConfigModule } from '@nestjs/config';
|
|
13
4
|
import { AuthController } from './auth.controller';
|
|
14
|
-
|
|
15
|
-
const ENV_PATH = 'apps/microservices/auth/.env';
|
|
16
|
-
|
|
17
|
-
// Env vars each provider needs (besides AUTH_PROVIDER itself).
|
|
18
|
-
const REQUIRED_ENV: Record<string, string[]> = {
|
|
19
|
-
supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
|
|
20
|
-
firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_WEB_API_KEY'],
|
|
21
|
-
mongodb: ['MONGODB_URI', 'JWT_SECRET'],
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
function requireEnv(cfg: ConfigService, key: string): string {
|
|
25
|
-
return cfg.getOrThrow<string>(key);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function makeSupabaseAuth(cfg: ConfigService): AuthStrategy {
|
|
29
|
-
const client = createClient(
|
|
30
|
-
requireEnv(cfg, 'SUPABASE_URL'),
|
|
31
|
-
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
32
|
-
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
33
|
-
);
|
|
34
|
-
return new SupabaseAuthStrategy({ client });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function makeFirebaseAuth(cfg: ConfigService): AuthStrategy {
|
|
38
|
-
const app = getFirebaseAdmin(cfg);
|
|
39
|
-
const identityToolkit = new HttpIdentityToolkitClient(requireEnv(cfg, 'FIREBASE_WEB_API_KEY'));
|
|
40
|
-
return new FirebaseAuthStrategy({
|
|
41
|
-
identityToolkit,
|
|
42
|
-
adminAuth: app.auth(),
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function makeMongoDbAuth(connection: Connection, cfg: ConfigService): AuthStrategy {
|
|
47
|
-
return new MongoDbAuthStrategy({
|
|
48
|
-
connection,
|
|
49
|
-
jwtSecret: requireEnv(cfg, 'JWT_SECRET'),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
5
|
+
import { AuthProviderModule } from './auth.provider';
|
|
52
6
|
|
|
53
7
|
@Module({
|
|
54
8
|
imports: [
|
|
@@ -59,51 +13,8 @@ function makeMongoDbAuth(connection: Connection, cfg: ConfigService): AuthStrate
|
|
|
59
13
|
join(process.cwd(), '.env'),
|
|
60
14
|
],
|
|
61
15
|
}),
|
|
62
|
-
|
|
63
|
-
useFactory: (cfg: ConfigService) => ({
|
|
64
|
-
uri: cfg.get<string>('MONGODB_URI'),
|
|
65
|
-
}),
|
|
66
|
-
inject: [ConfigService],
|
|
67
|
-
}),
|
|
16
|
+
AuthProviderModule,
|
|
68
17
|
],
|
|
69
18
|
controllers: [AuthController],
|
|
70
|
-
providers: [
|
|
71
|
-
{
|
|
72
|
-
provide: 'AuthStrategy',
|
|
73
|
-
useFactory: (cfg: ConfigService, connection: Connection): AuthStrategy => {
|
|
74
|
-
const logger = new Logger('AuthStrategy');
|
|
75
|
-
const provider = cfg.get<string>('AUTH_PROVIDER')?.trim();
|
|
76
|
-
const keys = provider ? REQUIRED_ENV[provider] : undefined;
|
|
77
|
-
const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
|
|
78
|
-
|
|
79
|
-
// Prod: fail fast — never silently run a fake auth strategy.
|
|
80
|
-
// Dev: warn with a boxed banner + fall back to the in-memory fake.
|
|
81
|
-
const fallback = (reason?: string): AuthStrategy => {
|
|
82
|
-
const banner = formatEnvBanner({
|
|
83
|
-
service: 'auth MS',
|
|
84
|
-
provider,
|
|
85
|
-
missing,
|
|
86
|
-
envPath: ENV_PATH,
|
|
87
|
-
reason,
|
|
88
|
-
});
|
|
89
|
-
if (process.env.NODE_ENV === 'production') throw new Error(banner);
|
|
90
|
-
logger.warn(banner);
|
|
91
|
-
return new FakeAuthStrategy();
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
if (!keys || missing.length > 0) return fallback();
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
if (provider === 'supabase') return makeSupabaseAuth(cfg);
|
|
98
|
-
if (provider === 'mongodb') return makeMongoDbAuth(connection, cfg);
|
|
99
|
-
return makeFirebaseAuth(cfg);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
// Vars present but invalid (e.g. placeholder URL the SDK rejects).
|
|
102
|
-
return fallback(err instanceof Error ? err.message : String(err));
|
|
103
|
-
}
|
|
104
|
-
},
|
|
105
|
-
inject: [ConfigService, getConnectionToken()],
|
|
106
|
-
},
|
|
107
|
-
],
|
|
108
19
|
})
|
|
109
20
|
export class AppModule {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SupabaseAuthModule } from '@icore/auth-supabase';
|
|
2
|
+
|
|
3
|
+
// Auth provider wiring. Selected at scaffold time by create-icore; the committed
|
|
4
|
+
// default is supabase (matches AUTH_PROVIDER=supabase in .env.example). The
|
|
5
|
+
// chosen XAuthModule.forRoot owns construction, required-env, and the
|
|
6
|
+
// dev-fake / prod-fail fallback (see @icore/shared buildStrategyWithFallback).
|
|
7
|
+
const ENV_PATH = 'apps/microservices/auth/.env';
|
|
8
|
+
|
|
9
|
+
export const AuthProviderModule = SupabaseAuthModule.forRoot(ENV_PATH);
|
|
@@ -1,50 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { Module
|
|
3
|
-
import { ConfigModule
|
|
4
|
-
import { createClient } from '@supabase/supabase-js';
|
|
5
|
-
import { SupabaseDBStrategy } from '@icore/db-supabase';
|
|
6
|
-
import { MongoDbDBStrategy } from '@icore/db-mongodb';
|
|
7
|
-
import { FirestoreDBStrategy } from '@icore/db-firestore';
|
|
8
|
-
import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
|
|
9
|
-
import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
|
|
10
|
-
import { Connection } from 'mongoose';
|
|
11
|
-
import { FakeDBStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
|
|
12
|
-
import type { DBStrategy } from '@icore/shared';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import { ConfigModule } from '@nestjs/config';
|
|
13
4
|
import { NotesController } from './notes.controller';
|
|
14
|
-
|
|
15
|
-
const ENV_PATH = 'apps/microservices/notes/.env';
|
|
16
|
-
|
|
17
|
-
// DB_PROVIDER accepts supabase | firestore | firebase (latter two are Firestore).
|
|
18
|
-
const REQUIRED_ENV: Record<string, string[]> = {
|
|
19
|
-
supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY'],
|
|
20
|
-
firestore: [...FIREBASE_ADMIN_REQUIRED_ENV],
|
|
21
|
-
firebase: [...FIREBASE_ADMIN_REQUIRED_ENV],
|
|
22
|
-
mongodb: ['MONGODB_URI'],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
function requireEnv(cfg: ConfigService, key: string): string {
|
|
26
|
-
return cfg.getOrThrow<string>(key);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function makeSupabaseDB(cfg: ConfigService): DBStrategy {
|
|
30
|
-
const client = createClient(
|
|
31
|
-
requireEnv(cfg, 'SUPABASE_URL'),
|
|
32
|
-
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
33
|
-
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
34
|
-
);
|
|
35
|
-
return new SupabaseDBStrategy({ client });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function makeFirestoreDB(cfg: ConfigService): DBStrategy {
|
|
39
|
-
const app = getFirebaseAdmin(cfg);
|
|
40
|
-
return new FirestoreDBStrategy({
|
|
41
|
-
db: app.firestore() as unknown as ConstructorParameters<typeof FirestoreDBStrategy>[0]['db'],
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function makeMongoDb(connection: Connection): DBStrategy {
|
|
46
|
-
return new MongoDbDBStrategy({ connection });
|
|
47
|
-
}
|
|
5
|
+
import { DbProviderModule } from './db.provider';
|
|
48
6
|
|
|
49
7
|
@Module({
|
|
50
8
|
imports: [
|
|
@@ -55,48 +13,8 @@ function makeMongoDb(connection: Connection): DBStrategy {
|
|
|
55
13
|
join(process.cwd(), '.env'),
|
|
56
14
|
],
|
|
57
15
|
}),
|
|
58
|
-
|
|
59
|
-
useFactory: (cfg: ConfigService) => ({
|
|
60
|
-
uri: cfg.get<string>('MONGODB_URI'),
|
|
61
|
-
}),
|
|
62
|
-
inject: [ConfigService],
|
|
63
|
-
}),
|
|
16
|
+
DbProviderModule,
|
|
64
17
|
],
|
|
65
18
|
controllers: [NotesController],
|
|
66
|
-
providers: [
|
|
67
|
-
{
|
|
68
|
-
provide: 'DBStrategy',
|
|
69
|
-
useFactory: (cfg: ConfigService, connection: Connection): DBStrategy => {
|
|
70
|
-
const logger = new Logger('DBStrategy');
|
|
71
|
-
const provider = cfg.get<string>('DB_PROVIDER')?.trim();
|
|
72
|
-
const keys = provider ? REQUIRED_ENV[provider] : undefined;
|
|
73
|
-
const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
|
|
74
|
-
|
|
75
|
-
const fallback = (reason?: string): DBStrategy => {
|
|
76
|
-
const banner = formatEnvBanner({
|
|
77
|
-
service: 'notes MS',
|
|
78
|
-
provider,
|
|
79
|
-
missing,
|
|
80
|
-
envPath: ENV_PATH,
|
|
81
|
-
reason,
|
|
82
|
-
});
|
|
83
|
-
if (process.env.NODE_ENV === 'production') throw new Error(banner);
|
|
84
|
-
logger.warn(banner);
|
|
85
|
-
return new FakeDBStrategy();
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
if (!keys || missing.length > 0) return fallback();
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
if (provider === 'supabase') return makeSupabaseDB(cfg);
|
|
92
|
-
if (provider === 'mongodb') return makeMongoDb(connection);
|
|
93
|
-
return makeFirestoreDB(cfg);
|
|
94
|
-
} catch (err) {
|
|
95
|
-
return fallback(err instanceof Error ? err.message : String(err));
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
inject: [ConfigService, getConnectionToken()],
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
19
|
})
|
|
102
20
|
export class AppModule {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SupabaseDbModule } from '@icore/db-supabase';
|
|
2
|
+
|
|
3
|
+
// DB provider wiring. Selected at scaffold time by create-icore; the committed
|
|
4
|
+
// default is supabase (matches DB_PROVIDER=supabase in .env.example). The chosen
|
|
5
|
+
// XDbModule.forRoot owns construction, required-env, and the dev-fake / prod-fail
|
|
6
|
+
// fallback.
|
|
7
|
+
const ENV_PATH = 'apps/microservices/notes/.env';
|
|
8
|
+
|
|
9
|
+
export const DbProviderModule = SupabaseDbModule.forRoot(ENV_PATH);
|
|
@@ -1,103 +1,8 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { Module
|
|
3
|
-
import { ConfigModule
|
|
4
|
-
import { createClient } from '@supabase/supabase-js';
|
|
5
|
-
import { v2 as cloudinary } from 'cloudinary';
|
|
6
|
-
import { SupabaseStorageStrategy } from '@icore/storage-supabase';
|
|
7
|
-
import { MongoDbStorageStrategy } from '@icore/storage-mongodb';
|
|
8
|
-
import { FirebaseStorageStrategy } from '@icore/storage-firebase';
|
|
9
|
-
import { CloudinaryStorageStrategy, type CloudinaryApiLike } from '@icore/storage-cloudinary';
|
|
10
|
-
import { getFirebaseAdmin, FIREBASE_ADMIN_REQUIRED_ENV } from '@icore/firebase-admin';
|
|
11
|
-
import { MongooseModule, getConnectionToken } from '@nestjs/mongoose';
|
|
12
|
-
import { Connection } from 'mongoose';
|
|
13
|
-
import { FakeStorageStrategy, missingEnv, formatEnvBanner } from '@icore/shared';
|
|
14
|
-
import type { StorageStrategy } from '@icore/shared';
|
|
2
|
+
import { Module } from '@nestjs/common';
|
|
3
|
+
import { ConfigModule } from '@nestjs/config';
|
|
15
4
|
import { StorageController } from './storage.controller';
|
|
16
|
-
|
|
17
|
-
const ENV_PATH = 'apps/microservices/upload/.env';
|
|
18
|
-
|
|
19
|
-
const REQUIRED_ENV: Record<string, string[]> = {
|
|
20
|
-
supabase: ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'SUPABASE_STORAGE_BUCKET'],
|
|
21
|
-
firebase: [...FIREBASE_ADMIN_REQUIRED_ENV, 'FIREBASE_STORAGE_BUCKET'],
|
|
22
|
-
cloudinary: ['CLOUDINARY_CLOUD_NAME', 'CLOUDINARY_API_KEY', 'CLOUDINARY_API_SECRET'],
|
|
23
|
-
mongodb: ['MONGODB_URI'],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function requireEnv(cfg: ConfigService, key: string): string {
|
|
27
|
-
return cfg.getOrThrow<string>(key);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makeSupabaseStorage(cfg: ConfigService): StorageStrategy {
|
|
31
|
-
const client = createClient(
|
|
32
|
-
requireEnv(cfg, 'SUPABASE_URL'),
|
|
33
|
-
requireEnv(cfg, 'SUPABASE_SERVICE_ROLE_KEY'),
|
|
34
|
-
{ auth: { autoRefreshToken: false, persistSession: false } },
|
|
35
|
-
);
|
|
36
|
-
return new SupabaseStorageStrategy({
|
|
37
|
-
client,
|
|
38
|
-
bucket: requireEnv(cfg, 'SUPABASE_STORAGE_BUCKET'),
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function makeFirebaseStorage(cfg: ConfigService): StorageStrategy {
|
|
43
|
-
const bucketName = requireEnv(cfg, 'FIREBASE_STORAGE_BUCKET');
|
|
44
|
-
const app = getFirebaseAdmin(cfg);
|
|
45
|
-
return new FirebaseStorageStrategy({
|
|
46
|
-
bucket: app
|
|
47
|
-
.storage()
|
|
48
|
-
.bucket(bucketName) as unknown as import('@icore/storage-firebase').FirebaseStorageBucketLike,
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function makeCloudinaryStorage(cfg: ConfigService): StorageStrategy {
|
|
53
|
-
cloudinary.config({
|
|
54
|
-
cloud_name: requireEnv(cfg, 'CLOUDINARY_CLOUD_NAME'),
|
|
55
|
-
api_key: requireEnv(cfg, 'CLOUDINARY_API_KEY'),
|
|
56
|
-
api_secret: requireEnv(cfg, 'CLOUDINARY_API_SECRET'),
|
|
57
|
-
secure: true,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const api: CloudinaryApiLike = {
|
|
61
|
-
async upload(buffer, opts) {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
const stream = cloudinary.uploader.upload_stream(
|
|
64
|
-
{ public_id: opts.public_id, resource_type: opts.resource_type ?? 'raw' },
|
|
65
|
-
(error, result) => {
|
|
66
|
-
if (error || !result) reject(error ?? new Error('upload_failed'));
|
|
67
|
-
else resolve({ public_id: result.public_id, secure_url: result.secure_url });
|
|
68
|
-
},
|
|
69
|
-
);
|
|
70
|
-
stream.end(buffer);
|
|
71
|
-
});
|
|
72
|
-
},
|
|
73
|
-
async destroy(publicId) {
|
|
74
|
-
await cloudinary.uploader.destroy(publicId);
|
|
75
|
-
},
|
|
76
|
-
privateDownloadUrl(publicId, format, opts) {
|
|
77
|
-
return cloudinary.utils.private_download_url(publicId, format ?? '', opts ?? {});
|
|
78
|
-
},
|
|
79
|
-
async resources(opts) {
|
|
80
|
-
const res = await cloudinary.api.resources({
|
|
81
|
-
prefix: opts.prefix,
|
|
82
|
-
type: opts.type ?? 'upload',
|
|
83
|
-
});
|
|
84
|
-
return {
|
|
85
|
-
resources: (res.resources ?? []).map((r: { public_id: string }) => ({
|
|
86
|
-
public_id: r.public_id,
|
|
87
|
-
})),
|
|
88
|
-
};
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return new CloudinaryStorageStrategy({
|
|
93
|
-
api,
|
|
94
|
-
bucket: cfg.get<string>('CLOUDINARY_BUCKET_TAG') ?? 'cloudinary',
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function makeMongoDbStorage(connection: Connection): StorageStrategy {
|
|
99
|
-
return new MongoDbStorageStrategy({ connection });
|
|
100
|
-
}
|
|
5
|
+
import { StorageProviderModule } from './storage.provider';
|
|
101
6
|
|
|
102
7
|
@Module({
|
|
103
8
|
imports: [
|
|
@@ -108,49 +13,8 @@ function makeMongoDbStorage(connection: Connection): StorageStrategy {
|
|
|
108
13
|
join(process.cwd(), '.env'),
|
|
109
14
|
],
|
|
110
15
|
}),
|
|
111
|
-
|
|
112
|
-
useFactory: (cfg: ConfigService) => ({
|
|
113
|
-
uri: cfg.get<string>('MONGODB_URI'),
|
|
114
|
-
}),
|
|
115
|
-
inject: [ConfigService],
|
|
116
|
-
}),
|
|
16
|
+
StorageProviderModule,
|
|
117
17
|
],
|
|
118
18
|
controllers: [StorageController],
|
|
119
|
-
providers: [
|
|
120
|
-
{
|
|
121
|
-
provide: 'StorageStrategy',
|
|
122
|
-
useFactory: (cfg: ConfigService, connection: Connection): StorageStrategy => {
|
|
123
|
-
const logger = new Logger('StorageStrategy');
|
|
124
|
-
const provider = cfg.get<string>('STORAGE_PROVIDER')?.trim();
|
|
125
|
-
const keys = provider ? REQUIRED_ENV[provider] : undefined;
|
|
126
|
-
const missing = keys ? missingEnv((k) => cfg.get<string>(k), keys) : [];
|
|
127
|
-
|
|
128
|
-
const fallback = (reason?: string): StorageStrategy => {
|
|
129
|
-
const banner = formatEnvBanner({
|
|
130
|
-
service: 'upload MS',
|
|
131
|
-
provider,
|
|
132
|
-
missing,
|
|
133
|
-
envPath: ENV_PATH,
|
|
134
|
-
reason,
|
|
135
|
-
});
|
|
136
|
-
if (process.env.NODE_ENV === 'production') throw new Error(banner);
|
|
137
|
-
logger.warn(banner);
|
|
138
|
-
return new FakeStorageStrategy();
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
if (!keys || missing.length > 0) return fallback();
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
if (provider === 'supabase') return makeSupabaseStorage(cfg);
|
|
145
|
-
if (provider === 'firebase') return makeFirebaseStorage(cfg);
|
|
146
|
-
if (provider === 'mongodb') return makeMongoDbStorage(connection);
|
|
147
|
-
return makeCloudinaryStorage(cfg);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
return fallback(err instanceof Error ? err.message : String(err));
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
inject: [ConfigService, getConnectionToken()],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
19
|
})
|
|
156
20
|
export class AppModule {}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { SupabaseStorageModule } from '@icore/storage-supabase';
|
|
2
|
+
|
|
3
|
+
// Storage provider wiring. Selected at scaffold time by create-icore; the
|
|
4
|
+
// committed default is supabase (matches STORAGE_PROVIDER=supabase in
|
|
5
|
+
// .env.example). The chosen XStorageModule.forRoot owns construction,
|
|
6
|
+
// required-env, and the dev-fake / prod-fail fallback.
|
|
7
|
+
const ENV_PATH = 'apps/microservices/upload/.env';
|
|
8
|
+
|
|
9
|
+
export const StorageProviderModule = SupabaseStorageModule.forRoot(ENV_PATH);
|
|
@@ -3,41 +3,33 @@ import { Layout, Menu, type MenuProps } from 'antd';
|
|
|
3
3
|
import { DashboardOutlined, FileTextOutlined, UserOutlined } from '@ant-design/icons';
|
|
4
4
|
import { Link, useRouterState } from '@tanstack/react-router';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
import { NAV_CONFIG, type NavItem } from '@/nav.config';
|
|
8
|
+
|
|
9
|
+
const ICONS: Record<NavItem['iconName'], ReactNode> = {
|
|
10
|
+
dashboard: <DashboardOutlined />,
|
|
11
|
+
notes: <FileTextOutlined />,
|
|
12
|
+
profile: <UserOutlined />,
|
|
13
|
+
};
|
|
6
14
|
|
|
7
15
|
export function LayoutSider() {
|
|
8
16
|
const { t } = useTranslation();
|
|
9
17
|
const [collapsed, setCollapsed] = useState(false);
|
|
10
18
|
const pathname = useRouterState({ select: (s) => s.location.pathname });
|
|
11
19
|
|
|
12
|
-
const
|
|
13
|
-
? 'notes'
|
|
14
|
-
: pathname.includes('/profile')
|
|
15
|
-
? 'profile'
|
|
16
|
-
: 'dashboard';
|
|
20
|
+
const selected = NAV_CONFIG.find((n) => pathname.includes(n.to))?.to ?? '/dashboard';
|
|
17
21
|
|
|
18
|
-
const items: MenuProps['items'] =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
key: 'notes',
|
|
26
|
-
icon: <FileTextOutlined />,
|
|
27
|
-
label: <Link to="/notes">{t('notes.title')}</Link>,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
key: 'profile',
|
|
31
|
-
icon: <UserOutlined />,
|
|
32
|
-
label: <Link to="/profile">{t('nav.profile')}</Link>,
|
|
33
|
-
},
|
|
34
|
-
];
|
|
22
|
+
const items: MenuProps['items'] = NAV_CONFIG.map((n) => ({
|
|
23
|
+
key: n.to,
|
|
24
|
+
icon: ICONS[n.iconName],
|
|
25
|
+
label: <Link to={n.to}>{t(n.labelKey)}</Link>,
|
|
26
|
+
}));
|
|
35
27
|
|
|
36
28
|
return (
|
|
37
29
|
<Layout.Sider collapsible collapsed={collapsed} onCollapse={setCollapsed}>
|
|
38
30
|
<Menu
|
|
39
31
|
mode="inline"
|
|
40
|
-
selectedKeys={[
|
|
32
|
+
selectedKeys={[selected]}
|
|
41
33
|
items={items}
|
|
42
34
|
style={{ height: '100%', borderRight: 0 }}
|
|
43
35
|
/>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sidebar navigation. UI-agnostic: each LayoutSider maps `iconName` to its own
|
|
3
|
+
* icon library. Generated by create-icore from a base (dashboard + profile) plus
|
|
4
|
+
* the chosen features' contributions (e.g. notes when example=notes).
|
|
5
|
+
*/
|
|
6
|
+
export interface NavItem {
|
|
7
|
+
to: string;
|
|
8
|
+
labelKey: string;
|
|
9
|
+
iconName: 'dashboard' | 'notes' | 'profile';
|
|
10
|
+
exact?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const NAV_CONFIG: NavItem[] = [
|
|
14
|
+
{ to: '/dashboard', labelKey: 'nav.dashboard', iconName: 'dashboard', exact: true },
|
|
15
|
+
{ to: '/notes', labelKey: 'nav.notes', iconName: 'notes' },
|
|
16
|
+
{ to: '/profile', labelKey: 'nav.profile', iconName: 'profile' },
|
|
17
|
+
];
|