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

14 KiB

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:
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;
  1. Add activation invitation metadata:
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()
);
  1. 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.

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