swallowkit 1.0.0-beta.46 → 1.0.0-beta.48
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 +3 -0
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +350 -17
- 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 +104 -13
- 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 +56 -30
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +9 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +84 -8
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +5 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +7 -2
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +81 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/mock/connector-mock-server.js +2 -2
- package/dist/core/mock/connector-mock-server.js.map +1 -1
- package/dist/core/project/manifest.js +2 -2
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +17 -0
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts +28 -3
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +493 -23
- 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 +23 -21
- 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 +20 -18
- package/dist/core/scaffold/functions-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +8 -3
- package/dist/core/scaffold/model-parser.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/core/scaffold/ui-generator.d.ts +3 -0
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +12 -8
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/types/index.d.ts +35 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +1 -0
- package/dist/utils/package-manager.js.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 +107 -0
- package/src/__tests__/config.test.ts +22 -1
- package/src/__tests__/dev.test.ts +27 -0
- package/src/__tests__/init.test.ts +19 -0
- package/src/__tests__/mcp.test.ts +2 -2
- package/src/__tests__/multi-auth.test.ts +133 -0
- package/src/__tests__/nextjs-generator.test.ts +8 -0
- package/src/cli/commands/add-auth.ts +351 -18
- package/src/cli/commands/dev.ts +116 -16
- package/src/cli/commands/init.ts +56 -30
- package/src/cli/commands/scaffold.ts +86 -10
- package/src/cli/index.ts +5 -1
- package/src/core/config.ts +69 -2
- package/src/core/mock/connector-mock-server.ts +2 -2
- package/src/core/project/manifest.ts +3 -3
- package/src/core/project/validation.ts +16 -0
- package/src/core/scaffold/auth-generator.ts +500 -24
- package/src/core/scaffold/connector-functions-generator.ts +26 -21
- package/src/core/scaffold/functions-generator.ts +21 -19
- package/src/core/scaffold/model-parser.ts +9 -3
- package/src/core/scaffold/nextjs-generator.ts +13 -1
- package/src/core/scaffold/ui-generator.ts +15 -8
- package/src/types/index.ts +36 -5
- package/src/utils/package-manager.ts +1 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
|
-
import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition,
|
|
8
|
+
import { ensureSwallowKitProject, getBackendLanguage, getConnectorDefinition, getValidatedFullConfig } from "../../core/config";
|
|
9
9
|
import { AuthProvider, BackendLanguage, CustomJwtConfig, RdbConnectorConfig } from "../../types";
|
|
10
10
|
import {
|
|
11
11
|
generateAuthModels,
|
|
@@ -22,12 +22,213 @@ import {
|
|
|
22
22
|
generateLoginPage,
|
|
23
23
|
generateAuthContext,
|
|
24
24
|
generateBFFCallFunctionWithAuth,
|
|
25
|
+
generateBFFCallFunctionWithSwaAuth,
|
|
26
|
+
generateBFFCallFunctionWithMultipleAuth,
|
|
27
|
+
generateSwaAuthHelperTS,
|
|
28
|
+
generateSwaAuthHelperCSharp,
|
|
29
|
+
generateSwaAuthHelperPython,
|
|
30
|
+
generateExternalTokenAdapter,
|
|
31
|
+
generateAuthenticatedFetch,
|
|
32
|
+
generateExternalTokenBFFMeRoute,
|
|
33
|
+
generateExternalTokenAuthContext,
|
|
34
|
+
generateExternalTokenVerifierTS,
|
|
35
|
+
generateExternalTokenHelperTS,
|
|
36
|
+
generateExternalAuthMeTS,
|
|
37
|
+
generateExternalTokenVerifierCSharp,
|
|
38
|
+
generateExternalTokenHelperCSharp,
|
|
39
|
+
generateExternalAuthMeCSharp,
|
|
40
|
+
generateExternalTokenVerifierPython,
|
|
41
|
+
generateExternalTokenHelperPython,
|
|
42
|
+
generateExternalAuthMePython,
|
|
43
|
+
generateNamedExternalTokenVerifierCSharp,
|
|
44
|
+
generateNamedExternalTokenVerifierPython,
|
|
25
45
|
} from "../../core/scaffold/auth-generator";
|
|
26
46
|
import { syncProjectManifest } from "../../core/project/manifest";
|
|
27
47
|
import { buildSharedTsConfig } from "./init";
|
|
28
48
|
|
|
29
49
|
interface AddAuthOptions {
|
|
30
50
|
provider?: string;
|
|
51
|
+
scheme?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function writeIfMissing(filePath: string, content: string): void {
|
|
55
|
+
if (fs.existsSync(filePath)) return;
|
|
56
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
57
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function defaultCustomJwtConfig(): CustomJwtConfig {
|
|
61
|
+
return {
|
|
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
|
+
|
|
72
|
+
function setupSwaAuth(cwd: string, backendLanguage: BackendLanguage): void {
|
|
73
|
+
const helperPath = backendLanguage === "typescript"
|
|
74
|
+
? path.join(cwd, "functions", "src", "auth", "jwt-helper.ts")
|
|
75
|
+
: backendLanguage === "csharp"
|
|
76
|
+
? path.join(cwd, "functions", "Auth", "JwtHelper.cs")
|
|
77
|
+
: path.join(cwd, "functions", "auth", "jwt_helper.py");
|
|
78
|
+
fs.mkdirSync(path.dirname(helperPath), { recursive: true });
|
|
79
|
+
const helper = backendLanguage === "typescript"
|
|
80
|
+
? generateSwaAuthHelperTS()
|
|
81
|
+
: backendLanguage === "csharp"
|
|
82
|
+
? generateSwaAuthHelperCSharp()
|
|
83
|
+
: generateSwaAuthHelperPython();
|
|
84
|
+
fs.writeFileSync(helperPath, helper, "utf-8");
|
|
85
|
+
|
|
86
|
+
const callFnPath = path.join(cwd, "lib", "api", "call-function.ts");
|
|
87
|
+
fs.mkdirSync(path.dirname(callFnPath), { recursive: true });
|
|
88
|
+
fs.writeFileSync(callFnPath, generateBFFCallFunctionWithSwaAuth(), "utf-8");
|
|
89
|
+
|
|
90
|
+
const authDir = path.join(cwd, "lib", "auth");
|
|
91
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
92
|
+
fs.writeFileSync(path.join(authDir, "auth-context.tsx"), generateSwaAuthContext(), "utf-8");
|
|
93
|
+
updateRootLayoutWithAuthProvider(cwd);
|
|
94
|
+
const loginDir = path.join(cwd, "app", "login");
|
|
95
|
+
fs.mkdirSync(loginDir, { recursive: true });
|
|
96
|
+
fs.writeFileSync(path.join(loginDir, "page.tsx"), generateSwaLoginPage(), "utf-8");
|
|
97
|
+
updateSwaRouteConfig(cwd);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function setupExternalTokenAuth(cwd: string, backendLanguage: BackendLanguage): void {
|
|
101
|
+
const authDir = path.join(cwd, "lib", "auth");
|
|
102
|
+
fs.mkdirSync(authDir, { recursive: true });
|
|
103
|
+
writeIfMissing(path.join(authDir, "external-token-adapter.ts"), generateExternalTokenAdapter());
|
|
104
|
+
fs.writeFileSync(path.join(authDir, "authenticated-fetch.ts"), generateAuthenticatedFetch(), "utf-8");
|
|
105
|
+
fs.writeFileSync(path.join(authDir, "auth-context.tsx"), generateExternalTokenAuthContext(), "utf-8");
|
|
106
|
+
fs.writeFileSync(path.join(cwd, "lib", "api", "call-function.ts"), generateBFFCallFunctionWithAuth(), "utf-8");
|
|
107
|
+
const meDir = path.join(cwd, "app", "api", "auth", "me");
|
|
108
|
+
fs.mkdirSync(meDir, { recursive: true });
|
|
109
|
+
fs.writeFileSync(path.join(meDir, "route.ts"), generateExternalTokenBFFMeRoute(), "utf-8");
|
|
110
|
+
updateRootLayoutWithAuthProvider(cwd);
|
|
111
|
+
|
|
112
|
+
if (backendLanguage === "typescript") {
|
|
113
|
+
const verifier = path.join(cwd, "functions", "src", "auth", "external-token-verifier.ts");
|
|
114
|
+
writeIfMissing(verifier, generateExternalTokenVerifierTS());
|
|
115
|
+
fs.writeFileSync(path.join(cwd, "functions", "src", "auth", "jwt-helper.ts"), generateExternalTokenHelperTS(), "utf-8");
|
|
116
|
+
fs.writeFileSync(path.join(cwd, "functions", "src", "auth-me.ts"), generateExternalAuthMeTS(), "utf-8");
|
|
117
|
+
} else if (backendLanguage === "csharp") {
|
|
118
|
+
const verifier = path.join(cwd, "functions", "Auth", "ExternalTokenVerifier.cs");
|
|
119
|
+
writeIfMissing(verifier, generateExternalTokenVerifierCSharp());
|
|
120
|
+
fs.writeFileSync(path.join(cwd, "functions", "Auth", "JwtHelper.cs"), generateExternalTokenHelperCSharp(), "utf-8");
|
|
121
|
+
fs.writeFileSync(path.join(cwd, "functions", "Auth", "ExternalAuthMe.cs"), generateExternalAuthMeCSharp(), "utf-8");
|
|
122
|
+
} else {
|
|
123
|
+
const verifier = path.join(cwd, "functions", "auth", "external_token_verifier.py");
|
|
124
|
+
writeIfMissing(verifier, generateExternalTokenVerifierPython());
|
|
125
|
+
fs.writeFileSync(path.join(cwd, "functions", "auth", "jwt_helper.py"), generateExternalTokenHelperPython(), "utf-8");
|
|
126
|
+
const authMePath = path.join(cwd, "functions", "blueprints", "auth_me.py");
|
|
127
|
+
fs.mkdirSync(path.dirname(authMePath), { recursive: true });
|
|
128
|
+
fs.writeFileSync(authMePath, generateExternalAuthMePython(), "utf-8");
|
|
129
|
+
updatePythonAuthMeRegistration(cwd);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function updatePythonAuthMeRegistration(cwd: string): void {
|
|
134
|
+
const appPath = path.join(cwd, "functions", "function_app.py");
|
|
135
|
+
if (!fs.existsSync(appPath)) return;
|
|
136
|
+
const importLine = "from blueprints.auth_me import bp as external_auth_me_bp";
|
|
137
|
+
const registerLine = "app.register_blueprint(external_auth_me_bp)";
|
|
138
|
+
let content = fs.readFileSync(appPath, "utf-8");
|
|
139
|
+
if (content.includes(importLine)) return;
|
|
140
|
+
const marker = "# SwallowKit scaffold registrations";
|
|
141
|
+
content = content.includes(marker)
|
|
142
|
+
? content.replace(marker, `${importLine}\n${registerLine}\n${marker}`)
|
|
143
|
+
: `${content.trimEnd()}\n${importLine}\n${registerLine}\n`;
|
|
144
|
+
fs.writeFileSync(appPath, content, "utf-8");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function generateSwaLoginPage(): string {
|
|
148
|
+
return `'use client';
|
|
149
|
+
export default function LoginPage() {
|
|
150
|
+
return <main><h1>Sign in</h1><a href="/.auth/login/aad?post_login_redirect_uri=/">Sign in with Microsoft</a></main>;
|
|
151
|
+
}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function generateSwaAuthContext(): string {
|
|
156
|
+
return `'use client';
|
|
157
|
+
import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
|
|
158
|
+
|
|
159
|
+
export interface AuthUser { id: string; loginId: string; name: string; email: string; roles: string[]; }
|
|
160
|
+
interface AuthContextValue { user: AuthUser | null; loading: boolean; login: () => void; logout: () => void; hasRole: (role: string) => boolean; hasAnyRole: (roles: string[]) => boolean; }
|
|
161
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
162
|
+
|
|
163
|
+
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
164
|
+
const [user, setUser] = useState<AuthUser | null>(null);
|
|
165
|
+
const [loading, setLoading] = useState(true);
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
fetch('/.auth/me')
|
|
168
|
+
.then(async response => {
|
|
169
|
+
if (!response.ok) throw new Error('SWA authentication is unavailable');
|
|
170
|
+
const body: unknown = await response.json();
|
|
171
|
+
if (!body || typeof body !== 'object' || !('clientPrincipal' in body)) return null;
|
|
172
|
+
const principal = body.clientPrincipal;
|
|
173
|
+
if (!principal || typeof principal !== 'object') return null;
|
|
174
|
+
const value = principal as Record<string, unknown>;
|
|
175
|
+
if (typeof value.userId !== 'string') return null;
|
|
176
|
+
return {
|
|
177
|
+
id: value.userId,
|
|
178
|
+
loginId: typeof value.userDetails === 'string' ? value.userDetails : '',
|
|
179
|
+
name: typeof value.userDetails === 'string' ? value.userDetails : '',
|
|
180
|
+
email: typeof value.userDetails === 'string' ? value.userDetails : '',
|
|
181
|
+
roles: Array.isArray(value.userRoles) ? value.userRoles.filter((role): role is string => typeof role === 'string') : [],
|
|
182
|
+
} satisfies AuthUser;
|
|
183
|
+
})
|
|
184
|
+
.then(setUser)
|
|
185
|
+
.catch(() => setUser(null))
|
|
186
|
+
.finally(() => setLoading(false));
|
|
187
|
+
}, []);
|
|
188
|
+
const login = () => { window.location.href = '/.auth/login/aad?post_login_redirect_uri=/'; };
|
|
189
|
+
const logout = () => { window.location.href = '/.auth/logout?post_logout_redirect_uri=/'; };
|
|
190
|
+
const hasRole = (role: string) => user?.roles.includes(role) ?? false;
|
|
191
|
+
return <AuthContext.Provider value={{ user, loading, login, logout, hasRole, hasAnyRole: roles => roles.some(hasRole) }}>{children}</AuthContext.Provider>;
|
|
192
|
+
}
|
|
193
|
+
export function useAuth() { const value = useContext(AuthContext); if (!value) throw new Error('useAuth must be used within an AuthProvider'); return value; }
|
|
194
|
+
`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function addAuthProviderToRootLayout(layout: string): string {
|
|
198
|
+
let updated = layout;
|
|
199
|
+
if (!/import\s*\{[^}]*\bAuthProvider\b[^}]*\}\s*from\s*['"]@\/lib\/auth\/auth-context['"]/.test(updated)) {
|
|
200
|
+
const directive = updated.match(/^(?:\s*['"]use (?:client|server)['"];?\s*\r?\n)/)?.[0] ?? "";
|
|
201
|
+
updated = `${directive}import { AuthProvider } from '@/lib/auth/auth-context';\n${updated.slice(directive.length)}`;
|
|
202
|
+
}
|
|
203
|
+
if (!/<AuthProvider(?:\s|>)/.test(updated)) {
|
|
204
|
+
const childrenIndex = updated.lastIndexOf("{children}");
|
|
205
|
+
if (childrenIndex === -1) {
|
|
206
|
+
throw new Error("Could not find {children} in app/layout.tsx");
|
|
207
|
+
}
|
|
208
|
+
updated = `${updated.slice(0, childrenIndex)}<AuthProvider>{children}</AuthProvider>${updated.slice(childrenIndex + "{children}".length)}`;
|
|
209
|
+
}
|
|
210
|
+
return updated;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function updateRootLayoutWithAuthProvider(cwd: string): void {
|
|
214
|
+
const layoutPath = path.join(cwd, "app", "layout.tsx");
|
|
215
|
+
if (!fs.existsSync(layoutPath)) {
|
|
216
|
+
throw new Error("app/layout.tsx was not found; cannot install AuthProvider");
|
|
217
|
+
}
|
|
218
|
+
const layout = fs.readFileSync(layoutPath, "utf-8");
|
|
219
|
+
const updated = addAuthProviderToRootLayout(layout);
|
|
220
|
+
if (updated !== layout) fs.writeFileSync(layoutPath, updated, "utf-8");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function updateSwaRouteConfig(cwd: string): void {
|
|
224
|
+
const configPath = path.join(cwd, "staticwebapp.config.json");
|
|
225
|
+
const config = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, "utf-8")) : {};
|
|
226
|
+
config.routes = Array.isArray(config.routes)
|
|
227
|
+
? config.routes.filter((route: { route?: string }) => route.route !== "/api/*")
|
|
228
|
+
: [];
|
|
229
|
+
config.routes.unshift({ route: "/api/*", allowedRoles: ["authenticated"] });
|
|
230
|
+
config.responseOverrides = { ...(config.responseOverrides || {}), "401": { statusCode: 302, redirect: "/.auth/login/aad?post_login_redirect_uri=.referrer" } };
|
|
231
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
31
232
|
}
|
|
32
233
|
|
|
33
234
|
export async function addAuthCommand(options: AddAuthOptions) {
|
|
@@ -36,15 +237,49 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
36
237
|
console.log(" SwallowKit Add-Auth: Setting up authentication...\n");
|
|
37
238
|
|
|
38
239
|
const provider = (options.provider || "custom-jwt") as AuthProvider;
|
|
39
|
-
if (!["custom-jwt", "swa", "swa-custom", "none"].includes(provider)) {
|
|
40
|
-
console.error(` Unknown provider: ${provider}. Use: custom-jwt | swa | swa-custom | none`);
|
|
240
|
+
if (!["custom-jwt", "swa", "external-token", "swa-custom", "none"].includes(provider)) {
|
|
241
|
+
console.error(` Unknown provider: ${provider}. Use: custom-jwt | swa | external-token | swa-custom | none`);
|
|
41
242
|
process.exit(1);
|
|
42
243
|
}
|
|
43
244
|
|
|
44
245
|
const backendLanguage = getBackendLanguage();
|
|
45
|
-
const config =
|
|
246
|
+
const config = getValidatedFullConfig();
|
|
46
247
|
const cwd = process.cwd();
|
|
47
248
|
|
|
249
|
+
if (options.scheme) {
|
|
250
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(options.scheme)) throw new Error("--scheme must start with a letter and contain only letters, digits, '_' or '-'");
|
|
251
|
+
if (provider === "none" || provider === "swa-custom") throw new Error(`Provider '${provider}' cannot be added as a named scheme`);
|
|
252
|
+
addNamedScheme(cwd, options.scheme, provider, backendLanguage);
|
|
253
|
+
await syncProjectManifest();
|
|
254
|
+
console.log(`\n Named authentication scheme '${options.scheme}' added.`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (provider === "swa-custom") {
|
|
259
|
+
throw new Error("The swa-custom provider is not implemented. Use --provider swa or --provider custom-jwt.");
|
|
260
|
+
}
|
|
261
|
+
if (provider === "none") {
|
|
262
|
+
updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
|
|
263
|
+
await syncProjectManifest();
|
|
264
|
+
console.log(" Authentication disabled in swallowkit.config.js");
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (provider === "swa") {
|
|
268
|
+
setupSwaAuth(cwd, backendLanguage);
|
|
269
|
+
updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
|
|
270
|
+
await syncProjectManifest();
|
|
271
|
+
console.log("\n SWA built-in authentication setup complete!");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (provider === "external-token") {
|
|
275
|
+
setupExternalTokenAuth(cwd, backendLanguage);
|
|
276
|
+
updateConfigWithAuth(cwd, provider, config.auth?.customJwt || defaultCustomJwtConfig());
|
|
277
|
+
await syncProjectManifest();
|
|
278
|
+
console.log("\n External token authentication setup complete!");
|
|
279
|
+
console.log(" Implement the generated frontend adapter and backend verifier before use.");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
48
283
|
// Read project name from package.json
|
|
49
284
|
const pkgPath = path.join(cwd, "package.json");
|
|
50
285
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
@@ -59,15 +294,9 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
59
294
|
}
|
|
60
295
|
|
|
61
296
|
// Default custom-jwt config
|
|
62
|
-
const customJwtConfig: CustomJwtConfig = config.auth?.customJwt ||
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
loginIdColumn: "login_id",
|
|
66
|
-
passwordHashColumn: "password_hash",
|
|
67
|
-
rolesColumn: "roles",
|
|
68
|
-
jwtSecretEnv: "JWT_SECRET",
|
|
69
|
-
tokenExpiry: "24h",
|
|
70
|
-
};
|
|
297
|
+
const customJwtConfig: CustomJwtConfig = config.auth?.customJwt || defaultCustomJwtConfig();
|
|
298
|
+
|
|
299
|
+
// custom-jwt setup continues below.
|
|
71
300
|
|
|
72
301
|
// 1. Generate shared/models/auth.ts
|
|
73
302
|
console.log(" Generating auth models...");
|
|
@@ -148,6 +377,103 @@ export async function addAuthCommand(options: AddAuthOptions) {
|
|
|
148
377
|
console.log(` 5. Run scaffold to regenerate functions with auth guards`);
|
|
149
378
|
}
|
|
150
379
|
|
|
380
|
+
function findObjectEnd(content: string, open: number): number {
|
|
381
|
+
let depth = 0, quote = "";
|
|
382
|
+
for (let i = open; i < content.length; i++) {
|
|
383
|
+
const ch = content[i];
|
|
384
|
+
if (quote) { if (ch === quote && content[i - 1] !== "\\") quote = ""; continue; }
|
|
385
|
+
if (ch === "'" || ch === '"' || ch === "`") { quote = ch; continue; }
|
|
386
|
+
if (ch === "{") depth++;
|
|
387
|
+
if (ch === "}" && --depth === 0) return i;
|
|
388
|
+
}
|
|
389
|
+
return -1;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function updateConfigWithNamedScheme(cwd: string, scheme: string, provider: AuthProvider): void {
|
|
393
|
+
const jsPath = path.join(cwd, "swallowkit.config.js");
|
|
394
|
+
const jsonPath = path.join(cwd, "swallowkit.config.json");
|
|
395
|
+
const configPath = fs.existsSync(jsPath) ? jsPath : jsonPath;
|
|
396
|
+
if (!fs.existsSync(configPath)) throw new Error("swallowkit.config.js or swallowkit.config.json is required");
|
|
397
|
+
if (configPath.endsWith(".json")) {
|
|
398
|
+
const value = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
399
|
+
value.auth ??= {};
|
|
400
|
+
value.auth.schemes ??= {};
|
|
401
|
+
if (value.auth.schemes[scheme]) throw new Error(`auth.schemes.${scheme} already exists; no files were changed`);
|
|
402
|
+
value.auth.schemes[scheme] = provider === "custom-jwt" ? { provider, customJwt: defaultCustomJwtConfig() } : { provider };
|
|
403
|
+
value.auth.authorization ??= { defaultPolicy: "anonymous", policies: {} };
|
|
404
|
+
fs.writeFileSync(configPath, JSON.stringify(value, null, 2) + "\n", "utf-8");
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
let content = fs.readFileSync(configPath, "utf-8");
|
|
408
|
+
const authMatch = /\bauth\s*:\s*\{/.exec(content);
|
|
409
|
+
const customJwt = defaultCustomJwtConfig();
|
|
410
|
+
const entry = provider === "custom-jwt"
|
|
411
|
+
? `\n ${scheme}: { provider: 'custom-jwt', customJwt: ${JSON.stringify(customJwt)} },`
|
|
412
|
+
: `\n ${scheme}: { provider: '${provider}' },`;
|
|
413
|
+
if (!authMatch) {
|
|
414
|
+
const rootEnd = content.lastIndexOf("}");
|
|
415
|
+
const beforeRootEnd = content.slice(0, rootEnd).trimEnd();
|
|
416
|
+
const separator = beforeRootEnd.endsWith(",") || beforeRootEnd.endsWith("{") ? "" : ",";
|
|
417
|
+
content = `${beforeRootEnd}${separator}\n auth: {\n schemes: {${entry}\n },\n authorization: { defaultPolicy: 'anonymous', policies: {} },\n },\n${content.slice(rootEnd)}`;
|
|
418
|
+
} else {
|
|
419
|
+
const authOpen = content.indexOf("{", authMatch.index);
|
|
420
|
+
const authEnd = findObjectEnd(content, authOpen);
|
|
421
|
+
const authBody = content.slice(authOpen + 1, authEnd);
|
|
422
|
+
const schemesMatch = /\bschemes\s*:\s*\{/.exec(authBody);
|
|
423
|
+
if (schemesMatch) {
|
|
424
|
+
const schemesOpen = authOpen + 1 + authBody.indexOf("{", schemesMatch.index);
|
|
425
|
+
const schemesEnd = findObjectEnd(content, schemesOpen);
|
|
426
|
+
const existing = content.slice(schemesOpen + 1, schemesEnd);
|
|
427
|
+
if (new RegExp(`(^|[,\\s])${scheme}\\s*:`).test(existing)) throw new Error(`auth.schemes.${scheme} already exists; no files were changed`);
|
|
428
|
+
content = content.slice(0, schemesEnd) + entry + "\n " + content.slice(schemesEnd);
|
|
429
|
+
} else {
|
|
430
|
+
content = content.slice(0, authOpen + 1) + `\n schemes: {${entry}\n },` + content.slice(authOpen + 1);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
fs.writeFileSync(configPath, content, "utf-8");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function addNamedScheme(cwd: string, scheme: string, provider: AuthProvider, language: BackendLanguage): void {
|
|
437
|
+
// Validate/update config before creating files, so duplicate names stop safely.
|
|
438
|
+
updateConfigWithNamedScheme(cwd, scheme, provider);
|
|
439
|
+
const slug = scheme.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
|
|
440
|
+
if (language === "typescript") {
|
|
441
|
+
const dir = path.join(cwd, "functions", "src", "auth", "schemes", slug);
|
|
442
|
+
if (provider === "external-token") writeIfMissing(path.join(dir, "verifier.ts"), generateExternalTokenVerifierTS());
|
|
443
|
+
else if (provider === "swa") writeIfMissing(path.join(dir, "adapter.ts"), generateSwaAuthHelperTS());
|
|
444
|
+
else writeIfMissing(path.join(dir, "jwt-helper.ts"), generateJwtHelperTS());
|
|
445
|
+
} else if (language === "csharp") {
|
|
446
|
+
const dir = path.join(cwd, "functions", "Auth", "Schemes", slug);
|
|
447
|
+
if (provider === "external-token") writeIfMissing(path.join(dir, "ExternalTokenVerifier.cs"), generateNamedExternalTokenVerifierCSharp(scheme));
|
|
448
|
+
else if (provider === "swa") writeIfMissing(path.join(dir, "SwaAdapter.cs"), generateSwaAuthHelperCSharp());
|
|
449
|
+
else writeIfMissing(path.join(dir, "JwtHelper.cs"), generateJwtHelperCSharp());
|
|
450
|
+
} else {
|
|
451
|
+
const dir = path.join(cwd, "functions", "auth", "schemes", slug.replace(/-/g, "_"));
|
|
452
|
+
if (provider === "external-token") writeIfMissing(path.join(dir, "verifier.py"), generateNamedExternalTokenVerifierPython(scheme));
|
|
453
|
+
else if (provider === "swa") writeIfMissing(path.join(dir, "adapter.py"), generateSwaAuthHelperPython());
|
|
454
|
+
else writeIfMissing(path.join(dir, "jwt_helper.py"), generateJwtHelperPython());
|
|
455
|
+
}
|
|
456
|
+
const clientDir = path.join(cwd, "lib", "auth", "schemes", slug);
|
|
457
|
+
if (provider === "external-token") {
|
|
458
|
+
writeIfMissing(path.join(clientDir, "token-adapter.ts"), generateExternalTokenAdapter());
|
|
459
|
+
writeIfMissing(path.join(clientDir, "authenticated-fetch.ts"), generateAuthenticatedFetch().replace("'./external-token-adapter'", "'./token-adapter'"));
|
|
460
|
+
writeIfMissing(path.join(clientDir, "auth-context.tsx"), generateExternalTokenAuthContext().replace(/external-token-adapter/g, "token-adapter"));
|
|
461
|
+
} else if (provider === "swa") {
|
|
462
|
+
writeIfMissing(path.join(clientDir, "auth-context.tsx"), generateSwaAuthContext());
|
|
463
|
+
} else if (provider === "custom-jwt") {
|
|
464
|
+
writeIfMissing(path.join(clientDir, "auth-context.tsx"), generateAuthContext());
|
|
465
|
+
}
|
|
466
|
+
const callFunctionPath = path.join(cwd, "lib", "api", "call-function.ts");
|
|
467
|
+
if (!fs.existsSync(callFunctionPath)) writeIfMissing(callFunctionPath, generateBFFCallFunctionWithMultipleAuth());
|
|
468
|
+
else {
|
|
469
|
+
const current = fs.readFileSync(callFunctionPath, "utf-8");
|
|
470
|
+
if (!current.includes("x-ms-client-principal") || !current.includes("fetchHeaders['Authorization']")) {
|
|
471
|
+
if (current.includes("SwallowKit BFF Call Function Helper")) fs.writeFileSync(callFunctionPath, generateBFFCallFunctionWithMultipleAuth(), "utf-8");
|
|
472
|
+
else console.warn(" Existing customized lib/api/call-function.ts was preserved. Ensure it forwards Bearer and SWA credentials without logging them.");
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
151
477
|
/**
|
|
152
478
|
* Ensure the shared package has proper build infrastructure
|
|
153
479
|
* (tsconfig.json, build script, typescript devDependency).
|
|
@@ -359,10 +685,7 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
|
|
|
359
685
|
const beforeClosing = content.substring(0, closingBraceIdx).trimEnd();
|
|
360
686
|
const needsComma = !beforeClosing.endsWith(",") && !beforeClosing.endsWith("{");
|
|
361
687
|
|
|
362
|
-
const
|
|
363
|
-
// 認証認可設定
|
|
364
|
-
auth: {
|
|
365
|
-
provider: '${provider}',
|
|
688
|
+
const providerSettings = provider === "custom-jwt" ? `
|
|
366
689
|
customJwt: {
|
|
367
690
|
userConnector: '${config.userConnector}',
|
|
368
691
|
userTable: '${config.userTable}',
|
|
@@ -371,7 +694,17 @@ function updateConfigWithAuth(cwd: string, provider: AuthProvider, config: Custo
|
|
|
371
694
|
rolesColumn: '${config.rolesColumn}',
|
|
372
695
|
jwtSecretEnv: '${config.jwtSecretEnv || "JWT_SECRET"}',
|
|
373
696
|
tokenExpiry: '${config.tokenExpiry || "24h"}',
|
|
374
|
-
}
|
|
697
|
+
},` : provider === "swa" ? `
|
|
698
|
+
swa: {
|
|
699
|
+
allowedProviders: ['aad'],
|
|
700
|
+
roleSource: 'swa-roles',
|
|
701
|
+
},` : "";
|
|
702
|
+
|
|
703
|
+
const authBlock = `${needsComma ? "," : ""}
|
|
704
|
+
// 認証認可設定
|
|
705
|
+
auth: {
|
|
706
|
+
provider: '${provider}',
|
|
707
|
+
${providerSettings}
|
|
375
708
|
authorization: {
|
|
376
709
|
defaultPolicy: 'authenticated',
|
|
377
710
|
},
|
package/src/cli/commands/dev.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as fs from 'fs';
|
|
|
7
7
|
import * as os from 'os';
|
|
8
8
|
import * as net from 'net';
|
|
9
9
|
import { CosmosClient, PartitionKeyKind } from '@azure/cosmos';
|
|
10
|
-
import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig } from '../../core/config';
|
|
10
|
+
import { ensureSwallowKitProject, getBackendLanguage, getAuthConfig, normalizeAuthConfig } from '../../core/config';
|
|
11
11
|
import { ModelInfo } from '../../core/scaffold/model-parser';
|
|
12
12
|
import {
|
|
13
13
|
applyDevSeedEnvironment,
|
|
@@ -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,25 @@ 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 normalizedAuth = normalizeAuthConfig(authConfig);
|
|
652
|
+
const useSwaEmulator = Boolean(normalizedAuth && Object.values(normalizedAuth.schemes).some(scheme => scheme.provider === 'swa')) && !options.noSwa;
|
|
653
|
+
const swaCommand = useSwaEmulator ? await resolveSwaCliCommand(process.cwd()) : null;
|
|
654
|
+
|
|
655
|
+
if (useSwaEmulator && !swaCommand) {
|
|
656
|
+
const installCommand = getSwaCliInstallCommand(pm);
|
|
657
|
+
console.error('❌ SWA authentication requires Azure Static Web Apps CLI for local development.');
|
|
658
|
+
console.error(' Install it in this project and retry:');
|
|
659
|
+
console.error(`\n ${installCommand}\n`);
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
626
663
|
|
|
627
664
|
// プロセスを管理する配列
|
|
628
665
|
const processes: ChildProcess[] = [];
|
|
@@ -925,7 +962,6 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
925
962
|
const connectorModels = allModels.filter((m) => m.connectorConfig);
|
|
926
963
|
|
|
927
964
|
// Resolve auth config — auth functions use RDB connectors, mocked alongside other models
|
|
928
|
-
const authConfig = getAuthConfig();
|
|
929
965
|
let mockAuthConfig: { jwtSecret: string; tokenExpiry?: string; customJwt?: { userTable: string; loginIdColumn: string; passwordHashColumn: string; rolesColumn: string }; defaultPolicy?: "authenticated" | "anonymous" } | undefined;
|
|
930
966
|
if (authConfig?.provider === 'custom-jwt' && authConfig.customJwt) {
|
|
931
967
|
// Read JWT_SECRET from functions/local.settings.json if available
|
|
@@ -946,7 +982,9 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
946
982
|
passwordHashColumn: authConfig.customJwt.passwordHashColumn,
|
|
947
983
|
rolesColumn: authConfig.customJwt.rolesColumn,
|
|
948
984
|
},
|
|
949
|
-
defaultPolicy: authConfig.authorization?.defaultPolicy
|
|
985
|
+
defaultPolicy: authConfig.authorization?.defaultPolicy === "public" ? "anonymous" :
|
|
986
|
+
authConfig.authorization?.defaultPolicy === "authenticated" || authConfig.authorization?.defaultPolicy === "anonymous"
|
|
987
|
+
? authConfig.authorization.defaultPolicy : undefined,
|
|
950
988
|
};
|
|
951
989
|
}
|
|
952
990
|
|
|
@@ -985,18 +1023,6 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
985
1023
|
// 5. Start Next.js development server
|
|
986
1024
|
const nextArgs = buildNextDevArgs(pm, port);
|
|
987
1025
|
|
|
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
1026
|
// Ensure .env.local points to bffTargetPort so Next.js reads the correct backend URL.
|
|
1001
1027
|
// When --mock-connectors is active, bffTargetPort = mock port (7072); otherwise = Functions port (7071).
|
|
1002
1028
|
// Next.js may load .env.local values that override spawn env vars, so we must keep them in sync.
|
|
@@ -1063,10 +1089,52 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1063
1089
|
}
|
|
1064
1090
|
}
|
|
1065
1091
|
|
|
1092
|
+
let swaReady = false;
|
|
1093
|
+
if (useSwaEmulator && swaCommand) {
|
|
1094
|
+
const nextUrl = `http://${options.host || 'localhost'}:${port}`;
|
|
1095
|
+
console.log(`⏳ Waiting for Next.js before starting the SWA emulator at ${nextUrl}...`);
|
|
1096
|
+
const nextReady = await waitForHttpServerReady(nextUrl);
|
|
1097
|
+
if (!nextReady) {
|
|
1098
|
+
throw new Error(`Next.js did not become ready at ${nextUrl}`);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
console.log(`🔐 Starting SWA authentication emulator (port: ${swaPort})...`);
|
|
1102
|
+
const swaProcess = spawn(swaCommand, buildSwaStartArgs(options.host, port, swaPort), {
|
|
1103
|
+
cwd: process.cwd(),
|
|
1104
|
+
shell: true,
|
|
1105
|
+
stdio: 'inherit',
|
|
1106
|
+
env: process.env,
|
|
1107
|
+
});
|
|
1108
|
+
processes.push(swaProcess);
|
|
1109
|
+
swaProcess.on('error', (error) => {
|
|
1110
|
+
console.error('❌ SWA CLI startup error:', error.message);
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1114
|
+
swaReady = await waitForHttpServerReady(swaUrl);
|
|
1115
|
+
if (!swaReady) {
|
|
1116
|
+
throw new Error(`SWA authentication emulator did not become ready at ${swaUrl}`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1066
1120
|
console.log('');
|
|
1067
1121
|
console.log('✅ SwallowKit development environment is running!');
|
|
1068
1122
|
console.log('');
|
|
1069
|
-
|
|
1123
|
+
if (useSwaEmulator) {
|
|
1124
|
+
const swaUrl = `http://${options.host || 'localhost'}:${swaPort}`;
|
|
1125
|
+
console.log('🔐 SWA authenticated app:');
|
|
1126
|
+
console.log(` ${swaUrl}`);
|
|
1127
|
+
console.log('');
|
|
1128
|
+
console.log('🔑 Local sign-in:');
|
|
1129
|
+
console.log(` ${swaUrl}/.auth/login/aad`);
|
|
1130
|
+
console.log(' Enter any username and select Login.');
|
|
1131
|
+
console.log(' The "authenticated" role is added automatically.');
|
|
1132
|
+
console.log('');
|
|
1133
|
+
console.log('📱 Next.js direct access (authentication is not emulated):');
|
|
1134
|
+
console.log(` http://${options.host || 'localhost'}:${port}`);
|
|
1135
|
+
} else {
|
|
1136
|
+
console.log(`📱 Next.js: http://${options.host || 'localhost'}:${port}`);
|
|
1137
|
+
}
|
|
1070
1138
|
if (hasFunctions && !options.noFunctions) {
|
|
1071
1139
|
console.log(`${functionsReady ? '⚡ Azure Functions' : '⏳ Azure Functions (starting)'}: ${functionsBaseUrl}`);
|
|
1072
1140
|
}
|
|
@@ -1082,7 +1150,22 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1082
1150
|
if (mockServer) {
|
|
1083
1151
|
console.log('💡 Connector models served from mock server (Zod-generated data)');
|
|
1084
1152
|
}
|
|
1153
|
+
if (swaReady) {
|
|
1154
|
+
console.log('💡 Open the app through the SWA port when testing authenticated pages and CRUD APIs.');
|
|
1155
|
+
}
|
|
1085
1156
|
console.log('');
|
|
1157
|
+
|
|
1158
|
+
if (options.open) {
|
|
1159
|
+
const url = useSwaEmulator
|
|
1160
|
+
? `http://${options.host || 'localhost'}:${swaPort}`
|
|
1161
|
+
: `http://${options.host || 'localhost'}:${port}`;
|
|
1162
|
+
console.log(`🌐 Opening browser: ${url}`);
|
|
1163
|
+
const start = process.platform === 'darwin' ? 'open' :
|
|
1164
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
1165
|
+
spawn(start, [url], { shell: true });
|
|
1166
|
+
console.log('');
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1086
1169
|
console.log('🛑 Press Ctrl+C to stop');
|
|
1087
1170
|
console.log('');
|
|
1088
1171
|
|
|
@@ -1097,6 +1180,23 @@ async function startDevEnvironment(options: DevOptions) {
|
|
|
1097
1180
|
}
|
|
1098
1181
|
}
|
|
1099
1182
|
|
|
1183
|
+
async function resolveSwaCliCommand(projectRoot: string): Promise<string | null> {
|
|
1184
|
+
const binaryName = process.platform === 'win32' ? 'swa.cmd' : 'swa';
|
|
1185
|
+
const localCommand = path.join(projectRoot, 'node_modules', '.bin', binaryName);
|
|
1186
|
+
if (fs.existsSync(localCommand) && await checkCommand(localCommand, ['--version'], {
|
|
1187
|
+
cwd: projectRoot,
|
|
1188
|
+
shell: process.platform === 'win32',
|
|
1189
|
+
})) {
|
|
1190
|
+
return localCommand;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (await checkCommand('swa', ['--version'], { cwd: projectRoot })) {
|
|
1194
|
+
return 'swa';
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return null;
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1100
1200
|
async function runCommand(
|
|
1101
1201
|
command: string,
|
|
1102
1202
|
args: string[],
|