Security Implementation Guide

Complete Security Implementation Guide

Public App + Invite-Only Admin + Three-Tier Roles

This guide documents the tested and working security architecture for applications that need:

  • Public guest portal (no login required for form submission)
  • Private admin area (invite-only access)
  • Three-tier role system (User → Super User → Admin)

Architecture Overview

Public Guest (No Auth) → GuestAgreementForm (token-based access)
                ↓
Authenticated User → Base44 Login
                ↓
         Invite Validation Check
         ├─ NOT invited → Immediate logout to home
         ├─ Invited as 'user' → Blank page (no access)
         ├─ Invited as 'super_user' → Full workflow access
         └─ Invited as 'admin' → Full system access

Step 1: Define the User Entity with Three-Tier Roles

File: entities/User.json

{
  "name": "User",
  "type": "object",
  "properties": {
    "role": {
      "type": "string",
      "enum": ["user", "super_user", "admin"],
      "default": "user",
      "description": "User role level"
    }
  }
}

Role Definitions:

  • user — Standard login, sees "Access Restricted" message. No workflow access.
  • super_user — Can draft agreements, manage workflows, send emails.
  • admin — Full system access: user management, security monitoring, email templates.

Step 2: Create Invite Tracking & Security Entities

File: entities/PendingInvite.json

{
  "name": "PendingInvite",
  "type": "object",
  "properties": {
    "email": {
      "type": "string"
    },
    "role": {
      "type": "string",
      "enum": ["user", "super_user", "admin"]
    },
    "sent_at": {
      "type": "string",
      "format": "date-time"
    }
  },
  "required": ["email", "role"]
}

File: entities/SecurityAlert.json

{
  "name": "SecurityAlert",
  "type": "object",
  "properties": {
    "alert_type": {
      "type": "string",
      "enum": ["suspicious_login", "unauthorized_access", "multiple_failures"]
    },
    "email": {
      "type": "string"
    },
    "description": {
      "type": "string"
    },
    "severity": {
      "type": "string",
      "enum": ["info", "warning", "critical"],
      "default": "warning"
    },
    "resolved": {
      "type": "boolean",
      "default": false
    }
  },
  "required": ["alert_type", "email", "description"]
}

File: entities/LoginAttempt.json

{
  "name": "LoginAttempt",
  "type": "object",
  "properties": {
    "email": {
      "type": "string"
    },
    "success": {
      "type": "boolean",
      "default": false
    },
    "ip_address": {
      "type": "string"
    }
  },
  "required": ["email", "success"]
}

Step 3: Invite Validation Hook (The Core Security Layer)

File: lib/useInviteValidation.js

This hook must be added to ALL protected pages. It:

  1. Skips validation for admins and super_users
  2. Checks if regular users are in the PendingInvite table
  3. IMMEDIATELY LOGS OUT and redirects uninvited users
import { useEffect } from "react";
import { base44 } from "@/api/base44Client";

export function useInviteValidation() {
  useEffect(() => {
    (async () => {
      try {
        const user = await base44.auth.me();
        
        // Admins and super users always pass
        if (user?.role === 'admin' || user?.role === 'super_user') {
          return;
        }

        // Regular users must have a pending invite
        if (user?.email) {
          const invites = await base44.entities.PendingInvite.filter({
            email: user.email.toLowerCase()
          });

          if (!invites.length) {
            // Not invited - log out immediately
            await base44.entities.SecurityAlert.create({
              alert_type: 'unauthorized_access',
              email: user.email,
              description: `Uninvited user ${user.email} attempted to access protected page`,
              severity: 'warning'
            });
            base44.auth.logout("/GuestAgreementForm");
          }
        }
      } catch (err) {
        console.error("Invite validation failed:", err);
      }
    })();
  }, []);
}

Step 4: Layout Access Control

File: layout.jsx - Three-Tier Rendering

The layout enforces the security model at the UI level:

For Standard Users:

if (isStandardUser && !isPublicPage) {
  return (
    <div className="min-h-screen bg-[#F8F7F4] flex flex-col items-center justify-center">
      <h1 className="text-2xl font-semibold text-stone-900">Access Restricted</h1>
      <p className="text-sm text-stone-400 mt-2">You don't have access to this area yet.</p>
      <button onClick={() => base44.auth.logout("/GuestAgreementForm")}>
        Sign Out
      </button>
    </div>
  );
}

For Non-Invited Users:

if (!isAdmin && !isSuperUser) {
  return (
    <div className="min-h-screen bg-[#F8F7F4] flex flex-col items-center justify-center">
      <h1 className="text-2xl font-semibold text-stone-900">Access Denied</h1>
      <p className="text-sm text-stone-400 mt-2">You don't have permission to access this portal.</p>
      <button onClick={() => base44.auth.logout("/GuestAgreementForm")}>
        Sign Out
      </button>
    </div>
  );
}

Key Implementation Detail: Use base44.auth.logout("/GuestAgreementForm") with a public page redirect. This ensures:

  • The session is properly cleared
  • User cannot bypass auth by reloading
  • They land on a public page (no auth check loop)

Step 5: User Invitation Flow

File: components/admin/UserManagement.jsx

The admin invites users and assigns roles:

// Invite user with role
await base44.users.inviteUser(email, role);

// Record the pending invite
await base44.entities.PendingInvite.create({
  email: email.toLowerCase(),
  role: role,
  sent_at: new Date().toISOString()
});

When the invited user signs up:

  1. Base44 creates their User account
  2. useInviteValidation checks PendingInvite table
  3. If found, they're allowed in based on their role
  4. If not found, they're logged out immediately

Step 6: Security Monitoring Dashboard

File: components/admin/SecurityMonitor.jsx

Admins see:

  • Unresolved security alerts (unauthorized access attempts)
  • Recent login attempts table
  • Active alert count badge

Step 7: Authorization Codes for Sensitive Operations

File: components/admin/ApprovalAuthCodeManager.jsx and DeleteAuthCodeManager.jsx

For critical operations (approving agreements, deleting completed records):

  1. Admin generates a one-time code
  2. Code is stored in AppSettings entity
  3. User must enter code to perform action
  4. Provides audit trail and prevents accidental operations

Testing the Implementation

Test 1: Unauthorized Signup

  • Sign up with new email (not invited)
  • Verify: Can log in, but see "Access Denied" screen
  • Verify: Clicking "Sign Out" logs out completely (no login loop)

Test 2: Authorized Super User

  • Admin invites user@example.com as super_user
  • User signs up with that email
  • Verify: Sees full dashboard and workflows

Test 3: Authorized Admin

  • Admin invites admin2@example.com as admin
  • User signs up and sees AdminPanel access

Test 4: Standard User

  • Admin invites user@example.com as user
  • User signs up and sees "Access Restricted" message
  • Verify: Cannot access any workflows

Complete Security Checklist

  • [ ] PendingInvite entity created
  • [ ] SecurityAlert entity created
  • [ ] User entity has role field
  • [ ] useInviteValidation hook added to ALL protected pages
  • [ ] Layout.jsx has three-tier role checks
  • [ ] Logout uses base44.auth.logout("/GuestAgreementForm")
  • [ ] UserManagement component invites users and creates PendingInvite records
  • [ ] SecurityMonitor dashboard displays alerts
  • [ ] Tested: Uninvited users cannot access portal
  • [ ] Tested: Logout clears session completely

Deployment Checklist

  1. Deploy entities first (PendingInvite, SecurityAlert, etc.)
  2. Deploy backend functions (invite validation)
  3. Deploy layout changes with proper logout handling
  4. Invite known users via UserManagement dashboard
  5. Test with fake signup to verify security
  6. Monitor SecurityAlert dashboard for unauthorized attempts
  7. Share this guide across all apps

Key Implementation Details

Critical: Logout must use redirect parameter

// CORRECT - clears session and redirects to public page
base44.auth.logout("/GuestAgreementForm");

// INCORRECT - does not properly clear session
base44.auth.logout();
window.location.href = '/';

Critical: useInviteValidation must run on protected pages Add this single line to every admin/super_user page:

import { useInviteValidation } from "@/lib/useInviteValidation";

export default function Dashboard() {
  useInviteValidation(); // ← Add this
  // ... rest of component
}

Important: Guest Portal is Completely Separate from This Security System

Clients completing agreement forms are NOT affected by any of this security system.

The GuestAgreementForm page is a public page — it has:

  • No login requirement
  • No invite check
  • No role validation
  • No useInviteValidation hook

Clients access their form via a secure token-based URL sent to them by email:

https://yourapp.com/GuestAgreementForm?id={agreementId}&token={secureToken}

The token is validated server-side. As long as the URL and token are valid, clients can complete their agreement without any account, login, or invite. This entire security system only applies to internal staff accessing the admin portal.


Troubleshooting (Admin/Staff Access Only)

Staff member can't access portal even though invited:

  • Check PendingInvite table — email must match exactly (lowercase)
  • Check User entity — role must be user/super_user/admin
  • Test with a fresh browser (clear cookies)

Staff member stuck in login loop after logout:

  • Verify logout uses redirect parameter: base44.auth.logout("/GuestAgreementForm")
  • Check that "/" route redirects to a public page, not a protected one
  • Ensure public pages do NOT have the useInviteValidation hook

Staff member sees blank page instead of "Access Denied":

  • Check layout.jsx — verify all three role checks are in place
  • Verify isAdmin, isSuperUser checks happen BEFORE layout renders
  • Make sure user.role is being set correctly in User entity

Questions?

This guide is production-tested. Customize logos, colors, and authorized pages as needed. Share across all applications that need invite-only admin access.