Spaces:
Sleeping
Sleeping
| # 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) | |