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
|
@@ -15,7 +15,8 @@ function httpRequest(
|
|
|
15
15
|
port: number,
|
|
16
16
|
method: string,
|
|
17
17
|
path: string,
|
|
18
|
-
body?: unknown
|
|
18
|
+
body?: unknown,
|
|
19
|
+
headers?: Record<string, string>
|
|
19
20
|
): Promise<{ status: number; body: unknown }> {
|
|
20
21
|
return new Promise((resolve, reject) => {
|
|
21
22
|
const opts: http.RequestOptions = {
|
|
@@ -23,7 +24,7 @@ function httpRequest(
|
|
|
23
24
|
port,
|
|
24
25
|
path,
|
|
25
26
|
method,
|
|
26
|
-
headers: { "Content-Type": "application/json" },
|
|
27
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
const req = http.request(opts, (res) => {
|
|
@@ -250,3 +251,189 @@ describe("ConnectorMockServer", () => {
|
|
|
250
251
|
expect(store[0].id).toBe("user-001");
|
|
251
252
|
});
|
|
252
253
|
});
|
|
254
|
+
|
|
255
|
+
// ============================================================
|
|
256
|
+
// Mock Auth Endpoints
|
|
257
|
+
// ============================================================
|
|
258
|
+
describe("ConnectorMockServer - Auth Endpoints", () => {
|
|
259
|
+
let server: ConnectorMockServer;
|
|
260
|
+
const AUTH_PORT = 19877;
|
|
261
|
+
const JWT_SECRET = "test-jwt-secret-for-mock-auth-tests";
|
|
262
|
+
|
|
263
|
+
// Auth-compatible User model (has loginId, password, roles fields)
|
|
264
|
+
const authUserModel = createRdbConnectorModelInfo({
|
|
265
|
+
name: "User",
|
|
266
|
+
displayName: "User",
|
|
267
|
+
schemaName: "userSchema",
|
|
268
|
+
filePath: "/models/user.ts",
|
|
269
|
+
fields: [
|
|
270
|
+
{ name: "id", type: "string", isOptional: false, isArray: false },
|
|
271
|
+
{ name: "loginId", type: "string", isOptional: false, isArray: false },
|
|
272
|
+
{ name: "password", type: "string", isOptional: false, isArray: false },
|
|
273
|
+
{ name: "name", type: "string", isOptional: false, isArray: false },
|
|
274
|
+
{ name: "email", type: "string", isOptional: false, isArray: false },
|
|
275
|
+
{ name: "roles", type: "string", isOptional: false, isArray: true },
|
|
276
|
+
],
|
|
277
|
+
connectorConfig: {
|
|
278
|
+
connector: "mysql",
|
|
279
|
+
operations: ["getAll", "getById"],
|
|
280
|
+
table: "users",
|
|
281
|
+
idColumn: "id",
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const testUsers = [
|
|
286
|
+
{ id: "1", loginId: "admin", password: "password123", name: "Admin User", email: "admin@example.com", roles: ["admin"] },
|
|
287
|
+
{ id: "2", loginId: "user", password: "password123", name: "Test User", email: "user@example.com", roles: ["user"] },
|
|
288
|
+
];
|
|
289
|
+
|
|
290
|
+
afterEach(async () => {
|
|
291
|
+
if (server) {
|
|
292
|
+
await server.stop();
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
/** Start mock server with auth-compatible User model and seed data */
|
|
297
|
+
async function startAuthServer() {
|
|
298
|
+
server = new ConnectorMockServer({
|
|
299
|
+
port: AUTH_PORT,
|
|
300
|
+
functionsTarget: "localhost:7071",
|
|
301
|
+
connectorModels: [authUserModel],
|
|
302
|
+
mockCount: 0,
|
|
303
|
+
authConfig: {
|
|
304
|
+
jwtSecret: JWT_SECRET,
|
|
305
|
+
tokenExpiry: "1h",
|
|
306
|
+
customJwt: {
|
|
307
|
+
userTable: "users",
|
|
308
|
+
loginIdColumn: "loginId",
|
|
309
|
+
passwordHashColumn: "password",
|
|
310
|
+
rolesColumn: "roles",
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
await server.start();
|
|
315
|
+
// Populate user store with known test data
|
|
316
|
+
const store = server.getStore("User");
|
|
317
|
+
store.push(...testUsers);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
it("handles POST /api/auth/login with users from RDB mock store", async () => {
|
|
321
|
+
await startAuthServer();
|
|
322
|
+
|
|
323
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
324
|
+
loginId: "admin",
|
|
325
|
+
password: "password123",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
expect(res.status).toBe(200);
|
|
329
|
+
const body = res.body as any;
|
|
330
|
+
expect(body.user).toBeDefined();
|
|
331
|
+
expect(body.user.loginId).toBe("admin");
|
|
332
|
+
expect(body.user.roles).toContain("admin");
|
|
333
|
+
expect(body.token).toBeDefined();
|
|
334
|
+
expect(typeof body.token).toBe("string");
|
|
335
|
+
expect(body.expiresAt).toBeDefined();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("returns 401 for invalid credentials", async () => {
|
|
339
|
+
await startAuthServer();
|
|
340
|
+
|
|
341
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
342
|
+
loginId: "admin",
|
|
343
|
+
password: "wrong-password",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(res.status).toBe(401);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("returns 401 for non-existent user", async () => {
|
|
350
|
+
await startAuthServer();
|
|
351
|
+
|
|
352
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
353
|
+
loginId: "nobody",
|
|
354
|
+
password: "password123",
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
expect(res.status).toBe(401);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it("handles GET /api/auth/me with valid JWT", async () => {
|
|
361
|
+
await startAuthServer();
|
|
362
|
+
|
|
363
|
+
// Login first
|
|
364
|
+
const loginRes = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
365
|
+
loginId: "admin",
|
|
366
|
+
password: "password123",
|
|
367
|
+
});
|
|
368
|
+
const token = (loginRes.body as any).token;
|
|
369
|
+
|
|
370
|
+
// Then call /me
|
|
371
|
+
const meRes = await httpRequest(AUTH_PORT, "GET", "/api/auth/me", undefined, {
|
|
372
|
+
Authorization: `Bearer ${token}`,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(meRes.status).toBe(200);
|
|
376
|
+
const meBody = meRes.body as any;
|
|
377
|
+
expect(meBody.loginId).toBe("admin");
|
|
378
|
+
expect(meBody.roles).toContain("admin");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("returns 401 for /api/auth/me without token", async () => {
|
|
382
|
+
await startAuthServer();
|
|
383
|
+
|
|
384
|
+
const res = await httpRequest(AUTH_PORT, "GET", "/api/auth/me");
|
|
385
|
+
expect(res.status).toBe(401);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("handles POST /api/auth/logout", async () => {
|
|
389
|
+
await startAuthServer();
|
|
390
|
+
|
|
391
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/logout");
|
|
392
|
+
expect(res.status).toBe(200);
|
|
393
|
+
expect((res.body as any).message).toBe("Logged out");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("does not intercept auth routes when authConfig is not set", async () => {
|
|
397
|
+
// Without authConfig, auth routes should be proxied (which will fail since no real Functions)
|
|
398
|
+
server = new ConnectorMockServer({
|
|
399
|
+
port: AUTH_PORT,
|
|
400
|
+
functionsTarget: "localhost:19999", // non-existent to trigger proxy error
|
|
401
|
+
connectorModels: [],
|
|
402
|
+
});
|
|
403
|
+
await server.start();
|
|
404
|
+
|
|
405
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
406
|
+
loginId: "admin",
|
|
407
|
+
password: "password123",
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Should get 502 (proxy error) since auth is not handled by mock
|
|
411
|
+
expect(res.status).toBe(502);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("returns 500 when no user model matches the configured userTable", async () => {
|
|
415
|
+
server = new ConnectorMockServer({
|
|
416
|
+
port: AUTH_PORT,
|
|
417
|
+
functionsTarget: "localhost:7071",
|
|
418
|
+
connectorModels: [], // no models at all
|
|
419
|
+
authConfig: {
|
|
420
|
+
jwtSecret: JWT_SECRET,
|
|
421
|
+
customJwt: {
|
|
422
|
+
userTable: "users",
|
|
423
|
+
loginIdColumn: "loginId",
|
|
424
|
+
passwordHashColumn: "password",
|
|
425
|
+
rolesColumn: "roles",
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
});
|
|
429
|
+
await server.start();
|
|
430
|
+
|
|
431
|
+
const res = await httpRequest(AUTH_PORT, "POST", "/api/auth/login", {
|
|
432
|
+
loginId: "admin",
|
|
433
|
+
password: "password123",
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(res.status).toBe(500);
|
|
437
|
+
expect((res.body as any).error).toContain("No user model found");
|
|
438
|
+
});
|
|
439
|
+
});
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SwallowKit Add-Auth コマンド
|
|
3
|
+
* 認証認可基盤ファイルを生成する
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "fs";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition, getFullConfig } from "../../core/config";
|
|
9
|
+
import { AuthProvider, BackendLanguage, CustomJwtConfig, RdbConnectorConfig } from "../../types";
|
|
10
|
+
import {
|
|
11
|
+
generateAuthModels,
|
|
12
|
+
generateAuthFunctionsTS,
|
|
13
|
+
generateJwtHelperTS,
|
|
14
|
+
generateAuthFunctionsCSharp,
|
|
15
|
+
generateJwtHelperCSharp,
|
|
16
|
+
generateAuthFunctionsPython,
|
|
17
|
+
generateJwtHelperPython,
|
|
18
|
+
generateBFFAuthLoginRoute,
|
|
19
|
+
generateBFFAuthLogoutRoute,
|
|
20
|
+
generateBFFAuthMeRoute,
|
|
21
|
+
generateMiddleware,
|
|
22
|
+
generateLoginPage,
|
|
23
|
+
generateAuthContext,
|
|
24
|
+
generateBFFCallFunctionWithAuth,
|
|
25
|
+
} from "../../core/scaffold/auth-generator";
|
|
26
|
+
import { detectFromProject, getCommands } from "../../utils/package-manager";
|
|
27
|
+
|
|
28
|
+
interface AddAuthOptions {
|
|
29
|
+
provider?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function addAuthCommand(options: AddAuthOptions) {
|
|
33
|
+
ensureSwallowKitProject("add-auth");
|
|
34
|
+
|
|
35
|
+
console.log(" SwallowKit Add-Auth: Setting up authentication...\n");
|
|
36
|
+
|
|
37
|
+
const provider = (options.provider || "custom-jwt") as AuthProvider;
|
|
38
|
+
if (!["custom-jwt", "swa", "swa-custom", "none"].includes(provider)) {
|
|
39
|
+
console.error(` Unknown provider: ${provider}. Use: custom-jwt | swa | swa-custom | none`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const backendLanguage = getBackendLanguage();
|
|
44
|
+
const config = getFullConfig();
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
|
|
47
|
+
// Read project name from package.json
|
|
48
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
49
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
50
|
+
const projectName = pkg.name || "app";
|
|
51
|
+
|
|
52
|
+
// Read shared package name
|
|
53
|
+
const sharedPkgPath = path.join(cwd, "shared", "package.json");
|
|
54
|
+
let sharedPackageName = `@${projectName}/shared`;
|
|
55
|
+
if (fs.existsSync(sharedPkgPath)) {
|
|
56
|
+
const sharedPkg = JSON.parse(fs.readFileSync(sharedPkgPath, "utf-8"));
|
|
57
|
+
sharedPackageName = sharedPkg.name || sharedPackageName;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Default custom-jwt config
|
|
61
|
+
const customJwtConfig: CustomJwtConfig = config.auth?.customJwt || {
|
|
62
|
+
userConnector: "mysql",
|
|
63
|
+
userTable: "users",
|
|
64
|
+
loginIdColumn: "login_id",
|
|
65
|
+
passwordHashColumn: "password_hash",
|
|
66
|
+
rolesColumn: "roles",
|
|
67
|
+
jwtSecretEnv: "JWT_SECRET",
|
|
68
|
+
tokenExpiry: "24h",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// 1. Generate shared/models/auth.ts
|
|
72
|
+
console.log(" Generating auth models...");
|
|
73
|
+
const modelsDir = path.join(cwd, "shared", "models");
|
|
74
|
+
fs.mkdirSync(modelsDir, { recursive: true });
|
|
75
|
+
const authModelPath = path.join(modelsDir, "auth.ts");
|
|
76
|
+
fs.writeFileSync(authModelPath, generateAuthModels(), "utf-8");
|
|
77
|
+
console.log(` Created: shared/models/auth.ts`);
|
|
78
|
+
|
|
79
|
+
// Update shared/index.ts to re-export auth
|
|
80
|
+
updateSharedIndex(cwd);
|
|
81
|
+
|
|
82
|
+
// Resolve RDB provider for dependency installation
|
|
83
|
+
const connDef = getConnectorDefinition(customJwtConfig.userConnector);
|
|
84
|
+
const rdbProvider = (connDef as RdbConnectorConfig | undefined)?.provider ?? "mysql";
|
|
85
|
+
|
|
86
|
+
// 2. Generate Functions auth code
|
|
87
|
+
console.log("\n Generating auth functions...");
|
|
88
|
+
generateFunctionsAuth(cwd, backendLanguage, sharedPackageName, customJwtConfig);
|
|
89
|
+
|
|
90
|
+
// 3. Generate BFF auth routes
|
|
91
|
+
console.log("\n Generating BFF auth routes...");
|
|
92
|
+
generateBFFAuth(cwd, projectName, sharedPackageName);
|
|
93
|
+
|
|
94
|
+
// 4. Generate middleware
|
|
95
|
+
console.log("\n Generating middleware...");
|
|
96
|
+
const middlewarePath = path.join(cwd, "middleware.ts");
|
|
97
|
+
fs.writeFileSync(middlewarePath, generateMiddleware(projectName), "utf-8");
|
|
98
|
+
console.log(` Created: middleware.ts`);
|
|
99
|
+
|
|
100
|
+
// 5. Generate login page
|
|
101
|
+
console.log("\n Generating login page...");
|
|
102
|
+
const loginDir = path.join(cwd, "app", "login");
|
|
103
|
+
fs.mkdirSync(loginDir, { recursive: true });
|
|
104
|
+
fs.writeFileSync(path.join(loginDir, "page.tsx"), generateLoginPage(), "utf-8");
|
|
105
|
+
console.log(` Created: app/login/page.tsx`);
|
|
106
|
+
|
|
107
|
+
// 6. Generate auth context
|
|
108
|
+
console.log("\n Generating auth context...");
|
|
109
|
+
const authLibDir = path.join(cwd, "lib", "auth");
|
|
110
|
+
fs.mkdirSync(authLibDir, { recursive: true });
|
|
111
|
+
fs.writeFileSync(path.join(authLibDir, "auth-context.tsx"), generateAuthContext(), "utf-8");
|
|
112
|
+
console.log(`✅ Created: lib/auth/auth-context.tsx`);
|
|
113
|
+
|
|
114
|
+
// 7. Update callFunction with auth support
|
|
115
|
+
console.log("\n Updating callFunction with auth support...");
|
|
116
|
+
const callFnPath = path.join(cwd, "lib", "api", "call-function.ts");
|
|
117
|
+
const callFnDir = path.dirname(callFnPath);
|
|
118
|
+
fs.mkdirSync(callFnDir, { recursive: true });
|
|
119
|
+
fs.writeFileSync(callFnPath, generateBFFCallFunctionWithAuth(), "utf-8");
|
|
120
|
+
console.log(` Updated: lib/api/call-function.ts`);
|
|
121
|
+
|
|
122
|
+
// 8. Update swallowkit.config.js
|
|
123
|
+
console.log("\n Updating configuration...");
|
|
124
|
+
updateConfigWithAuth(cwd, provider, customJwtConfig);
|
|
125
|
+
|
|
126
|
+
// 9. Update environment files
|
|
127
|
+
console.log("\n Updating environment files...");
|
|
128
|
+
updateEnvironmentFiles(cwd);
|
|
129
|
+
|
|
130
|
+
// 10. Install dependencies
|
|
131
|
+
console.log("\n Installing auth dependencies...");
|
|
132
|
+
await installAuthDependencies(cwd, backendLanguage, rdbProvider);
|
|
133
|
+
|
|
134
|
+
console.log("\n Authentication setup complete!");
|
|
135
|
+
console.log("\n Next steps:");
|
|
136
|
+
console.log(" 1. Review the generated files");
|
|
137
|
+
console.log(" 2. Set JWT_SECRET in functions/local.settings.json");
|
|
138
|
+
if (provider === "custom-jwt") {
|
|
139
|
+
console.log(" 3. Ensure your user database table matches the config");
|
|
140
|
+
console.log(" 4. Add authPolicy to your models for role-based access control:");
|
|
141
|
+
console.log(" export const authPolicy = { roles: ['admin'] };");
|
|
142
|
+
}
|
|
143
|
+
console.log(` 5. Run scaffold to regenerate functions with auth guards`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function updateSharedIndex(cwd: string): void {
|
|
147
|
+
const indexPath = path.join(cwd, "shared", "index.ts");
|
|
148
|
+
if (fs.existsSync(indexPath)) {
|
|
149
|
+
let content = fs.readFileSync(indexPath, "utf-8");
|
|
150
|
+
if (!content.includes("./models/auth")) {
|
|
151
|
+
content += `\nexport { LoginRequest, AuthUser, LoginResponse } from './models/auth';\n`;
|
|
152
|
+
fs.writeFileSync(indexPath, content, "utf-8");
|
|
153
|
+
console.log(` Updated: shared/index.ts`);
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
fs.writeFileSync(indexPath, `export { LoginRequest, AuthUser, LoginResponse } from './models/auth';\n`, "utf-8");
|
|
157
|
+
console.log(` Created: shared/index.ts`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function generateFunctionsAuth(
|
|
162
|
+
cwd: string,
|
|
163
|
+
backendLanguage: BackendLanguage,
|
|
164
|
+
sharedPackageName: string,
|
|
165
|
+
config: CustomJwtConfig,
|
|
166
|
+
): void {
|
|
167
|
+
const functionsDir = path.join(cwd, "functions");
|
|
168
|
+
|
|
169
|
+
// Resolve the RDB provider from the connector definition
|
|
170
|
+
const connDef = getConnectorDefinition(config.userConnector);
|
|
171
|
+
const provider = (connDef as RdbConnectorConfig | undefined)?.provider ?? "mysql";
|
|
172
|
+
|
|
173
|
+
if (backendLanguage === "typescript") {
|
|
174
|
+
// Auth functions
|
|
175
|
+
const srcDir = path.join(functionsDir, "src");
|
|
176
|
+
fs.mkdirSync(srcDir, { recursive: true });
|
|
177
|
+
fs.writeFileSync(
|
|
178
|
+
path.join(srcDir, "auth.ts"),
|
|
179
|
+
generateAuthFunctionsTS(sharedPackageName, config, provider),
|
|
180
|
+
"utf-8"
|
|
181
|
+
);
|
|
182
|
+
console.log(` Created: functions/src/auth.ts`);
|
|
183
|
+
|
|
184
|
+
// JWT helper
|
|
185
|
+
const authDir = path.join(srcDir, "auth");
|
|
186
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
187
|
+
fs.writeFileSync(
|
|
188
|
+
path.join(authDir, "jwt-helper.ts"),
|
|
189
|
+
generateJwtHelperTS(),
|
|
190
|
+
"utf-8"
|
|
191
|
+
);
|
|
192
|
+
console.log(` Created: functions/src/auth/jwt-helper.ts`);
|
|
193
|
+
} else if (backendLanguage === "csharp") {
|
|
194
|
+
const authDir = path.join(functionsDir, "Auth");
|
|
195
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
196
|
+
fs.writeFileSync(
|
|
197
|
+
path.join(authDir, "AuthFunctions.cs"),
|
|
198
|
+
generateAuthFunctionsCSharp(config, provider),
|
|
199
|
+
"utf-8"
|
|
200
|
+
);
|
|
201
|
+
console.log(` Created: functions/Auth/AuthFunctions.cs`);
|
|
202
|
+
fs.writeFileSync(
|
|
203
|
+
path.join(authDir, "JwtHelper.cs"),
|
|
204
|
+
generateJwtHelperCSharp(),
|
|
205
|
+
"utf-8"
|
|
206
|
+
);
|
|
207
|
+
console.log(` Created: functions/Auth/JwtHelper.cs`);
|
|
208
|
+
} else if (backendLanguage === "python") {
|
|
209
|
+
const blueprintsDir = path.join(functionsDir, "blueprints");
|
|
210
|
+
fs.mkdirSync(blueprintsDir, { recursive: true });
|
|
211
|
+
fs.writeFileSync(
|
|
212
|
+
path.join(blueprintsDir, "auth.py"),
|
|
213
|
+
generateAuthFunctionsPython(config, provider),
|
|
214
|
+
"utf-8"
|
|
215
|
+
);
|
|
216
|
+
console.log(` Created: functions/blueprints/auth.py`);
|
|
217
|
+
|
|
218
|
+
const authDir = path.join(functionsDir, "auth");
|
|
219
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
220
|
+
fs.writeFileSync(
|
|
221
|
+
path.join(authDir, "jwt_helper.py"),
|
|
222
|
+
generateJwtHelperPython(),
|
|
223
|
+
"utf-8"
|
|
224
|
+
);
|
|
225
|
+
console.log(` Created: functions/auth/jwt_helper.py`);
|
|
226
|
+
|
|
227
|
+
// __init__.py
|
|
228
|
+
fs.writeFileSync(path.join(authDir, "__init__.py"), "", "utf-8");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function generateBFFAuth(cwd: string, projectName: string, sharedPackageName: string): void {
|
|
233
|
+
const authApiDir = path.join(cwd, "app", "api", "auth");
|
|
234
|
+
|
|
235
|
+
// Login route
|
|
236
|
+
const loginDir = path.join(authApiDir, "login");
|
|
237
|
+
fs.mkdirSync(loginDir, { recursive: true });
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
path.join(loginDir, "route.ts"),
|
|
240
|
+
generateBFFAuthLoginRoute(projectName, sharedPackageName),
|
|
241
|
+
"utf-8"
|
|
242
|
+
);
|
|
243
|
+
console.log(` Created: app/api/auth/login/route.ts`);
|
|
244
|
+
|
|
245
|
+
// Logout route
|
|
246
|
+
const logoutDir = path.join(authApiDir, "logout");
|
|
247
|
+
fs.mkdirSync(logoutDir, { recursive: true });
|
|
248
|
+
fs.writeFileSync(
|
|
249
|
+
path.join(logoutDir, "route.ts"),
|
|
250
|
+
generateBFFAuthLogoutRoute(projectName),
|
|
251
|
+
"utf-8"
|
|
252
|
+
);
|
|
253
|
+
console.log(` Created: app/api/auth/logout/route.ts`);
|
|
254
|
+
|
|
255
|
+
// Me route
|
|
256
|
+
const meDir = path.join(authApiDir, "me");
|
|
257
|
+
fs.mkdirSync(meDir, { recursive: true });
|
|
258
|
+
fs.writeFileSync(
|
|
259
|
+
path.join(meDir, "route.ts"),
|
|
260
|
+
generateBFFAuthMeRoute(sharedPackageName),
|
|
261
|
+
"utf-8"
|
|
262
|
+
);
|
|
263
|
+
console.log(` Created: app/api/auth/me/route.ts`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: CustomJwtConfig): void {
|
|
267
|
+
const configPath = path.join(cwd, "swallowkit.config.js");
|
|
268
|
+
if (!fs.existsSync(configPath)) {
|
|
269
|
+
console.warn(" swallowkit.config.js not found. Please add auth config manually.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
274
|
+
|
|
275
|
+
if (content.includes("auth:") || content.includes("auth :")) {
|
|
276
|
+
console.log(" 'auth' section already exists in swallowkit.config.js");
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Find the last property before the closing of module.exports
|
|
281
|
+
const closingBraceIdx = content.lastIndexOf("}");
|
|
282
|
+
if (closingBraceIdx === -1) {
|
|
283
|
+
console.error(" Could not parse config file structure.");
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const beforeClosing = content.substring(0, closingBraceIdx).trimEnd();
|
|
288
|
+
const needsComma = !beforeClosing.endsWith(",") && !beforeClosing.endsWith("{");
|
|
289
|
+
|
|
290
|
+
const authBlock = `${needsComma ? "," : ""}
|
|
291
|
+
// 認証認可設定
|
|
292
|
+
auth: {
|
|
293
|
+
provider: '${provider}',
|
|
294
|
+
customJwt: {
|
|
295
|
+
userConnector: '${config.userConnector}',
|
|
296
|
+
userTable: '${config.userTable}',
|
|
297
|
+
loginIdColumn: '${config.loginIdColumn}',
|
|
298
|
+
passwordHashColumn: '${config.passwordHashColumn}',
|
|
299
|
+
rolesColumn: '${config.rolesColumn}',
|
|
300
|
+
jwtSecretEnv: '${config.jwtSecretEnv || "JWT_SECRET"}',
|
|
301
|
+
tokenExpiry: '${config.tokenExpiry || "24h"}',
|
|
302
|
+
},
|
|
303
|
+
authorization: {
|
|
304
|
+
defaultPolicy: 'authenticated',
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
`;
|
|
308
|
+
|
|
309
|
+
const newContent = content.substring(0, closingBraceIdx) + authBlock + content.substring(closingBraceIdx);
|
|
310
|
+
fs.writeFileSync(configPath, newContent, "utf-8");
|
|
311
|
+
console.log(` Updated: swallowkit.config.js`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function updateEnvironmentFiles(cwd: string): void {
|
|
315
|
+
// Update functions/local.settings.json
|
|
316
|
+
const localSettingsPath = path.join(cwd, "functions", "local.settings.json");
|
|
317
|
+
if (fs.existsSync(localSettingsPath)) {
|
|
318
|
+
const settings = JSON.parse(fs.readFileSync(localSettingsPath, "utf-8"));
|
|
319
|
+
if (!settings.Values) settings.Values = {};
|
|
320
|
+
if (!settings.Values.JWT_SECRET) {
|
|
321
|
+
settings.Values.JWT_SECRET = "dev-jwt-secret-change-in-production-min-32-chars!!";
|
|
322
|
+
}
|
|
323
|
+
fs.writeFileSync(localSettingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
324
|
+
console.log(` Updated: functions/local.settings.json`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Update .env.example
|
|
328
|
+
const envExamplePath = path.join(cwd, ".env.example");
|
|
329
|
+
if (fs.existsSync(envExamplePath)) {
|
|
330
|
+
let content = fs.readFileSync(envExamplePath, "utf-8");
|
|
331
|
+
if (!content.includes("JWT_SECRET")) {
|
|
332
|
+
content += "\n# Authentication\nJWT_SECRET=your-jwt-secret-key-at-least-32-chars\n";
|
|
333
|
+
fs.writeFileSync(envExamplePath, content, "utf-8");
|
|
334
|
+
console.log(` Updated: .env.example`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function installAuthDependencies(cwd: string, backendLanguage: BackendLanguage, provider: "mysql" | "postgres" | "sqlserver" = "mysql"): Promise<void> {
|
|
340
|
+
const pm = detectFromProject();
|
|
341
|
+
const cmds = getCommands(pm);
|
|
342
|
+
|
|
343
|
+
if (backendLanguage === "typescript") {
|
|
344
|
+
const funcPkgPath = path.join(cwd, "functions", "package.json");
|
|
345
|
+
if (fs.existsSync(funcPkgPath)) {
|
|
346
|
+
const funcPkg = JSON.parse(fs.readFileSync(funcPkgPath, "utf-8"));
|
|
347
|
+
if (!funcPkg.dependencies) funcPkg.dependencies = {};
|
|
348
|
+
funcPkg.dependencies["jsonwebtoken"] = "^9.0.0";
|
|
349
|
+
funcPkg.dependencies["bcryptjs"] = "^2.4.3";
|
|
350
|
+
// RDB driver based on provider
|
|
351
|
+
if (provider === "mysql") funcPkg.dependencies["mysql2"] = "^3.11.0";
|
|
352
|
+
else if (provider === "postgres") funcPkg.dependencies["pg"] = "^8.13.0";
|
|
353
|
+
else funcPkg.dependencies["mssql"] = "^11.0.0";
|
|
354
|
+
if (!funcPkg.devDependencies) funcPkg.devDependencies = {};
|
|
355
|
+
funcPkg.devDependencies["@types/jsonwebtoken"] = "^9.0.0";
|
|
356
|
+
funcPkg.devDependencies["@types/bcryptjs"] = "^2.4.0";
|
|
357
|
+
if (provider === "postgres") funcPkg.devDependencies["@types/pg"] = "^8.11.0";
|
|
358
|
+
fs.writeFileSync(funcPkgPath, JSON.stringify(funcPkg, null, 2), "utf-8");
|
|
359
|
+
console.log(` Updated: functions/package.json with auth dependencies (${provider})`);
|
|
360
|
+
}
|
|
361
|
+
} else if (backendLanguage === "csharp") {
|
|
362
|
+
// Add NuGet package references to .csproj
|
|
363
|
+
const functionsDir = path.join(cwd, "functions");
|
|
364
|
+
const csprojFiles = fs.readdirSync(functionsDir).filter((f: string) => f.endsWith(".csproj"));
|
|
365
|
+
if (csprojFiles.length > 0) {
|
|
366
|
+
const csprojPath = path.join(functionsDir, csprojFiles[0]);
|
|
367
|
+
let csprojContent = fs.readFileSync(csprojPath, "utf-8");
|
|
368
|
+
const nugetPackages: { name: string; version: string }[] = [
|
|
369
|
+
{ name: "System.IdentityModel.Tokens.Jwt", version: "7.0.0" },
|
|
370
|
+
{ name: "Microsoft.IdentityModel.Tokens", version: "7.0.0" },
|
|
371
|
+
{ name: "BCrypt.Net-Next", version: "4.0.3" },
|
|
372
|
+
];
|
|
373
|
+
// RDB driver based on provider
|
|
374
|
+
if (provider === "mysql") nugetPackages.push({ name: "MySqlConnector", version: "2.3.0" });
|
|
375
|
+
else if (provider === "postgres") nugetPackages.push({ name: "Npgsql", version: "8.0.0" });
|
|
376
|
+
else nugetPackages.push({ name: "Microsoft.Data.SqlClient", version: "5.2.0" });
|
|
377
|
+
for (const pkg of nugetPackages) {
|
|
378
|
+
if (!csprojContent.includes(`"${pkg.name}"`)) {
|
|
379
|
+
const insertPoint = csprojContent.lastIndexOf("</ItemGroup>");
|
|
380
|
+
if (insertPoint >= 0) {
|
|
381
|
+
csprojContent =
|
|
382
|
+
csprojContent.slice(0, insertPoint) +
|
|
383
|
+
` <PackageReference Include="${pkg.name}" Version="${pkg.version}" />\n ` +
|
|
384
|
+
csprojContent.slice(insertPoint);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
fs.writeFileSync(csprojPath, csprojContent, "utf-8");
|
|
389
|
+
console.log(` Updated: ${csprojFiles[0]} with auth NuGet packages (${provider})`);
|
|
390
|
+
}
|
|
391
|
+
} else if (backendLanguage === "python") {
|
|
392
|
+
// Add Python dependencies to requirements.txt
|
|
393
|
+
const requirementsPath = path.join(cwd, "functions", "requirements.txt");
|
|
394
|
+
const baseDeps = ["PyJWT>=2.8.0", "bcrypt>=4.1.0"];
|
|
395
|
+
// RDB driver based on provider
|
|
396
|
+
if (provider === "mysql") baseDeps.push("mysql-connector-python>=8.3.0");
|
|
397
|
+
else if (provider === "postgres") baseDeps.push("psycopg2-binary>=2.9.0");
|
|
398
|
+
else baseDeps.push("pymssql>=2.2.0");
|
|
399
|
+
if (fs.existsSync(requirementsPath)) {
|
|
400
|
+
let content = fs.readFileSync(requirementsPath, "utf-8");
|
|
401
|
+
for (const dep of baseDeps) {
|
|
402
|
+
const pkgName = dep.split(">=")[0].split("==")[0];
|
|
403
|
+
if (!content.includes(pkgName)) {
|
|
404
|
+
content += `${dep}\n`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
fs.writeFileSync(requirementsPath, content, "utf-8");
|
|
408
|
+
} else {
|
|
409
|
+
fs.writeFileSync(requirementsPath, baseDeps.join("\n") + "\n", "utf-8");
|
|
410
|
+
}
|
|
411
|
+
console.log(` Updated: functions/requirements.txt with auth dependencies`);
|
|
412
|
+
}
|
|
413
|
+
}
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as os from 'os';
|
|
6
6
|
import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
|
|
7
|
-
import { ensureSwallowKitProject, getBackendLanguage } from '../../core/config';
|
|
7
|
+
import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, getFullConfig } from '../../core/config';
|
|
8
8
|
import { ModelInfo } from '../../core/scaffold/model-parser';
|
|
9
9
|
import { applyDevSeedEnvironment, getContainerNameForModel, loadProjectModels } from './dev-seeds';
|
|
10
10
|
import { BackendLanguage } from '../../types';
|
|
@@ -709,7 +709,34 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
709
709
|
const allModels = await loadProjectModels();
|
|
710
710
|
const connectorModels = allModels.filter((m) => m.connectorConfig);
|
|
711
711
|
|
|
712
|
-
|
|
712
|
+
// Resolve auth config — auth functions use RDB connectors, mocked alongside other models
|
|
713
|
+
const authConfig = getAuthConfig();
|
|
714
|
+
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
715
|
+
if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
|
|
716
|
+
const fullConfig = getFullConfig();
|
|
717
|
+
// Read JWT_SECRET from functions/local.settings.json if available
|
|
718
|
+
let jwtSecret = 'dev-jwt-secret-change-in-production-min-32-chars!!';
|
|
719
|
+
try {
|
|
720
|
+
const localSettingsPath = path.join(process.cwd(), 'functions', 'local.settings.json');
|
|
721
|
+
if (fs.existsSync(localSettingsPath)) {
|
|
722
|
+
const localSettings = JSON.parse(fs.readFileSync(localSettingsPath, 'utf-8'));
|
|
723
|
+
jwtSecret = localSettings.Values?.[authConfig.customJwt.jwtSecretEnv || 'JWT_SECRET'] || jwtSecret;
|
|
724
|
+
}
|
|
725
|
+
} catch { /* ignore */ }
|
|
726
|
+
mockAuthConfig = {
|
|
727
|
+
jwtSecret,
|
|
728
|
+
tokenExpiry: authConfig.customJwt.tokenExpiry,
|
|
729
|
+
customJwt: {
|
|
730
|
+
userTable: authConfig.customJwt.userTable,
|
|
731
|
+
loginIdColumn: authConfig.customJwt.loginIdColumn,
|
|
732
|
+
passwordHashColumn: authConfig.customJwt.passwordHashColumn,
|
|
733
|
+
rolesColumn: authConfig.customJwt.rolesColumn,
|
|
734
|
+
},
|
|
735
|
+
defaultPolicy: authConfig.authorization?.defaultPolicy,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (connectorModels.length > 0 || mockAuthConfig) {
|
|
713
740
|
const mockPort = parseInt(functionsPort, 10) + 1;
|
|
714
741
|
mockServer = new ConnectorMockServer({
|
|
715
742
|
port: mockPort,
|
|
@@ -718,18 +745,22 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
718
745
|
allModels,
|
|
719
746
|
seedEnv: options.seedEnv,
|
|
720
747
|
host: options.host || 'localhost',
|
|
748
|
+
authConfig: mockAuthConfig,
|
|
721
749
|
});
|
|
722
750
|
|
|
723
751
|
await mockServer.start();
|
|
724
752
|
bffTargetPort = String(mockPort);
|
|
725
753
|
|
|
754
|
+
const modelCount = connectorModels.length + (mockAuthConfig ? 1 : 0);
|
|
726
755
|
console.log('');
|
|
727
|
-
console.log(`🔌
|
|
728
|
-
console.log(` Mocking ${connectorModels.length} connector model(s):`);
|
|
756
|
+
console.log(`🔌 Mock server started (port: ${mockPort}) — ${modelCount} model(s) mocked via Zod/seed data`);
|
|
729
757
|
for (const m of connectorModels) {
|
|
730
758
|
const ops = m.connectorConfig!.operations.join(', ');
|
|
731
759
|
console.log(` - ${m.name} [${ops}]`);
|
|
732
760
|
}
|
|
761
|
+
if (mockAuthConfig) {
|
|
762
|
+
console.log(` - auth [login, me, logout]`);
|
|
763
|
+
}
|
|
733
764
|
console.log(` Other routes → proxied to Azure Functions (port: ${functionsPort})`);
|
|
734
765
|
} else {
|
|
735
766
|
console.log('');
|