intent/intent/orgs/admin.py
Alec Perkins 99f810d043 Harden auth, invites and deployment after security review
- Signed-out visitors get the same login redirect for missing and
  private projects, so the redirect no longer reveals private ones.
- Production refuses the console email backend; sign-in codes must
  never reach the logs.
- Database cache shared by all workers for allauth rate limits; invite
  sending throttled per sender and per recipient.
- Email invite links are claimed on first use and the token rotated,
  so a token copied from request logs is worthless.
- allauth password URLs removed; adding an email requires a fresh code
  and notifies existing addresses.
- Markdown drops remote images until uploads exist.
- Project and org slugs lowercased; only superusers can grant staff
  status or permissions in the admin.
- Container runs as a non-root user; CI actions pinned to commits.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-27 08:42:19 -04:00

54 lines
1.7 KiB
Python

from django import forms
from django.contrib import admin
from django.contrib.auth import get_user_model
from .models import Organization, OrgInvite, OrgMembership, Role
class OrganizationAddForm(forms.ModelForm):
owner = forms.ModelChoiceField(
queryset=get_user_model().objects.filter(is_staff=False),
help_text="The first owner. They can invite everyone else.",
)
class Meta:
model = Organization
fields = ["name", "slug", "owner"]
def clean_slug(self):
return self.cleaned_data["slug"].lower()
class OrgMembershipInline(admin.TabularInline):
model = OrgMembership
extra = 0
autocomplete_fields = ["user"]
@admin.register(Organization)
class OrganizationAdmin(admin.ModelAdmin):
list_display = ["name", "slug", "created_at"]
search_fields = ["name", "slug"]
prepopulated_fields = {"slug": ["name"]}
inlines = [OrgMembershipInline]
def get_form(self, request, obj=None, **kwargs):
if obj is None:
kwargs["form"] = OrganizationAddForm
return super().get_form(request, obj, **kwargs)
def get_inlines(self, request, obj):
# On add, the owner field replaces the membership inline.
return self.inlines if obj else []
def save_model(self, request, obj, form, change):
super().save_model(request, obj, form, change)
if not change:
OrgMembership.objects.create(org=obj, user=form.cleaned_data["owner"], role=Role.OWNER)
@admin.register(OrgInvite)
class OrgInviteAdmin(admin.ModelAdmin):
list_display = ["org", "recipient", "role", "status", "expires_at"]
list_filter = ["status"]
readonly_fields = ["token_hash"]