Files
OmniOilPersonal/openspec/changes/notify-approved-access-requests/proposal.md

181 lines
14 KiB
Markdown

# Proposal: Provision Approved Access Requests
## Intent
Close the provider intake loop by provisioning the approved customer admin account and notifying the business contact when a platform operator approves an access request. Today approval changes the internal status only; the customer does not receive access or a next step, so operations must manually create users/follow up outside Console. That creates delay, duplicated work, and poor auditability.
## Scope
### In Scope
- Backend (`services/backend-api`):
- Persist provisioning, activation, and email delivery metadata for approved access requests.
- Create the customer operational scope automatically for the request. In the current schema this means creating an `edge_projects` row plus `user_project_access`; if a future `tenants` table is introduced, the design must map this flow to that table explicitly.
- Create the initial customer admin user from the request contact using role `admin`, not `platform_admin`; if a user with the same email already exists, link that existing user to the new project instead of creating a duplicate.
- Store the new user as inactive/pending activation until the invite is consumed.
- Generate a single-use, expiring activation token after approval.
- Create an initial 7-day trial subscription for the provisioned project using the existing `subscriptions` table.
- Send an approval/activation email to the request contact using SMTP configuration.
- Record audit events for project provisioning, admin user creation, activation invitation creation, email sent, email failed, and resend.
- Add provider-only resend/rotate-invitation endpoint for failed/expired activation emails.
- Add a public activation endpoint that validates the token and lets the customer set their password.
- Frontend Console (`services/frontend-console`):
- On approval, collect/confirm the minimum provisioning data that cannot be safely inferred from the request.
- Show provisioned project/admin user and activation email status in the selected request detail.
- Surface a `Reenviar invitacion` action when email delivery failed or the activation invitation expired.
- Keep reject/review flow unchanged except for showing provisioning context.
- Frontend Admin (`services/frontend-admin`):
- Add or reuse an activation route where the invited customer admin sets their password and activates the account.
- Deployment/docs:
- Document required SMTP env vars and invitation base URL.
- Keep secrets out of repository examples.
### Out of Scope
- Building a full email template CMS.
- Sending passwords by email.
- Creating full production assets/devices/variables for the customer.
- Creating multiple customer users during first approval.
- Creating `platform_admin` or `platform_operator` accounts for customers.
- Replacing existing alarm notification code wholesale.
- Customer self-service registration redesign beyond the activation link target required by this flow.
- Bulk resends or marketing emails.
- Payment enforcement and project suspension after trial expiry.
## Capabilities
### New Capabilities
- `access-request-provisioning`: When an access request is approved, the system SHALL provision the customer admin account, bind it to the approved operational scope, create a secure activation invitation, and notify the business contact by email.
### Modified Capabilities
- `access-requests`: Approval responses SHALL expose provisioning and notification/invitation status so Console operators can see whether access was created and whether the customer was notified.
## Approach
Use approval as an explicit provisioning workflow, not just a status change. The backend must create durable internal access first: approved request, customer admin user, operational scope link, and activation invitation metadata. Only after that durable state exists should it attempt SMTP delivery. If SMTP fails, the request remains approved/provisioned and the failure is stored for operator follow-up. Console then shows the failure and exposes a resend action.
Key decisions:
1. **Create a customer `admin`, not a platform user**. The requester becomes an `admin` role user for the customer side (`frontend-admin`), never `platform_admin`/`platform_operator`.
2. **Pending activation over temporary password**. The created user must not receive a password by email. Use inactive/pending state until the activation token is consumed.
3. **Bind access to a new operational scope**. In the current schema, users see data through `user_project_access`; approval creates a new `edge_projects` row from request data and links the customer admin to it.
4. **Do not send passwords**. The email contains a time-limited activation link only.
5. **Store token hashes, not raw tokens**. Generate a high-entropy token, store only its hash, and put the raw token only in the outbound link. Activation links expire after 7 days.
6. **Provisioning is not rolled back by SMTP failure**. Email problems are operational, not domain approval problems.
7. **Reuse SMTP config, extract generic mailer**. `notifier.rs` already uses `lettre` and `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, `SMTP_PORT`, `SMTP_FROM`; this change should extract or add a small generic mailer instead of duplicating SMTP setup.
8. **Operator-visible provisioning and delivery status**. Console must make it obvious whether the admin user/project were created and whether the customer was notified.
9. **Manual resend is provider-only and rotates tokens**. Resend must require `PlatformClaims`, invalidate previous unconsumed activation tokens, create a fresh token, and write audit metadata.
10. **Activation belongs to Admin, not Console**. The email link targets `https://app.omnioil.app/activate?token=...`; Console remains internal provider-only.
11. **Trial is separate from activation**. Provisioned projects start with `subscriptions.status = 'trial'` and `trial_ends_at = now() + 7 days`. Activation-token expiry does not suspend projects; billing lifecycle enforcement is a later change.
## Affected Areas
| Area | Impact | Description |
|------|--------|-------------|
| `services/backend-api/migrations/*_access_request_provisioning.sql` | New | Add provisioning/invitation metadata. Preferred: separate `access_request_invitations` table plus columns linking request to `provisioned_user_id` and `provisioned_project_id`, or a single provisioning table. |
| `services/backend-api/src/handlers/access_requests.rs` | Modified | Approval provisions customer admin/project access, creates activation invitation, sends email, returns provisioning status; new resend endpoint. |
| `services/backend-api/src/main.rs` | Modified | Register provider-only resend route under `/api/platform/access-requests/{id}/invitation/resend` and public activation route. |
| `services/backend-api/src/handlers/auth.rs` or new `handlers/invitations.rs` | Modified/New | Public activation endpoint validates token and sets password. |
| `services/backend-api/src/mailer.rs` or `services/backend-api/src/email/*` | New | Generic SMTP email sender and approval/activation email template. |
| `services/backend-api/src/notifier.rs` | Modified | Reuse generic mailer for alarm emails or leave alarm path intact and share only SMTP helper. |
| `services/backend-api/src/handlers/billing*` or approval service | Modified/New | Create initial trial subscription for the provisioned project. |
| `services/frontend-console/src/features/access-requests/api/access-requests-api.ts` | Modified | Add notification fields and resend API call. |
| `services/frontend-console/src/features/access-requests/pages/AccessRequestsPage.tsx` | Modified | Show provisioning/email/invitation status, collect/confirm project data, and expose resend action. |
| `services/frontend-admin/src/**` | Modified | Activation page accepts token, sets password, and redirects to login. |
| `docker-compose.yml`, `.env.example`, `docs/DESPLIEGUE.md` | Modified | Document SMTP and `INVITATION_BASE_URL`/`CONSOLE_APP_ORIGIN` style env. |
## Data Model Sketch
Preferred additive model:
1. Add provisioning linkage to access requests, or keep it in a separate provisioning table:
```sql
ALTER TABLE access_requests
ADD COLUMN provisioned_user_id UUID REFERENCES users(id) ON DELETE SET NULL,
ADD COLUMN provisioned_project_id UUID REFERENCES edge_projects(id) ON DELETE SET NULL,
ADD COLUMN provisioned_at TIMESTAMPTZ;
```
2. Add activation invitation metadata:
```sql
CREATE TABLE access_request_invitations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
access_request_id UUID NOT NULL REFERENCES access_requests(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
project_id UUID REFERENCES edge_projects(id) ON DELETE SET NULL,
token_hash TEXT NOT NULL UNIQUE,
email_status VARCHAR(30) NOT NULL DEFAULT 'pending'
CHECK (email_status IN ('pending', 'sent', 'failed')),
email_sent_at TIMESTAMPTZ,
email_error TEXT,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ,
revoked_reason TEXT,
created_by UUID REFERENCES users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
3. User activation state:
The current `users.is_active` boolean can represent pending activation by inserting the customer admin with `is_active = false`, then setting it to `true` after password setup. If we need clearer semantics, design may add `activation_status VARCHAR(30)`; default recommendation is to start minimal with `is_active = false` plus invitation state to avoid unnecessary schema churn.
The design phase should confirm whether one invitation per request is enough or whether resends should create new rows. Default recommendation: create a new token on resend and keep previous rows for audit, marking older unconsumed invitations as superseded if that status is added.
4. Initial trial state:
The existing `subscriptions` table already has `status IN ('active','suspended','cancelled','trial')` and `trial_ends_at`. Approval should insert a `subscriptions` row for the new project with `status = 'trial'` and `trial_ends_at = NOW() + INTERVAL '7 days'`. Payment enforcement and automatic suspension after trial expiry are explicitly deferred.
## Risks
| Risk | Likelihood | Mitigation |
|------|------------|------------|
| SMTP outage blocks onboarding | Medium | Do not rollback approval; store `email_status = failed`; expose resend. |
| Invitation token leak | Medium | Store only hash, expire tokens, single-use consumption, HTTPS-only base URL. |
| Duplicate emails on retry | Medium | Make resend explicit; approval should not repeatedly send if already sent. |
| Duplicate user email | Medium | Approval links the existing user by email to the newly created project; no duplicate account is created. |
| Project/tenant ambiguity | Medium | Approval creates `edge_projects` from request-derived data; design must define deterministic defaults and metadata for fields not present in the request. |
| Email sent before DB commit | Low | Persist approval/invitation first; send after durable state exists. |
| Customer receives link but account flow missing | Medium | Implement activation endpoint/page in same change; do not ship email-only notification. |
| Trial confused with activation expiry | Medium | Store token expiry and subscription trial separately; token validation must not decide billing access. |
| SMTP secrets accidentally committed | Low | Document env vars only; never commit real `.env`. |
## Rollback Plan
1. Disable email delivery by unsetting `SMTP_HOST` or feature flagging resend/approval email behavior if introduced.
2. Revert frontend Console changes; approval status remains readable through existing request fields.
3. Revert backend code paths to approve without provisioning/email if necessary.
4. Leave additive invitation/provisioning columns or tables in place during rollback to avoid destructive data loss; drop them only in a later migration after confirming no active invitations exist.
5. For users created during the rollout window, keep them inactive unless already activated; operators can manually activate/reset via existing admin tooling if required.
6. Notify operators that approved requests during rollback require manual customer follow-up.
## Dependencies
- Existing `lettre` dependency used by `services/backend-api/src/notifier.rs`.
- SMTP env vars: `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS`, `SMTP_PORT`, `SMTP_FROM`.
- New public base URL env for links: `INVITATION_BASE_URL=https://app.omnioil.app`, route `/activate`.
- Existing provider auth via `PlatformClaims`.
- Existing audit helper `crate::audit::log`.
- Existing roles: customer admin role is `admin`; provider console roles are `platform_admin` and `platform_operator`.
- Existing data access model: `users` gain operational access through `user_project_access` rows tied to `edge_projects`.
- Product decisions: auto-create `edge_projects`, link existing user emails, 7-day activation expiry, resend invalidates previous unconsumed tokens.
- Existing billing model: `subscriptions` supports `status = 'trial'` and `trial_ends_at` per project.
## Success Criteria
- [ ] Approving a pending/in-review request persists `approved` status, provisions a customer admin user with role `admin`, binds operational access, and creates an activation token hash with expiration.
- [ ] The provisioned project receives a `trial` subscription with `trial_ends_at` 7 days after approval.
- [ ] The customer admin is not a platform user and cannot access Console-only provider routes.
- [ ] Approval email is sent to `access_requests.email` with company name, activation link, expiry, and support contact.
- [ ] The activation link lets the customer set their password, activates the user, and redirects to login.
- [ ] Raw invitation token is never stored in the database or logs.
- [ ] If SMTP fails, the request remains approved/provisioned and Console shows email failure with a resend action.
- [ ] Resend creates or rotates an invitation token and writes an audit event.
- [ ] Already approved requests cannot be re-approved to spam emails.
- [ ] Console displays provisioning and notification status for approved requests.
- [ ] Backend tests cover approval success, user/project provisioning, SMTP failure, resend, expired token, activation, duplicate email handling, and no raw-token persistence.
- [ ] Frontend tests cover visible provisioning/delivery status, approval data capture, activation page, and resend button state.
- [ ] Docs explain required SMTP and invitation URL env vars.