diff --git a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-05-SUMMARY.md b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-05-SUMMARY.md index 89d24be3..bd68dc05 100644 --- a/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-05-SUMMARY.md +++ b/.planning/phases/19.5-social-login-in-sm-user-plugin/19.5-05-SUMMARY.md @@ -2,7 +2,7 @@ phase: 19.5-social-login-in-sm-user-plugin plan: 05 subsystem: auth -tags: [sociallogin, sm-user-plugin, email-proof, pre-account-takeover, postgres, gormigrate, gate] +tags: [sociallogin, sm-user-plugin, email-proof, pre-account-takeover, postgres, gormigrate, gate, vitest, bm] status: draft requires: - phase: 19.5-04 @@ -11,25 +11,29 @@ provides: - "users.email_verified_at (migration 202610210001_add_users_email_verified_at), written only by VerifyActivationCode and ResetPassword, cleared on an email change" - "sociallogin ownerVerifiedEmail reads EmailVerifiedAt (D-26)" - "TestOwnerVerifiedEmail (8 subtests), TestEmailVerifiedAt, TestAdminEmailVerifiedAt, TestEmailVerifiedAtMigration" - - "gate: USER_ADDED_MIGRATIONS, refused_migration_changes, migration_writes_rows, DOCKER_USER_UPDATES_TESTS" -affects: [19.5-05 Task 4 (bm view test), 19.5-05 Task 5 (security review, validation map), 19.5-05 Task 7 (release)] + - "bm src/views/OAuthCallbackView.test.ts (D-28): the real view mounted with vue's createRenderer, no new npm dependency" + - "gate: USER_ADDED_MIGRATIONS, refused_migration_changes, migration_writes_rows, DOCKER_USER_UPDATES_TESTS, detect_vitest REQUIRE_FILES" + - "19.5-SECURITY-REVIEW.md T-19.5-39..46 and CR-02 open; 19.5-VALIDATION.md rows 19.5-05-01..07, D-26..D-28" +affects: [19.5-05 Task 6 (release go-ahead), 19.5-05 Task 7 (release or hold)] actuals: - tokens: 18621 # chars/4 over the Task 1-3 diffs (sm-user-plugin, sm-sociallogin-plugin, gate script) - tasks: 3 # of 7; hand-off after Task 3 - commits: 2 # summercms.go only (7170c5b, 30e0cd9); rev-list from the ledger counts 4 because two 19.8-03 commits of another session landed in between + tokens: 30829 # chars/4: Tasks 1-3 diffs 18621 + Tasks 4-5 diffs 12208 (bm view test, gate change, security review and validation map) + tasks: 5 # of 7; at the Task 6 checkpoint + commits: 5 # summercms.go commits of this plan before this draft's commit (7170c5b, 30e0cd9, c2c8c6a, 7e43873, 8c7794e); rev-list from the ledger counts 7 because two foreign 19.8-03 commits landed in between plan_head_before: 33460607db95276e8180487a8ece7e4f0070b1e7 -plan_head_after: 30e0cd9 +plan_head_after: 8c7794efa250e6b5f03fcae1ce2c075f779e94f9 tech-stack: added: [] patterns: - "Proof of address as a nullable timestamp column, stamped in the same write as the mailed-code action that proves it" - "Gate migration allow-list: git diff --name-status filtered through refused_migration_changes, plus a comment-blind SQL write scan" + - "SFC view test without a DOM package: vue createRenderer over an in-memory host, the template compiled for the client with the script's binding metadata, createMemoryHistory router" key-files: created: - ../fonoteka.go/plugins/golem15/user/updates/202610210001_add_users_email_verified_at.go - ../fonoteka.go/plugins/golem15/user/updates/email_verified_at_test.go - ../fonoteka.go/plugins/golem15/user/email_verified_test.go - ../sm-sociallogin-plugin/owner_verified_test.go + - ../sm-bm-app/frontend/src/views/OAuthCallbackView.test.ts modified: - ../fonoteka.go/plugins/golem15/user/models/user.go - ../fonoteka.go/plugins/golem15/user/classes/codes.go @@ -43,31 +47,34 @@ key-files: - ../sm-sociallogin-plugin/testdata/PHP_METHOD_MAP.md - ../sm-sociallogin-plugin/README.md - scripts/check-phase19.5.sh + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-SECURITY-REVIEW.md + - .planning/phases/19.5-social-login-in-sm-user-plugin/19.5-VALIDATION.md - .planning/phases/19.5-social-login-in-sm-user-plugin/deferred-items.md key-decisions: - "Proof writers are exactly VerifyActivationCode (activate, activate-by-code, invitation link) and ResetPassword's password write; RegisterUser, admin create, ActivateUsers, ChangePassword and every sociallogin path write nothing" - "An email change through /update or the admin form clears email_verified_at; a case-only change (strings.EqualFold) keeps it" - "Harness e.user is a proven account so plan 02-04 link cases keep testing the link path; e.unprovenUser models accounts without proof" -requirements-completed: [] # D-26, D-27, D-28, SC-3 are marked by the final context (Task 7), not this hand-off -duration: 29min + - "The bm view test compiles the view's own template for the client at test time (vitest's node environment gives the SFC only ssrRender) and provides an empty SSR context; vite.config.ts and the dependencies stay unchanged" +requirements-completed: [] # D-26, D-27, D-28, SC-3 are marked by the final context (Task 7), not this draft +duration: 68min completed: 2026-10-21 --- # Phase 19.5 Plan 05: Proof of Address for Social Sign-in Linking Summary -Status: draft (hand-off after Task 3) +Status: draft (at Task 6 checkpoint) -**users.email_verified_at (stamped only by the mailed activation code and the completed reset-code password reset, cleared on an email change) is now the owner-side condition for auto-linking a verified provider, so an auto-mode `/register` account goes pending instead of handing its sign-in to a squatter** +**users.email_verified_at (stamped only by the mailed activation code and the completed reset-code password reset, cleared on an email change) is now the owner-side condition for auto-linking a verified provider, so an auto-mode `/register` account goes pending instead of handing its sign-in to a squatter; the bm OAuth callback view's code handling is pinned by a vitest view test without a new dependency** -This draft covers Tasks 1-3 of 7. A fresh agent runs Tasks 4 (bm view test) and 5 (security review, validation map, `--all`, release table) and stops at the Task 6 checkpoint; Task 7 finalizes this file. +This draft covers Tasks 1-5 of 7. The Task 6 go-ahead (release or hold) is pending; Task 7 finalizes this file. ## Performance -- **Duration:** 29 min (this context) +- **Duration:** 29 min (Tasks 1-3) + 39 min (Tasks 4-5, about 25 of them spent on two gate runs that timed out on blocked loopback; see Issues) - **Started:** 2026-10-21T11:37:14Z -- **Hand-off:** 2026-10-21T12:06:29Z -- **Tasks:** 3 of 7 -- **Files modified:** 17 across three repositories (plus deferred-items.md) +- **Second context:** 2026-10-21T12:08Z to 12:47Z +- **Tasks:** 5 of 7 +- **Files modified:** 20 across four repositories (plus deferred-items.md) ## Accomplishments @@ -75,7 +82,9 @@ This draft covers Tasks 1-3 of 7. A fresh agent runs Tasks 4 (bm view test) and - `classes.VerifyActivationCode` stamps the proof in the same `UpdateColumns` write that activates; `controllers.ResetPassword` stamps it in the `Updates` that stores the new password. - `controllers.Update` and `usersAdminController.FormBeforeUpdate` clear it when the email changes (case-only change keeps it). `storedEmail` is an unexported helper now shared with `guardCredentials`. - sociallogin `ownerVerifiedEmail(u)` is `u != nil && u.EmailVerifiedAt != nil`; `classes/flow.go` no longer reads `HasSelfSetPassword`. -- Gate: the userplugin stage accepts exactly the one added migration and refuses any other added, modified, renamed or deleted non-test file under `updates/`, and a migration that writes or drops data; the new tests are required by name. +- bm `src/views/OAuthCallbackView.test.ts`: the real view mounted with `createRenderer` from `vue` on an in-memory host and a `createMemoryHistory` router; 5 cases (route replaced before the single redeem, no second redeem, `?error=` in the alert, pending registration replaced to `oauth-register`, expired code explained). `frontend/package.json` and `package-lock.json` unchanged (last commit touching them `fe967c4` before and after). +- Gate: the userplugin stage accepts exactly the one added migration and refuses any other change under `updates/` and a migration that writes or drops data; the new Go tests are required by name; `detect_vitest` takes `REQUIRE_FILES` and the bm stage requires both `src/lib/oauth.test.ts` and `src/views/OAuthCallbackView.test.ts`. +- 19.5-SECURITY-REVIEW.md: T-19.5-38 describes the proof as `email_verified_at`, T-19.5-39..T-19.5-46 added, T-19.5-26 closed by the view test, CR-02 under "Open findings" (open, no user decision). 19.5-VALIDATION.md: rows 19.5-05-01..07, D-26, D-27, D-28, the view check moved from manual to automated. ## Commits per repository @@ -95,16 +104,25 @@ This draft covers Tasks 1-3 of 7. A fresh agent runs Tasks 4 (bm view test) and | 3 | `962190b` | test: owner proof table for verified sign-ins | | 3 | `c4ebf05` | docs: proof of address in the matching table and the PHP map | -**summercms.go** (gate script; base 3346060): +**sm-bm-app** (base 99af8c2, nothing pushed): + +| Task | Commit | Message | +|------|--------|---------| +| 4 | `5fd0cc3` | test(frontend): OAuth callback view drops the code before one redeem | + +**summercms.go** (base 3346060, never pushed by this plan): | Task | Commit | Message | |------|--------|---------| | 1 | `7170c5b` | test(19.5): gate the email_verified_at migration and TestOwnerVerifiedEmail | | 2 | `30e0cd9` | test(19.5): gate the user-plugin proof tests | +| hand-off | `c2c8c6a` | docs(19.5-05): hand-off draft after Task 3 (email proof in user and sociallogin) | +| 4 | `7e43873` | test(19.5): gate the bm callback view test | +| 5 | `8c7794e` | docs(19.5): proof of address in the security review and validation map | Two foreign commits of another session (`2057643`, `4895b25`, both `fix(19.8-03)`) landed between them on summercms.go master; they are not this plan's. -**sm-bm-app, sm-demo-app, fonoteka.go:** no commit in this context. fonoteka.go's worktree shows `plugins/golem15/user` modified (the checkout is now at 35cf166); it is not staged until Task 7. +**sm-demo-app, fonoteka.go:** no commit yet. fonoteka.go's worktree shows `plugins/golem15/user` modified (the checkout is at 35cf166); it is not staged until Task 7. ## Tests added (all pass) @@ -113,44 +131,88 @@ Two foreign commits of another session (`2057643`, `4895b25`, both `fix(19.8-03) - **sm-user-plugin** `TestEmailVerifiedAt` (email_verified_test.go): `register_auto_no_proof`, `register_admin_mode_no_proof`, `register_ignores_submitted_proof`, `activate_by_code_stamps`, `activate_stamps`, `activate_wrong_code_no_proof`, `reset_password_stamps`, `reset_wrong_code_no_proof`, `change_password_no_proof`, `update_email_change_clears`, `update_same_email_keeps`, `update_ignores_submitted_proof`. - **sm-user-plugin** `TestAdminEmailVerifiedAt`: `admin_create_no_proof`, `admin_activate_action_no_proof`, `admin_invite_link_stamps`, `admin_email_change_clears`, `admin_save_keeps_proof`, `admin_json_hides_proof`. - **sm-user-plugin** `TestEmailVerifiedAtMigration` (updates/email_verified_at_test.go): `column_shape`, `no_backfill`, `existing_column_kept`, `rollback_keeps_column`, `history_once`. `TestCreateOrganisationsMigration` expects the new ID last and rolls back 12 steps. +- **sm-bm-app** vitest `src/views/OAuthCallbackView.test.ts`, describe `OAuthCallbackView`: `replaces the route before the single redeem`, `does not redeem a code twice`, `shows the error message`, `routes a pending registration`, `explains an expired code`. +- **summercms.go** gate self-test: vitest cases on both required files, `missing-view-file` (exit 5), `single-file-fallback` (REQUIRE_FILE, exit 5). -RED evidence: with the old `ownerVerifiedEmail` (self-set password), `TestOwnerVerifiedEmail/register_auto_then_google_pending` failed (the redirect carried a `code`, i.e. the auto-mode account was linked). With the Task 2 controllers reverted to HEAD, exactly `TestEmailVerifiedAt/update_email_change_clears` and `TestAdminEmailVerifiedAt/admin_email_change_clears` failed. +RED evidence: +- With the old `ownerVerifiedEmail` (self-set password), `TestOwnerVerifiedEmail/register_auto_then_google_pending` failed (the redirect carried a `code`, i.e. the auto-mode account was linked). +- With the Task 2 controllers reverted to HEAD, exactly `TestEmailVerifiedAt/update_email_change_clears` and `TestAdminEmailVerifiedAt/admin_email_change_clears` failed. +- View mutations, each reverted: removing the `router.replace({ path })` before the redeem fails 3 cases (`replaces the route…`, `does not redeem…`, `shows the error…`); dropping the `redeemed` check fails `does not redeem a code twice`; `push` instead of `replace` for the pending route fails `routes a pending registration`. -## Gate stages (scripts/check-phase19.5.sh) at hand-off +## Gate stages (scripts/check-phase19.5.sh) + +Final run, 14:42-14:46 local: `PHASE195_REQUIRE_DOCKER=1 TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1 bash scripts/check-phase19.5.sh --all`, exit 0. | Stage | State | Notes | |-------|-------|-------| -| --self-test | PASS | new migration-check cases (allowed `A`, refused `M` 00_base.go, other `A`, `D`, `R100`; planted row update refused, word in a `//` comment accepted) | -| --userplugin | PASS | "13 declarations added; 1 allowed migration(s) added, column only" | -| --sociallogin | PASS | includes TestPHPMethodMap on the edited map | -| --fonoteka | PASS | go vet and go test -short of the workspace at the changed user plugin | -| --docker | PASS | cooler 1, user root 12, user updates 2, sociallogin 29 top-level tests; run with `TESTCONTAINERS_HOST_OVERRIDE=127.0.0.1` (see Issues) | -| --go, --demo, --bm, --docs | NOT RUN | not touched by Tasks 1-3; Task 5 runs `PHASE195_REQUIRE_DOCKER=1 --all` | +| --self-test | PASS | detectors, migration-check cases, vitest `REQUIRE_FILES` cases incl. `missing-view-file`, docker NOT RUN rule | +| --go | PASS | summercms.go vet and short suite; cooler 4 named tests | +| --userplugin | PASS | 2 named tests; "13 declarations added; 1 allowed migration(s) added, column only" | +| --sociallogin | PASS | 9 named Docker-free tests incl. TestPHPMethodMap; deps and goth importer checks | +| --demo | PASS | build, vet, 3 named tests | +| --bm | PASS | build, vet, 1 + 2 named tests; vitest 216 passed (216) with both required files; typecheck | +| --fonoteka | PASS | vet and short suite of the workspace at the changed user plugin | +| --docs | PASS | TestDocsTree, docs:build --check, decision note, no consumer names | +| --docker | PASS | cooler 1, user root 12, user updates 2, sociallogin 29 top-level tests | ## Decisions Made Followed the plan's decisions. Within them: - `storedEmail` was extracted from `guardCredentials` so `FormBeforeUpdate` compares the stored and submitted email without a second copy of the query; `guardCredentials`' behaviour is unchanged. - `TestOwnerVerifiedEmail` boots its own environment with `gothFakeRegistry` (helper `ownerEnv`) so the Google and Facebook wrappers run on fake APIs, as `TestPHPSocialAuthGate` does. +- `mountAt(url, router)` builds a fresh router and app per mount (vue-router resets its state when its last app unmounts), so the second-mount case pushes the same callback URL again on a new router while the view module's `redeemed` set carries over. ## Deviations from Plan -None in production code or tests - Tasks 1-3 executed as written. +**1. [Rule 3 - Blocking] The bm view test compiles the view's template at test time** +- **Found during:** Task 4 +- **Issue:** Under vitest's `environment: 'node'`, @vitejs/plugin-vue compiles SFCs in SSR mode: the imported component has `setup` and `ssrRender` but no `render`, so `createRenderer` alone mounts nothing. The SSR module's setup also calls `useSSRContext()` and fails without one. A client-compiled template without binding metadata cannot read `