swallowkit 1.0.0-beta.45 → 1.0.0-beta.47
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/dist/cli/commands/add-auth.d.ts +2 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +214 -16
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/dev.d.ts +4 -0
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +100 -12
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +74 -30
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +37 -5
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +20 -2
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +265 -12
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- 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 +19 -19
- package/dist/core/scaffold/connector-functions-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.d.ts +2 -2
- package/dist/core/scaffold/functions-generator.d.ts.map +1 -1
- package/dist/core/scaffold/functions-generator.js +16 -16
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/nextjs-generator.d.ts.map +1 -1
- package/dist/core/scaffold/nextjs-generator.js +13 -1
- package/dist/core/scaffold/nextjs-generator.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +20 -20
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +13 -1
- package/src/__tests__/auth.test.ts +103 -0
- package/src/__tests__/dev.test.ts +27 -0
- package/src/__tests__/init.test.ts +27 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/cli/commands/add-auth.ts +239 -16
- package/src/cli/commands/dev.ts +111 -14
- package/src/cli/commands/init.ts +76 -30
- package/src/cli/commands/scaffold.ts +37 -5
- package/src/cli/index.ts +3 -1
- package/src/core/scaffold/auth-generator.ts +267 -13
- package/src/core/scaffold/connector-functions-generator.ts +22 -19
- package/src/core/scaffold/functions-generator.ts +17 -17
- package/src/core/scaffold/nextjs-generator.ts +13 -1
- package/src/types/index.ts +1 -1
package/src/cli/commands/dev.ts
CHANGED
|
@@ -38,10 +38,13 @@ export interface DevOptions {
|
|
|
38
38
|
noFunctions?: boolean;
|
|
39
39
|
seedEnv?: string;
|
|
40
40
|
mockConnectors?: boolean;
|
|
41
|
+
swaPort?: string;
|
|
42
|
+
noSwa?: boolean;
|
|
41
43
|
}
|
|
42
44
|
|
|
43
45
|
type ParsedDevActionOptions = DevOptions & {
|
|
44
46
|
functions?: boolean;
|
|
47
|
+
swa?: boolean;
|
|
45
48
|
};
|
|
46
49
|
|
|
47
50
|
interface FunctionsCoreToolsCommand {
|
|
@@ -57,6 +60,7 @@ function normalizeParsedDevOptions(options: ParsedDevActionOptions): DevOptions
|
|
|
57
60
|
return {
|
|
58
61
|
...options,
|
|
59
62
|
noFunctions: options.noFunctions ?? options.functions === false,
|
|
63
|
+
noSwa: options.noSwa ?? options.swa === false,
|
|
60
64
|
};
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -121,6 +125,23 @@ export function buildFunctionsBaseUrl(host: string | undefined, functionsPort: s
|
|
|
121
125
|
return `http://${host || 'localhost'}:${functionsPort}`;
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
export function buildSwaStartArgs(host: string | undefined, nextPort: string, swaPort: string): string[] {
|
|
129
|
+
return [
|
|
130
|
+
'start',
|
|
131
|
+
`http://${host || 'localhost'}:${nextPort}`,
|
|
132
|
+
'--swa-config-location',
|
|
133
|
+
'.',
|
|
134
|
+
'--port',
|
|
135
|
+
swaPort,
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getSwaCliInstallCommand(pm: string): string {
|
|
140
|
+
return pm === 'pnpm'
|
|
141
|
+
? 'pnpm add -Dw @azure/static-web-apps-cli'
|
|
142
|
+
: 'npm install -D @azure/static-web-apps-cli';
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
export function getFunctionsReadinessTimeoutMs(backendLanguage: BackendLanguage): number {
|
|
125
146
|
return backendLanguage === 'csharp' ? 90_000 : 30_000;
|
|
126
147
|
}
|
|
@@ -506,6 +527,8 @@ export function buildDevCommand(
|
|
|
506
527
|
.option('--no-functions', 'Skip Azure Functions startup')
|
|
507
528
|
.option('--seed-env <environment>', 'Replace Cosmos DB Emulator data from dev-seeds/<environment> before startup')
|
|
508
529
|
.option('--mock-connectors', 'Start mock server for connector models (serves Zod-generated data)', false)
|
|
530
|
+
.option('--swa-port <port>', 'SWA authentication emulator port', '4280')
|
|
531
|
+
.option('--no-swa', 'Skip the SWA authentication emulator')
|
|
509
532
|
.action(async (options: ParsedDevActionOptions) => {
|
|
510
533
|
const normalizedOptions = normalizeParsedDevOptions(options);
|
|
511
534
|
|
|
@@ -618,11 +641,24 @@ async function initializeCosmosDB(databaseName: string): Promise<CosmosInitializ
|
|
|
618
641
|
async function startDevEnvironment(options: DevOptions) {
|
|
619
642
|
const port = options.port || '3000';
|
|
620
643
|
const functionsPort = options.functionsPort || '7071';
|
|
644
|
+
const swaPort = options.swaPort || '4280';
|
|
621
645
|
|
|
622
646
|
// Detect package manager from project lockfile
|
|
623
647
|
const pm = detectFromProject();
|
|
624
648
|
const pmCmd = getCommands(pm);
|
|
625
649
|
const backendLanguage = getBackendLanguage();
|
|
650
|
+
const authConfig = getAuthConfig();
|
|
651
|
+
const useSwaEmulator = authConfig?.provider === 'swa' && !options.noSwa;
|
|
652
|
+
const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
|
|
653
|
+
|
|
654
|
+
if (useSwaEmulator && !swaCommand) {
|
|
655
|
+
const installCommand = getSwaCliInstallCommand(pm);
|
|
656
|
+
console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
|
|
657
|
+
console.error(' Install it in this project and retry:');
|
|
658
|
+
console.error(`\n ${installCommand}\n`);
|
|
659
|
+
process.exitCode = 1;
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
626
662
|
|
|
627
663
|
// プロセスを管理する配列
|
|
628
664
|
const processes: ChildProcess[] = [];
|
|
@@ -925,7 +961,6 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
925
961
|
const connectorModels = allModels.filter((m) => m.connectorConfig);
|
|
926
962
|
|
|
927
963
|
// Resolve auth config — auth functions use RDB connectors, mocked alongside other models
|
|
928
|
-
const authConfig = getAuthConfig();
|
|
929
964
|
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
930
965
|
if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
|
|
931
966
|
// Read JWT_SECRET from functions/local.settings.json if available
|
|
@@ -985,18 +1020,6 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
985
1020
|
// 5. Start Next.js development server
|
|
986
1021
|
const nextArgs = buildNextDevArgs(pm, port);
|
|
987
1022
|
|
|
988
|
-
if (options.open) {
|
|
989
|
-
// Next.js 14+ deprecated --open option, so we open browser manually
|
|
990
|
-
setTimeout(() => {
|
|
991
|
-
const url = `http://${options.host || 'localhost'}:${port}`;
|
|
992
|
-
console.log(`\n🌐 Opening browser: ${url}`);
|
|
993
|
-
|
|
994
|
-
const start = process.platform === 'darwin' ? 'open' :
|
|
995
|
-
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
996
|
-
spawn(start, [url], { shell: true });
|
|
997
|
-
}, 3000);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1023
|
// Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
|
|
1001
1024
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
1002
1025
|
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
@@ -1063,10 +1086,52 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1063
1086
|
}
|
|
1064
1087
|
}
|
|
1065
1088
|
|
|
1089
|
+
let swaReady = false;
|
|
1090
|
+
if (useSwaEmulator && swaCommand) {
|
|
1091
|
+
const nextUrl = `http://${options.host || 'localhost'}:${port}`;
|
|
1092
|
+
console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
|
|
1093
|
+
const nextReady = await waitForHttpServerReady(nextUrl);
|
|
1094
|
+
if (!nextReady) {
|
|
1095
|
+
throw new Error(`Next.js did not become ready at ${nextUrl}`);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
|
|
1099
|
+
const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
|
|
1100
|
+
cwd: process.cwd(),
|
|
1101
|
+
shell: true,
|
|
1102
|
+
stdio: 'inherit',
|
|
1103
|
+
env: process.env,
|
|
1104
|
+
});
|
|
1105
|
+
processes.push(swaProcess);
|
|
1106
|
+
swaProcess.on('error', (error) => {
|
|
1107
|
+
console.error('❌ SWA CLI startup error:', error.message);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1111
|
+
swaReady = await waitForHttpServerReady(swaUrl);
|
|
1112
|
+
if (!swaReady) {
|
|
1113
|
+
throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1066
1117
|
console.log('');
|
|
1067
1118
|
console.log('✅ SwallowKit development environment is running!');
|
|
1068
1119
|
console.log('');
|
|
1069
|
-
|
|
1120
|
+
if (useSwaEmulator) {
|
|
1121
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1122
|
+
console.log('🔐 SWA authenticated app:');
|
|
1123
|
+
console.log(` ${swaUrl}`);
|
|
1124
|
+
console.log('');
|
|
1125
|
+
console.log('🔑 Local sign-in:');
|
|
1126
|
+
console.log(` ${swaUrl}/.auth/login/aad`);
|
|
1127
|
+
console.log(' Enter any username and select Login.');
|
|
1128
|
+
console.log(' The "authenticated" role is added automatically.');
|
|
1129
|
+
console.log('');
|
|
1130
|
+
console.log('📱 Next.js direct access (authentication is not emulated):');
|
|
1131
|
+
console.log(` http://${options.host || 'localhost'}:${port}`);
|
|
1132
|
+
} else {
|
|
1133
|
+
console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
1134
|
+
}
|
|
1070
1135
|
if (hasFunctions && !options.noFunctions) {
|
|
1071
1136
|
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
1072
1137
|
}
|
|
@@ -1082,7 +1147,22 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1082
1147
|
if (mockServer) {
|
|
1083
1148
|
console.log('💡 Connector models served from mock server (Zod-generated data)');
|
|
1084
1149
|
}
|
|
1150
|
+
if (swaReady) {
|
|
1151
|
+
console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
|
|
1152
|
+
}
|
|
1085
1153
|
console.log('');
|
|
1154
|
+
|
|
1155
|
+
if (options.open) {
|
|
1156
|
+
const url = useSwaEmulator
|
|
1157
|
+
? `http://${options.host || 'localhost'}:${swaPort}`
|
|
1158
|
+
: `http://${options.host || 'localhost'}:${port}`;
|
|
1159
|
+
console.log(`🌐 Opening browser: ${url}`);
|
|
1160
|
+
const start = process.platform === 'darwin' ? 'open' :
|
|
1161
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1162
|
+
spawn(start, [url], { shell: true });
|
|
1163
|
+
console.log('');
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1086
1166
|
console.log('🛑 Press Ctrl+C to stop');
|
|
1087
1167
|
console.log('');
|
|
1088
1168
|
|
|
@@ -1097,6 +1177,23 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1097
1177
|
}
|
|
1098
1178
|
}
|
|
1099
1179
|
|
|
1180
|
+
async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
|
|
1181
|
+
const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
|
|
1182
|
+
const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
|
|
1183
|
+
if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
|
|
1184
|
+
cwd: projectRoot,
|
|
1185
|
+
shell: process.platform === 'win32',
|
|
1186
|
+
})) {
|
|
1187
|
+
return localCommand;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
|
|
1191
|
+
return 'swa';
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return null;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1100
1197
|
async function runCommand(
|
|
1101
1198
|
command: string,
|
|
1102
1199
|
args: string[],
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -446,6 +446,7 @@ export async function withNpmLockfileSafeManifests(projectDir: string, action: (
|
|
|
446
446
|
const functionsPackageJsonPath = path.join(projectDir, 'functions', 'package.json');
|
|
447
447
|
const sharedPackageJsonPath = path.join(projectDir, 'shared', 'package.json');
|
|
448
448
|
const originalFiles = new Map<string, string>();
|
|
449
|
+
const hiddenNodeModules = new Map<string, string>();
|
|
449
450
|
|
|
450
451
|
function rememberOriginal(filePath: string): void {
|
|
451
452
|
if (!originalFiles.has(filePath)) {
|
|
@@ -493,11 +494,30 @@ export async function withNpmLockfileSafeManifests(projectDir: string, action: (
|
|
|
493
494
|
writeJson(filePath, packageJson);
|
|
494
495
|
}
|
|
495
496
|
|
|
497
|
+
function hideNodeModules(directory: string): void {
|
|
498
|
+
const nodeModulesPath = path.join(directory, 'node_modules');
|
|
499
|
+
if (!fs.existsSync(nodeModulesPath)) return;
|
|
500
|
+
|
|
501
|
+
const hiddenPath = path.join(directory, `.node_modules.swallowkit-npm-lockfile-${process.pid}`);
|
|
502
|
+
fs.renameSync(nodeModulesPath, hiddenPath);
|
|
503
|
+
hiddenNodeModules.set(nodeModulesPath, hiddenPath);
|
|
504
|
+
}
|
|
505
|
+
|
|
496
506
|
try {
|
|
497
507
|
normalizePackageJson(packageJsonPath, 'file:shared');
|
|
498
508
|
normalizePackageJson(functionsPackageJsonPath, 'file:../shared');
|
|
509
|
+
hideNodeModules(projectDir);
|
|
510
|
+
hideNodeModules(path.join(projectDir, 'shared'));
|
|
511
|
+
hideNodeModules(path.join(projectDir, 'functions'));
|
|
499
512
|
await action();
|
|
500
513
|
} finally {
|
|
514
|
+
for (const [nodeModulesPath, hiddenPath] of hiddenNodeModules) {
|
|
515
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
516
|
+
fs.rmSync(nodeModulesPath, { recursive: true, force: true });
|
|
517
|
+
}
|
|
518
|
+
fs.renameSync(hiddenPath, nodeModulesPath);
|
|
519
|
+
}
|
|
520
|
+
|
|
501
521
|
for (const [filePath, content] of originalFiles) {
|
|
502
522
|
fs.writeFileSync(filePath, content);
|
|
503
523
|
}
|
|
@@ -710,6 +730,7 @@ export function buildGeneratedProjectDependencies(projectName: string): Record<s
|
|
|
710
730
|
return {
|
|
711
731
|
'@azure/cosmos': '^4.0.0',
|
|
712
732
|
'applicationinsights': '^3.3.0',
|
|
733
|
+
'zod': '^4.0.0',
|
|
713
734
|
[`@${projectName}/shared`]: 'workspace:*',
|
|
714
735
|
};
|
|
715
736
|
}
|
|
@@ -872,6 +893,13 @@ function getFunctionsBaseUrl(): string {
|
|
|
872
893
|
return process.env.BACKEND_FUNCTIONS_BASE_URL || process.env.FUNCTIONS_BASE_URL || 'http://localhost:7071';
|
|
873
894
|
}
|
|
874
895
|
|
|
896
|
+
function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string> {
|
|
897
|
+
const key = process.env.BACKEND_FUNCTIONS_KEY;
|
|
898
|
+
if (key) return { 'x-functions-key': key };
|
|
899
|
+
if (functionsBaseUrl.startsWith('http://localhost:') || functionsBaseUrl.startsWith('http://127.0.0.1:')) return {};
|
|
900
|
+
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
901
|
+
}
|
|
902
|
+
|
|
875
903
|
/**
|
|
876
904
|
* Simple HTTP client for calling backend APIs
|
|
877
905
|
* Use this to make requests to BFF API routes (which forward to Azure Functions)
|
|
@@ -883,6 +911,7 @@ async function request<T>(
|
|
|
883
911
|
queryParams?: Record<string, string>
|
|
884
912
|
): Promise<T> {
|
|
885
913
|
const functionsBaseUrl = getFunctionsBaseUrl();
|
|
914
|
+
const keyHeaders = getFunctionsKeyHeaders(functionsBaseUrl);
|
|
886
915
|
let url = \`\${functionsBaseUrl}\${endpoint}\`;
|
|
887
916
|
if (queryParams) {
|
|
888
917
|
const params = new URLSearchParams(queryParams);
|
|
@@ -894,6 +923,7 @@ async function request<T>(
|
|
|
894
923
|
method,
|
|
895
924
|
headers: {
|
|
896
925
|
'Content-Type': 'application/json',
|
|
926
|
+
...keyHeaders,
|
|
897
927
|
},
|
|
898
928
|
body: body ? JSON.stringify(body) : undefined,
|
|
899
929
|
});
|
|
@@ -977,6 +1007,7 @@ export const api = {
|
|
|
977
1007
|
// 6. Create .env.example
|
|
978
1008
|
const envExample = `# Azure Functions Backend URL
|
|
979
1009
|
BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071
|
|
1010
|
+
BACKEND_FUNCTIONS_KEY=
|
|
980
1011
|
|
|
981
1012
|
# Azure Configuration
|
|
982
1013
|
AZURE_RESOURCE_GROUP=your-resource-group
|
|
@@ -1062,6 +1093,7 @@ export async function register() {
|
|
|
1062
1093
|
const envLocalContent = [
|
|
1063
1094
|
'# Azure Functions Backend URL (Local)',
|
|
1064
1095
|
'BACKEND_FUNCTIONS_BASE_URL=http://localhost:7071',
|
|
1096
|
+
'BACKEND_FUNCTIONS_KEY=',
|
|
1065
1097
|
''
|
|
1066
1098
|
].join('\n');
|
|
1067
1099
|
fs.writeFileSync(path.join(projectDir, '.env.local'), envLocalContent);
|
|
@@ -1358,7 +1390,7 @@ export async function greet(request: HttpRequest, context: InvocationContext): P
|
|
|
1358
1390
|
|
|
1359
1391
|
app.http('greet', {
|
|
1360
1392
|
methods: ['GET', 'POST'],
|
|
1361
|
-
authLevel: '
|
|
1393
|
+
authLevel: 'function',
|
|
1362
1394
|
handler: greet
|
|
1363
1395
|
});
|
|
1364
1396
|
`);
|
|
@@ -1390,7 +1422,7 @@ public sealed class GreetFunction
|
|
|
1390
1422
|
{
|
|
1391
1423
|
[Function("greet")]
|
|
1392
1424
|
public async Task<HttpResponseData> Run(
|
|
1393
|
-
[HttpTrigger(AuthorizationLevel.
|
|
1425
|
+
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "greet")] HttpRequestData request)
|
|
1394
1426
|
{
|
|
1395
1427
|
var query = request.Url.Query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries);
|
|
1396
1428
|
var name = "SwallowKit";
|
|
@@ -1458,7 +1490,7 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
|
|
|
1458
1490
|
|
|
1459
1491
|
from blueprints.greet import bp as greet_bp
|
|
1460
1492
|
|
|
1461
|
-
app = func.FunctionApp(http_auth_level=func.AuthLevel.
|
|
1493
|
+
app = func.FunctionApp(http_auth_level=func.AuthLevel.FUNCTION)
|
|
1462
1494
|
|
|
1463
1495
|
app.register_blueprint(greet_bp)
|
|
1464
1496
|
# SwallowKit scaffold registrations
|
|
@@ -2089,7 +2121,7 @@ const containerName = 'Models'; // PascalCase + 's'
|
|
|
2089
2121
|
app.http('{model}-get-all', {
|
|
2090
2122
|
methods: ['GET'],
|
|
2091
2123
|
route: '{model}',
|
|
2092
|
-
authLevel: '
|
|
2124
|
+
authLevel: 'function',
|
|
2093
2125
|
extraInputs: [{ type: 'cosmosDB', name: 'cosmosInput', containerName, ... }],
|
|
2094
2126
|
handler: async (request, context) => {
|
|
2095
2127
|
const documents = context.extraInputs.get('cosmosInput');
|
|
@@ -2380,7 +2412,7 @@ import { ModelSchema } from '@${projectName}/shared';
|
|
|
2380
2412
|
app.http('{model}-get-all', {
|
|
2381
2413
|
methods: ['GET'],
|
|
2382
2414
|
route: '{model}',
|
|
2383
|
-
authLevel: '
|
|
2415
|
+
authLevel: 'function',
|
|
2384
2416
|
extraInputs: [cosmosInput],
|
|
2385
2417
|
handler: async (request, context) => {
|
|
2386
2418
|
const documents = context.extraInputs.get(cosmosInput);
|
|
@@ -2411,6 +2443,43 @@ app.http('{model}-get-all', {
|
|
|
2411
2443
|
console.log('');
|
|
2412
2444
|
}
|
|
2413
2445
|
|
|
2446
|
+
export function buildFunctionsHostKeyBicepExpression(): string {
|
|
2447
|
+
return "listKeys('${functionsFlex.outputs.id}/host/default', '2023-12-01').functionKeys.default";
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
export function buildStaticWebAppConfigBicepSource(): string {
|
|
2451
|
+
return `@description('Static Web App name')
|
|
2452
|
+
param staticWebAppName string
|
|
2453
|
+
|
|
2454
|
+
@description('Functions App default hostname for backend API calls')
|
|
2455
|
+
param functionsDefaultHostname string
|
|
2456
|
+
|
|
2457
|
+
@secure()
|
|
2458
|
+
@description('Functions host key used only by the server-side BFF')
|
|
2459
|
+
param functionsHostKey string
|
|
2460
|
+
|
|
2461
|
+
@description('Application Insights connection string for SWA')
|
|
2462
|
+
param appInsightsConnectionString string
|
|
2463
|
+
|
|
2464
|
+
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2465
|
+
name: staticWebAppName
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2469
|
+
parent: staticWebApp
|
|
2470
|
+
name: 'appsettings'
|
|
2471
|
+
properties: {
|
|
2472
|
+
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2473
|
+
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2474
|
+
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2475
|
+
BACKEND_FUNCTIONS_KEY: functionsHostKey
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
output configName string = staticWebAppConfig.name
|
|
2480
|
+
`;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2414
2483
|
async function createInfrastructure(
|
|
2415
2484
|
projectDir: string,
|
|
2416
2485
|
projectName: string,
|
|
@@ -2585,6 +2654,7 @@ module staticWebAppConfig 'modules/staticwebapp-config.bicep' = {
|
|
|
2585
2654
|
params: {
|
|
2586
2655
|
staticWebAppName: staticWebApp.outputs.name
|
|
2587
2656
|
functionsDefaultHostname: functionsFlex.outputs.defaultHostname
|
|
2657
|
+
functionsHostKey: ${buildFunctionsHostKeyBicepExpression()}
|
|
2588
2658
|
appInsightsConnectionString: appInsightsSwa.outputs.connectionString
|
|
2589
2659
|
}
|
|
2590
2660
|
dependsOn: [
|
|
@@ -2678,31 +2748,7 @@ output defaultHostname string = staticWebApp.properties.defaultHostname
|
|
|
2678
2748
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp.bicep'), staticWebAppBicep);
|
|
2679
2749
|
|
|
2680
2750
|
// modules/staticwebapp-config.bicep (for updating config after Functions deployment)
|
|
2681
|
-
const staticWebAppConfigBicep =
|
|
2682
|
-
param staticWebAppName string
|
|
2683
|
-
|
|
2684
|
-
@description('Functions App default hostname for backend API calls')
|
|
2685
|
-
param functionsDefaultHostname string
|
|
2686
|
-
|
|
2687
|
-
@description('Application Insights connection string for SWA')
|
|
2688
|
-
param appInsightsConnectionString string
|
|
2689
|
-
|
|
2690
|
-
resource staticWebApp 'Microsoft.Web/staticSites@2023-01-01' existing = {
|
|
2691
|
-
name: staticWebAppName
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
resource staticWebAppConfig 'Microsoft.Web/staticSites/config@2023-01-01' = {
|
|
2695
|
-
parent: staticWebApp
|
|
2696
|
-
name: 'appsettings'
|
|
2697
|
-
properties: {
|
|
2698
|
-
APPLICATIONINSIGHTS_CONNECTION_STRING: appInsightsConnectionString
|
|
2699
|
-
ApplicationInsightsAgent_EXTENSION_VERSION: '~3'
|
|
2700
|
-
BACKEND_FUNCTIONS_BASE_URL: 'https://\${functionsDefaultHostname}'
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
output configName string = staticWebAppConfig.name
|
|
2705
|
-
`;
|
|
2751
|
+
const staticWebAppConfigBicep = buildStaticWebAppConfigBicepSource();
|
|
2706
2752
|
fs.writeFileSync(path.join(modulesDir, 'staticwebapp-config.bicep'), staticWebAppConfigBicep);
|
|
2707
2753
|
|
|
2708
2754
|
// modules/loganalytics.bicep (Shared Log Analytics Workspace)
|
|
@@ -162,6 +162,9 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
162
162
|
? { authPolicy: uiAuthPolicy }
|
|
163
163
|
: undefined;
|
|
164
164
|
await generateUIComponents(modelInfo, sharedPackageName, uiAuthOptions);
|
|
165
|
+
if (uiAuthConfig?.provider === 'external-token') {
|
|
166
|
+
applyExternalTokenFetchToGeneratedUi(modelInfo);
|
|
167
|
+
}
|
|
165
168
|
await updateNavigationMenu(modelInfo);
|
|
166
169
|
}
|
|
167
170
|
|
|
@@ -190,6 +193,31 @@ export async function scaffoldCommand(options: ScaffoldOptions) {
|
|
|
190
193
|
}
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
function applyExternalTokenFetchToGeneratedUi(modelInfo: ModelInfo): void {
|
|
197
|
+
const uiRoot = path.join(process.cwd(), 'app', toKebabCase(modelInfo.name));
|
|
198
|
+
if (!fs.existsSync(uiRoot)) return;
|
|
199
|
+
const visit = (dir: string): void => {
|
|
200
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
201
|
+
const target = path.join(dir, entry.name);
|
|
202
|
+
if (entry.isDirectory()) { visit(target); continue; }
|
|
203
|
+
if (!entry.name.endsWith('.tsx')) continue;
|
|
204
|
+
let content = fs.readFileSync(target, 'utf-8');
|
|
205
|
+
if (!content.includes('fetch(')) continue;
|
|
206
|
+
content = content.replace(/\bfetch\(/g, 'authenticatedFetch(');
|
|
207
|
+
content = content.replace(
|
|
208
|
+
/(if \(!res\.ok\) throw new Error\([^\n]+\);)/g,
|
|
209
|
+
`if (res.status === 401) throw new Error('Authentication required');\n $1`
|
|
210
|
+
);
|
|
211
|
+
if (!content.includes("@/lib/auth/authenticated-fetch")) {
|
|
212
|
+
const directive = content.match(/^'use client';\s*\r?\n/)?.[0] ?? '';
|
|
213
|
+
content = `${directive}import { authenticatedFetch } from '@/lib/auth/authenticated-fetch';\n${content.slice(directive.length)}`;
|
|
214
|
+
}
|
|
215
|
+
fs.writeFileSync(target, content, 'utf-8');
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
visit(uiRoot);
|
|
219
|
+
}
|
|
220
|
+
|
|
193
221
|
/**
|
|
194
222
|
* モデルの authPolicy と auth config から実効的な認可ポリシーを解決
|
|
195
223
|
* - モデルに authPolicy があればそれを使用
|
|
@@ -305,8 +333,10 @@ async function generateCallFunctionHelper(): Promise<void> {
|
|
|
305
333
|
const hasAuth = authConfig && authConfig.provider !== 'none';
|
|
306
334
|
let helperCode: string;
|
|
307
335
|
if (hasAuth) {
|
|
308
|
-
const { generateBFFCallFunctionWithAuth } = await import("../../core/scaffold/auth-generator");
|
|
309
|
-
helperCode =
|
|
336
|
+
const { generateBFFCallFunctionWithAuth, generateBFFCallFunctionWithSwaAuth } = await import("../../core/scaffold/auth-generator");
|
|
337
|
+
helperCode = authConfig?.provider === "swa"
|
|
338
|
+
? generateBFFCallFunctionWithSwaAuth()
|
|
339
|
+
: generateBFFCallFunctionWithAuth();
|
|
310
340
|
} else {
|
|
311
341
|
helperCode = generateBFFCallFunction();
|
|
312
342
|
}
|
|
@@ -347,7 +377,7 @@ async function generateFunctionsCode(
|
|
|
347
377
|
fs.mkdirSync(functionDir, { recursive: true });
|
|
348
378
|
}
|
|
349
379
|
|
|
350
|
-
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy);
|
|
380
|
+
const code = generateCompactAzureFunctionsCRUD(modelInfo, sharedPackageName, authPolicy, authConfig?.provider);
|
|
351
381
|
fs.writeFileSync(functionFilePath, code, "utf-8");
|
|
352
382
|
console.log(`✅ Created: ${functionFilePath}`);
|
|
353
383
|
return;
|
|
@@ -425,14 +455,16 @@ async function generateConnectorFunctionsCode(
|
|
|
425
455
|
modelInfo, sharedPackageName,
|
|
426
456
|
connectorDef as RdbConnectorConfig,
|
|
427
457
|
connectorConfig as RdbModelConnectorConfig,
|
|
428
|
-
authPolicy
|
|
458
|
+
authPolicy,
|
|
459
|
+
authConfig?.provider
|
|
429
460
|
);
|
|
430
461
|
} else {
|
|
431
462
|
code = generateApiConnectorFunctionTS(
|
|
432
463
|
modelInfo, sharedPackageName,
|
|
433
464
|
connectorDef as ApiConnectorConfig,
|
|
434
465
|
connectorConfig as ApiModelConnectorConfig,
|
|
435
|
-
authPolicy
|
|
466
|
+
authPolicy,
|
|
467
|
+
authConfig?.provider
|
|
436
468
|
);
|
|
437
469
|
}
|
|
438
470
|
|
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ const DEV_OPTION_ARITY = new Map<string, 0 | 1>([
|
|
|
26
26
|
["--verbose", 0],
|
|
27
27
|
["--no-functions", 0],
|
|
28
28
|
["--mock-connectors", 0],
|
|
29
|
+
["--swa-port", 1],
|
|
30
|
+
["--no-swa", 0],
|
|
29
31
|
]);
|
|
30
32
|
|
|
31
33
|
function ensureUtf8ConsoleOnWindows(): void {
|
|
@@ -168,7 +170,7 @@ export function createProgram(devCommandOverride: Command = devCommand): Command
|
|
|
168
170
|
program
|
|
169
171
|
.command("add-auth")
|
|
170
172
|
.description("Add authentication and authorization to the project")
|
|
171
|
-
.option("--provider <provider>", "Auth provider: custom-jwt | swa | swa-custom | none", "custom-jwt")
|
|
173
|
+
.option("--provider <provider>", "Auth provider: custom-jwt | swa | external-token | swa-custom | none", "custom-jwt")
|
|
172
174
|
.action((options) => {
|
|
173
175
|
addAuthCommand({
|
|
174
176
|
provider: options.provider,
|