spiderly 19.12.2 → 19.12.4
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/package.json
CHANGED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: verify-ui
|
|
3
|
-
description: Log past Spiderly's email-code auth wall to visually verify the running Angular admin panel — screenshot a page, click through a flow, or dogfood a UI change without writing a test. Use when asked to "verify the admin UI", "check this page renders", "screenshot the admin panel", "does this screen look right", or to eyeball a change in the live app. For authoring Playwright test suites or debugging CI traces, use the e2e-testing skill instead.
|
|
4
|
-
allowed-tools: Bash(node:*), Bash(npx:*), Bash(agent-browser:*), Bash(curl:*)
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# Verify Spiderly Admin UI (past the auth wall)
|
|
8
|
-
|
|
9
|
-
The Spiderly admin panel sits entirely behind a passwordless **email-code** login, which blocks any agent that just opens `localhost:4200`. This skill gets you a logged-in session headlessly so you can look at the real UI.
|
|
10
|
-
|
|
11
|
-
**This is for ad-hoc visual verification, not test authoring.** If you're writing a Playwright suite or debugging a failing CI run, use the `e2e-testing` skill — it owns the canonical login helper and the PrimeNG selector quirks.
|
|
12
|
-
|
|
13
|
-
## Two auth flows — check which one first
|
|
14
|
-
|
|
15
|
-
- **Header flow** (`Login` + `RefreshTokenWithHeaders`): tokens in localStorage → recipe below.
|
|
16
|
-
- **Cookie flow** (`LoginWithCookies` + `RefreshTokenWithCookies`): the refresh token is an HttpOnly cookie — localStorage injection can't work. Symptom: bearer fetches return 200, yet every load bounces to `/login` with a 401 on `RefreshTokenWithCookies`. Log in **from inside the browser** with `credentials: 'include'` instead:
|
|
17
|
-
|
|
18
|
-
```js
|
|
19
|
-
// eval on the admin origin; email must have the Admin role
|
|
20
|
-
const browserId = 'verify-ui';
|
|
21
|
-
localStorage.setItem('browser_id', browserId); // must match the login below
|
|
22
|
-
const { verificationCode } = await (await fetch(API + '/api/Security/SendLoginVerificationEmail', {
|
|
23
|
-
method: 'POST', headers: {'Content-Type':'application/json'}, credentials: 'include',
|
|
24
|
-
body: JSON.stringify({ email, browserId }) })).json();
|
|
25
|
-
await fetch(API + '/api/Security/LoginWithCookies', { method: 'POST',
|
|
26
|
-
headers: {'Content-Type':'application/json'}, credentials: 'include',
|
|
27
|
-
body: JSON.stringify({ verificationCode, email, browserId }) });
|
|
28
|
-
// reload — boot's RefreshTokenWithCookies now finds the cookie
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## The core idea — header flow (driver-agnostic)
|
|
32
|
-
|
|
33
|
-
In development, `SecurityService.SendLoginVerificationEmail` returns the verification code **in the response body** (when `ShouldShowVerificationCodeInNotification()` is true — i.e. `IWebHostEnvironment.IsDevelopment()` **and** SMTP is not fully configured, so `emailingService.IsConfigured()` is false). So no email inbox is needed. The flow is always:
|
|
34
|
-
|
|
35
|
-
1. `POST /api/Security/SendLoginVerificationEmail` `{ email, browserId }` → read `verificationCode`.
|
|
36
|
-
2. `POST /api/Security/Login` `{ verificationCode, email, browserId }` → get `accessToken` + `refreshToken`.
|
|
37
|
-
3. In the browser, set three `localStorage` keys on the admin origin, then reload:
|
|
38
|
-
- `access_token`, `refresh_token`, `browser_id` (the last must equal the `browserId` used above).
|
|
39
|
-
4. Wait for `sidebar-menu` to appear — that confirms you're authenticated and in.
|
|
40
|
-
|
|
41
|
-
Step 1+2 are scripted for you: **`scripts/get-admin-token.mjs`** (pure Node, no dependencies, fails loudly).
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
node scripts/get-admin-token.mjs --email admin@example.com
|
|
45
|
-
# → { "accessToken": "...", "refreshToken": "...", "browserId": "verify-ui" }
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
Flags: `--email` (required — must have the **Admin role**, see gotcha below), `--api` (default `http://localhost:5000`), `--browser-id` (default `verify-ui`).
|
|
49
|
-
|
|
50
|
-
> **Don't trust the default ports — read them from the project.** The `:5000` (backend) and `:4200` (admin) values used throughout this skill are only the Spiderly scaffold defaults; any project can change them. The authoritative values live in **`Frontend/src/environments/environment.ts`**:
|
|
51
|
-
> - `apiUrl` (e.g. `http://localhost:5000/api`) → pass its **origin** (strip the trailing `/api`) to `--api`.
|
|
52
|
-
> - `frontendUrl` (e.g. `http://localhost:4200`) → the admin URL to open in the browser.
|
|
53
|
-
>
|
|
54
|
-
> The backend's actually-bound port is in **`Backend/<App>.WebAPI/Properties/launchSettings.json`** → `applicationUrl`. If a connection fails or the project looks non-standard, read those files first instead of assuming the defaults below.
|
|
55
|
-
|
|
56
|
-
> **CORS origin must match.** The backend allows one origin (`appsettings.json` → `Spiderly.Shared:FrontendUrl`). Serving the admin on a different port blocks every API call and surfaces as a misleading **"Connection Lost"** toast + bounce to `/login` (looks like an auth failure).
|
|
57
|
-
|
|
58
|
-
## Prerequisites — validate before doing anything
|
|
59
|
-
|
|
60
|
-
The token script already fails loudly on the first two. Confirm all four up front so you don't burn a turn on a half-running stack:
|
|
61
|
-
|
|
62
|
-
| Requirement | How to confirm | If it fails |
|
|
63
|
-
|---|---|---|
|
|
64
|
-
| Backend reachable | the script's first POST succeeds (port from `launchSettings.json` → `applicationUrl`) | start the backend (`dotnet run` in the WebAPI project) |
|
|
65
|
-
| Dev-mode code exposure on | response contains `verificationCode` | run backend with `ASPNETCORE_ENVIRONMENT=Development` **and** SMTP unconfigured (`IsConfigured()` false) — a fully-configured SMTP setup does a real send even in Development |
|
|
66
|
-
| Admin SPA running | `frontendUrl` (from `environment.ts`, default `:4200`) responds | start the admin (`npm start` in `Frontend/`) |
|
|
67
|
-
| Account has Admin role | login lands on a populated sidebar, not an empty shell | use an Admin-roled email (see gotcha) |
|
|
68
|
-
|
|
69
|
-
## Driver A — agent-browser (preferred when available)
|
|
70
|
-
|
|
71
|
-
[agent-browser](https://www.skills.sh/vercel-labs/agent-browser/agent-browser) gives an agent live, interactive control — ideal for clicking through a flow and screenshotting. If it isn't installed, install once (or skip to Driver B):
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
npx skills add https://github.com/vercel-labs/agent-browser --skill agent-browser
|
|
75
|
-
npm i -g agent-browser && agent-browser install # CLI + Chrome binary
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
Then log in and verify (replace `<AT>` / `<RT>` with the script's output, and `verify-ui` if you changed `--browser-id`):
|
|
79
|
-
|
|
80
|
-
```bash
|
|
81
|
-
# 1. Open the admin origin first so localStorage writes land on the right origin
|
|
82
|
-
agent-browser open http://localhost:4200
|
|
83
|
-
|
|
84
|
-
# 2. Inject the session (use --stdin so quoting can't corrupt the values)
|
|
85
|
-
agent-browser eval --stdin <<'EOF'
|
|
86
|
-
localStorage.setItem('access_token', '<AT>');
|
|
87
|
-
localStorage.setItem('refresh_token', '<RT>');
|
|
88
|
-
localStorage.setItem('browser_id', 'verify-ui');
|
|
89
|
-
EOF
|
|
90
|
-
|
|
91
|
-
# 3. Navigate straight to the page under review — this load reads the just-set
|
|
92
|
-
# localStorage; the sidebar-menu wait confirms auth (it renders on every
|
|
93
|
-
# authenticated route, so no separate dashboard round-trip is needed).
|
|
94
|
-
agent-browser open http://localhost:4200/administration/banner \
|
|
95
|
-
&& agent-browser wait "sidebar-menu" \
|
|
96
|
-
&& agent-browser wait --load networkidle \
|
|
97
|
-
&& agent-browser screenshot --full
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
From here use the normal agent-browser loop (`snapshot -i` → `click`/`fill` → re-snapshot) to drive a flow. Close the session when done: `agent-browser close`.
|
|
101
|
-
|
|
102
|
-
## Driver B — Playwright (guaranteed floor, zero extra install)
|
|
103
|
-
|
|
104
|
-
Every Spiderly app ships Playwright for e2e, so this always works without installing anything new. Run it **from the `Frontend/` directory** so `playwright` resolves. It reuses the same token script.
|
|
105
|
-
|
|
106
|
-
Save as `Frontend/verify-page.mjs` (gitignore it — it's a throwaway), then:
|
|
107
|
-
|
|
108
|
-
```bash
|
|
109
|
-
node verify-page.mjs --email admin@example.com \
|
|
110
|
-
--url http://localhost:4200/administration/banner \
|
|
111
|
-
--token-script <skill-dir>/scripts/get-admin-token.mjs --out ./_verify.png
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
It reuses the same token helper (`--token-script`, required — pass the absolute path to this skill's `scripts/get-admin-token.mjs`), so the localStorage key names and `sidebar-menu` wait stay defined in one place. Keep that injection block in sync with `e2e-testing`'s `authenticateBrowser` if either changes.
|
|
115
|
-
|
|
116
|
-
```js
|
|
117
|
-
import { chromium } from "playwright";
|
|
118
|
-
import { execFileSync } from "node:child_process";
|
|
119
|
-
|
|
120
|
-
function arg(name, fallback) {
|
|
121
|
-
const i = process.argv.indexOf(`--${name}`);
|
|
122
|
-
return i !== -1 ? process.argv[i + 1] : fallback;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const email = arg("email");
|
|
126
|
-
const url = arg("url", "http://localhost:4200");
|
|
127
|
-
const api = arg("api", "http://localhost:5000");
|
|
128
|
-
const out = arg("out", "./_verify.png");
|
|
129
|
-
const tokenScript = arg("token-script");
|
|
130
|
-
if (!email || !tokenScript) {
|
|
131
|
-
throw new Error("Pass --email <admin-roled account> and --token-script <path to get-admin-token.mjs>");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const { accessToken, refreshToken, browserId } = JSON.parse(
|
|
135
|
-
execFileSync("node", [tokenScript, "--email", email, "--api", api], { encoding: "utf8" })
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const browser = await chromium.launch();
|
|
139
|
-
try {
|
|
140
|
-
const page = await browser.newPage();
|
|
141
|
-
// Tokens must be set on the admin origin before the app reads them, so land there first.
|
|
142
|
-
await page.goto(new URL(url).origin);
|
|
143
|
-
await page.evaluate(
|
|
144
|
-
([at, rt, bid]) => {
|
|
145
|
-
localStorage.setItem("access_token", at);
|
|
146
|
-
localStorage.setItem("refresh_token", rt);
|
|
147
|
-
localStorage.setItem("browser_id", bid);
|
|
148
|
-
},
|
|
149
|
-
[accessToken, refreshToken, browserId]
|
|
150
|
-
);
|
|
151
|
-
await page.goto(url);
|
|
152
|
-
await page.locator("sidebar-menu").waitFor({ state: "visible", timeout: 15000 });
|
|
153
|
-
await page.screenshot({ path: out, fullPage: true });
|
|
154
|
-
console.log(`Saved ${out}`);
|
|
155
|
-
} finally {
|
|
156
|
-
await browser.close();
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Gotcha — login can succeed but show an empty shell
|
|
161
|
-
|
|
162
|
-
`SendLoginVerificationEmail` mints a session for any email when `OnlyAdminCanAddUsers` is `false` (the default) — a not-yet-existing user is **auto-provisioned with no role**, so the sidebar comes up empty and most pages are inaccessible. If you can log in but see nothing useful, the account lacks permissions. Use an email that already has the **Admin role** (role/permission assignment is seeded per-project, e.g. via a `PermissionSeeder`). Permissions hang off roles, not off the user directly.
|
|
163
|
-
|
|
164
|
-
## Why a separate skill from e2e-testing
|
|
165
|
-
|
|
166
|
-
Same login mechanism, different job: `e2e-testing` is for *authoring* Playwright suites and debugging CI traces; this is for *looking at* the running admin UI ad-hoc. Keeping them separate keeps each skill's trigger sharp. The login flow lives in both only as a thin reference — `e2e-testing` holds the canonical Playwright helper (`authenticateBrowser`); this skill holds the dependency-free token script and the agent-browser path.
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Obtain a Spiderly admin session token without an email inbox.
|
|
3
|
-
//
|
|
4
|
-
// In development, SecurityService.SendLoginVerificationEmail returns the
|
|
5
|
-
// verification code directly in the response body (when
|
|
6
|
-
// ShouldShowVerificationCodeInNotification() is true). This script uses that
|
|
7
|
-
// to complete the passwordless login flow headlessly and print the tokens an
|
|
8
|
-
// admin SPA expects in localStorage.
|
|
9
|
-
//
|
|
10
|
-
// The --api default (http://localhost:5000) is the Spiderly scaffold default only.
|
|
11
|
-
// The authoritative backend URL for a project is the origin of `apiUrl` in
|
|
12
|
-
// Frontend/src/environments/environment.ts (strip the trailing /api); the bound
|
|
13
|
-
// port is in Backend/<App>.WebAPI/Properties/launchSettings.json -> applicationUrl.
|
|
14
|
-
// Pass --api explicitly when a project overrides the default.
|
|
15
|
-
//
|
|
16
|
-
// Usage:
|
|
17
|
-
// node get-admin-token.mjs --email admin@example.com
|
|
18
|
-
// node get-admin-token.mjs --email admin@example.com --api http://localhost:5000 --browser-id verify-ui
|
|
19
|
-
//
|
|
20
|
-
// Output (stdout, JSON): { "accessToken": "...", "refreshToken": "...", "browserId": "..." }
|
|
21
|
-
// Exit code 0 on success, 1 on any failure (fails loudly — no partial success).
|
|
22
|
-
|
|
23
|
-
function parseArgs(argv) {
|
|
24
|
-
const args = {};
|
|
25
|
-
for (let i = 0; i < argv.length; i++) {
|
|
26
|
-
const token = argv[i];
|
|
27
|
-
if (!token.startsWith("--")) continue;
|
|
28
|
-
|
|
29
|
-
// Support both --flag=value and --flag value.
|
|
30
|
-
const eq = token.indexOf("=");
|
|
31
|
-
if (eq !== -1) {
|
|
32
|
-
args[token.slice(2, eq)] = token.slice(eq + 1);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const key = token.slice(2);
|
|
37
|
-
const next = argv[i + 1];
|
|
38
|
-
if (next !== undefined && !next.startsWith("--")) {
|
|
39
|
-
args[key] = next;
|
|
40
|
-
i++;
|
|
41
|
-
} else {
|
|
42
|
-
args[key] = true;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return args;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function fail(message) {
|
|
49
|
-
console.error(`[get-admin-token] ${message}`);
|
|
50
|
-
process.exit(1);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const args = parseArgs(process.argv.slice(2));
|
|
54
|
-
|
|
55
|
-
const email = args.email;
|
|
56
|
-
const apiBaseUrl = (args.api || "http://localhost:5000").replace(/\/+$/, "");
|
|
57
|
-
const browserId = args["browser-id"] || "verify-ui";
|
|
58
|
-
|
|
59
|
-
if (!email || email === true) {
|
|
60
|
-
fail(
|
|
61
|
-
"Missing required --email. Pass an account that has the Admin role in the target DB.\n" +
|
|
62
|
-
" Usage: node get-admin-token.mjs --email admin@example.com [--api http://localhost:5000] [--browser-id verify-ui]"
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function postJson(path, body) {
|
|
67
|
-
let response;
|
|
68
|
-
try {
|
|
69
|
-
response = await fetch(`${apiBaseUrl}${path}`, {
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: { "Content-Type": "application/json" },
|
|
72
|
-
body: JSON.stringify(body),
|
|
73
|
-
});
|
|
74
|
-
} catch (err) {
|
|
75
|
-
fail(
|
|
76
|
-
`Could not reach the backend at ${apiBaseUrl}${path}. Is the backend running?\n ${err.message}`
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const text = await response.text();
|
|
81
|
-
let json;
|
|
82
|
-
try {
|
|
83
|
-
json = text ? JSON.parse(text) : {};
|
|
84
|
-
} catch {
|
|
85
|
-
json = { raw: text };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (!response.ok) {
|
|
89
|
-
fail(`POST ${path} failed (HTTP ${response.status}): ${text || "(empty body)"}`);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return json;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const sendCodeResult = await postJson("/api/Security/SendLoginVerificationEmail", {
|
|
96
|
-
email,
|
|
97
|
-
browserId,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const verificationCode = sendCodeResult.verificationCode;
|
|
101
|
-
if (!verificationCode) {
|
|
102
|
-
fail(
|
|
103
|
-
"SendLoginVerificationEmail did not return a verificationCode.\n" +
|
|
104
|
-
" This means dev-mode code exposure is OFF (ShouldShowVerificationCodeInNotification() returned false).\n" +
|
|
105
|
-
" The gate is IsDevelopment() && !IsConfigured(): run the backend with ASPNETCORE_ENVIRONMENT=Development\n" +
|
|
106
|
-
" AND with SMTP left unconfigured. A fully-configured SMTP setup (EmailSender.Email + EmailSenderPassword\n" +
|
|
107
|
-
" + SmtpHost + SmtpPort) does a real email send instead, even in Development."
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const loginResult = await postJson("/api/Security/Login", {
|
|
112
|
-
verificationCode,
|
|
113
|
-
email,
|
|
114
|
-
browserId,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
if (!loginResult.accessToken || !loginResult.refreshToken) {
|
|
118
|
-
fail(
|
|
119
|
-
`Login did not return tokens. Response: ${JSON.stringify(loginResult)}\n` +
|
|
120
|
-
" The email may not exist, or the code expired. Check the account and retry."
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
process.stdout.write(
|
|
125
|
-
JSON.stringify(
|
|
126
|
-
{
|
|
127
|
-
accessToken: loginResult.accessToken,
|
|
128
|
-
refreshToken: loginResult.refreshToken,
|
|
129
|
-
browserId,
|
|
130
|
-
},
|
|
131
|
-
null,
|
|
132
|
-
2
|
|
133
|
-
) + "\n"
|
|
134
|
-
);
|