|
| 1 | +--- |
| 2 | +phase: 17-account-storage-foundation |
| 3 | +plan: 01 |
| 4 | +type: execute |
| 5 | +wave: 1 |
| 6 | +depends_on: [] |
| 7 | +files_modified: |
| 8 | + - src/Ray.BiliBiliTool.Web/Program.cs |
| 9 | + - src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/IBiliAccountPageWorkflow.cs |
| 10 | + - src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountPageWorkflow.cs |
| 11 | + - src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountDto.cs |
| 12 | + - src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs |
| 13 | + - src/Ray.BiliBiliTool.Web/Components/Pages/BiliAccount.razor |
| 14 | + - src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor |
| 15 | +autonomous: true |
| 16 | +requirements: |
| 17 | + - ACCT-07 |
| 18 | + - ACCT-01 |
| 19 | + |
| 20 | +must_haves: |
| 21 | + truths: |
| 22 | + - "Web host no longer loads config/cookies.json — SQLite bili_appsettings is the sole cookie config source for Web" |
| 23 | + - "Maintainer sees a 'Bili Account' top-level nav item in the sidebar" |
| 24 | + - "Maintainer sees all configured Bili accounts listed with UserId and full cookie string" |
| 25 | + - "Account list reflects the current state in SQLite bili_appsettings" |
| 26 | + artifacts: |
| 27 | + - path: "src/Ray.BiliBiliTool.Web/Program.cs" |
| 28 | + provides: "cookies.json config source removed" |
| 29 | + contains: "AddSqlite" |
| 30 | + - path: "src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/IBiliAccountPageWorkflow.cs" |
| 31 | + provides: "Workflow seam contract for Bili Account page" |
| 32 | + exports: ["IBiliAccountPageWorkflow"] |
| 33 | + - path: "src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountPageWorkflow.cs" |
| 34 | + provides: "Workflow implementation reading accounts from IConfiguration" |
| 35 | + - path: "src/Ray.BiliBiliTool.Web/Components/Pages/BiliAccount.razor" |
| 36 | + provides: "Account list page with MudTable" |
| 37 | + - path: "src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor" |
| 38 | + provides: "Bili Account nav entry" |
| 39 | + key_links: |
| 40 | + - from: "src/Ray.BiliBiliTool.Web/Components/Pages/BiliAccount.razor" |
| 41 | + to: "IBiliAccountPageWorkflow" |
| 42 | + via: "Inject and call GetAllAccountsAsync on init" |
| 43 | + pattern: "IBiliAccountPageWorkflow" |
| 44 | + - from: "src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountPageWorkflow.cs" |
| 45 | + to: "IConfiguration" |
| 46 | + via: "Read BiliBiliCookies section" |
| 47 | + pattern: "GetSection.*BiliBiliCookies" |
| 48 | + - from: "src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs" |
| 49 | + to: "IBiliAccountPageWorkflow" |
| 50 | + via: "DI registration" |
| 51 | + pattern: "IBiliAccountPageWorkflow" |
| 52 | +--- |
| 53 | + |
| 54 | +<objective> |
| 55 | +Remove `cookies.json` from the Web host configuration pipeline and create the Bili Account page with a read-only account list view. |
| 56 | + |
| 57 | +Purpose: Establishes SQLite as the sole cookie config source for Web (ACCT-07) and gives the maintainer visibility into configured accounts (ACCT-01). This is the foundation for CRUD operations in Phase 18. |
| 58 | + |
| 59 | +Output: Modified Program.cs (cookies.json removed), new workflow seam (`IBiliAccountPageWorkflow`), new Bili Account page with MudTable listing all accounts, NavMenu entry. |
| 60 | +</objective> |
| 61 | + |
| 62 | +<execution_context> |
| 63 | +@~/.copilot/get-shit-done/workflows/execute-plan.md |
| 64 | +@~/.copilot/get-shit-done/templates/summary.md |
| 65 | +</execution_context> |
| 66 | + |
| 67 | +<context> |
| 68 | +@.planning/PROJECT.md |
| 69 | +@.planning/ROADMAP.md |
| 70 | +@.planning/STATE.md |
| 71 | +@.planning/REQUIREMENTS.md |
| 72 | +@.planning/research/SUMMARY.md |
| 73 | + |
| 74 | +<interfaces> |
| 75 | +<!-- Key types and contracts the executor needs. --> |
| 76 | + |
| 77 | +From src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieStrFactory.cs: |
| 78 | +```csharp |
| 79 | +// Reads BiliBiliCookies__0, BiliBiliCookies__1, etc. from IConfiguration |
| 80 | +// configuration.GetSection("BiliBiliCookies").Get<List<string>>() returns the cookie strings |
| 81 | +public class CookieStrFactory<TCookieInfo>(IConfiguration configuration) where TCookieInfo : CookieInfo |
| 82 | +{ |
| 83 | + public int Count => CookieDictionary.Count; |
| 84 | + public TCookieInfo GetCookie(int index); |
| 85 | +} |
| 86 | +``` |
| 87 | + |
| 88 | +From src/Ray.BiliBiliTool.Agent/BiliCookie.cs: |
| 89 | +```csharp |
| 90 | +public class BiliCookie(Dictionary<string, string> cookieDic) : CookieInfo(cookieDic) |
| 91 | +{ |
| 92 | + public string UserId => // reads "DedeUserID" from cookie dict |
| 93 | + public string SessData => // reads "SESSDATA" |
| 94 | + public string BiliJct => // reads "bili_jct" |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +From src/Ray.BiliBiliTool.Infrastructure/Cookie/CookieInfo.cs: |
| 99 | +```csharp |
| 100 | +public class CookieInfo(Dictionary<string, string> cookieDic) |
| 101 | +{ |
| 102 | + public string CookieStr => // joins all cookie key=value pairs with "; " |
| 103 | +} |
| 104 | +``` |
| 105 | + |
| 106 | +From src/Ray.BiliBiliTool.Web/Services/Pages/Admin/IAdminPageWorkflow.cs (pattern to follow): |
| 107 | +```csharp |
| 108 | +public interface IAdminPageWorkflow |
| 109 | +{ |
| 110 | + Task<AdminPasswordChangeResult> ChangePasswordAsync(AdminPasswordChangeRequest request); |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +From src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs: |
| 115 | +```csharp |
| 116 | +public static IServiceCollection AddWebServices(this IServiceCollection services) |
| 117 | +{ |
| 118 | + services.AddScoped<IAuthService, AuthService>(); |
| 119 | + services.AddScoped<ILoginPageStateFactory, LoginPageStateFactory>(); |
| 120 | + services.AddScoped<IAdminPageWorkflow, AdminPageWorkflow>(); |
| 121 | + // ... add IBiliAccountPageWorkflow here |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +From src/Ray.BiliBiliTool.Web/Program.cs (line 21 — the line to remove): |
| 126 | +```csharp |
| 127 | +builder.Configuration.AddJsonFile("config/cookies.json", optional: true, reloadOnChange: true); |
| 128 | +``` |
| 129 | + |
| 130 | +From src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor: |
| 131 | +```razor |
| 132 | +<!-- Add "Bili Account" nav item BEFORE the Configurations submenu --> |
| 133 | +<!-- Pattern: same as other nav items --> |
| 134 | +<div class="nav-item px-3"> |
| 135 | + <NavLink class="nav-link" href="BiliAccount"> |
| 136 | + <span class="bi bi-people-fill" aria-hidden="true"></span> Bili Account |
| 137 | + </NavLink> |
| 138 | +</div> |
| 139 | +``` |
| 140 | +</interfaces> |
| 141 | +</context> |
| 142 | + |
| 143 | +<tasks> |
| 144 | + |
| 145 | +<task type="auto"> |
| 146 | + <name>Task 1: Remove cookies.json source and create workflow seam</name> |
| 147 | + <files> |
| 148 | + src/Ray.BiliBiliTool.Web/Program.cs, |
| 149 | + src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/IBiliAccountPageWorkflow.cs, |
| 150 | + src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountPageWorkflow.cs, |
| 151 | + src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountDto.cs, |
| 152 | + src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs |
| 153 | + </files> |
| 154 | + <action> |
| 155 | +**ACCT-07 — Remove cookies.json from Web host:** |
| 156 | + |
| 157 | +In `src/Ray.BiliBiliTool.Web/Program.cs`, delete line 21: |
| 158 | +```csharp |
| 159 | +builder.Configuration.AddJsonFile("config/cookies.json", optional: true, reloadOnChange: true); |
| 160 | +``` |
| 161 | +Keep the `AddSqlite` block (lines 22-30) unchanged. The `AddJsonFile` for `appsettings.json` and `appsettings.Development.json` (if any) must NOT be touched — only the `cookies.json` line. |
| 162 | + |
| 163 | +**Create workflow seam (following v4.0.0.6 IAdminPageWorkflow pattern):** |
| 164 | + |
| 165 | +1. Create `src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountDto.cs`: |
| 166 | +```csharp |
| 167 | +namespace Ray.BiliBiliTool.Web.Services.Pages.BiliAccount; |
| 168 | + |
| 169 | +public record BiliAccountDto(int Index, string UserId, string CookieStr); |
| 170 | +``` |
| 171 | + |
| 172 | +2. Create `src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/IBiliAccountPageWorkflow.cs`: |
| 173 | +```csharp |
| 174 | +namespace Ray.BiliBiliTool.Web.Services.Pages.BiliAccount; |
| 175 | + |
| 176 | +public interface IBiliAccountPageWorkflow |
| 177 | +{ |
| 178 | + Task<List<BiliAccountDto>> GetAllAccountsAsync(); |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +3. Create `src/Ray.BiliBiliTool.Web/Services/Pages/BiliAccount/BiliAccountPageWorkflow.cs`: |
| 183 | +- Inject `IConfiguration` (not `CookieStrFactory` — the workflow reads the raw config section directly to avoid singleton lifetime issues) |
| 184 | +- `GetAllAccountsAsync()` reads `configuration.GetSection("BiliBiliCookies").Get<List<string>>() ?? []` |
| 185 | +- For each cookie string at index N, parse it into a `Dictionary<string, string>` (split on ";", then on "="), extract `DedeUserID` as UserId, and return `new BiliAccountDto(N, userId, cookieStr)` |
| 186 | +- Use the same parsing logic as `CookieStrFactory.CkStrToDictionary` — split on ";", trim, then split on first "=" to get key/value |
| 187 | +- If parsing fails for a cookie, still include it in the list with UserId="(unknown)" |
| 188 | + |
| 189 | +4. Register in `src/Ray.BiliBiliTool.Web/Extensions/ServiceCollectionExtension.cs`: |
| 190 | +Add `services.AddScoped<IBiliAccountPageWorkflow, BiliAccountPageWorkflow>();` in `AddWebServices()`, after the existing workflow registrations. |
| 191 | + </action> |
| 192 | + <verify> |
| 193 | + <automated>cd "d:\Codes\My\Bili\branch\blazor\BiliBiliToolPro" && dotnet build src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj --no-restore 2>&1 | Select-String -Pattern "error|Error|Build succeeded"</automated> |
| 194 | + </verify> |
| 195 | + <done> |
| 196 | +- `cookies.json` AddJsonFile line removed from Program.cs |
| 197 | +- `IBiliAccountPageWorkflow` interface exists with `GetAllAccountsAsync()` method |
| 198 | +- `BiliAccountPageWorkflow` reads cookies from IConfiguration and returns List<BiliAccountDto> |
| 199 | +- `BiliAccountDto` record with Index, UserId, CookieStr |
| 200 | +- DI registration in ServiceCollectionExtension.cs |
| 201 | +- Build succeeds with 0 errors |
| 202 | + </done> |
| 203 | +</task> |
| 204 | + |
| 205 | +<task type="auto"> |
| 206 | + <name>Task 2: Create Bili Account page and NavMenu entry</name> |
| 207 | + <files> |
| 208 | + src/Ray.BiliBiliTool.Web/Components/Pages/BiliAccount.razor, |
| 209 | + src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor |
| 210 | + </files> |
| 211 | + <action> |
| 212 | +**Create Bili Account page:** |
| 213 | + |
| 214 | +Create `src/Ray.BiliBiliTool.Web/Components/Pages/BiliAccount.razor`: |
| 215 | +- `@page "/BiliAccount"` |
| 216 | +- `@using Microsoft.AspNetCore.Authorization` |
| 217 | +- `@using Ray.BiliBiliTool.Web.Services.Pages.BiliAccount` |
| 218 | +- `@rendermode InteractiveServer` |
| 219 | +- `@attribute [Authorize]` |
| 220 | +- Inject `IBiliAccountPageWorkflow` |
| 221 | +- On `OnInitializedAsync`, call `_accounts = await workflow.GetAllAccountsAsync()` |
| 222 | +- Display accounts in a `MudTable<T="BiliAccountDto"` with columns: |
| 223 | + - **#** — `context.Index` (the BiliBiliCookies__N index) |
| 224 | + - **UserId** — `context.UserId` |
| 225 | + - **Cookie** — `context.CookieStr` (truncated display with tooltip for full value, use `MudTooltip` or just show first 80 chars + "...") |
| 226 | +- Show `MudProgressCircular` while loading |
| 227 | +- Show "No accounts configured" message if list is empty |
| 228 | +- Page title: "Bili Account" |
| 229 | +- Use MudBlazor components consistent with existing pages (MudContainer, MudText, MudTable, etc.) |
| 230 | + |
| 231 | +**Add NavMenu entry:** |
| 232 | + |
| 233 | +In `src/Ray.BiliBiliTool.Web/Components/Layout/NavMenu.razor`, add a new nav item BEFORE the Configurations submenu div (before the `<div class="nav-item px-3">` that contains the Configurations toggle). Use the same pattern as other nav items: |
| 234 | + |
| 235 | +```razor |
| 236 | +<div class="nav-item px-3"> |
| 237 | + <NavLink class="nav-link" href="BiliAccount"> |
| 238 | + <span class="bi bi-people-fill" aria-hidden="true"></span> Bili Account |
| 239 | + </NavLink> |
| 240 | +</div> |
| 241 | +``` |
| 242 | + |
| 243 | +Place it after the "Schedules" nav item and before the "Configurations" submenu. |
| 244 | + </action> |
| 245 | + <verify> |
| 246 | + <automated>cd "d:\Codes\My\Bili\branch\blazor\BiliBiliToolPro" && dotnet build src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj --no-restore 2>&1 | Select-String -Pattern "error|Error|Build succeeded"</automated> |
| 247 | + </verify> |
| 248 | + <done> |
| 249 | +- BiliAccount.razor page exists at /BiliAccount route with @attribute [Authorize] |
| 250 | +- Page displays MudTable with columns: #, UserId, Cookie |
| 251 | +- Page shows loading spinner and empty state |
| 252 | +- NavMenu has "Bili Account" entry between Schedules and Configurations |
| 253 | +- Build succeeds with 0 errors |
| 254 | + </done> |
| 255 | +</task> |
| 256 | + |
| 257 | +</tasks> |
| 258 | + |
| 259 | +<threat_model> |
| 260 | +## Trust Boundaries |
| 261 | + |
| 262 | +| Boundary | Description | |
| 263 | +|----------|-------------| |
| 264 | +| Browser → Blazor Server | Cookie strings displayed in the browser are sensitive session credentials | |
| 265 | +| Blazor Server → SQLite | Configuration writes must be validated before persistence | |
| 266 | + |
| 267 | +## STRIDE Threat Register |
| 268 | + |
| 269 | +| Threat ID | Category | Component | Disposition | Mitigation Plan | |
| 270 | +|-----------|----------|-----------|-------------|-----------------| |
| 271 | +| T-17-01 | Information Disclosure | BiliAccount.razor | accept | Cookie strings shown only to authenticated users (@attribute [Authorize]); page is admin-only by design | |
| 272 | +| T-17-02 | Tampering | Program.cs config pipeline | mitigate | Removing cookies.json eliminates stale/external config tampering; SQLite is the single source of truth | |
| 273 | +</threat_model> |
| 274 | + |
| 275 | +<verification> |
| 276 | +1. `dotnet build src/Ray.BiliBiliTool.Web/Ray.BiliBiliTool.Web.csproj` — 0 errors |
| 277 | +2. `dotnet test test/Ray.BiliBiliTool.ArchitectureTests/` — all pass (no new dependency violations) |
| 278 | +3. `dotnet test test/Ray.BiliBiliTool.Host.IntegrationTests/` — all pass (config pipeline change doesn't break existing tests) |
| 279 | +4. Manual: navigate to /BiliAccount after login — table shows configured accounts |
| 280 | +</verification> |
| 281 | + |
| 282 | +<success_criteria> |
| 283 | +- Web host no longer loads `config/cookies.json` (grep Program.cs confirms line removed) |
| 284 | +- "Bili Account" appears in NavMenu between Schedules and Configurations |
| 285 | +- BiliAccount.razor renders a MudTable with all configured accounts showing Index, UserId, CookieStr |
| 286 | +- `IBiliAccountPageWorkflow` follows the v4.0.0.6 seam pattern (interface + implementation + DI registration) |
| 287 | +- Build 0 errors | architecture tests pass | integration tests pass |
| 288 | +</success_criteria> |
| 289 | + |
| 290 | +<output> |
| 291 | +After completion, create `.planning/phases/17-account-storage-foundation/17-01-SUMMARY.md` |
| 292 | +</output> |
0 commit comments