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_projectsrow plususer_project_access; if a futuretenantstable 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, notplatform_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
subscriptionstable. - 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 invitacionaction 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_adminorplatform_operatoraccounts 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:
- Create a customer
admin, not a platform user. The requester becomes anadminrole user for the customer side (frontend-admin), neverplatform_admin/platform_operator. - 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.
- Bind access to a new operational scope. In the current schema, users see data through
user_project_access; approval creates a newedge_projectsrow from request data and links the customer admin to it. - Do not send passwords. The email contains a time-limited activation link only.
- 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.
- Provisioning is not rolled back by SMTP failure. Email problems are operational, not domain approval problems.
- Reuse SMTP config, extract generic mailer.
notifier.rsalready useslettreandSMTP_HOST,SMTP_USER,SMTP_PASS,SMTP_PORT,SMTP_FROM; this change should extract or add a small generic mailer instead of duplicating SMTP setup. - Operator-visible provisioning and delivery status. Console must make it obvious whether the admin user/project were created and whether the customer was notified.
- 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. - Activation belongs to Admin, not Console. The email link targets
https://app.omnioil.app/activate?token=...; Console remains internal provider-only. - Trial is separate from activation. Provisioned projects start with
subscriptions.status = 'trial'andtrial_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:
- 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;
- 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()
);
- 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.
- 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
- Disable email delivery by unsetting
SMTP_HOSTor feature flagging resend/approval email behavior if introduced. - Revert frontend Console changes; approval status remains readable through existing request fields.
- Revert backend code paths to approve without provisioning/email if necessary.
- 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.
- For users created during the rollout window, keep them inactive unless already activated; operators can manually activate/reset via existing admin tooling if required.
- Notify operators that approved requests during rollback require manual customer follow-up.
Dependencies
- Existing
lettredependency used byservices/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 areplatform_adminandplatform_operator. - Existing data access model:
usersgain operational access throughuser_project_accessrows tied toedge_projects. - Product decisions: auto-create
edge_projects, link existing user emails, 7-day activation expiry, resend invalidates previous unconsumed tokens. - Existing billing model:
subscriptionssupportsstatus = 'trial'andtrial_ends_atper project.
Success Criteria
- Approving a pending/in-review request persists
approvedstatus, provisions a customer admin user with roleadmin, binds operational access, and creates an activation token hash with expiration. - The provisioned project receives a
trialsubscription withtrial_ends_at7 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.emailwith 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.