Commit Graph

12 Commits

Author SHA1 Message Date
6f66df3ba4 feat(ui): platform control panel and styled auth screens
Add the `controlpanel` app: a platform-wide (not club-scoped) admin panel for
creating clubs, archiving/restoring them, managing club admins, and per-club
statistics (members, teams & staff, events, shop). Statistics are annotated in
one query so the club list cannot fan out into N+1, and are returned as stat
*groups* so growing the domain means adding one entry.

Two access rules, both enforced by PlatformStaffRequiredMixin:
- staff only (is_staff/is_superuser); anonymous are sent to login, signed-in
  non-staff get a 403. Staff already need a second factor, so the panel is
  2FA-protected for free.
- base domain only: the panel manages *all* clubs, so it 404s if the tenant
  middleware resolved a club from the subdomain.

Granting admin to an unknown email creates the account (unusable password —
they set one via password reset) and the Member behind it, since a ClubRole
hangs off a Member. A member who already holds a role is promoted in place,
because there is only one role per member per club.

UI is Tailwind + daisyUI. allauth ships an element system, so overriding
allauth/layouts/base.html plus ~13 element partials restyles *every* auth and
2FA screen at once — login, signup, password reset, the 2FA challenge, TOTP
enrolment, passkeys and recovery codes — rather than templating 20+ pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 15:29:24 +02:00
10736fd5ee feat(auth): two-factor authentication (TOTP, passkeys, recovery codes)
Adopt django-allauth with allauth.mfa, giving TOTP, WebAuthn passkeys and
recovery codes — and the signup/password-reset flows we'll need next. There was
no login UI at all before this (only /admin/), so this brings the auth stack.

The critical piece is authentication/adapters.py. A passkey is bound to a
WebAuthn Relying Party ID (a domain), and allauth derives that from the request
host — which under our subdomain tenancy would bind a passkey to a *single* club
(ajax-united.clubmanager.app) and silently fail at every other one. The adapter
pins the RP ID to CLUBMANAGER_BASE_DOMAIN so one passkey works across all clubs.
Note this cuts both ways: changing that base domain invalidates every existing
passkey.

RequireMFAMiddleware makes a second factor mandatory for anyone who can change
other people's data — Django staff/superusers and holders of an elevated
ClubRole (ADMIN/EDITOR), via the access service — while leaving it optional for
regular members. /admin/login/ is routed through allauth, since Django's own
admin login knows nothing about second factors.

allauth is installed WITHOUT django.contrib.sites (optional since allauth 65),
so ARCHITECTURE.md's rejection of the Sites framework stands and no Club.site
bridge is needed. Sessions are shared across club subdomains, matching the
one-passkey-everywhere model; tenancy still scopes what you can see.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 14:43:55 +02:00
57f20fe544 chore(shop): scaffold empty shop app
startapp scaffold registered in INSTALLED_APPS; no models yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:13:32 +02:00
5dd3715c1f feat(formbuilder): fix tenancy bugs, add admin + services
Fix multi-tenancy/integrity bugs in the models: Form.slug and Field.key
were globally unique (unique=True), so two clubs couldn't reuse a form
slug and two forms couldn't reuse a field key — make slug unique per club
(constraint already present) and key unique per form. Add a
(submission, field) uniqueness constraint on Answer.

Register all four models in the admin (Field inline on Form, Answer inline
on Submission) and add formbuilder to the admin registration smoke test.

Add a service layer:
- submit_form(form, member, data): enforces is_active / login_required /
  open window / max_submissions, validates required + choice fields, and
  writes a Submission with Answers atomically (FormSubmissionError carries
  per-field errors).
- build_form(form): a live django.forms.Form built from a Form's active
  fields, mapping each FieldType to the matching form field.
- form_report(form): a tabular overview of every submission's answers plus
  per-value tallies for choice-type fields.

Full suite at 100% coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 21:13:16 +02:00
866716d196 feat(events): team/invited audience with attendance sync
Rework an event's audience: replace the single team FK with a teams M2M
plus invited_members and excluded_members, and add a season FK (derived
from the start date when blank) so team rosters resolve correctly. A data
migration copies existing team -> teams.

Add an attendance sync service: the effective audience is the union of the
teams' rosters for the event's season plus invited, minus excluded;
sync_event_attendances reconciles Attendance rows for future events only,
adding NO_RESPONSE rows for new members and hard-deleting rows for members
no longer invited. Signals drive it: editing an event or its audience
re-syncs that event, and adding/removing a team-roster member re-syncs
that team's future events.

Register all events models in the admin (with an attendance inline) and
add events to the admin registration smoke test. Add Season.covering()
and pillow (Opponent.logo ImageField). Full suite at 100% coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 17:44:48 +02:00
076a9cacbc feat(teams): add teams app with roster and staff assignments
Introduce the teams app: Team and Position (both club-scoped, with
per-club unique names), TeamMembership (season-scoped roster with jersey
number + captain flags, unique member and jersey per team/season), and
StaffAssignment (coaching/staff, filtered to staff positions). All
uniqueness expressed as UniqueConstraints.

Register every model in the admin (with roster + staff inlines on Team)
and cover the models with tests. Fix two model bugs surfaced by the
system check: StaffAssignment.position reused TeamMembership's reverse
accessor (Position.team_memberships) and duplicated its constraint name.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 16:47:52 +02:00
72e7b5e070 feat(tenancy): resolve active club from request subdomain
Add row-based multi-tenancy plumbing keyed on Club as the tenant root:

- ClubTenantMiddleware maps the request's subdomain to a Club by slug,
  storing it on request.club and in a contextvar so service-layer code
  and management commands can read it via get_current_club(). Resolution
  honours CLUBMANAGER_BASE_DOMAIN (e.g. ajax-united.clubmanager.app),
  falling back to generic slug.example.com hosts, and ignores the bare
  base domain, www, and unknown slugs.
- Club gains a unique slug (auto-derived from name on save) plus a
  ClubManager.current() accessor for the active tenant.
- ClubScopedModel gets a TenantQuerySet (.for_club()/.current()) and
  auto-fills club from the active context on save.

Contextvar helpers live in club.tenancy; Club is imported lazily there
and in clubmanager.base to avoid an import cycle with club.models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 15:32:05 +02:00
f3b1ae7e82 Complete people-domain move into members app; keep names on Member
Finish relocating Member/Family/FamilyMembership from authentication into
a dedicated members app, and revert the half-applied move of member names
onto the global User.

- members: add first_name/last_name back to Member (the whole codebase —
  tests, CSV importer, club app, admin — assumes them, and login-less
  children in families need a name); restore local ordering/index and the
  member__last_name lookups in Family.__str__ and FamilyMembership.
- authentication: drop first_name/last_name from User; get_full_name/
  get_short_name delegate to the linked member, else fall back to email.
- migrations: create members.0001_initial, repoint club.ClubMembership FK
  (club.0005), delete the models from authentication (0004, rewritten to
  plain DeleteModel ops in child-first order to avoid a SQLite table-remake
  crash).
- fix stale imports across apps (authentication/club tests, CSV importer)
  and missing imports/URLs in members tests; add missing _ import in
  members/admin.py; export MemberCsvImporter from members.services.

Full suite green (64 tests), ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 00:16:41 +02:00
8f71bb74c0 Refactor accounts app into authentication and club apps
- Split the accounts app into new authentication and club apps for better separation of concerns.
- Migrate the custom User, Member, and Family models to the authentication app.
- Introduce Club and ClubMembership models in the club app.
- Refactor Family model to use UUID as the primary key and consolidate family-role relationships into a new FamilyMembership model.
- Update tests, managers, and migrations to align with the new structure.
2026-07-02 09:40:06 +02:00
2ead824c5d Add accounts app: custom User, Member, families & guardianship
Introduce the foundational accounts app:
- Custom email-as-username User (AbstractBaseUser + PermissionsMixin) set
  as AUTH_USER_MODEL, decoupled from membership so children can be members
  without a login.
- Member model holding personal/roster data (names, contact email, phone +
  emergency phone via django-phonenumber-field, license number, DOB) with an
  optional link to a User.
- Family household grouping and directional Guardianship (guardian -> child)
  with uniqueness and no-self-guardian constraints.
- Custom UserAdmin plus Member/Family admin with inlines and autocomplete.
- Settings: register apps, AUTH_USER_MODEL, phonenumber defaults (BE/E164).
- Add CLAUDE.md and a tracked static/ directory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:08:51 +02:00
537c023258 Update project dependencies, settings, and environment configuration 2026-06-17 22:37:32 +02:00
e654311321 Basic app skeleton 2026-06-17 21:40:21 +02:00