Files
ServerKit/backend/app/api/invitations.py
T
Juan Denis b7fab1152b Add invitations, email models, admin APIs
Introduce team invitation support and a revamped email subsystem plus admin features. Adds Invitation model and API endpoints (create, list, revoke, resend, validate) and integrates invitation validation into user registration. Replaces old email account model with richer email models (EmailDomain, EmailAccount, EmailAlias, EmailForwardingRule, DNSProviderConfig) and updates models __init__. Refactors email API to manage domains, accounts, aliases, forwarding, DNS providers, spamassassin, Roundcube/Postfix services and queue/log endpoints. Adds several backend services (invitation, DNS provider, DKIM, Dovecot, Postfix, Roundcube, SpamAssassin, permission service) and a DB migration for permissions/invitations. Enhances admin API with permission management and activity dashboard endpoints, extends audit log actions, and adds a permission_required RBAC decorator. Includes frontend additions for settings/invitations UI and miscellaneous config/service updates and tests.
2026-03-04 02:34:49 -05:00

141 lines
4.4 KiB
Python

"""Invitation API endpoints for team invitations."""
from flask import Blueprint, request, jsonify
from flask_jwt_extended import get_jwt_identity
from app import db
from app.models import AuditLog
from app.middleware.rbac import admin_required
from app.services.invitation_service import InvitationService
from app.services.audit_service import AuditService
invitations_bp = Blueprint('invitations', __name__)
@invitations_bp.route('/', methods=['GET'])
@admin_required
def list_invitations():
"""List all invitations, optionally filtered by status."""
status = request.args.get('status')
invitations = InvitationService.list_invitations(status=status)
return jsonify({
'invitations': [inv.to_dict() for inv in invitations]
}), 200
@invitations_bp.route('/', methods=['POST'])
@admin_required
def create_invitation():
"""Create a new invitation."""
data = request.get_json()
current_user_id = get_jwt_identity()
if not data:
return jsonify({'error': 'No data provided'}), 400
email = data.get('email') # Optional
role = data.get('role', 'developer')
permissions = data.get('permissions')
expires_in_days = data.get('expires_in_days', 7)
result = InvitationService.create_invitation(
email=email,
role=role,
permissions=permissions,
invited_by=current_user_id,
expires_in_days=expires_in_days
)
if not result['success']:
return jsonify({'error': result['error']}), 400
invitation = result['invitation']
# Build invite URL
base_url = request.host_url.rstrip('/')
invite_url = f"{base_url}/register?invite={invitation.token}"
# Try to send email if email provided
email_sent = False
email_error = None
if email:
email_result = InvitationService.send_invitation_email(invitation, base_url)
email_sent = email_result['success']
if not email_sent:
email_error = email_result.get('error')
# Audit log
AuditService.log(
action=AuditLog.ACTION_INVITATION_CREATE,
user_id=current_user_id,
target_type='invitation',
target_id=invitation.id,
details={'email': email, 'role': role, 'email_sent': email_sent}
)
db.session.commit()
return jsonify({
'message': 'Invitation created successfully',
'invitation': invitation.to_dict(),
'invite_url': invite_url,
'email_sent': email_sent,
'email_error': email_error,
}), 201
@invitations_bp.route('/<int:invitation_id>', methods=['DELETE'])
@admin_required
def revoke_invitation(invitation_id):
"""Revoke a pending invitation."""
current_user_id = get_jwt_identity()
result = InvitationService.revoke_invitation(invitation_id)
if not result['success']:
return jsonify({'error': result['error']}), 400
AuditService.log(
action=AuditLog.ACTION_INVITATION_REVOKE,
user_id=current_user_id,
target_type='invitation',
target_id=invitation_id,
)
db.session.commit()
return jsonify({'message': 'Invitation revoked'}), 200
@invitations_bp.route('/resend/<int:invitation_id>', methods=['POST'])
@admin_required
def resend_invitation(invitation_id):
"""Resend invitation email."""
from app.models import Invitation
invitation = Invitation.query.get(invitation_id)
if not invitation:
return jsonify({'error': 'Invitation not found'}), 404
if invitation.status != Invitation.STATUS_PENDING:
return jsonify({'error': 'Can only resend pending invitations'}), 400
if not invitation.email:
return jsonify({'error': 'Invitation has no email address'}), 400
base_url = request.host_url.rstrip('/')
result = InvitationService.send_invitation_email(invitation, base_url)
if not result['success']:
return jsonify({'error': result.get('error', 'Failed to send email')}), 500
return jsonify({'message': 'Invitation email resent'}), 200
@invitations_bp.route('/validate/<token>', methods=['GET'])
def validate_invitation(token):
"""Validate an invite token (public endpoint for registration page)."""
invitation = InvitationService.validate_token(token)
if not invitation:
return jsonify({'error': 'Invalid or expired invitation'}), 404
return jsonify({
'valid': True,
'email': invitation.email,
'role': invitation.role,
}), 200