swallowkit 1.0.0-beta.13 → 1.0.0-beta.15
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.ja.md +2 -0
- package/README.md +2 -0
- package/dist/cli/commands/add-auth.d.ts +10 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -0
- package/dist/cli/commands/add-auth.js +365 -0
- package/dist/cli/commands/add-auth.js.map +1 -0
- package/dist/cli/commands/dev.js +34 -3
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +145 -10
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.js +10 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +5 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +12 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.d.ts +34 -0
- package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
- package/dist/core/mock/connector-mock-server.js +192 -0
- package/dist/core/mock/connector-mock-server.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +38 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
- package/dist/core/scaffold/auth-generator.js +1246 -0
- package/dist/core/scaffold/auth-generator.js.map +1 -0
- package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
- package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/connector-functions-generator.js +37 -18
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -1
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +21 -12
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +7 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +45 -0
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +10 -4
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +120 -12
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +9 -1
- package/src/__tests__/auth.test.ts +654 -0
- package/src/__tests__/connector-mock-server.test.ts +189 -2
- package/src/cli/commands/add-auth.ts +413 -0
- package/src/cli/commands/dev.ts +35 -4
- package/src/cli/commands/scaffold.ts +165 -11
- package/src/cli/index.ts +11 -0
- package/src/core/config.ts +13 -2
- package/src/core/mock/connector-mock-server.ts +248 -0
- package/src/core/scaffold/auth-generator.ts +1286 -0
- package/src/core/scaffold/connector-functions-generator.ts +42 -18
- package/src/core/scaffold/functions-generator.ts +23 -12
- package/src/core/scaffold/model-parser.ts +50 -1
- package/src/core/scaffold/ui-generator.ts +132 -12
- package/src/types/index.ts +42 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { spawn, spawnSync } from "child_process";
|
|
9
|
-
import { getBackendLanguage, getConnectorDefinition, ensureSwallowKitProject } from "../../core/config";
|
|
9
|
+
import { getBackendLanguage, getConnectorDefinition, getAuthConfig, ensureSwallowKitProject } from "../../core/config";
|
|
10
10
|
import { ModelInfo, parseModelFile, toKebabCase, toPascalCase, toCamelCase } from "../../core/scaffold/model-parser";
|
|
11
11
|
import {
|
|
12
12
|
generateCSharpAzureFunctionsCRUD,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
generateFormComponent,
|
|
31
31
|
generateNewPage,
|
|
32
32
|
generateEditPage,
|
|
33
|
+
UIAuthOptions,
|
|
33
34
|
} from "../../core/scaffold/ui-generator";
|
|
34
35
|
import { detectFromProject, getCommands } from "../../utils/package-manager";
|
|
35
36
|
import {
|
|
@@ -38,6 +39,8 @@ import {
|
|
|
38
39
|
ApiConnectorConfig,
|
|
39
40
|
RdbModelConnectorConfig,
|
|
40
41
|
ApiModelConnectorConfig,
|
|
42
|
+
ModelAuthPolicy,
|
|
43
|
+
AuthConfig,
|
|
41
44
|
} from "../../types";
|
|
42
45
|
|
|
43
46
|
interface ScaffoldOptions {
|
|
@@ -103,6 +106,8 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
103
106
|
// 6. Generate Azure Functions code
|
|
104
107
|
if (isConnectorModel) {
|
|
105
108
|
await generateConnectorFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
|
|
109
|
+
// 6b. Install connector driver dependencies
|
|
110
|
+
await installConnectorDriverDependencies(modelInfo, functionsDir, backendLanguage);
|
|
106
111
|
} else {
|
|
107
112
|
await generateFunctionsCode(modelInfo, functionsDir, sharedPackageName, backendLanguage);
|
|
108
113
|
|
|
@@ -126,7 +131,13 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
126
131
|
|
|
127
132
|
// 9. Generate UI components (unless --api-only)
|
|
128
133
|
if (!options.apiOnly) {
|
|
129
|
-
|
|
134
|
+
const uiAuthConfig = getAuthConfig();
|
|
135
|
+
const uiAuthPolicy = resolveAuthPolicy(modelInfo, uiAuthConfig);
|
|
136
|
+
const uiAuthOptions: UIAuthOptions | undefined =
|
|
137
|
+
uiAuthPolicy && uiAuthConfig && uiAuthConfig.provider !== 'none'
|
|
138
|
+
? { authPolicy: uiAuthPolicy }
|
|
139
|
+
: undefined;
|
|
140
|
+
await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
|
|
130
141
|
await updateNavigationMenu(modelInfo);
|
|
131
142
|
}
|
|
132
143
|
|
|
@@ -153,6 +164,33 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
153
164
|
}
|
|
154
165
|
}
|
|
155
166
|
|
|
167
|
+
/**
|
|
168
|
+
* モデルの authPolicy と auth config から実効的な認可ポリシーを解決
|
|
169
|
+
* - モデルに authPolicy があればそれを使用
|
|
170
|
+
* - なければ auth.authorization.defaultPolicy に従う
|
|
171
|
+
* - auth 設定がなければ undefined(ガードなし)
|
|
172
|
+
*/
|
|
173
|
+
function resolveAuthPolicy(modelInfo: ModelInfo, authConfig: AuthConfig | undefined): ModelAuthPolicy | undefined {
|
|
174
|
+
// モデルに明示的な authPolicy がある場合はそれを使用
|
|
175
|
+
if (modelInfo.authPolicy) {
|
|
176
|
+
return modelInfo.authPolicy;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// auth 設定がない場合はガードなし
|
|
180
|
+
if (!authConfig || authConfig.provider === 'none') {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// defaultPolicy が 'authenticated' なら認証のみ(ロール指定なし)のポリシーを返す
|
|
185
|
+
const defaultPolicy = authConfig.authorization?.defaultPolicy ?? 'authenticated';
|
|
186
|
+
if (defaultPolicy === 'authenticated') {
|
|
187
|
+
return {}; // 空のポリシー = 認証のみ、ロール制限なし
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// defaultPolicy が 'anonymous' なら認証不要
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
156
194
|
/**
|
|
157
195
|
* モデルファイルのパスを解決
|
|
158
196
|
*/
|
|
@@ -236,7 +274,17 @@ async function generateCallFunctionHelper(): Promise<void> {
|
|
|
236
274
|
fs.mkdirSync(helperDir, { recursive: true });
|
|
237
275
|
}
|
|
238
276
|
|
|
239
|
-
|
|
277
|
+
// auth 設定がある場合は Authorization ヘッダー転送版を生成
|
|
278
|
+
const authConfig = getAuthConfig();
|
|
279
|
+
const hasAuth = authConfig && authConfig.provider !== 'none';
|
|
280
|
+
let helperCode: string;
|
|
281
|
+
if (hasAuth) {
|
|
282
|
+
const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
|
|
283
|
+
helperCode = generateBFFCallFunctionWithAuth();
|
|
284
|
+
} else {
|
|
285
|
+
helperCode = generateBFFCallFunction();
|
|
286
|
+
}
|
|
287
|
+
|
|
240
288
|
fs.writeFileSync(helperPath, helperCode, "utf-8");
|
|
241
289
|
console.log(`✅ Created: ${helperPath}`);
|
|
242
290
|
}
|
|
@@ -252,6 +300,13 @@ async function generateFunctionsCode(
|
|
|
252
300
|
): Promise<void> {
|
|
253
301
|
console.log("\n🔨 Generating Azure Functions CRUD code...");
|
|
254
302
|
|
|
303
|
+
// Resolve auth policy: model-level authPolicy or global defaultPolicy
|
|
304
|
+
const authConfig = getAuthConfig();
|
|
305
|
+
const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
|
|
306
|
+
if (authPolicy) {
|
|
307
|
+
console.log(`🔐 Auth policy detected: ${JSON.stringify(authPolicy)}`);
|
|
308
|
+
}
|
|
309
|
+
|
|
255
310
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
256
311
|
if (backendLanguage === "typescript") {
|
|
257
312
|
const functionFilePath = path.join(
|
|
@@ -266,7 +321,7 @@ async function generateFunctionsCode(
|
|
|
266
321
|
fs.mkdirSync(functionDir, { recursive: true });
|
|
267
322
|
}
|
|
268
323
|
|
|
269
|
-
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName);
|
|
324
|
+
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
|
|
270
325
|
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
271
326
|
console.log(`✅ Created: ${functionFilePath}`);
|
|
272
327
|
return;
|
|
@@ -316,6 +371,13 @@ async function generateConnectorFunctionsCode(
|
|
|
316
371
|
);
|
|
317
372
|
}
|
|
318
373
|
|
|
374
|
+
// Resolve auth policy (same logic as Cosmos model scaffolding)
|
|
375
|
+
const authConfig = getAuthConfig();
|
|
376
|
+
const authPolicy = resolveAuthPolicy(modelInfo, authConfig);
|
|
377
|
+
if (authPolicy) {
|
|
378
|
+
console.log(`🔐 Auth policy detected: ${JSON.stringify(authPolicy)}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
319
381
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
320
382
|
|
|
321
383
|
if (backendLanguage === "typescript") {
|
|
@@ -332,13 +394,15 @@ async function generateConnectorFunctionsCode(
|
|
|
332
394
|
code = generateRdbConnectorFunctionTS(
|
|
333
395
|
modelInfo, sharedPackageName,
|
|
334
396
|
connectorDef as RdbConnectorConfig,
|
|
335
|
-
connectorConfig as RdbModelConnectorConfig
|
|
397
|
+
connectorConfig as RdbModelConnectorConfig,
|
|
398
|
+
authPolicy
|
|
336
399
|
);
|
|
337
400
|
} else {
|
|
338
401
|
code = generateApiConnectorFunctionTS(
|
|
339
402
|
modelInfo, sharedPackageName,
|
|
340
403
|
connectorDef as ApiConnectorConfig,
|
|
341
|
-
connectorConfig as ApiModelConnectorConfig
|
|
404
|
+
connectorConfig as ApiModelConnectorConfig,
|
|
405
|
+
authPolicy
|
|
342
406
|
);
|
|
343
407
|
}
|
|
344
408
|
|
|
@@ -401,6 +465,96 @@ async function generateConnectorFunctionsCode(
|
|
|
401
465
|
console.log(`✅ Created: ${blueprintPath}`);
|
|
402
466
|
}
|
|
403
467
|
|
|
468
|
+
/**
|
|
469
|
+
* コネクタモデルの scaffold 後に、生成コードが必要とする RDB/API ドライバを
|
|
470
|
+
* functions ディレクトリにインストールする。
|
|
471
|
+
*/
|
|
472
|
+
async function installConnectorDriverDependencies(
|
|
473
|
+
modelInfo: ModelInfo,
|
|
474
|
+
functionsDir: string,
|
|
475
|
+
backendLanguage: BackendLanguage
|
|
476
|
+
): Promise<void> {
|
|
477
|
+
const connectorConfig = modelInfo.connectorConfig!;
|
|
478
|
+
const connectorDef = getConnectorDefinition(connectorConfig.connector);
|
|
479
|
+
if (!connectorDef || connectorDef.type !== "rdb") return;
|
|
480
|
+
|
|
481
|
+
const rdbDef = connectorDef as RdbConnectorConfig;
|
|
482
|
+
const functionsPath = path.join(process.cwd(), functionsDir);
|
|
483
|
+
|
|
484
|
+
if (backendLanguage === "typescript") {
|
|
485
|
+
const driverMap: Record<string, { deps: string[]; devDeps: string[] }> = {
|
|
486
|
+
mysql: { deps: ["mysql2"], devDeps: [] },
|
|
487
|
+
postgres: { deps: ["pg"], devDeps: ["@types/pg"] },
|
|
488
|
+
sqlserver: { deps: ["mssql"], devDeps: [] },
|
|
489
|
+
};
|
|
490
|
+
const entry = driverMap[rdbDef.provider];
|
|
491
|
+
if (!entry) return;
|
|
492
|
+
|
|
493
|
+
// package.json を読んで、既にインストール済みならスキップ
|
|
494
|
+
const pkgJsonPath = path.join(functionsPath, "package.json");
|
|
495
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
496
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
497
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
498
|
+
const missingDeps = entry.deps.filter(d => !allDeps[d]);
|
|
499
|
+
const missingDevDeps = entry.devDeps.filter(d => !allDeps[d]);
|
|
500
|
+
if (missingDeps.length === 0 && missingDevDeps.length === 0) return;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
console.log(`\n📦 Installing ${rdbDef.provider} driver dependencies...`);
|
|
504
|
+
const pm = detectFromProject(functionsPath);
|
|
505
|
+
const cmds = getCommands(pm);
|
|
506
|
+
|
|
507
|
+
const { spawnSync } = require("child_process");
|
|
508
|
+
if (entry.deps.length > 0) {
|
|
509
|
+
spawnSync(cmds.name, [pm === "pnpm" ? "add" : "install", ...entry.deps], {
|
|
510
|
+
cwd: functionsPath, stdio: "inherit", shell: true,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (entry.devDeps.length > 0) {
|
|
514
|
+
spawnSync(cmds.name, [pm === "pnpm" ? "add" : "install", "-D", ...entry.devDeps], {
|
|
515
|
+
cwd: functionsPath, stdio: "inherit", shell: true,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
console.log(`✅ ${rdbDef.provider} driver installed`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (backendLanguage === "csharp") {
|
|
523
|
+
const nugetMap: Record<string, string> = {
|
|
524
|
+
mysql: "MySqlConnector",
|
|
525
|
+
postgres: "Npgsql",
|
|
526
|
+
sqlserver: "Microsoft.Data.SqlClient",
|
|
527
|
+
};
|
|
528
|
+
const pkg = nugetMap[rdbDef.provider];
|
|
529
|
+
if (!pkg) return;
|
|
530
|
+
console.log(`\n📦 Installing ${rdbDef.provider} NuGet package...`);
|
|
531
|
+
const { spawnSync } = require("child_process");
|
|
532
|
+
spawnSync("dotnet", ["add", path.join(functionsPath, "functions.csproj"), "package", pkg], {
|
|
533
|
+
cwd: functionsPath, stdio: "inherit", shell: true,
|
|
534
|
+
});
|
|
535
|
+
console.log(`✅ ${pkg} installed`);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Python — requirements.txt に追記
|
|
540
|
+
const pipMap: Record<string, string> = {
|
|
541
|
+
mysql: "mysql-connector-python",
|
|
542
|
+
postgres: "psycopg2-binary",
|
|
543
|
+
sqlserver: "pymssql",
|
|
544
|
+
};
|
|
545
|
+
const pipPkg = pipMap[rdbDef.provider];
|
|
546
|
+
if (!pipPkg) return;
|
|
547
|
+
const reqPath = path.join(functionsPath, "requirements.txt");
|
|
548
|
+
if (fs.existsSync(reqPath)) {
|
|
549
|
+
const existing = fs.readFileSync(reqPath, "utf-8");
|
|
550
|
+
if (!existing.includes(pipPkg)) {
|
|
551
|
+
console.log(`\n📦 Adding ${pipPkg} to requirements.txt...`);
|
|
552
|
+
fs.appendFileSync(reqPath, `\n${pipPkg}\n`);
|
|
553
|
+
console.log(`✅ ${pipPkg} added`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
404
558
|
/**
|
|
405
559
|
* コネクタモデル用 BFF ルート生成(操作制限に対応)
|
|
406
560
|
*/
|
|
@@ -874,7 +1028,7 @@ module ${containerModuleName} 'containers/${modelKebab}-container.bicep' = {
|
|
|
874
1028
|
/**
|
|
875
1029
|
* Generate UI components (list, detail, form, create, edit pages)
|
|
876
1030
|
*/
|
|
877
|
-
async function generateUIComponents(modelInfo: any, sharedPackageName: string): Promise<void> {
|
|
1031
|
+
async function generateUIComponents(modelInfo: any, sharedPackageName: string, authOptions?: UIAuthOptions): Promise<void> {
|
|
878
1032
|
console.log("\n🎨 Generating UI components...");
|
|
879
1033
|
|
|
880
1034
|
const modelKebab = toKebabCase(modelInfo.name);
|
|
@@ -896,11 +1050,11 @@ async function generateUIComponents(modelInfo: any, sharedPackageName: string):
|
|
|
896
1050
|
});
|
|
897
1051
|
|
|
898
1052
|
// Generate and write files
|
|
899
|
-
const listPage = generateListPage(modelInfo, sharedPackageName);
|
|
900
|
-
const detailPage = generateDetailPage(modelInfo, sharedPackageName);
|
|
1053
|
+
const listPage = generateListPage(modelInfo, sharedPackageName, authOptions);
|
|
1054
|
+
const detailPage = generateDetailPage(modelInfo, sharedPackageName, authOptions);
|
|
901
1055
|
const formComponent = generateFormComponent(modelInfo, sharedPackageName);
|
|
902
|
-
const newPage = generateNewPage(modelInfo);
|
|
903
|
-
const editPage = generateEditPage(modelInfo, sharedPackageName);
|
|
1056
|
+
const newPage = generateNewPage(modelInfo, authOptions);
|
|
1057
|
+
const editPage = generateEditPage(modelInfo, sharedPackageName, authOptions);
|
|
904
1058
|
|
|
905
1059
|
fs.writeFileSync(path.join(modelDir, "page.tsx"), listPage, "utf-8");
|
|
906
1060
|
fs.writeFileSync(path.join(idDir, "page.tsx"), detailPage, "utf-8");
|
package/src/cli/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { Command } from "commander";
|
|
|
11
11
|
import { initCommand, devCommand, devSeedsCommand, scaffoldCommand, createModelCommand } from "./commands";
|
|
12
12
|
import { provisionCommand } from "./commands/provision";
|
|
13
13
|
import { addConnectorCommand } from "./commands/add-connector";
|
|
14
|
+
import { addAuthCommand } from "./commands/add-auth";
|
|
14
15
|
|
|
15
16
|
const program = new Command();
|
|
16
17
|
|
|
@@ -87,4 +88,14 @@ program
|
|
|
87
88
|
});
|
|
88
89
|
});
|
|
89
90
|
|
|
91
|
+
program
|
|
92
|
+
.command("add-auth")
|
|
93
|
+
.description("Add authentication and authorization to the project")
|
|
94
|
+
.option("--provider <provider>", "Auth provider: custom-jwt | swa | swa-custom | none", "custom-jwt")
|
|
95
|
+
.action((options) => {
|
|
96
|
+
addAuthCommand({
|
|
97
|
+
provider: options.provider,
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
90
101
|
program.parse();
|
package/src/core/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
|
-
import { BackendLanguage, ConnectorDefinition, SwallowKitConfig } from "../types";
|
|
3
|
+
import { AuthConfig, BackendLanguage, ConnectorDefinition, SwallowKitConfig } from "../types";
|
|
4
4
|
import { detectFromProject, getCommands } from "../utils/package-manager";
|
|
5
5
|
|
|
6
6
|
const VALID_BACKEND_LANGUAGES: BackendLanguage[] = ["typescript", "csharp", "python"];
|
|
@@ -81,7 +81,10 @@ function mergeConfig(defaultConfig: SwallowKitConfig, userConfig: Partial<Swallo
|
|
|
81
81
|
...userConfig.api?.cors,
|
|
82
82
|
},
|
|
83
83
|
},
|
|
84
|
-
|
|
84
|
+
connectors: userConfig.connectors ?? defaultConfig.connectors,
|
|
85
|
+
auth: userConfig.auth
|
|
86
|
+
? { ...defaultConfig.auth, ...userConfig.auth }
|
|
87
|
+
: defaultConfig.auth,
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -177,6 +180,14 @@ export function getConnectorDefinition(connectorName: string, configPath?: strin
|
|
|
177
180
|
return config.connectors?.[connectorName];
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
/**
|
|
184
|
+
* 認証設定を取得
|
|
185
|
+
*/
|
|
186
|
+
export function getAuthConfig(configPath?: string): AuthConfig | undefined {
|
|
187
|
+
const config = getFullConfig(configPath);
|
|
188
|
+
return config.auth;
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
/**
|
|
181
192
|
* 設定の検証
|
|
182
193
|
*/
|
|
@@ -26,6 +26,23 @@ export interface ConnectorMockServerOptions {
|
|
|
26
26
|
mockCount?: number;
|
|
27
27
|
/** ホスト名 */
|
|
28
28
|
host?: string;
|
|
29
|
+
/** Auth config — auth functions use RDB connector, mocked the same way */
|
|
30
|
+
authConfig?: {
|
|
31
|
+
/** JWT secret for mock token generation/verification */
|
|
32
|
+
jwtSecret: string;
|
|
33
|
+
/** Token expiry (e.g., '24h') */
|
|
34
|
+
tokenExpiry?: string;
|
|
35
|
+
/** Custom JWT config from swallowkit.config.js */
|
|
36
|
+
customJwt?: {
|
|
37
|
+
/** RDB table name that holds user records (e.g., "users") */
|
|
38
|
+
userTable: string;
|
|
39
|
+
loginIdColumn: string;
|
|
40
|
+
passwordHashColumn: string;
|
|
41
|
+
rolesColumn: string;
|
|
42
|
+
};
|
|
43
|
+
/** Default auth policy: 'authenticated' = all models need auth, 'anonymous' = only models with authPolicy */
|
|
44
|
+
defaultPolicy?: "authenticated" | "anonymous";
|
|
45
|
+
};
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
type MockDocument = Record<string, unknown>;
|
|
@@ -128,6 +145,12 @@ export class ConnectorMockServer {
|
|
|
128
145
|
private handleRequest(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
129
146
|
const { route, id } = this.parseRoute(req.url || "");
|
|
130
147
|
|
|
148
|
+
// Auth endpoints (when authConfig is set)
|
|
149
|
+
if (this.options.authConfig && route === "auth") {
|
|
150
|
+
this.handleAuthRequest(req, res, id);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
131
154
|
// コネクタモデルのルートか判定
|
|
132
155
|
if (route && this.routeMap.has(route)) {
|
|
133
156
|
this.handleMockCrud(req, res, route, id);
|
|
@@ -165,6 +188,11 @@ export class ConnectorMockServer {
|
|
|
165
188
|
const model = this.routeMap.get(route)!;
|
|
166
189
|
const ops = model.connectorConfig?.operations || [];
|
|
167
190
|
|
|
191
|
+
const isWrite = method === "POST" || method === "PUT" || method === "DELETE";
|
|
192
|
+
const requiredRoles = this.resolveRequiredRoles(model, isWrite);
|
|
193
|
+
const authResult = this.checkAuth(req, res, model, requiredRoles);
|
|
194
|
+
if (authResult === "error") return; // 401/403 already sent
|
|
195
|
+
|
|
168
196
|
switch (method) {
|
|
169
197
|
case "GET":
|
|
170
198
|
req.resume();
|
|
@@ -258,8 +286,228 @@ export class ConnectorMockServer {
|
|
|
258
286
|
req.pipe(proxyReq, { end: true });
|
|
259
287
|
}
|
|
260
288
|
|
|
289
|
+
// ─── Auth Routes (RDB user queries + JWT generation) ───
|
|
290
|
+
|
|
291
|
+
private handleAuthRequest(
|
|
292
|
+
req: http.IncomingMessage,
|
|
293
|
+
res: http.ServerResponse,
|
|
294
|
+
endpoint: string | null
|
|
295
|
+
) {
|
|
296
|
+
const method = (req.method || "GET").toUpperCase();
|
|
297
|
+
|
|
298
|
+
// Handle CORS preflight
|
|
299
|
+
if (method === "OPTIONS") {
|
|
300
|
+
req.resume();
|
|
301
|
+
res.writeHead(200, {
|
|
302
|
+
"Access-Control-Allow-Origin": "*",
|
|
303
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
304
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
305
|
+
});
|
|
306
|
+
res.end();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (endpoint === "login" && method === "POST") {
|
|
311
|
+
this.handleLogin(req, res);
|
|
312
|
+
} else if (endpoint === "me" && method === "GET") {
|
|
313
|
+
this.handleMe(req, res);
|
|
314
|
+
} else if (endpoint === "logout" && method === "POST") {
|
|
315
|
+
req.resume();
|
|
316
|
+
this.sendJson(res, 200, { message: "Logged out" });
|
|
317
|
+
} else {
|
|
318
|
+
req.resume();
|
|
319
|
+
this.sendJson(res, 404, { error: "Auth endpoint not found" });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private handleLogin(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
324
|
+
this.readBody(req, (body) => {
|
|
325
|
+
const loginId = body.loginId as string;
|
|
326
|
+
const password = body.password as string;
|
|
327
|
+
|
|
328
|
+
if (!loginId || !password) {
|
|
329
|
+
return this.sendJson(res, 400, { error: "loginId and password are required" });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const users = this.resolveUserStore();
|
|
333
|
+
if (!users) {
|
|
334
|
+
return this.sendJson(res, 500, {
|
|
335
|
+
error: "No user model found — ensure a connector model with the configured userTable exists",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const loginField = this.options.authConfig?.customJwt?.loginIdColumn || "loginId";
|
|
340
|
+
const passwordField = this.options.authConfig?.customJwt?.passwordHashColumn || "password";
|
|
341
|
+
const rolesField = this.options.authConfig?.customJwt?.rolesColumn || "roles";
|
|
342
|
+
|
|
343
|
+
const user = users.find(
|
|
344
|
+
(u) => (u[loginField] || u.loginId) === loginId
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
if (!user) {
|
|
348
|
+
return this.sendJson(res, 401, { error: "Invalid credentials" });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Plaintext password comparison (mock mode only)
|
|
352
|
+
const storedPassword = (user[passwordField] || user.password) as string;
|
|
353
|
+
if (storedPassword !== password) {
|
|
354
|
+
return this.sendJson(res, 401, { error: "Invalid credentials" });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Parse roles
|
|
358
|
+
let roles: string[] = [];
|
|
359
|
+
const rolesValue = user[rolesField] || user.roles;
|
|
360
|
+
if (Array.isArray(rolesValue)) {
|
|
361
|
+
roles = rolesValue as string[];
|
|
362
|
+
} else if (typeof rolesValue === "string") {
|
|
363
|
+
try {
|
|
364
|
+
roles = JSON.parse(rolesValue);
|
|
365
|
+
} catch {
|
|
366
|
+
roles = (rolesValue as string).split(",").map((r: string) => r.trim());
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const authUser = {
|
|
371
|
+
id: String(user.id),
|
|
372
|
+
loginId: String(user[loginField] || user.loginId),
|
|
373
|
+
name: String(user.name || user[loginField] || user.loginId),
|
|
374
|
+
email: String(user.email || ""),
|
|
375
|
+
roles,
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Generate JWT
|
|
379
|
+
const jwt = require("jsonwebtoken");
|
|
380
|
+
const secret = this.options.authConfig!.jwtSecret;
|
|
381
|
+
const expiry = this.options.authConfig!.tokenExpiry || "24h";
|
|
382
|
+
|
|
383
|
+
const token = jwt.sign(
|
|
384
|
+
{
|
|
385
|
+
sub: authUser.id,
|
|
386
|
+
loginId: authUser.loginId,
|
|
387
|
+
name: authUser.name,
|
|
388
|
+
email: authUser.email,
|
|
389
|
+
roles: authUser.roles,
|
|
390
|
+
},
|
|
391
|
+
secret,
|
|
392
|
+
{ expiresIn: expiry }
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
|
|
396
|
+
|
|
397
|
+
this.sendJson(res, 200, { user: authUser, token, expiresAt });
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
private handleMe(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
402
|
+
req.resume();
|
|
403
|
+
|
|
404
|
+
const authHeader = req.headers["authorization"];
|
|
405
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
406
|
+
return this.sendJson(res, 401, { error: "Missing or invalid Authorization header" });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const token = authHeader.slice(7);
|
|
410
|
+
try {
|
|
411
|
+
const jwt = require("jsonwebtoken");
|
|
412
|
+
const secret = this.options.authConfig!.jwtSecret;
|
|
413
|
+
const payload = jwt.verify(token, secret) as Record<string, unknown>;
|
|
414
|
+
this.sendJson(res, 200, {
|
|
415
|
+
sub: payload.sub,
|
|
416
|
+
loginId: payload.loginId,
|
|
417
|
+
name: payload.name,
|
|
418
|
+
email: payload.email,
|
|
419
|
+
roles: payload.roles,
|
|
420
|
+
});
|
|
421
|
+
} catch {
|
|
422
|
+
this.sendJson(res, 401, { error: "Invalid or expired token" });
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
261
426
|
// ─── Utilities ────────────────────────────────────────────
|
|
262
427
|
|
|
428
|
+
/**
|
|
429
|
+
* authConfig.customJwt.userTable に対応するモデルのストアを返す。
|
|
430
|
+
* ユーザーテーブルが見つからない場合は null を返す。
|
|
431
|
+
*/
|
|
432
|
+
private resolveUserStore(): MockDocument[] | null {
|
|
433
|
+
const userTable = this.options.authConfig?.customJwt?.userTable;
|
|
434
|
+
if (!userTable) return null;
|
|
435
|
+
|
|
436
|
+
for (const model of this.options.connectorModels) {
|
|
437
|
+
const cfg = model.connectorConfig;
|
|
438
|
+
if (cfg && "table" in cfg && cfg.table === userTable) {
|
|
439
|
+
return this.stores.get(toCamelCase(model.name)) || null;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* JWT を検証し、ペイロード(roles 含む)を返す。
|
|
447
|
+
* 認証不要な場合は null を返す。401/403 の場合はレスポンスを送信して 'error' を返す。
|
|
448
|
+
*/
|
|
449
|
+
private checkAuth(
|
|
450
|
+
req: http.IncomingMessage,
|
|
451
|
+
res: http.ServerResponse,
|
|
452
|
+
model: ModelInfo,
|
|
453
|
+
requiredRoles: string[] | undefined
|
|
454
|
+
): Record<string, unknown> | null | "error" {
|
|
455
|
+
const authCfg = this.options.authConfig;
|
|
456
|
+
if (!authCfg) return null; // auth 未設定 → 全スルー
|
|
457
|
+
|
|
458
|
+
const policy = model.authPolicy;
|
|
459
|
+
const defaultPolicy = authCfg.defaultPolicy || "anonymous";
|
|
460
|
+
|
|
461
|
+
// モデルに authPolicy がなく defaultPolicy が anonymous → 認証不要
|
|
462
|
+
if (!policy && defaultPolicy === "anonymous") return null;
|
|
463
|
+
|
|
464
|
+
// JWT 検証
|
|
465
|
+
const authHeader = req.headers["authorization"];
|
|
466
|
+
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
467
|
+
this.sendJson(res, 401, { error: "Missing or invalid Authorization header" });
|
|
468
|
+
return "error";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const token = authHeader.slice(7);
|
|
472
|
+
try {
|
|
473
|
+
const jwt = require("jsonwebtoken");
|
|
474
|
+
const payload = jwt.verify(token, authCfg.jwtSecret) as Record<string, unknown>;
|
|
475
|
+
|
|
476
|
+
// ロールチェック
|
|
477
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
478
|
+
const userRoles = Array.isArray(payload.roles) ? (payload.roles as string[]) : [];
|
|
479
|
+
const hasRole = requiredRoles.some((r) => userRoles.includes(r));
|
|
480
|
+
if (!hasRole) {
|
|
481
|
+
this.sendJson(res, 403, {
|
|
482
|
+
error: `Requires one of roles: ${requiredRoles.join(", ")}`,
|
|
483
|
+
});
|
|
484
|
+
return "error";
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return payload;
|
|
489
|
+
} catch {
|
|
490
|
+
this.sendJson(res, 401, { error: "Invalid or expired token" });
|
|
491
|
+
return "error";
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* モデルの authPolicy から read/write に必要なロールを解決
|
|
497
|
+
*/
|
|
498
|
+
private resolveRequiredRoles(
|
|
499
|
+
model: ModelInfo,
|
|
500
|
+
isWrite: boolean
|
|
501
|
+
): string[] | undefined {
|
|
502
|
+
const policy = model.authPolicy;
|
|
503
|
+
if (!policy) return undefined; // defaultPolicy: authenticated → 認証のみ、ロールチェック不要
|
|
504
|
+
|
|
505
|
+
if (isWrite) {
|
|
506
|
+
return policy.write || policy.roles;
|
|
507
|
+
}
|
|
508
|
+
return policy.read || policy.roles;
|
|
509
|
+
}
|
|
510
|
+
|
|
263
511
|
private readBody(req: http.IncomingMessage, callback: (body: MockDocument) => void) {
|
|
264
512
|
let data = "";
|
|
265
513
|
req.on("data", (chunk) => (data += chunk));
|