- 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>
54 lines
1.7 KiB
Python
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"]
|