# 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.