Files
KohakuHub/docs/api/invitations.md
2025-10-24 18:45:55 +08:00

18 KiB

title, description, icon
title description icon
Invitation API Invite users to organizations with flexible invitation types mail

Invitation API

Create and manage invitations to join organizations. Supports both single-use and reusable invitations.

Overview

KohakuHub invitation system supports:

  • Email-specific invitations: Sent to a specific email address
  • Reusable invitations: Can be used multiple times (link sharing)
  • Unlimited invitations: No usage limit (perfect for public organizations)
  • Expiration control: Set custom expiration (1-365 days)
  • Role assignment: Specify member role when creating invitation

Endpoints

Create Organization Invitation

Create an invitation for users to join an organization.

Endpoint: POST /api/invitations/org/{org_name}/create

Parameters:

Parameter Type Location Required Description
org_name string path Yes Organization name

Request Body:

{
  "email": "alice@example.com",
  "role": "member",
  "max_usage": null,
  "expires_days": 7
}
Field Type Required Description Default
email string (email) No Recipient email (null for reusable invites) null
role string Yes Member role: visitor, member, or admin -
max_usage integer No Max uses: null (single-use), -1 (unlimited), or positive number null
expires_days integer Yes Days until expiration (1-365) 7

Authentication: Required (admin or super-admin)

Response:

{
  "success": true,
  "token": "abc123xyz789...",
  "invitation_link": "http://localhost:28080/invite/abc123xyz789...",
  "expires_at": "2025-01-22T10:30:00Z",
  "max_usage": null,
  "is_reusable": false
}

Invitation Types:

  1. Single-use (default): max_usage: null - Can be used once, then expired
  2. Limited reusable: max_usage: 5 - Can be used up to 5 times
  3. Unlimited: max_usage: -1 - Can be used unlimited times (until expiration)

Email Behavior:

  • If email is provided and SMTP is configured, invitation email is sent automatically
  • If email is null, invitation link can be shared manually (for reusable invites)
  • Email-specific invitations check if user is already a member before creating

Error Responses:

Status Description
400 Invalid role, user already a member, or invalid parameters
403 Not authorized to invite members
404 Organization not found

Example:

import requests

BASE_URL = "http://localhost:28080"
TOKEN = "YOUR_TOKEN"

headers = {"Authorization": f"Bearer {TOKEN}"}

# Single-use email invitation
response = requests.post(
    f"{BASE_URL}/api/invitations/org/myorg/create",
    headers=headers,
    json={
        "email": "alice@example.com",
        "role": "member",
        "expires_days": 7
    }
)
invite = response.json()
print(f"Invitation sent to alice@example.com")

# Reusable link (5 uses)
response = requests.post(
    f"{BASE_URL}/api/invitations/org/myorg/create",
    headers=headers,
    json={
        "role": "member",
        "max_usage": 5,
        "expires_days": 30
    }
)
invite = response.json()
print(f"Share this link: {invite['invitation_link']}")

# Unlimited public invite
response = requests.post(
    f"{BASE_URL}/api/invitations/org/myorg/create",
    headers=headers,
    json={
        "role": "visitor",
        "max_usage": -1,
        "expires_days": 365
    }
)
invite = response.json()
print(f"Public invite link: {invite['invitation_link']}")

Get Invitation Details

Get invitation information without accepting it.

Endpoint: GET /api/invitations/{token}

Parameters:

Parameter Type Location Required Description
token string path Yes Invitation token

Authentication: Optional

Response:

{
  "action": "join_org",
  "org_name": "myorg",
  "role": "member",
  "email": "alice@example.com",
  "inviter_username": "admin",
  "expires_at": "2025-01-22T10:30:00Z",
  "is_expired": false,
  "is_available": true,
  "error_message": null,
  "max_usage": null,
  "usage_count": 0,
  "is_reusable": false
}

Field Descriptions:

Field Type Description
action string Invitation action type (join_org or register_account)
org_name string Organization name
role string Assigned role
email string Target email (null for reusable invites)
inviter_username string User who created the invitation
expires_at string ISO 8601 expiration timestamp
is_expired boolean Whether invitation has expired
is_available boolean Whether invitation can be used
error_message string Reason if not available (null if available)
max_usage integer Maximum uses (null = single-use, -1 = unlimited)
usage_count integer Number of times used
is_reusable boolean Whether invitation can be used multiple times

Availability Conditions:

An invitation is available if ALL of the following are true:

  • Not expired (expires_at > now)
  • Usage count < max_usage (or max_usage is -1 for unlimited)
  • For single-use invitations: not yet used

Example:

response = requests.get(
    f"{BASE_URL}/api/invitations/{token}"
)
invite = response.json()

if invite["is_available"]:
    print(f"Valid invite to join {invite['org_name']} as {invite['role']}")
else:
    print(f"Invite not available: {invite['error_message']}")

if invite["is_reusable"]:
    print(f"Used {invite['usage_count']} / {invite['max_usage']} times")

Accept Invitation

Accept an invitation and join the organization.

Endpoint: POST /api/invitations/{token}/accept

Parameters:

Parameter Type Location Required Description
token string path Yes Invitation token

Authentication: Required

Response:

{
  "success": true,
  "message": "You have successfully joined myorg as a member",
  "org_name": "myorg",
  "role": "member"
}

Behavior:

  • Adds authenticated user to organization with specified role
  • Increments usage count for reusable invitations
  • Marks single-use invitations as used
  • Checks if user is already a member (returns error if so)

Error Responses:

Status Description
400 Invitation expired, max usage reached, or already a member
401 Authentication required
404 Invitation not found

Example:

# User accepts invitation
response = requests.post(
    f"{BASE_URL}/api/invitations/{token}/accept",
    headers={"Authorization": f"Bearer {USER_TOKEN}"}
)

result = response.json()
print(result["message"])

List Organization Invitations

List all invitations for an organization (admin only).

Endpoint: GET /api/invitations/org/{org_name}/list

Parameters:

Parameter Type Location Required Description
org_name string path Yes Organization name

Authentication: Required (admin or super-admin)

Response:

{
  "invitations": [
    {
      "id": 1,
      "token": "abc123...",
      "email": "alice@example.com",
      "role": "member",
      "created_by": "admin",
      "created_at": "2025-01-15T10:30:00Z",
      "expires_at": "2025-01-22T10:30:00Z",
      "max_usage": null,
      "usage_count": 0,
      "is_reusable": false,
      "is_available": true,
      "used_at": null,
      "is_pending": true
    },
    {
      "id": 2,
      "token": "xyz789...",
      "email": null,
      "role": "visitor",
      "created_by": "admin",
      "created_at": "2025-01-10T10:30:00Z",
      "expires_at": "2026-01-10T10:30:00Z",
      "max_usage": -1,
      "usage_count": 42,
      "is_reusable": true,
      "is_available": true,
      "used_at": null,
      "is_pending": false
    }
  ]
}

Field Descriptions:

Field Description
is_pending True if available and never used (single-use invites only)
is_available Whether invitation can still be used
used_at Timestamp when first used (for single-use invites)

Example:

response = requests.get(
    f"{BASE_URL}/api/invitations/org/myorg/list",
    headers={"Authorization": f"Bearer {TOKEN}"}
)

invitations = response.json()["invitations"]

# Filter by status
pending = [i for i in invitations if i["is_pending"]]
active_reusable = [i for i in invitations if i["is_reusable"] and i["is_available"]]
expired = [i for i in invitations if not i["is_available"]]

print(f"Pending: {len(pending)}")
print(f"Active reusable: {len(active_reusable)}")
print(f"Expired: {len(expired)}")

Delete Invitation

Cancel/delete an invitation (admin only).

Endpoint: DELETE /api/invitations/{token}

Parameters:

Parameter Type Location Required Description
token string path Yes Invitation token

Authentication: Required (admin or super-admin)

Response:

{
  "success": true,
  "message": "Invitation deleted successfully"
}

Use Cases:

  • Cancel pending invitations
  • Remove expired invitations
  • Revoke reusable invitation links

Error Responses:

Status Description
403 Not authorized to delete this invitation
404 Invitation not found

Example:

response = requests.delete(
    f"{BASE_URL}/api/invitations/{token}",
    headers={"Authorization": f"Bearer {TOKEN}"}
)
print("Invitation cancelled")

Usage Examples

Managing Invitations Workflow

import requests
from datetime import datetime

BASE_URL = "http://localhost:28080"
TOKEN = "YOUR_TOKEN"

headers = {"Authorization": f"Bearer {TOKEN}"}

class InvitationManager:
    def __init__(self, base_url: str, token: str):
        self.base_url = base_url
        self.headers = {"Authorization": f"Bearer {token}"}

    def create_invite(self, org_name: str, email: str = None, role: str = "member",
                     max_usage: int = None, expires_days: int = 7):
        """Create invitation."""
        response = requests.post(
            f"{self.base_url}/api/invitations/org/{org_name}/create",
            headers=self.headers,
            json={
                "email": email,
                "role": role,
                "max_usage": max_usage,
                "expires_days": expires_days
            }
        )
        response.raise_for_status()
        return response.json()

    def list_invites(self, org_name: str):
        """List all organization invitations."""
        response = requests.get(
            f"{self.base_url}/api/invitations/org/{org_name}/list",
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()["invitations"]

    def get_invite_details(self, token: str):
        """Get invitation details."""
        response = requests.get(
            f"{self.base_url}/api/invitations/{token}"
        )
        response.raise_for_status()
        return response.json()

    def cancel_invite(self, token: str):
        """Cancel/delete invitation."""
        response = requests.delete(
            f"{self.base_url}/api/invitations/{token}",
            headers=self.headers
        )
        response.raise_for_status()
        return response.json()

    def cleanup_expired(self, org_name: str):
        """Delete all expired invitations."""
        invites = self.list_invites(org_name)
        expired = [i for i in invites if not i["is_available"]]

        for invite in expired:
            self.cancel_invite(invite["token"])
            print(f"Deleted expired invite: {invite['token'][:8]}...")

        return len(expired)

# Usage
mgr = InvitationManager(BASE_URL, TOKEN)

# Create different types of invitations
email_invite = mgr.create_invite("myorg", email="alice@example.com", role="member")
reusable_invite = mgr.create_invite("myorg", role="member", max_usage=10, expires_days=30)
public_invite = mgr.create_invite("myorg", role="visitor", max_usage=-1, expires_days=365)

print(f"Email invite: {email_invite['invitation_link']}")
print(f"Reusable invite (10 uses): {reusable_invite['invitation_link']}")
print(f"Public invite (unlimited): {public_invite['invitation_link']}")

# List and cleanup
invites = mgr.list_invites("myorg")
print(f"Total invitations: {len(invites)}")

expired_count = mgr.cleanup_expired("myorg")
print(f"Cleaned up {expired_count} expired invitations")

Invitation Statistics

def get_invitation_stats(org_name: str, token: str):
    """Get comprehensive invitation statistics."""
    headers = {"Authorization": f"Bearer {token}"}
    response = requests.get(
        f"{BASE_URL}/api/invitations/org/{org_name}/list",
        headers=headers
    )
    invites = response.json()["invitations"]

    stats = {
        "total": len(invites),
        "pending": 0,
        "used": 0,
        "expired": 0,
        "reusable_active": 0,
        "total_uses": 0,
        "by_role": {"visitor": 0, "member": 0, "admin": 0}
    }

    for invite in invites:
        # Count by status
        if invite["is_pending"]:
            stats["pending"] += 1
        elif not invite["is_available"]:
            stats["expired"] += 1
        elif invite["is_reusable"]:
            stats["reusable_active"] += 1

        if invite["usage_count"] > 0:
            stats["used"] += 1
            stats["total_uses"] += invite["usage_count"]

        # Count by role
        stats["by_role"][invite["role"]] += 1

    return stats

# Usage
stats = get_invitation_stats("myorg", TOKEN)
print(f"Total invitations: {stats['total']}")
print(f"  Pending: {stats['pending']}")
print(f"  Used: {stats['used']} ({stats['total_uses']} total uses)")
print(f"  Expired: {stats['expired']}")
print(f"  Active reusable: {stats['reusable_active']}")
print(f"\nBy role:")
for role, count in stats["by_role"].items():
    print(f"  {role}: {count}")

User Accepting Invitation

def accept_invite_flow(token: str, user_token: str):
    """Complete invitation acceptance flow."""

    # 1. Check invitation details (no auth needed)
    response = requests.get(f"{BASE_URL}/api/invitations/{token}")
    invite = response.json()

    if not invite["is_available"]:
        print(f"Error: {invite['error_message']}")
        return False

    print(f"You are invited to join {invite['org_name']} as {invite['role']}")
    print(f"Invited by: {invite['inviter_username']}")
    print(f"Expires: {invite['expires_at']}")

    if invite["is_reusable"]:
        print(f"This is a reusable link (used {invite['usage_count']} times)")

    # 2. Accept invitation (requires authentication)
    response = requests.post(
        f"{BASE_URL}/api/invitations/{token}/accept",
        headers={"Authorization": f"Bearer {user_token}"}
    )

    if response.status_code == 200:
        result = response.json()
        print(f"\nSuccess! {result['message']}")
        return True
    else:
        error = response.json()
        print(f"\nFailed: {error.get('detail', 'Unknown error')}")
        return False

# Usage
success = accept_invite_flow("abc123xyz789...", "USER_TOKEN")

JavaScript/TypeScript Example

class InvitationAPI {
  constructor(baseURL, token) {
    this.baseURL = baseURL;
    this.headers = {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    };
  }

  async createInvite(orgName, options = {}) {
    const {
      email = null,
      role = 'member',
      maxUsage = null,
      expiresDays = 7
    } = options;

    const response = await fetch(
      `${this.baseURL}/api/invitations/org/${orgName}/create`,
      {
        method: 'POST',
        headers: this.headers,
        body: JSON.stringify({
          email,
          role,
          max_usage: maxUsage,
          expires_days: expiresDays
        })
      }
    );
    return await response.json();
  }

  async listInvites(orgName) {
    const response = await fetch(
      `${this.baseURL}/api/invitations/org/${orgName}/list`,
      { headers: this.headers }
    );
    const data = await response.json();
    return data.invitations;
  }

  async getInviteDetails(token) {
    const response = await fetch(
      `${this.baseURL}/api/invitations/${token}`
    );
    return await response.json();
  }

  async acceptInvite(token) {
    const response = await fetch(
      `${this.baseURL}/api/invitations/${token}/accept`,
      {
        method: 'POST',
        headers: this.headers
      }
    );
    return await response.json();
  }

  async cancelInvite(token) {
    const response = await fetch(
      `${this.baseURL}/api/invitations/${token}`,
      {
        method: 'DELETE',
        headers: this.headers
      }
    );
    return await response.json();
  }
}

// Usage
const inviteAPI = new InvitationAPI('http://localhost:28080', 'YOUR_TOKEN');

// Create invitations
const emailInvite = await inviteAPI.createInvite('myorg', {
  email: 'alice@example.com',
  role: 'member'
});

const reusableInvite = await inviteAPI.createInvite('myorg', {
  role: 'member',
  maxUsage: 10,
  expiresDays: 30
});

console.log('Share this link:', reusableInvite.invitation_link);

// List and manage
const invites = await inviteAPI.listInvites('myorg');
const pending = invites.filter(i => i.is_pending);
console.log(`${pending.length} pending invitations`);

CLI Usage

See CLI Documentation for command-line interface:

# Create email invitation
kohub-cli invite create myorg alice@example.com --role member --expires 7

# Create reusable invitation
kohub-cli invite create myorg --role member --max-usage 10 --expires 30

# List invitations
kohub-cli invite list myorg

# Cancel invitation
kohub-cli invite cancel TOKEN

# Accept invitation (as user)
kohub-cli invite accept TOKEN

Next Steps