The GitHub Actions job "Comment commands" on texera.git/main has succeeded. Run started by GitHub user roshiiiz (triggered by roshiiiz).
Head commit for run: 2edbdf9d6a2d0e42461078825243f78968c94090 / Neil Ketteringham <[email protected]> feat(frontend): ask an account with no email address for one at sign-in (#7758) ### What changes were proposed in this PR? https://github.com/user-attachments/assets/346249b8-2ef4-47b3-be32-f87c558fbd74 #### TL;DR Email *is* identity in Texera: dataset storage paths are built from it and every access grant names one. Local and Google login both guarantee an address, so nothing has ever had to ask for one, but a guaranteed address is the exception among login providers, not the norm. Adding ORCID (or any provider that authenticates someone without asserting an address) would today produce an account that cannot be shared with. This PR adds the remedy such a provider would reuse, and closes the one hole that already exists. #### New Process ``` Before: sign in -> token without email claim -> user with no address, sharing silently broken After: sign in -> token without email claim -> modal (Save | Sign out) -> reissued token -> user ``` `AuthService.loginWithExistingToken` now hands out **no `User` at all** while the stored token's `email` claim is empty, so an addressless account cannot reach the app in a half-working state. The prompt is not dismissable — cancelling signs out — mirroring how the invite-only registration request already behaves. **Backend — `PUT /auth/email`** (`AuthResource`) Takes the address for whichever account the request is authenticated as; there is no `uid` in the body. The row is re-read inside the transaction, because the pojo on the session was built from the token and is not evidence about the row as it stands now. | Address belongs to | Outcome | | --- | --- | | nobody | written to the caller's row | | a contributor placeholder | placeholder adopted — credential moves onto it, caller's row is dropped | | a real account | `409`, "sign in to that account instead" | | the caller, already set | `409`, an address cannot be replaced through this endpoint | Adoption keeps the *placeholder's* uid, because dataset contributor rows already reference it; re-pointing those instead would mean touching every table that FKs to `"user"`. This mirrors what `register` already does when a registration presents a placeholder's address. It is refused unless the caller is an empty `INACTIVE` account and the placeholder holds no credential of its own. **Backend — `AdminUserResource`** `addUser` was the one path that wrote a credential without an address, so it was the source of the addressless rows in the first place. It now inserts a bare `INACTIVE` row and no credential at all. | | Before | After | | --- | --- | --- | | `addUser` | row + LOCAL credential, no email | row only | | `updateUser`, blank email, role ≠ `INACTIVE` | activated an unreachable account | `400` | | `updateUser`, blank email over an existing one | silently cleared the address | `400` | The row is deliberately *not* marked `is_placeholder`, though an unclaimed stub is arguably what it is: `DatasetAccessResource` refuses to share with a placeholder, so an admin could no longer pre-share with an address before its owner first signs in. **Frontend** - `EmailRequestModalComponent` — the prompt itself: who you are signed in as, why the address is needed, one input. - `AuthService` — `setEmail()`, `promptForEmail()` (re-entry guarded, so a second prompt cannot stack), and a `sessionChanged()` observable. Both outcomes announce themselves through it: a save replaces the token, a cancel throws it away, and either way the current user has to be re-derived. - `UserService` — subscribes to `sessionChanged()` and re-derives, which is what makes the user appear behind the modal on save and disappear on cancel. - `util/email.ts` — `validateEmailFormat`, now shared by registration and the prompt. It lives outside both services because they import each other's module. Authoritative validation stays on the backend (`EmailUtil.isValid`); this only catches the typo before a round trip. Scope note: this is groundwork. With local and Google login it is unreachable by design — the only accounts that reach it today are the addressless rows older deployments still carry from the previous `addUser`. It is live code on a rare path, not dead code, and it is the path a future provider plugs into. ### Any related issues, documentation, discussions? Closes #7757 ### How was this PR tested? <!-- If tests were added, say they were added here. Or simply mention that if the PR is tested with existing test cases. Make sure to include/update test cases that check the changes thoroughly including negative and positive cases if possible. If it was tested in a way different from regular unit tests, please clarify how you tested step by step, ideally copy and paste-able, so that other reviewers can test and check, and descendants can verify in the future. If tests were not added, please describe why they were not added and/or why it was difficult to add. --> Added coverage on both sides, positive and negative: - `AuthResourceSpec` — each row of the table above: a fresh address is stored *and* comes back in the reissued token's claim; placeholder adoption (credential moved, caller's row and its `user_last_active_time` gone, placeholder uid kept, and again with a caller that has an activity row); a placeholder holding its own credential refused; an address owned by a real account refused; an address already set refused; a caller no longer `INACTIVE` refused; blank and malformed input refused. - `AdminUserResourceSpec` — `addUser` writes no credential and does not mark the row a placeholder; both `updateUser` guards; and an emailless account that stays `INACTIVE` is still editable, so the guards don't over-fire. - `auth.service.spec.ts` — prompt opens only when the claim is absent, does not stack, precedes the invite-only branch, stores the reissued token and emits `sessionChanged`, keeps the dialog open and reports why on refusal, rejects a malformed address without calling the backend, signs out on cancel, and can prompt again afterwards. - `email-request-modal.component.spec.ts`, `user.service.spec.ts` — the modal's own contract and the re-derive subscription. ```sh sbt 'WorkflowExecutionService/testOnly *AuthResourceSpec *AdminUserResourceSpec' # Tests: succeeded 67, failed 0 cd frontend && npx ng test \ --include src/app/common/service/user/auth.service.spec.ts \ --include src/app/common/service/user/user.service.spec.ts \ --include src/app/common/service/user/email-request-modal/email-request-modal.component.spec.ts # Test Files 3 passed | Tests 52 passed ``` Manually, on the local dev stack (Chrome, `bin/local-dev.sh up`) — no login provider issues an addressless token yet, so the account has to be put in that state directly: ```sh # a local account, then take its address away curl -s -X POST http://localhost:8080/api/auth/register -H 'Content-Type: application/json' \ -d '{"username":"jsmith","email":"[email protected]","password":"password"}' docker exec texera-postgres psql -U texera -d texera_db \ -c "UPDATE texera_db.\"user\" SET name = 'Jane Smith', email = NULL WHERE name = 'jsmith';" ``` Sign in as `jsmith` / `password` at `localhost:4200`: | Step | Expected | | --- | --- | | sign in | prompt appears, greets "Jane Smith", no close button, mask does not dismiss | | Save `bad@@x` | client-side error, dialog stays open, no request sent | | Save an address another account owns | `409` surfaced in the dialog, stays open | | Save a free address | dialog closes, dashboard loads as that user | | re-run the `UPDATE`, sign in, Sign out | returns to the login page, token cleared | ### Was this PR authored or co-authored using generative AI tooling? <!-- If generative AI tooling has been used in the process of authoring this PR, please include the phrase: 'Generated-by: ' followed by the name of the tool and its version. If no, write 'No'. Please refer to the [ASF Generative Tooling Guidance](https://www.apache.org/legal/generative-tooling.html) for details. --> Co-Authored with Claude [Generated-by: Claude Opus 5] Report URL: https://github.com/apache/texera/actions/runs/32533201013 With regards, GitHub Actions via GitBox
