# User Invitation System - Implementation Plan ## Overview Implement a secure user invitation system that allows platform admins to invite users to organizations (clients/contractors) via WhatsApp (primary) or Email (fallback). Users are created in Supabase Auth ONLY after accepting the invitation. --- ## 1. Database Schema Changes ### Important Note on User Status The existing `user_status` enum (`invited`, `pending_setup`, `active`, `suspended`) is **KEPT**. - `user_invitations` table tracks PRE-acceptance state (before user exists) - `users.status` tracks POST-acceptance state (after user is created) - Flow: Invitation sent → User accepts → User created with status `active` → User can be `suspended` later ### New Table: `user_invitations` ```sql CREATE TYPE invitation_status AS ENUM ( 'pending', 'accepted', 'expired', 'cancelled' ); CREATE TYPE invitation_method AS ENUM ( 'whatsapp', 'email', 'both' ); CREATE TABLE user_invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Invitation Details email TEXT NOT NULL, phone TEXT, invited_role app_role NOT NULL, -- Organization Link (one must be set based on role) client_id UUID REFERENCES clients(id) ON DELETE CASCADE, contractor_id UUID REFERENCES contractors(id) ON DELETE CASCADE, -- Invitation Token & Status token TEXT NOT NULL UNIQUE, status invitation_status DEFAULT 'pending', invitation_method invitation_method DEFAULT 'whatsapp', -- Lifecycle invited_by_user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, invited_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()), expires_at TIMESTAMP WITH TIME ZONE NOT NULL, accepted_at TIMESTAMP WITH TIME ZONE, -- Delivery Tracking whatsapp_sent BOOLEAN DEFAULT FALSE, whatsapp_sent_at TIMESTAMP WITH TIME ZONE, email_sent BOOLEAN DEFAULT FALSE, email_sent_at TIMESTAMP WITH TIME ZONE, -- Metadata invitation_metadata JSONB DEFAULT '{}', created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()), updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()), -- Constraints CONSTRAINT chk_invitation_org_link CHECK ( (client_id IS NOT NULL AND contractor_id IS NULL) OR (client_id IS NULL AND contractor_id IS NOT NULL) OR (invited_role = 'platform_admin' AND client_id IS NULL AND contractor_id IS NULL) ), CONSTRAINT chk_invitation_contact CHECK ( email IS NOT NULL OR phone IS NOT NULL ) ); -- Indexes CREATE INDEX idx_invitations_token ON user_invitations(token) WHERE status = 'pending'; CREATE INDEX idx_invitations_email ON user_invitations(email, status); CREATE INDEX idx_invitations_status ON user_invitations(status, expires_at); CREATE INDEX idx_invitations_client ON user_invitations(client_id) WHERE client_id IS NOT NULL; CREATE INDEX idx_invitations_contractor ON user_invitations(contractor_id) WHERE contractor_id IS NOT NULL; -- Prevent duplicate pending invitations CREATE UNIQUE INDEX idx_invitations_unique_pending ON user_invitations(email, client_id, contractor_id) WHERE status = 'pending'; COMMENT ON TABLE user_invitations IS 'Tracks user invitations before Supabase Auth account creation'; ``` --- ## 2. Environment Variables Add to `.env`: ```env # Base URL for invitation links BASE_URL=https://swiftops.atomio.tech # Invitation Configuration INVITATION_TOKEN_EXPIRY_HOURS=72 # Notification Services (already exist) RESEND_API_KEY=re_xxx WASENDER_API_KEY=xxx WASENDER_PHONE_NUMBER=+254xxx ``` --- ## 3. Project Structure ``` src/app/ ├── services/ │ ├── __init__.py │ ├── invitation_service.py # Core invitation logic │ ├── notification_service.py # WhatsApp/Email delivery │ └── token_service.py # Token generation/validation ├── templates/ │ ├── emails/ │ │ ├── base.html # Base email template │ │ ├── invitation.html # User invitation email │ │ └── invitation_reminder.html │ └── whatsapp/ │ ├── invitation.txt # WhatsApp invitation message │ └── invitation_reminder.txt ├── models/ │ └── invitation.py # Invitation model ├── schemas/ │ └── invitation.py # Invitation schemas └── api/v1/ └── invitations.py # Invitation endpoints ``` --- ## 4. Implementation Steps ### Step 1: Database Migration - [ ] Create migration file for `user_invitations` table - [ ] Add `invitation_status` and `invitation_method` enums - [ ] Run migration ### Step 2: Models & Schemas - [ ] Create `src/app/models/invitation.py` - [ ] Create `src/app/schemas/invitation.py` with: - `InvitationCreate` - `InvitationResponse` - `InvitationAccept` - `InvitationValidate` ### Step 3: Core Services #### A. Token Service (`src/app/services/token_service.py`) - [ ] `generate_invitation_token()` - Secure random token (32 chars) - [ ] `validate_invitation_token(token)` - Check validity & expiry - [ ] `invalidate_token(token)` - Mark as used/expired #### B. Notification Service (`src/app/services/notification_service.py`) - [ ] `send_whatsapp_invitation(phone, name, invitation_url)` - WaSender integration - [ ] `send_email_invitation(email, name, invitation_url)` - Resend integration - [ ] `send_invitation(invitation, method)` - Smart routing (WhatsApp → Email fallback) - [ ] Template rendering with Jinja2 #### C. Invitation Service (`src/app/services/invitation_service.py`) - [ ] `create_invitation(email, phone, role, org_id, invited_by, method)` - [ ] `validate_invitation(token)` - Check token validity - [ ] `accept_invitation(token, user_data)` - Create Supabase user + local profile - [ ] `cancel_invitation(invitation_id)` - Cancel pending invitation - [ ] `resend_invitation(invitation_id)` - Resend notification - [ ] `cleanup_expired_invitations()` - Background job ### Step 4: Email Templates #### `src/app/templates/emails/base.html` - [ ] Create responsive HTML base template - [ ] Include SwiftOps branding - [ ] Header, footer, styling #### `src/app/templates/emails/invitation.html` - [ ] Personalized greeting - [ ] Organization name - [ ] Role information - [ ] CTA button with invitation URL - [ ] Expiry notice - [ ] Support contact ### Step 5: WhatsApp Templates #### `src/app/templates/whatsapp/invitation.txt` ``` Hi {{name}}, You've been invited to join {{organization_name}} on SwiftOps as a {{role}}. Click here to accept: {{invitation_url}} This invitation expires in {{expiry_hours}} hours. Need help? Contact support@atomio.tech ``` ### Step 6: API Endpoints #### `src/app/api/v1/invitations.py` - [ ] `POST /invitations` - Create invitation (platform_admin, client_admin, contractor_admin) - [ ] `GET /invitations` - List invitations (filtered by org) - [ ] `GET /invitations/{id}` - Get invitation details - [ ] `POST /invitations/{id}/resend` - Resend invitation - [ ] `DELETE /invitations/{id}` - Cancel invitation - [ ] `POST /invitations/validate` - Validate token (public endpoint) - [ ] `POST /invitations/accept` - Accept invitation & create user (public endpoint) ### Step 7: Update Existing Endpoints #### `src/app/api/v1/clients.py` - [ ] Add existence check in `create_client()` - [ ] Return existing client if found (with flag `already_exists: true`) #### `src/app/api/v1/contractors.py` - [ ] Add existence check in `create_contractor()` - [ ] Return existing contractor if found (with flag `already_exists: true`) #### `src/app/api/v1/auth.py` - [ ] Remove self-registration endpoint OR restrict to invitation-only - [ ] Update `register()` to require invitation token - [ ] Link new user to invitation record ### Step 8: Authorization Rules **Who can invite whom:** - `platform_admin` → Can invite anyone to any organization - `client_admin` → Can invite users to their client organization only - `contractor_admin` → Can invite users to their contractor organization only - Other roles → Cannot invite users **Validation:** - Check inviter has permission for target organization - Validate role is appropriate for organization type - Prevent duplicate pending invitations ### Step 9: Frontend Integration Points **Invitation Flow:** 1. User receives WhatsApp/Email with link: `https://swiftops.atomio.tech/accept-invitation?token=xxx` 2. Frontend validates token via `POST /invitations/validate` 3. Shows registration form pre-filled with email/phone 4. User completes profile (name, password) 5. Frontend submits to `POST /invitations/accept` 6. Backend creates Supabase user + local profile 7. Returns auth token 8. User is logged in --- ## 5. Security Considerations - [ ] Tokens are cryptographically secure (secrets.token_urlsafe) - [ ] Tokens expire after 72 hours (configurable) - [ ] Rate limiting on invitation endpoints - [ ] Validate email/phone format - [ ] Prevent invitation spam (max 3 pending per email) - [ ] Log all invitation activities - [ ] HTTPS only for invitation URLs --- ## 6. Testing Checklist - [ ] Unit tests for token generation/validation - [ ] Integration tests for invitation flow - [ ] Test WhatsApp delivery (WaSender) - [ ] Test email delivery (Resend) - [ ] Test fallback mechanism (WhatsApp → Email) - [ ] Test expiry handling - [ ] Test duplicate invitation prevention - [ ] Test authorization rules - [ ] Test Supabase user creation on acceptance --- ## 7. Background Jobs (Future Enhancement) - [ ] Cleanup expired invitations (daily cron) - [ ] Send reminder for pending invitations (24h before expiry) - [ ] Generate invitation analytics --- ## 8. API Response Examples ### Create Invitation ```json POST /api/v1/invitations { "email": "john@example.com", "phone": "+254712345678", "role": "field_agent", "contractor_id": "uuid", "invitation_method": "whatsapp" } Response: { "id": "uuid", "email": "john@example.com", "status": "pending", "invitation_url": "https://swiftops.atomio.tech/accept-invitation?token=xxx", "expires_at": "2025-11-19T12:00:00Z", "whatsapp_sent": true, "email_sent": false } ``` ### Accept Invitation ```json POST /api/v1/invitations/accept { "token": "xxx", "first_name": "John", "last_name": "Doe", "password": "SecurePass123!" } Response: { "access_token": "jwt_token", "token_type": "bearer", "user": { "id": "uuid", "email": "john@example.com", "full_name": "John Doe", "role": "field_agent", "contractor_id": "uuid" } } ``` --- ## 9. Implementation Order 1. **Phase 1: Foundation** (Day 1) - Database migration - Models & schemas - Token service 2. **Phase 2: Core Logic** (Day 2) - Invitation service - Notification service - Templates (email + WhatsApp) 3. **Phase 3: API** (Day 3) - Invitation endpoints - Update client/contractor endpoints - Authorization logic 4. **Phase 4: Integration** (Day 4) - Update auth flow - Frontend integration - Testing 5. **Phase 5: Polish** (Day 5) - Error handling - Logging - Documentation --- ## 10. Success Criteria ✅ Platform admin can invite users to any organization ✅ Org admins can invite users to their organization ✅ Invitations sent via WhatsApp (primary) or Email (fallback) ✅ Secure token-based invitation links ✅ Users created in Supabase ONLY after acceptance ✅ Duplicate invitation prevention ✅ Expiry handling ✅ Audit trail for all invitations ✅ Client/contractor existence checks before creation --- ## Notes - **WhatsApp Priority**: Default to WhatsApp to conserve Resend credits - **Fallback Logic**: If WhatsApp fails, automatically try email - **Token Security**: Use `secrets.token_urlsafe(32)` for tokens - **Expiry**: 72 hours default, configurable via env - **Email Domain**: swiftops@atomio.tech (configured in Resend) - **Base URL**: https://swiftops.atomio.tech (from env)