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

Reply via email to