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.
Files changed (59) hide show
  1. package/README.ja.md +2 -0
  2. package/README.md +2 -0
  3. package/dist/cli/commands/add-auth.d.ts +10 -0
  4. package/dist/cli/commands/add-auth.d.ts.map +1 -0
  5. package/dist/cli/commands/add-auth.js +365 -0
  6. package/dist/cli/commands/add-auth.js.map +1 -0
  7. package/dist/cli/commands/dev.js +34 -3
  8. package/dist/cli/commands/dev.js.map +1 -1
  9. package/dist/cli/commands/scaffold.d.ts.map +1 -1
  10. package/dist/cli/commands/scaffold.js +145 -10
  11. package/dist/cli/commands/scaffold.js.map +1 -1
  12. package/dist/cli/index.js +10 -0
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/core/config.d.ts +5 -1
  15. package/dist/core/config.d.ts.map +1 -1
  16. package/dist/core/config.js +12 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/core/mock/connector-mock-server.d.ts +34 -0
  19. package/dist/core/mock/connector-mock-server.d.ts.map +1 -1
  20. package/dist/core/mock/connector-mock-server.js +192 -0
  21. package/dist/core/mock/connector-mock-server.js.map +1 -1
  22. package/dist/core/scaffold/auth-generator.d.ts +38 -0
  23. package/dist/core/scaffold/auth-generator.d.ts.map +1 -0
  24. package/dist/core/scaffold/auth-generator.js +1246 -0
  25. package/dist/core/scaffold/auth-generator.js.map +1 -0
  26. package/dist/core/scaffold/connector-functions-generator.d.ts +3 -3
  27. package/dist/core/scaffold/connector-functions-generator.d.ts.map +1 -1
  28. package/dist/core/scaffold/connector-functions-generator.js +37 -18
  29. package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
  30. package/dist/core/scaffold/functions-generator.d.ts +2 -1
  31. package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
  32. package/dist/core/scaffold/functions-generator.js +21 -12
  33. package/dist/core/scaffold/functions-generator.js.map +1 -1
  34. package/dist/core/scaffold/model-parser.d.ts +7 -1
  35. package/dist/core/scaffold/model-parser.d.ts.map +1 -1
  36. package/dist/core/scaffold/model-parser.js +45 -0
  37. package/dist/core/scaffold/model-parser.js.map +1 -1
  38. package/dist/core/scaffold/ui-generator.d.ts +10 -4
  39. package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
  40. package/dist/core/scaffold/ui-generator.js +120 -12
  41. package/dist/core/scaffold/ui-generator.js.map +1 -1
  42. package/dist/types/index.d.ts +30 -0
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +4 -2
  45. package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +9 -1
  46. package/src/__tests__/auth.test.ts +654 -0
  47. package/src/__tests__/connector-mock-server.test.ts +189 -2
  48. package/src/cli/commands/add-auth.ts +413 -0
  49. package/src/cli/commands/dev.ts +35 -4
  50. package/src/cli/commands/scaffold.ts +165 -11
  51. package/src/cli/index.ts +11 -0
  52. package/src/core/config.ts +13 -2
  53. package/src/core/mock/connector-mock-server.ts +248 -0
  54. package/src/core/scaffold/auth-generator.ts +1286 -0
  55. package/src/core/scaffold/connector-functions-generator.ts +42 -18
  56. package/src/core/scaffold/functions-generator.ts +23 -12
  57. package/src/core/scaffold/model-parser.ts +50 -1
  58. package/src/core/scaffold/ui-generator.ts +132 -12
  59. package/src/types/index.ts +42 -0
@@ -0,0 +1,1246 @@
1
+ "use strict";
2
+ /**
3
+ * SwallowKit 認証・認可コード生成
4
+ * add-auth コマンドおよび scaffold ロールガード挿入で使用するテンプレート群
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.generateAuthModels = generateAuthModels;
8
+ exports.generateAuthFunctionsTS = generateAuthFunctionsTS;
9
+ exports.generateJwtHelperTS = generateJwtHelperTS;
10
+ exports.generateAuthFunctionsCSharp = generateAuthFunctionsCSharp;
11
+ exports.generateJwtHelperCSharp = generateJwtHelperCSharp;
12
+ exports.generateAuthFunctionsPython = generateAuthFunctionsPython;
13
+ exports.generateJwtHelperPython = generateJwtHelperPython;
14
+ exports.generateBFFAuthLoginRoute = generateBFFAuthLoginRoute;
15
+ exports.generateBFFAuthLogoutRoute = generateBFFAuthLogoutRoute;
16
+ exports.generateBFFAuthMeRoute = generateBFFAuthMeRoute;
17
+ exports.generateMiddleware = generateMiddleware;
18
+ exports.generateLoginPage = generateLoginPage;
19
+ exports.generateAuthContext = generateAuthContext;
20
+ exports.generateBFFCallFunctionWithAuth = generateBFFCallFunctionWithAuth;
21
+ exports.generateAuthImportTS = generateAuthImportTS;
22
+ exports.generateAuthGuardTS = generateAuthGuardTS;
23
+ exports.generateAuthGuardCSharp = generateAuthGuardCSharp;
24
+ exports.generateAuthGuardPython = generateAuthGuardPython;
25
+ // ============================================================
26
+ // 1. shared/models/auth.ts(Zod スキーマ)
27
+ // ============================================================
28
+ function generateAuthModels() {
29
+ return `import { z } from 'zod/v4';
30
+
31
+ // ログインリクエスト
32
+ export const LoginRequest = z.object({
33
+ loginId: z.string().min(1),
34
+ password: z.string().min(1),
35
+ });
36
+ export type LoginRequest = z.infer<typeof LoginRequest>;
37
+
38
+ // 認証済みユーザー情報(JWT claims 相当、機密情報なし)
39
+ export const AuthUser = z.object({
40
+ id: z.string(),
41
+ loginId: z.string(),
42
+ name: z.string(),
43
+ email: z.string().email(),
44
+ roles: z.array(z.string()),
45
+ });
46
+ export type AuthUser = z.infer<typeof AuthUser>;
47
+
48
+ // ログインレスポンス
49
+ export const LoginResponse = z.object({
50
+ user: AuthUser,
51
+ token: z.string(),
52
+ expiresAt: z.string(),
53
+ });
54
+ export type LoginResponse = z.infer<typeof LoginResponse>;
55
+
56
+ export const displayName = 'Auth';
57
+ `;
58
+ }
59
+ // ============================================================
60
+ // 2. TypeScript Functions テンプレート
61
+ // ============================================================
62
+ function generateAuthFunctionsTS(sharedPackageName, config, provider) {
63
+ const envVar = `${config.userConnector.toUpperCase()}_CONNECTION_STRING`;
64
+ const driverImport = provider === "mysql"
65
+ ? `import mysql from 'mysql2/promise';`
66
+ : provider === "postgres"
67
+ ? `import pg from 'pg';`
68
+ : `import sql from 'mssql';`;
69
+ const getConnection = provider === "mysql"
70
+ ? `function getConnection() {
71
+ return mysql.createConnection(process.env.${envVar} || '');
72
+ }`
73
+ : provider === "postgres"
74
+ ? `async function getConnection() {
75
+ const client = new pg.Client(process.env.${envVar} || '');
76
+ await client.connect();
77
+ return client;
78
+ }`
79
+ : `async function getConnection() {
80
+ return sql.connect(process.env.${envVar} || '');
81
+ }`;
82
+ const connVar = provider === "mysql" ? "conn" : provider === "postgres" ? "client" : "pool";
83
+ const queryExec = provider === "mysql"
84
+ ? `const [rows] = await ${connVar}.query(
85
+ 'SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = ?',
86
+ [body.loginId]
87
+ );
88
+ const users = rows as any[];`
89
+ : provider === "postgres"
90
+ ? `const result = await ${connVar}.query(
91
+ 'SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = $1',
92
+ [body.loginId]
93
+ );
94
+ const users = result.rows;`
95
+ : `const result = await ${connVar}.request()
96
+ .input('loginId', body.loginId)
97
+ .query('SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = @loginId');
98
+ const users = result.recordset;`;
99
+ const cleanup = provider === "mysql"
100
+ ? `await ${connVar}.end();`
101
+ : provider === "postgres"
102
+ ? `await ${connVar}.end();`
103
+ : `await ${connVar}.close();`;
104
+ return `import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions';
105
+ import { LoginRequest, LoginResponse, AuthUser } from '${sharedPackageName}';
106
+ import { requireAuth, generateToken, handleAuthError } from './auth/jwt-helper';
107
+ import bcrypt from 'bcryptjs';
108
+ ${driverImport}
109
+
110
+ ${getConnection}
111
+
112
+ // POST /api/auth/login - ログイン
113
+ app.http('auth-login', {
114
+ methods: ['POST'],
115
+ route: 'auth/login',
116
+ authLevel: 'anonymous',
117
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
118
+ try {
119
+ const body = LoginRequest.parse(await request.json());
120
+
121
+ const ${connVar} = await getConnection();
122
+ try {
123
+ ${queryExec}
124
+
125
+ if (users.length === 0) {
126
+ return { status: 401, jsonBody: { error: 'Invalid credentials' } };
127
+ }
128
+
129
+ const user = users[0];
130
+ const valid = await bcrypt.compare(body.password, user.${config.passwordHashColumn});
131
+ if (!valid) {
132
+ return { status: 401, jsonBody: { error: 'Invalid credentials' } };
133
+ }
134
+
135
+ // ロール取得(JSON配列 or カンマ区切り)
136
+ let roles: string[] = [];
137
+ if (typeof user.${config.rolesColumn} === 'string') {
138
+ try {
139
+ roles = JSON.parse(user.${config.rolesColumn});
140
+ } catch {
141
+ roles = user.${config.rolesColumn}.split(',').map((r: string) => r.trim());
142
+ }
143
+ } else if (Array.isArray(user.${config.rolesColumn})) {
144
+ roles = user.${config.rolesColumn};
145
+ }
146
+
147
+ const authUser: AuthUser = {
148
+ id: String(user.id),
149
+ loginId: user.${config.loginIdColumn},
150
+ name: user.name || user.${config.loginIdColumn},
151
+ email: user.email || '',
152
+ roles,
153
+ };
154
+
155
+ const token = generateToken(authUser);
156
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
157
+
158
+ const response: LoginResponse = { user: authUser, token, expiresAt };
159
+ return { status: 200, jsonBody: response };
160
+ } finally {
161
+ ${cleanup}
162
+ }
163
+ } catch (error: any) {
164
+ context.error('Login error:', error);
165
+ return { status: 500, jsonBody: { error: 'Login failed' } };
166
+ }
167
+ },
168
+ });
169
+
170
+ // GET /api/auth/me - 現在のユーザー情報取得
171
+ app.http('auth-me', {
172
+ methods: ['GET'],
173
+ route: 'auth/me',
174
+ authLevel: 'anonymous',
175
+ handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
176
+ try {
177
+ const user = requireAuth(request);
178
+ return { status: 200, jsonBody: user };
179
+ } catch (error) {
180
+ const authErr = handleAuthError(error);
181
+ if (authErr) return authErr;
182
+ context.error('Auth/me error:', error);
183
+ return { status: 500, jsonBody: { error: 'Internal server error' } };
184
+ }
185
+ },
186
+ });
187
+
188
+ // POST /api/auth/logout - ログアウト(ステートレスJWTのためサーバー側処理なし)
189
+ app.http('auth-logout', {
190
+ methods: ['POST'],
191
+ route: 'auth/logout',
192
+ authLevel: 'anonymous',
193
+ handler: async (request: HttpRequest): Promise<HttpResponseInit> => {
194
+ return { status: 200, jsonBody: { message: 'Logged out' } };
195
+ },
196
+ });
197
+ `;
198
+ }
199
+ // ============================================================
200
+ // 3. TypeScript JWT Helper
201
+ // ============================================================
202
+ function generateJwtHelperTS() {
203
+ return `import jwt from 'jsonwebtoken';
204
+ import { HttpRequest, HttpResponseInit } from '@azure/functions';
205
+
206
+ export interface JwtPayload {
207
+ sub: string;
208
+ loginId: string;
209
+ name: string;
210
+ email: string;
211
+ roles: string[];
212
+ }
213
+
214
+ const JWT_SECRET = process.env.JWT_SECRET || '';
215
+
216
+ /**
217
+ * JWT 検証。無効な場合は AuthError をスロー。
218
+ */
219
+ export function requireAuth(request: HttpRequest): JwtPayload {
220
+ const authHeader = request.headers.get('authorization');
221
+ if (!authHeader?.startsWith('Bearer ')) {
222
+ throw new AuthError(401, 'Missing or invalid Authorization header');
223
+ }
224
+ const token = authHeader.slice(7);
225
+ try {
226
+ const payload = jwt.verify(token, JWT_SECRET) as JwtPayload;
227
+ return payload;
228
+ } catch {
229
+ throw new AuthError(401, 'Invalid or expired token');
230
+ }
231
+ }
232
+
233
+ /**
234
+ * ロール確認。必要なロールを持たない場合は AuthError をスロー。
235
+ */
236
+ export function requireRoles(user: JwtPayload, requiredRoles: string[]): void {
237
+ const hasRole = requiredRoles.some(role => user.roles.includes(role));
238
+ if (!hasRole) {
239
+ throw new AuthError(403, \`Requires one of roles: \${requiredRoles.join(', ')}\`);
240
+ }
241
+ }
242
+
243
+ /**
244
+ * JWT 生成
245
+ */
246
+ export function generateToken(payload: { id: string; loginId: string; name: string; email: string; roles: string[] }): string {
247
+ return jwt.sign(
248
+ { sub: payload.id, loginId: payload.loginId, name: payload.name, email: payload.email, roles: payload.roles },
249
+ JWT_SECRET,
250
+ { expiresIn: process.env.JWT_EXPIRY || '24h' } as jwt.SignOptions
251
+ );
252
+ }
253
+
254
+ /**
255
+ * 認証エラー
256
+ */
257
+ export class AuthError extends Error {
258
+ constructor(public statusCode: number, message: string) {
259
+ super(message);
260
+ this.name = 'AuthError';
261
+ }
262
+ }
263
+
264
+ /**
265
+ * AuthError を HTTP レスポンスに変換
266
+ */
267
+ export function handleAuthError(error: unknown): HttpResponseInit | null {
268
+ if (error instanceof AuthError) {
269
+ return { status: error.statusCode, jsonBody: { error: error.message } };
270
+ }
271
+ return null;
272
+ }
273
+ `;
274
+ }
275
+ // ============================================================
276
+ // 4. C# Auth Functions テンプレート
277
+ // ============================================================
278
+ function generateAuthFunctionsCSharp(config, provider) {
279
+ const usingStatement = provider === "mysql"
280
+ ? `using MySqlConnector;`
281
+ : provider === "postgres"
282
+ ? `using Npgsql;`
283
+ : `using Microsoft.Data.SqlClient;`;
284
+ const connType = provider === "mysql"
285
+ ? "MySqlConnection"
286
+ : provider === "postgres"
287
+ ? "NpgsqlConnection"
288
+ : "SqlConnection";
289
+ const cmdType = provider === "mysql"
290
+ ? "MySqlCommand"
291
+ : provider === "postgres"
292
+ ? "NpgsqlCommand"
293
+ : "SqlCommand";
294
+ return `using System;
295
+ using System.Text.Json;
296
+ using Microsoft.Azure.Functions.Worker;
297
+ using Microsoft.Azure.Functions.Worker.Http;
298
+ ${usingStatement}
299
+ using BCrypt.Net;
300
+
301
+ namespace Functions.Auth
302
+ {
303
+ public class AuthFunctions
304
+ {
305
+ [Function("auth-login")]
306
+ public async Task<HttpResponseData> Login(
307
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/login")] HttpRequestData request)
308
+ {
309
+ var body = await JsonSerializer.DeserializeAsync<LoginRequest>(request.Body);
310
+ if (body == null || string.IsNullOrEmpty(body.LoginId) || string.IsNullOrEmpty(body.Password))
311
+ {
312
+ var badReq = request.CreateResponse(System.Net.HttpStatusCode.BadRequest);
313
+ await badReq.WriteAsJsonAsync(new { error = "loginId and password are required" });
314
+ return badReq;
315
+ }
316
+
317
+ await using var conn = new ${connType}(
318
+ Environment.GetEnvironmentVariable("${config.userConnector.toUpperCase()}_CONNECTION_STRING"));
319
+ await conn.OpenAsync();
320
+
321
+ await using var cmd = new ${cmdType}(
322
+ "SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = @loginId", conn);
323
+ cmd.Parameters.AddWithValue("@loginId", body.LoginId);
324
+
325
+ await using var reader = await cmd.ExecuteReaderAsync();
326
+ if (!await reader.ReadAsync())
327
+ {
328
+ var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
329
+ await unauthorized.WriteAsJsonAsync(new { error = "Invalid credentials" });
330
+ return unauthorized;
331
+ }
332
+
333
+ var passwordHash = reader["${config.passwordHashColumn}"].ToString()!;
334
+ if (!BCrypt.Net.BCrypt.Verify(body.Password, passwordHash))
335
+ {
336
+ var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
337
+ await unauthorized.WriteAsJsonAsync(new { error = "Invalid credentials" });
338
+ return unauthorized;
339
+ }
340
+
341
+ var rolesStr = reader["${config.rolesColumn}"].ToString() ?? "[]";
342
+ string[] roles;
343
+ try { roles = JsonSerializer.Deserialize<string[]>(rolesStr) ?? Array.Empty<string>(); }
344
+ catch { roles = rolesStr.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); }
345
+
346
+ var userId = reader["id"].ToString()!;
347
+ var loginId = reader["${config.loginIdColumn}"].ToString()!;
348
+ var name = reader["name"]?.ToString() ?? loginId;
349
+ var email = reader["email"]?.ToString() ?? "";
350
+
351
+ var token = JwtHelper.GenerateToken(userId, loginId, name, email, roles);
352
+ var expiresAt = DateTime.UtcNow.AddHours(24).ToString("o");
353
+
354
+ var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
355
+ await response.WriteAsJsonAsync(new
356
+ {
357
+ user = new { id = userId, loginId, name, email, roles },
358
+ token,
359
+ expiresAt
360
+ });
361
+ return response;
362
+ }
363
+
364
+ [Function("auth-me")]
365
+ public async Task<HttpResponseData> Me(
366
+ [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "auth/me")] HttpRequestData request)
367
+ {
368
+ var (principal, errorResponse) = JwtHelper.Authorize(request);
369
+ if (errorResponse != null) return errorResponse;
370
+
371
+ var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
372
+ await response.WriteAsJsonAsync(new
373
+ {
374
+ sub = principal!.Sub,
375
+ loginId = principal.LoginId,
376
+ name = principal.Name,
377
+ email = principal.Email,
378
+ roles = principal.Roles
379
+ });
380
+ return response;
381
+ }
382
+
383
+ [Function("auth-logout")]
384
+ public async Task<HttpResponseData> Logout(
385
+ [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "auth/logout")] HttpRequestData request)
386
+ {
387
+ var response = request.CreateResponse(System.Net.HttpStatusCode.OK);
388
+ await response.WriteAsJsonAsync(new { message = "Logged out" });
389
+ return response;
390
+ }
391
+ }
392
+
393
+ public class LoginRequest
394
+ {
395
+ public string LoginId { get; set; } = "";
396
+ public string Password { get; set; } = "";
397
+ }
398
+ }
399
+ `;
400
+ }
401
+ // ============================================================
402
+ // 5. C# JWT Helper
403
+ // ============================================================
404
+ function generateJwtHelperCSharp() {
405
+ return `using System;
406
+ using System.IdentityModel.Tokens.Jwt;
407
+ using System.Security.Claims;
408
+ using System.Text;
409
+ using System.Text.Json;
410
+ using Microsoft.Azure.Functions.Worker.Http;
411
+ using Microsoft.IdentityModel.Tokens;
412
+
413
+ namespace Functions.Auth
414
+ {
415
+ public class JwtPayload
416
+ {
417
+ public string Sub { get; set; } = "";
418
+ public string LoginId { get; set; } = "";
419
+ public string Name { get; set; } = "";
420
+ public string Email { get; set; } = "";
421
+ public string[] Roles { get; set; } = Array.Empty<string>();
422
+ }
423
+
424
+ public static class JwtHelper
425
+ {
426
+ private static string JwtSecret => Environment.GetEnvironmentVariable("JWT_SECRET") ?? "";
427
+
428
+ public static string GenerateToken(string userId, string loginId, string name, string email, string[] roles)
429
+ {
430
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
431
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
432
+
433
+ var claims = new List<Claim>
434
+ {
435
+ new(JwtRegisteredClaimNames.Sub, userId),
436
+ new("loginId", loginId),
437
+ new("name", name),
438
+ new("email", email),
439
+ };
440
+ foreach (var role in roles)
441
+ {
442
+ claims.Add(new Claim("roles", role));
443
+ }
444
+
445
+ var token = new JwtSecurityToken(
446
+ expires: DateTime.UtcNow.AddHours(24),
447
+ claims: claims,
448
+ signingCredentials: creds
449
+ );
450
+
451
+ return new JwtSecurityTokenHandler().WriteToken(token);
452
+ }
453
+
454
+ public static JwtPayload? ValidateToken(HttpRequestData request)
455
+ {
456
+ var authHeader = request.Headers.TryGetValues("Authorization", out var values)
457
+ ? values.FirstOrDefault() : null;
458
+
459
+ if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
460
+ return null;
461
+
462
+ var token = authHeader["Bearer ".Length..];
463
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
464
+
465
+ try
466
+ {
467
+ var handler = new JwtSecurityTokenHandler();
468
+ var principal = handler.ValidateToken(token, new TokenValidationParameters
469
+ {
470
+ ValidateIssuer = false,
471
+ ValidateAudience = false,
472
+ ValidateLifetime = true,
473
+ IssuerSigningKey = key,
474
+ }, out _);
475
+
476
+ return new JwtPayload
477
+ {
478
+ Sub = principal.FindFirst(JwtRegisteredClaimNames.Sub)?.Value ?? "",
479
+ LoginId = principal.FindFirst("loginId")?.Value ?? "",
480
+ Name = principal.FindFirst("name")?.Value ?? "",
481
+ Email = principal.FindFirst("email")?.Value ?? "",
482
+ Roles = principal.FindAll("roles").Select(c => c.Value).ToArray(),
483
+ };
484
+ }
485
+ catch
486
+ {
487
+ return null;
488
+ }
489
+ }
490
+
491
+ public static (JwtPayload?, HttpResponseData?) Authorize(
492
+ HttpRequestData request, params string[] requiredRoles)
493
+ {
494
+ var payload = ValidateToken(request);
495
+ if (payload == null)
496
+ {
497
+ var unauthorized = request.CreateResponse(System.Net.HttpStatusCode.Unauthorized);
498
+ unauthorized.WriteAsJsonAsync(new { error = "Unauthorized" }).Wait();
499
+ return (null, unauthorized);
500
+ }
501
+
502
+ if (requiredRoles.Length > 0 && !requiredRoles.Any(r => payload.Roles.Contains(r)))
503
+ {
504
+ var forbidden = request.CreateResponse(System.Net.HttpStatusCode.Forbidden);
505
+ forbidden.WriteAsJsonAsync(new { error = "Forbidden" }).Wait();
506
+ return (null, forbidden);
507
+ }
508
+
509
+ return (payload, null);
510
+ }
511
+ }
512
+ }
513
+ `;
514
+ }
515
+ // ============================================================
516
+ // 6. Python Auth Functions テンプレート
517
+ // ============================================================
518
+ function generateAuthFunctionsPython(config, provider) {
519
+ const envPrefix = config.userConnector.toUpperCase();
520
+ const driverImport = provider === "mysql"
521
+ ? `import mysql.connector`
522
+ : provider === "postgres"
523
+ ? `import psycopg2\nimport psycopg2.extras`
524
+ : `import pymssql`;
525
+ const getConnection = provider === "mysql"
526
+ ? `def get_connection():
527
+ return mysql.connector.connect(
528
+ host=os.environ.get("${envPrefix}_HOST", "localhost"),
529
+ user=os.environ.get("${envPrefix}_USER", "root"),
530
+ password=os.environ.get("${envPrefix}_PASSWORD", ""),
531
+ database=os.environ.get("${envPrefix}_DATABASE", ""),
532
+ )`
533
+ : provider === "postgres"
534
+ ? `def get_connection():
535
+ return psycopg2.connect(os.environ.get("${envPrefix}_CONNECTION_STRING", ""))`
536
+ : `def get_connection():
537
+ return pymssql.connect(
538
+ server=os.environ.get("${envPrefix}_SERVER", "localhost"),
539
+ user=os.environ.get("${envPrefix}_USER", ""),
540
+ password=os.environ.get("${envPrefix}_PASSWORD", ""),
541
+ database=os.environ.get("${envPrefix}_DATABASE", ""),
542
+ )`;
543
+ const cursorCreate = provider === "mysql"
544
+ ? `cursor = conn.cursor(dictionary=True)`
545
+ : provider === "postgres"
546
+ ? `cursor = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)`
547
+ : `cursor = conn.cursor(as_dict=True)`;
548
+ return `import json
549
+ import os
550
+ import logging
551
+ import azure.functions as func
552
+ import jwt
553
+ import bcrypt
554
+ ${driverImport}
555
+
556
+ auth_bp = func.Blueprint()
557
+
558
+ ${getConnection}
559
+
560
+
561
+ @auth_bp.route(route="auth/login", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
562
+ def auth_login(req: func.HttpRequest) -> func.HttpResponse:
563
+ try:
564
+ body = req.get_json()
565
+ login_id = body.get("loginId", "")
566
+ password = body.get("password", "")
567
+
568
+ if not login_id or not password:
569
+ return func.HttpResponse(
570
+ json.dumps({"error": "loginId and password are required"}),
571
+ status_code=400, mimetype="application/json"
572
+ )
573
+
574
+ conn = get_connection()
575
+ ${cursorCreate}
576
+ cursor.execute(
577
+ "SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = %s",
578
+ (login_id,)
579
+ )
580
+ user = cursor.fetchone()
581
+ cursor.close()
582
+ conn.close()
583
+
584
+ if not user:
585
+ return func.HttpResponse(
586
+ json.dumps({"error": "Invalid credentials"}),
587
+ status_code=401, mimetype="application/json"
588
+ )
589
+
590
+ if not bcrypt.checkpw(
591
+ password.encode("utf-8"),
592
+ user["${config.passwordHashColumn}"].encode("utf-8")
593
+ ):
594
+ return func.HttpResponse(
595
+ json.dumps({"error": "Invalid credentials"}),
596
+ status_code=401, mimetype="application/json"
597
+ )
598
+
599
+ roles_raw = user.get("${config.rolesColumn}", "[]")
600
+ try:
601
+ roles = json.loads(roles_raw) if isinstance(roles_raw, str) else list(roles_raw)
602
+ except (json.JSONDecodeError, TypeError):
603
+ roles = [r.strip() for r in str(roles_raw).split(",")]
604
+
605
+ from auth.jwt_helper import generate_token
606
+ import datetime
607
+
608
+ auth_user = {
609
+ "id": str(user["id"]),
610
+ "loginId": user["${config.loginIdColumn}"],
611
+ "name": user.get("name", user["${config.loginIdColumn}"]),
612
+ "email": user.get("email", ""),
613
+ "roles": roles,
614
+ }
615
+
616
+ token = generate_token(auth_user)
617
+ expires_at = (
618
+ datetime.datetime.utcnow() + datetime.timedelta(hours=24)
619
+ ).isoformat() + "Z"
620
+
621
+ return func.HttpResponse(
622
+ json.dumps({"user": auth_user, "token": token, "expiresAt": expires_at}),
623
+ status_code=200, mimetype="application/json"
624
+ )
625
+ except Exception as e:
626
+ logging.error(f"Login error: {e}")
627
+ return func.HttpResponse(
628
+ json.dumps({"error": "Login failed"}),
629
+ status_code=500, mimetype="application/json"
630
+ )
631
+
632
+
633
+ @auth_bp.route(route="auth/me", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
634
+ def auth_me(req: func.HttpRequest) -> func.HttpResponse:
635
+ from auth.jwt_helper import require_auth, handle_auth_error
636
+
637
+ try:
638
+ user = require_auth(req)
639
+ return func.HttpResponse(
640
+ json.dumps(user), status_code=200, mimetype="application/json"
641
+ )
642
+ except Exception as e:
643
+ err = handle_auth_error(e)
644
+ if err:
645
+ return err
646
+ return func.HttpResponse(
647
+ json.dumps({"error": "Internal server error"}),
648
+ status_code=500, mimetype="application/json"
649
+ )
650
+
651
+
652
+ @auth_bp.route(route="auth/logout", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
653
+ def auth_logout(req: func.HttpRequest) -> func.HttpResponse:
654
+ return func.HttpResponse(
655
+ json.dumps({"message": "Logged out"}),
656
+ status_code=200, mimetype="application/json"
657
+ )
658
+ `;
659
+ }
660
+ // ============================================================
661
+ // 7. Python JWT Helper
662
+ // ============================================================
663
+ function generateJwtHelperPython() {
664
+ return `import os
665
+ import json
666
+ import jwt
667
+ import azure.functions as func
668
+
669
+
670
+ JWT_SECRET = os.environ.get("JWT_SECRET", "")
671
+
672
+
673
+ def require_auth(req: func.HttpRequest) -> dict:
674
+ """JWT 検証。無効な場合は AuthError を raise。"""
675
+ auth_header = req.headers.get("authorization", "")
676
+ if not auth_header.startswith("Bearer "):
677
+ raise AuthError(401, "Missing or invalid Authorization header")
678
+
679
+ token = auth_header[7:]
680
+ try:
681
+ payload = jwt.decode(token, JWT_SECRET, algorithms=["HS256"])
682
+ return {
683
+ "sub": payload.get("sub", ""),
684
+ "loginId": payload.get("loginId", ""),
685
+ "name": payload.get("name", ""),
686
+ "email": payload.get("email", ""),
687
+ "roles": payload.get("roles", []),
688
+ }
689
+ except jwt.ExpiredSignatureError:
690
+ raise AuthError(401, "Token expired")
691
+ except jwt.InvalidTokenError:
692
+ raise AuthError(401, "Invalid token")
693
+
694
+
695
+ def require_roles(user: dict, required_roles: list[str]) -> None:
696
+ """ロール確認。必要なロールを持たない場合は AuthError を raise。"""
697
+ user_roles = user.get("roles", [])
698
+ if not any(role in user_roles for role in required_roles):
699
+ raise AuthError(403, f"Requires one of roles: {', '.join(required_roles)}")
700
+
701
+
702
+ def generate_token(payload: dict) -> str:
703
+ """JWT 生成"""
704
+ import datetime
705
+
706
+ return jwt.encode(
707
+ {
708
+ "sub": payload["id"],
709
+ "loginId": payload["loginId"],
710
+ "name": payload["name"],
711
+ "email": payload["email"],
712
+ "roles": payload["roles"],
713
+ "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=24),
714
+ },
715
+ JWT_SECRET,
716
+ algorithm="HS256",
717
+ )
718
+
719
+
720
+ class AuthError(Exception):
721
+ def __init__(self, status_code: int, message: str):
722
+ super().__init__(message)
723
+ self.status_code = status_code
724
+
725
+
726
+ def handle_auth_error(error: Exception) -> func.HttpResponse | None:
727
+ """AuthError を HTTP レスポンスに変換"""
728
+ if isinstance(error, AuthError):
729
+ return func.HttpResponse(
730
+ json.dumps({"error": str(error)}),
731
+ status_code=error.status_code,
732
+ mimetype="application/json",
733
+ )
734
+ return None
735
+ `;
736
+ }
737
+ // ============================================================
738
+ // 8. BFF Auth Routes(Next.js API Routes)
739
+ // ============================================================
740
+ function generateBFFAuthLoginRoute(projectName, sharedPackageName) {
741
+ const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
742
+ return `import { NextRequest, NextResponse } from 'next/server';
743
+ import { LoginRequest, LoginResponse } from '${sharedPackageName}';
744
+
745
+ const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
746
+
747
+ export async function POST(request: NextRequest) {
748
+ try {
749
+ const body = await request.json();
750
+ const validated = LoginRequest.parse(body);
751
+
752
+ const result = await fetch(\`\${FUNCTIONS_BASE_URL}/api/auth/login\`, {
753
+ method: 'POST',
754
+ headers: { 'Content-Type': 'application/json' },
755
+ body: JSON.stringify(validated),
756
+ });
757
+
758
+ if (!result.ok) {
759
+ const error = await result.json().catch(() => ({ error: 'Authentication failed' }));
760
+ return NextResponse.json(error, { status: result.status });
761
+ }
762
+
763
+ const data = LoginResponse.parse(await result.json());
764
+
765
+ const response = NextResponse.json({ user: data.user });
766
+ response.cookies.set('${cookieName}', data.token, {
767
+ httpOnly: true,
768
+ secure: process.env.NODE_ENV === 'production',
769
+ sameSite: 'lax',
770
+ path: '/',
771
+ maxAge: 86400,
772
+ });
773
+ return response;
774
+ } catch (error: any) {
775
+ console.error('[BFF] Login error:', error);
776
+ return NextResponse.json({ error: 'Login failed' }, { status: 500 });
777
+ }
778
+ }
779
+ `;
780
+ }
781
+ function generateBFFAuthLogoutRoute(projectName) {
782
+ const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
783
+ return `import { NextResponse } from 'next/server';
784
+
785
+ export async function POST() {
786
+ const response = NextResponse.json({ message: 'Logged out' });
787
+ response.cookies.delete('${cookieName}');
788
+ return response;
789
+ }
790
+ `;
791
+ }
792
+ function generateBFFAuthMeRoute(sharedPackageName) {
793
+ return `import { NextResponse } from 'next/server';
794
+ import { headers } from 'next/headers';
795
+
796
+ const FUNCTIONS_BASE_URL = process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
797
+
798
+ export async function GET() {
799
+ try {
800
+ const reqHeaders = await headers();
801
+ const authorization = reqHeaders.get('authorization');
802
+
803
+ if (!authorization) {
804
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
805
+ }
806
+
807
+ const result = await fetch(\`\${FUNCTIONS_BASE_URL}/api/auth/me\`, {
808
+ headers: { Authorization: authorization },
809
+ });
810
+
811
+ if (!result.ok) {
812
+ return NextResponse.json({ error: 'Unauthorized' }, { status: result.status });
813
+ }
814
+
815
+ const data = await result.json();
816
+ return NextResponse.json(data);
817
+ } catch (error: any) {
818
+ console.error('[BFF] Auth/me error:', error);
819
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
820
+ }
821
+ }
822
+ `;
823
+ }
824
+ // ============================================================
825
+ // 9. Next.js Middleware
826
+ // ============================================================
827
+ function generateMiddleware(projectName) {
828
+ const cookieName = projectName.replace(/^@[^/]+\//, '').replace(/[^a-z0-9-]/g, '-') + '-auth-token';
829
+ return `import { NextResponse } from 'next/server';
830
+ import type { NextRequest } from 'next/server';
831
+
832
+ const AUTH_COOKIE_NAME = '${cookieName}';
833
+ const PUBLIC_PATHS = ['/login', '/api/auth/login', '/api/auth/logout'];
834
+
835
+ export function middleware(request: NextRequest) {
836
+ const { pathname } = request.nextUrl;
837
+
838
+ // 公開パス・静的アセットはスキップ
839
+ if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) return NextResponse.next();
840
+ if (pathname.startsWith('/_next') || pathname.match(/\\.(ico|png|jpg|svg|css|js)$/)) {
841
+ return NextResponse.next();
842
+ }
843
+
844
+ const token = request.cookies.get(AUTH_COOKIE_NAME)?.value;
845
+
846
+ if (!token) {
847
+ if (pathname.startsWith('/api/')) {
848
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
849
+ }
850
+ return NextResponse.redirect(new URL('/login', request.url));
851
+ }
852
+
853
+ // JWT 有効期限の簡易チェック(署名検証なし = Edge Runtime 互換)
854
+ try {
855
+ const payload = JSON.parse(atob(token.split('.')[1]));
856
+ if (payload.exp && payload.exp * 1000 < Date.now()) {
857
+ const response = pathname.startsWith('/api/')
858
+ ? NextResponse.json({ error: 'Token expired' }, { status: 401 })
859
+ : NextResponse.redirect(new URL('/login', request.url));
860
+ response.cookies.delete(AUTH_COOKIE_NAME);
861
+ return response;
862
+ }
863
+ } catch {
864
+ const response = pathname.startsWith('/api/')
865
+ ? NextResponse.json({ error: 'Invalid token' }, { status: 401 })
866
+ : NextResponse.redirect(new URL('/login', request.url));
867
+ response.cookies.delete(AUTH_COOKIE_NAME);
868
+ return response;
869
+ }
870
+
871
+ // Authorization ヘッダーを注入(BFF callFunction が自動転送できるように)
872
+ const requestHeaders = new Headers(request.headers);
873
+ requestHeaders.set('Authorization', \`Bearer \${token}\`);
874
+ return NextResponse.next({
875
+ request: { headers: requestHeaders },
876
+ });
877
+ }
878
+
879
+ export const config = {
880
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
881
+ };
882
+ `;
883
+ }
884
+ // ============================================================
885
+ // 10. ログインページ
886
+ // ============================================================
887
+ function generateLoginPage() {
888
+ return `'use client';
889
+
890
+ import { useState } from 'react';
891
+ import { useRouter } from 'next/navigation';
892
+
893
+ export default function LoginPage() {
894
+ const [loginId, setLoginId] = useState('');
895
+ const [password, setPassword] = useState('');
896
+ const [error, setError] = useState('');
897
+ const [loading, setLoading] = useState(false);
898
+ const router = useRouter();
899
+
900
+ const handleSubmit = async (e: React.FormEvent) => {
901
+ e.preventDefault();
902
+ setError('');
903
+ setLoading(true);
904
+
905
+ try {
906
+ const res = await fetch('/api/auth/login', {
907
+ method: 'POST',
908
+ headers: { 'Content-Type': 'application/json' },
909
+ body: JSON.stringify({ loginId, password }),
910
+ });
911
+
912
+ if (!res.ok) {
913
+ const data = await res.json().catch(() => ({}));
914
+ setError(data.error || 'ログインに失敗しました');
915
+ return;
916
+ }
917
+
918
+ router.push('/');
919
+ router.refresh();
920
+ } catch {
921
+ setError('通信エラーが発生しました');
922
+ } finally {
923
+ setLoading(false);
924
+ }
925
+ };
926
+
927
+ return (
928
+ <div className="min-h-screen flex items-center justify-center bg-gray-50">
929
+ <div className="w-full max-w-md p-8 bg-white rounded-lg shadow-md">
930
+ <h1 className="text-2xl font-bold text-center mb-6">ログイン</h1>
931
+ {error && (
932
+ <div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded text-sm">
933
+ {error}
934
+ </div>
935
+ )}
936
+ <form onSubmit={handleSubmit} className="space-y-4">
937
+ <div>
938
+ <label htmlFor="loginId" className="block text-sm font-medium text-gray-700 mb-1">
939
+ ログインID
940
+ </label>
941
+ <input
942
+ id="loginId"
943
+ type="text"
944
+ value={loginId}
945
+ onChange={(e) => setLoginId(e.target.value)}
946
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
947
+ required
948
+ autoFocus
949
+ />
950
+ </div>
951
+ <div>
952
+ <label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
953
+ パスワード
954
+ </label>
955
+ <input
956
+ id="password"
957
+ type="password"
958
+ value={password}
959
+ onChange={(e) => setPassword(e.target.value)}
960
+ className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
961
+ required
962
+ />
963
+ </div>
964
+ <button
965
+ type="submit"
966
+ disabled={loading}
967
+ className="w-full py-2 px-4 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
968
+ >
969
+ {loading ? 'ログイン中...' : 'ログイン'}
970
+ </button>
971
+ </form>
972
+ </div>
973
+ </div>
974
+ );
975
+ }
976
+ `;
977
+ }
978
+ // ============================================================
979
+ // 11. React Auth Context
980
+ // ============================================================
981
+ function generateAuthContext() {
982
+ return `'use client';
983
+
984
+ import { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react';
985
+ import { useRouter } from 'next/navigation';
986
+
987
+ interface AuthUser {
988
+ id: string;
989
+ loginId: string;
990
+ name: string;
991
+ email: string;
992
+ roles: string[];
993
+ }
994
+
995
+ interface AuthContextType {
996
+ user: AuthUser | null;
997
+ loading: boolean;
998
+ login: (loginId: string, password: string) => Promise<{ success: boolean; error?: string }>;
999
+ logout: () => Promise<void>;
1000
+ hasRole: (role: string) => boolean;
1001
+ hasAnyRole: (roles: string[]) => boolean;
1002
+ }
1003
+
1004
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
1005
+
1006
+ export function AuthProvider({ children }: { children: ReactNode }) {
1007
+ const [user, setUser] = useState<AuthUser | null>(null);
1008
+ const [loading, setLoading] = useState(true);
1009
+ const router = useRouter();
1010
+
1011
+ const fetchUser = useCallback(async () => {
1012
+ try {
1013
+ const res = await fetch('/api/auth/me');
1014
+ if (res.ok) {
1015
+ const data = await res.json();
1016
+ setUser(data);
1017
+ } else {
1018
+ setUser(null);
1019
+ }
1020
+ } catch {
1021
+ setUser(null);
1022
+ } finally {
1023
+ setLoading(false);
1024
+ }
1025
+ }, []);
1026
+
1027
+ useEffect(() => {
1028
+ fetchUser();
1029
+ }, [fetchUser]);
1030
+
1031
+ const login = async (loginId: string, password: string) => {
1032
+ try {
1033
+ const res = await fetch('/api/auth/login', {
1034
+ method: 'POST',
1035
+ headers: { 'Content-Type': 'application/json' },
1036
+ body: JSON.stringify({ loginId, password }),
1037
+ });
1038
+
1039
+ if (!res.ok) {
1040
+ const data = await res.json().catch(() => ({}));
1041
+ return { success: false, error: data.error || 'Login failed' };
1042
+ }
1043
+
1044
+ const data = await res.json();
1045
+ setUser(data.user);
1046
+ return { success: true };
1047
+ } catch {
1048
+ return { success: false, error: 'Network error' };
1049
+ }
1050
+ };
1051
+
1052
+ const logout = async () => {
1053
+ await fetch('/api/auth/logout', { method: 'POST' });
1054
+ setUser(null);
1055
+ router.push('/login');
1056
+ };
1057
+
1058
+ const hasRole = (role: string) => user?.roles?.includes(role) ?? false;
1059
+ const hasAnyRole = (roles: string[]) => roles.some(r => hasRole(r));
1060
+
1061
+ return (
1062
+ <AuthContext.Provider value={{ user, loading, login, logout, hasRole, hasAnyRole }}>
1063
+ {children}
1064
+ </AuthContext.Provider>
1065
+ );
1066
+ }
1067
+
1068
+ export function useAuth() {
1069
+ const context = useContext(AuthContext);
1070
+ if (!context) {
1071
+ throw new Error('useAuth must be used within an AuthProvider');
1072
+ }
1073
+ return context;
1074
+ }
1075
+ `;
1076
+ }
1077
+ // ============================================================
1078
+ // 12. callFunction 認証対応版
1079
+ // ============================================================
1080
+ function generateBFFCallFunctionWithAuth() {
1081
+ return `import { NextRequest, NextResponse } from 'next/server';
1082
+ import { headers } from 'next/headers';
1083
+ import { z } from 'zod/v4';
1084
+
1085
+ /**
1086
+ * SwallowKit BFF Call Function Helper (Auth-enabled)
1087
+ * Azure Functions を呼び出す汎用ヘルパー(Authorization ヘッダー自動転送対応)
1088
+ */
1089
+
1090
+ function getFunctionsBaseUrl(): string {
1091
+ return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
1092
+ }
1093
+
1094
+ interface CallFunctionConfig<TInput = any, TOutput = any> {
1095
+ /** HTTP メソッド */
1096
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
1097
+ /** Azure Functions のパス (例: '/api/todo', '/api/todo/123') */
1098
+ path: string;
1099
+ /** リクエストボディ (POST/PUT 用) */
1100
+ body?: any;
1101
+ /** 入力バリデーション用 Zod スキーマ (省略時はバリデーションなし) */
1102
+ inputSchema?: z.ZodSchema<TInput>;
1103
+ /** 出力バリデーション用 Zod スキーマ (省略時はそのまま返す) */
1104
+ responseSchema?: z.ZodSchema<TOutput>;
1105
+ /** 成功時の HTTP ステータスコード (デフォルト: 200) */
1106
+ successStatus?: number;
1107
+ }
1108
+
1109
+ export async function callFunction<TInput = any, TOutput = any>(
1110
+ config: CallFunctionConfig<TInput, TOutput>
1111
+ ): Promise<NextResponse> {
1112
+ const { method, path, body, inputSchema, responseSchema, successStatus = 200 } = config;
1113
+
1114
+ try {
1115
+ // 入力バリデーション
1116
+ let validatedBody = body;
1117
+ if (inputSchema && body !== undefined) {
1118
+ const result = inputSchema.safeParse(body);
1119
+ if (!result.success) {
1120
+ console.error('[BFF] Validation failed:', result.error.issues);
1121
+ return NextResponse.json(
1122
+ { error: 'Validation failed', details: result.error.issues },
1123
+ { status: 400 }
1124
+ );
1125
+ }
1126
+ validatedBody = result.data;
1127
+ }
1128
+
1129
+ // Azure Functions を呼び出し
1130
+ const functionsBaseUrl = getFunctionsBaseUrl();
1131
+ const url = functionsBaseUrl + path;
1132
+ console.log(\`[BFF] \${method} \${url}\`);
1133
+
1134
+ // Authorization ヘッダーの転送(Middleware が cookie → Authorization に変換済み)
1135
+ const fetchHeaders: Record<string, string> = {
1136
+ 'Content-Type': 'application/json',
1137
+ };
1138
+ try {
1139
+ const reqHeaders = await headers();
1140
+ const authorization = reqHeaders.get('authorization');
1141
+ if (authorization) {
1142
+ fetchHeaders['Authorization'] = authorization;
1143
+ }
1144
+ } catch {
1145
+ // headers() が使えないコンテキスト(ISR 等)では無視
1146
+ }
1147
+
1148
+ const response = await fetch(url, {
1149
+ method,
1150
+ headers: fetchHeaders,
1151
+ body: validatedBody !== undefined ? JSON.stringify(validatedBody) : undefined,
1152
+ });
1153
+
1154
+ console.log('[BFF] Functions response status:', response.status);
1155
+
1156
+ // エラーレスポンスの転送
1157
+ if (!response.ok) {
1158
+ const text = await response.text();
1159
+ console.error('[BFF] Functions error:', { status: response.status, body: text });
1160
+
1161
+ let errorMessage = 'Request failed';
1162
+ try {
1163
+ const error = JSON.parse(text);
1164
+ errorMessage = error.error || errorMessage;
1165
+ } catch {
1166
+ errorMessage = text || errorMessage;
1167
+ }
1168
+ return NextResponse.json({ error: errorMessage }, { status: response.status });
1169
+ }
1170
+
1171
+ // DELETE 204 の場合はボディなし
1172
+ if (response.status === 204 || method === 'DELETE') {
1173
+ return new NextResponse(null, { status: 204 });
1174
+ }
1175
+
1176
+ // レスポンスの取得と出力バリデーション
1177
+ const data = await response.json();
1178
+
1179
+ if (responseSchema) {
1180
+ const validated = responseSchema.parse(data);
1181
+ return NextResponse.json(validated, { status: successStatus });
1182
+ }
1183
+
1184
+ return NextResponse.json(data, { status: successStatus });
1185
+ } catch (error: any) {
1186
+ console.error(\`[BFF] Error:\`, error);
1187
+ return NextResponse.json(
1188
+ { error: error.message || 'Internal server error' },
1189
+ { status: 500 }
1190
+ );
1191
+ }
1192
+ }
1193
+ `;
1194
+ }
1195
+ // ============================================================
1196
+ // 13. scaffold 用ロールガード挿入ヘルパー
1197
+ // ============================================================
1198
+ /**
1199
+ * TypeScript Functions のハンドラーにロールガードを挿入するためのインポート文を生成
1200
+ */
1201
+ function generateAuthImportTS() {
1202
+ return `import { requireAuth, requireRoles, handleAuthError } from './auth/jwt-helper';`;
1203
+ }
1204
+ /**
1205
+ * TypeScript Functions のハンドラー先頭に挿入する認証チェックコードを生成
1206
+ */
1207
+ function generateAuthGuardTS(policy, operation) {
1208
+ const roles = operation === 'read'
1209
+ ? (policy.read || policy.roles || [])
1210
+ : (policy.write || policy.roles || []);
1211
+ if (roles.length > 0) {
1212
+ return ` const authUser = requireAuth(request);
1213
+ requireRoles(authUser, ${JSON.stringify(roles)});`;
1214
+ }
1215
+ return ` const authUser = requireAuth(request);`;
1216
+ }
1217
+ /**
1218
+ * C# Functions のハンドラーに挿入するロールガードコードを生成
1219
+ */
1220
+ function generateAuthGuardCSharp(policy, operation) {
1221
+ const roles = operation === 'read'
1222
+ ? (policy.read || policy.roles || [])
1223
+ : (policy.write || policy.roles || []);
1224
+ if (roles.length > 0) {
1225
+ const rolesStr = roles.map(r => `"${r}"`).join(', ');
1226
+ return ` var (principal, errorResponse) = JwtHelper.Authorize(request, ${rolesStr});
1227
+ if (errorResponse != null) return errorResponse;`;
1228
+ }
1229
+ return ` var (principal, errorResponse) = JwtHelper.Authorize(request);
1230
+ if (errorResponse != null) return errorResponse;`;
1231
+ }
1232
+ /**
1233
+ * Python Functions のハンドラーに挿入するロールガードコードを生成
1234
+ */
1235
+ function generateAuthGuardPython(policy, operation) {
1236
+ const roles = operation === 'read'
1237
+ ? (policy.read || policy.roles || [])
1238
+ : (policy.write || policy.roles || []);
1239
+ if (roles.length > 0) {
1240
+ const rolesStr = roles.map(r => `"${r}"`).join(', ');
1241
+ return ` user = require_auth(req)
1242
+ require_roles(user, [${rolesStr}])`;
1243
+ }
1244
+ return ` user = require_auth(req)`;
1245
+ }
1246
+ //# sourceMappingURL=auth-generator.js.map