Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
1a49346
refactor(events): split models.py into focused modules behind a barrel
Shaan-Shoukath Sep 1, 2026
330a989
feat(organizations): public directory, governance invitations and man…
Shaan-Shoukath Sep 1, 2026
eb7d32d
feat(evidence): retention mechanism A — object expiry with a truthful…
Shaan-Shoukath Sep 1, 2026
007e508
fix(modules): stop optional modules gating core surfaces, and prove it
Shaan-Shoukath Sep 1, 2026
24aba64
feat(events): registration cutoff and the approval lifecycle
Shaan-Shoukath Sep 1, 2026
f86aadd
fix(reports): gate every report on the module that owns its data
Shaan-Shoukath Sep 1, 2026
68d9743
feat(events): attendance history, post-event feedback and certificates
Shaan-Shoukath Sep 1, 2026
ac8465d
feat(reports): module coverage, charts and append-only rollups
Shaan-Shoukath Sep 1, 2026
694d588
feat(events): recurring series with materialised occurrences
Shaan-Shoukath Sep 1, 2026
fa6f461
feat(events): ICS calendar export, subscribable feeds and attendee ba…
Shaan-Shoukath Sep 2, 2026
2bb7a3d
merge: reporting coverage, charts and rollups (phases 0a + 8)
Shaan-Shoukath Sep 2, 2026
ef44d21
merge: organization directory, governance and managed organizers (pha…
Shaan-Shoukath Sep 2, 2026
212eaeb
merge: evidence retention mechanism A (phase 9)
Shaan-Shoukath Sep 2, 2026
d3b5191
feat(events): offline check-in roster and the PIN check-in station
Shaan-Shoukath Sep 2, 2026
141f852
merge: events programme phases 1-6 (split, registration policy, post-…
Shaan-Shoukath Sep 2, 2026
abccb73
fix(migration,encryption): classify series-collaborator edges, sweep …
Shaan-Shoukath Sep 2, 2026
b17455a
test(backup): prove the events programme round-trips through backup a…
Shaan-Shoukath Sep 2, 2026
cc02ad4
merge: backup and restore completeness for the events programme (phas…
Shaan-Shoukath Sep 2, 2026
622aac8
fix(migration): let the evidence retention models travel with a tenant
Shaan-Shoukath Sep 2, 2026
a674197
fix(backup,organizations,reports): close five defects the phase 10 ro…
Shaan-Shoukath Sep 2, 2026
fc3455a
merge: main back into dev — no content, only the PR #15/#16 merge com…
Shaan-Shoukath Sep 2, 2026
eee1a8d
fix(frontend): render the extracted LandingPage instead of App.tsx's …
Shaan-Shoukath Sep 2, 2026
40c96db
test(migration): guard the shape every projected model must have
Shaan-Shoukath Sep 2, 2026
e1b1bbb
docs: bring PROJECT-HISTORY and SOURCE-MAP current
Shaan-Shoukath Sep 2, 2026
2a3cced
docs: cover events, organizations and evidence retention in README
Shaan-Shoukath Sep 3, 2026
cf274d7
release: 0.8.1 — configured split-frontend deployment
Shaan-Shoukath Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ DISABLE_SERVER_SIDE_CURSORS=False
# upload-time content-length-range). "put" = Supabase Storage S3 presigned PUT (no
# upload-time size policy; the backend re-validates size server-side at attach time).
STORAGE_PRESIGN_METHOD=post
# Evidence photo metadata remains immutable; this independently bounds live object
# bytes. Preview policies before enabling. Range: 30-3650 days, batch: 1-1000.
EVIDENCE_OBJECT_RETENTION_DAYS=365
EVIDENCE_OBJECT_EXPIRY_ENABLED=False
EVIDENCE_RETENTION_BATCH_SIZE=100
# Shared secret for the cron return-reminder endpoint
# (POST /api/v1/internal/cron/return-reminders, header X-Cron-Secret). The endpoint
# 404s while this is unset. Use it when you can't run `manage.py send_return_reminders`
Expand Down
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Guidance for Claude Code (claude.ai/code) when working with code in this reposit

A multi-tenant system for managing community hardware loans across makerspaces. The central concern is
**traceability of physical handovers**: every issue and return must produce evidence (QR scans + photos +
remarks + audit log) so that accountability for lost/damaged hardware is never ambiguous. Public users
remarks + audit log). Photo bytes may expire under the configured retention policy, while immutable photo
metadata, remarks, scans and audit history preserve the durable accountability trail. Public users
browse and request; when self-checkout is enabled they may also issue/return eligible QR tools after
authentication and evidence upload. Staff physically issue reviewed requests and direct handouts according
to action scope.
Expand Down Expand Up @@ -70,8 +71,8 @@ channel only. Two architectural rules are load-bearing and easy to violate if yo
inventory reservation/issue/return.
- **Inventory Availability** — quantity math + asset status for QR-tracked tools.
- **QR Code & Box** — generates/resolves/revokes QR codes, assigns boxes to requests, tracks scan history.
- **Evidence Photo** — immutable issue/return photo storage linked to actor + request + QR scans; object
storage, never public.
- **Evidence Photo** — immutable issue/return photo metadata linked to actor + request + QR scans; private
object bytes may expire under the evidence-retention policy and are never public.
- **Check-In API Client** — **RETIRED** (`73a480c`, Part M7). `apps/checkin/` no longer exists and there is
no `CHECKIN_MODE` setting. Requester identity now comes from authenticated member accounts, so there is no
external verify dependency left to fail safe on.
Expand Down Expand Up @@ -120,8 +121,8 @@ the Auth module** — forgetting this is a cross-tenant data leak, not just a bu
makerspace settings.
- Evidence endpoints require per-makerspace `UPLOAD_EVIDENCE` plus active status; QR management also checks
active status.
- **Every presigned upload lands on the staging key; the final object key is never client-writable.** A workflow promotes it exactly once, so an accepted evidence photo cannot be replaced through a still-valid presign. Read paths — the evidence endpoint, the admin preview, and backup/tenant-migration object capture — therefore fall back to the staging key, or an uploaded-but-unconsumed photo reads as missing.
- Evidence photos and QR scan records are **immutable**; audit logs are **append-only**.
- **Every presigned upload lands on the staging key; the final object key is never client-writable.** A workflow promotes it exactly once, so an accepted evidence photo cannot be replaced through a still-valid presign. Before retention expiry, read paths — the evidence endpoint, the admin preview, and backup/tenant-migration object capture — therefore fall back to the staging key, or an uploaded-but-unconsumed photo reads as missing. A terminal expired state returns 410 and never consults storage.
- Evidence photo **rows** and QR scan records are **immutable**; audit logs are **append-only**. Evidence retention may delete every final and staging object version only after the configured window, but it does not update or delete the retained `EvidencePhoto` row.
- Public inventory must never expose: storage locations, box IDs, QR codes, scan history, evidence photos,
requester history, or hidden counts. Public visibility is governed per-item by `is_public`,
`show_public_count`, and `public_availability_mode` (`exact_count | status_only | hidden`).
Expand Down
11 changes: 6 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ Guidance for Claude Code (claude.ai/code) when working with code in this reposit

A multi-tenant system for managing community hardware loans across makerspaces. The central concern is
**traceability of physical handovers**: every issue and return must produce evidence (QR scans + photos +
remarks + audit log) so that accountability for lost/damaged hardware is never ambiguous. Public users
remarks + audit log). Photo bytes may expire under the configured retention policy, while immutable photo
metadata, remarks, scans and audit history preserve the durable accountability trail. Public users
browse and request; when self-checkout is enabled they may also issue/return eligible QR tools after
authentication and evidence upload. Staff physically issue reviewed requests and direct handouts according
to action scope.
Expand Down Expand Up @@ -70,8 +71,8 @@ channel only. Two architectural rules are load-bearing and easy to violate if yo
inventory reservation/issue/return.
- **Inventory Availability** — quantity math + asset status for QR-tracked tools.
- **QR Code & Box** — generates/resolves/revokes QR codes, assigns boxes to requests, tracks scan history.
- **Evidence Photo** — immutable issue/return photo storage linked to actor + request + QR scans; object
storage, never public.
- **Evidence Photo** — immutable issue/return photo metadata linked to actor + request + QR scans; private
object bytes may expire under the evidence-retention policy and are never public.
- **Check-In API Client** — **RETIRED** (`73a480c`, Part M7). `apps/checkin/` no longer exists and there is
no `CHECKIN_MODE` setting. Requester identity now comes from authenticated member accounts, so there is no
external verify dependency left to fail safe on.
Expand Down Expand Up @@ -120,8 +121,8 @@ the Auth module** — forgetting this is a cross-tenant data leak, not just a bu
makerspace settings.
- Evidence endpoints require per-makerspace `UPLOAD_EVIDENCE` plus active status; QR management also checks
active status.
- **Every presigned upload lands on the staging key; the final object key is never client-writable.** A workflow promotes it exactly once, so an accepted evidence photo cannot be replaced through a still-valid presign. Read paths — the evidence endpoint, the admin preview, and backup/tenant-migration object capture — therefore fall back to the staging key, or an uploaded-but-unconsumed photo reads as missing.
- Evidence photos and QR scan records are **immutable**; audit logs are **append-only**.
- **Every presigned upload lands on the staging key; the final object key is never client-writable.** A workflow promotes it exactly once, so an accepted evidence photo cannot be replaced through a still-valid presign. Before retention expiry, read paths — the evidence endpoint, the admin preview, and backup/tenant-migration object capture — therefore fall back to the staging key, or an uploaded-but-unconsumed photo reads as missing. A terminal expired state returns 410 and never consults storage.
- Evidence photo **rows** and QR scan records are **immutable**; audit logs are **append-only**. Evidence retention may delete every final and staging object version only after the configured window, but it does not update or delete the retained `EvidencePhoto` row.
- Public inventory must never expose: storage locations, box IDs, QR codes, scan history, evidence photos,
requester history, or hidden counts. Public visibility is governed per-item by `is_public`,
`show_public_count`, and `public_availability_mode` (`exact_count | status_only | hidden`).
Expand Down
62 changes: 58 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,20 @@ Telegram group, QR namespace, and audit scope — fully isolated from the others
remark) → accountability, all audited. Direct staff handouts too.
- **3D-printing manager** — public print requests, printer/spool management, filament tracking,
slicer estimates, and an optional (staff-private) cash charge at collection.
- **Events & bookings** — one-off events or recurring series, registration with optional approval and
waitlists, QR check-in at the door (plus an expiring offline roster and event-scoped PIN stations for
a desk with no signal), post-event feedback, attendance certificates, printable badges, member
calendar feeds, and bookable spaces.
- **Organizations across makerspaces** — a network, university or chain registered as an organization
linked to any number of spaces, with a public profile and a cross-makerspace event catalogue. An
organization grant confers **actions, never identity**.
- **QR everywhere** — boxes, tools, and individual assets; immutable scan history.
- **Action-based staff console** — editable per-makerspace roles over a fixed action set, four seeded
defaults, and a superadmin-only Django control plane.
- **Reports & ledger** — what's out, who has it, overdue tracking, CSV/XLSX export.
- **Reports & ledger** — what's out, who has it, overdue tracking, CSV/XLSX export, plus accessible
charts with table fallbacks and append-only metric rollups where a correction adds a revision rather
than rewriting history. Every module is covered either by a substantive report or an explicitly
gated row.
- **Notifications** — per-makerspace **Telegram, Slack, Mattermost and Discord** alerts plus async
(Celery) email, with a per-feature × per-channel matrix. Each channel is its own module.
- **Modular by install** — turn whole modules on and off per makerspace; uninstalling hides surfaces
Expand All @@ -52,7 +62,10 @@ Telegram group, QR namespace, and audit scope — fully isolated from the others
- **Maker profiles** — an opt-in per-makerspace profile with projects, interests, education and an
optional GitHub contribution count, plus a member directory that lists only the people who chose to
be listed.
- **Traceable by design** — append-only audit log; immutable evidence photos and scan records.
- **Traceable by design** — append-only audit log; immutable evidence photo records and scan history.
Photo *bytes* can expire under a per-makerspace retention policy, while the photo metadata, remarks,
scans and audit trail are kept — so the accountability record outlives the image itself, and an
expired photo reads as a truthful expired state rather than a missing one.

> **What works out of the box:** username/password. Google, Apple and OIDC need credentials you
> create with that provider, and phone sign-in needs an SMS account — none of them can ship
Expand Down Expand Up @@ -154,7 +167,7 @@ console shows you:
| **Inventory** *(always on)* | The catalogue, request workflow, QR/evidence spine, asset units, containers, transfers, QR print batches, front-desk handover and purchasing |
| **Stocktake** | Scan-first stock counts and variance reporting |
| **Machines** | Machine registry, the service/print queue, maintenance and warranty |
| **Events** | Event scheduling and registrations, QR check-in at the door, and cross-makerspace collaborative events |
| **Events** | Event scheduling and registrations — recurring series, approval and waitlists, QR check-in at the door, post-event feedback and attendance certificates, member calendar feeds, printable attendee badges, and cross-makerspace collaborative events |
| **Bookings** | Resource booking and public self-booking |
| **Membership** | Join requests, waivers, referrals, member activity, maker profiles, presence — and the member-facing identity ecosystem |
| **Notifications** | The in-app inbox and every outbound channel |
Expand Down Expand Up @@ -237,7 +250,7 @@ cannot be removed. **Default** means it is on when you install without choosing
| | [`machine_service`](docs/MODULES.md#machine_service) | | | The service/job queue |
| | [`printing`](docs/MODULES.md#printing) | | | 3D printing on top of `machine_service` |
| | [`maintenance`](docs/MODULES.md#maintenance) | | | Scheduled and reactive maintenance |
| **Events** | [`events`](docs/MODULES.md#events) | | | Scheduling, registrations, QR check-in, collaborative events |
| **Events** | [`events`](docs/MODULES.md#events) | | | Scheduling, recurring series, registrations and waitlists, QR check-in, feedback and certificates, collaborative events |
| **Bookings** | [`bookings`](docs/MODULES.md#bookings) | | | Resource booking and public self-booking |
| **Membership** | [`membership`](docs/MODULES.md#membership) | | | Join requests, waivers, referrals, maker profiles |
| | [`member_accounts`](docs/MODULES.md#member_accounts) | | | Member sign-up and member sign-in |
Expand Down Expand Up @@ -274,6 +287,8 @@ Manager** in the console rather than a superadmin.
| `payments.events` | `events` | | Charge for event registration |
| `payments.membership` | `membership` | | Charge membership dues |
| `mobile.push` | `mobile` | ● | Native push notifications |
| `events.offline_checkin` | `events` | | Expiring on-device roster and event-scoped PIN check-in stations |
| `notifications.delegated_recipients` | `notifications` | | Machine-scoped maintainers manage maintenance recipients for their own machines (also needs `maintenance` and `machines`) |
| `inventory.self_checkout` | — | ● | Member self-checkout and staff direct handouts |
| `presence.geofence` | — | ● | Advisory location check at check-in (never blocks) |

Expand Down Expand Up @@ -551,6 +566,45 @@ pointer/CAS adapter, never ambient shell state. The Cloud static-environment ini
callbacks are not implemented yet, so the older Supabase path is suitable only for a non-H1 demo—not a
supported restore topology.

### Split deployment: backend on your server, frontend on Netlify

Supported, and `netlify.toml` in the repo root configures it. Netlify builds **only** the React app —
it never touches the Dockerfiles or compose files, and because `frontend/src/generated/api.ts` is
committed, the build never has to reach your server.

Netlify picks up `base = frontend`, `npm run build`, `publish = dist` and `NODE_VERSION = 22` from
that file (Vite 8 needs Node 20.19+/22.12+, and Netlify's default image can be older). It also adds
the catch-all rewrite to `index.html`, without which every deep link — `/m/<slug>`, `/admin/*`,
`/event-check-in/<token>` — 404s on refresh. Set **`VITE_API_URL`** in the Netlify UI to your API,
e.g. `https://api.example.org/api`.

The backend then has to accept a browser on a different origin. Auth already defaults to cross-site
cookies (`AUTH_COOKIE_SAMESITE=None`, `AUTH_COOKIE_SECURE=True`), **which only works if both sides
are HTTPS**:

```env
ENABLE_HTTPS=True
ALLOWED_HOSTS=api.example.org
CORS_ALLOWED_ORIGINS=https://your-site.netlify.app
CSRF_TRUSTED_ORIGINS=https://your-site.netlify.app
```

Three more that are easy to miss:

- **Set each makerspace's `frontend_domain`** to the site's domain. Origin scoping validates it, so
tenant-scoped routes are rejected without it.
- **Object storage must be addressed publicly.** `AWS_S3_PUBLIC_ENDPOINT_URL` and
`PUBLIC_IMAGE_BASE_URL` are baked into presigned URLs and every public image `src`, so a
`localhost` value yields a site that works only from the server console and shows broken images to
everyone else. On R2 or S3 also set `STORAGE_PRESIGN_METHOD=put`, and allow your site's origin in
the bucket's CORS rules **in the provider dashboard**.
- **Keep a scheduler.** Hosting the frontend elsewhere does not affect scheduled work, but the
backend profile you choose does: `docker-compose.prod.yml` runs Celery `beat`, while
`docker-compose.cloud.yml` has no beat and relies on its `cron` service instead. With neither,
`.delay()` still runs inline so nothing looks broken, yet return reminders, the evidence-retention
sweep and event-series extension silently never fire. If you use the cloud profile, drop only its
`frontend` service — never `cron`.

### Moving a makerspace onto its own server

A space that started as a tenant on someone else's instance can take its data with it. A superadmin
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.8.0
0.8.1
5 changes: 5 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ APICLIENT_REQUIRE_NONCE=False
# registry; `manage.py check` fails with the list if any is missing, rather than the
# deployment silently 401-ing that whole prefix.
HMAC_PROTECTED_PATH_PREFIXES=/api/public/,/api/v1/public/
# Independent high-entropy HMAC key for event PIN verification; do not reuse API_CLIENT_ENC_KEY.
EVENT_STATION_PIN_PEPPER=
# Fernet key for ApiClient, Telegram bot token, and makerspace SMTP password
# encryption. Generate with:
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
Expand Down Expand Up @@ -111,6 +113,9 @@ THROTTLE_PUBLIC_REQUEST_SUBMIT=10/min
THROTTLE_ANONYMOUS_REQUEST_IP_BURST=2/min
THROTTLE_ANONYMOUS_REQUEST_IP_HOUR=10/hour
THROTTLE_ANONYMOUS_REQUEST_EMAIL=3/day
# Member calendar subscription endpoints use both token- and IP-shaped buckets.
THROTTLE_EVENT_CALENDAR_FEED_TOKEN=120/hour
THROTTLE_EVENT_CALENDAR_FEED_IP=300/hour
ANONYMOUS_REQUEST_OUTSTANDING_LIMIT=50
ANONYMOUS_REQUEST_IDEMPOTENCY_KEY_MAX_LENGTH=128

Expand Down
11 changes: 11 additions & 0 deletions backend/apps/accounts/claim_pre_auth_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,23 @@
"apps.events.throttles.CollaborativeRegistrationThrottle": {
"_tier", "allow_request", "get_cache_key"
},
# Public feedback reads and submissions take separate budgets. The scope is chosen in
# `allow_request` rather than a view-level `get_throttles()` override precisely BECAUSE
# this guard forbids a lifecycle-hook override on a pre-auth route; the override set is
# therefore identical to its ClientTierRateThrottle base.
"apps.events.throttles.PublicFeedbackRateThrottle": {
"_tier", "allow_request", "get_cache_key"
},
# The account-less borrow-request budgets. Both override `get_cache_key` only, to
# return None for an authenticated caller: they sit in `throttle_classes` beside the
# member throttle, and DRF applies every class on every request, so without the skip a
# makerspace behind one NAT would rate-limit its own signed-in members by egress IP.
"apps.hardware_requests.throttles.AnonymousRequestIpBurstThrottle": {"get_cache_key"},
"apps.hardware_requests.throttles.AnonymousRequestIpHourThrottle": {"get_cache_key"},
# The subscribable calendar feed is fetched by a calendar client with only its
# bearer token, so the budget keys on that token rather than the caller.
"apps.events.throttles.EventCalendarFeedTokenThrottle": {"get_cache_key"},
"apps.events.throttles.EventCalendarFeedIpThrottle": {"get_cache_key"},
"apps.machines.permissions.IsActiveRequester": {"has_permission"},
"apps.accounts.views_device.IsDeviceAccessToken": {"has_permission"},
"apps.makerspaces.throttles.MemberImagePresignThrottle": {"get_cache_key"},
Expand Down
5 changes: 5 additions & 0 deletions backend/apps/accounts/claim_routes_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ def _refused(name, methods, reason):
**_refused("auth-forgot-password", ("POST",), "walk-ins have no password credential"),
**_refused("auth-reset-password", ("POST",), "walk-ins have no password credential"),
**_refused("auth-member-sign-up", ("POST",), "claim sessions cannot create another account"),
**_refused(
"auth-organization-invitation-redeem",
("POST",),
"claim sessions cannot take on organization authority",
),
**_refused("auth-email-verification-resend", ("POST",), "walk-ins cannot verify email"),
**_refused("auth-email-verification-confirm", ("POST",), "walk-ins cannot verify email"),
}
Loading
Loading