Orgs: let any user create an organization and become its admin

Organization writes were superadmin-only, so standing up a tenant needed
an out-of-band superadmin. Creating one is now self-service, and an admin
manages the org they belong to.

- POST /api/orgs is open to any authenticated user. A creator who isn't a
  superadmin must have no organization yet (a single-valued membership
  relation means a second one would abandon the first), and is promoted to
  the new org's admin and first member in the same request. If that
  promotion fails the org is rolled back, so it is never left stranded
  with nobody able to administer it. Superadmins still create tenants
  without joining them.
- PATCH/DELETE are manager-gated and scope an admin to their own org. An
  admin deletes theirs only as its sole member: they are detached and
  demoted to a plain user before the record goes, so the org is empty when
  it is removed. Other members still block deletion with a 409.
- /api/me now carries organization + organizationName, which the clients
  need to tell "no org yet" from "org you administer".

The panel, Web App (new OrgManager.vue in Settings) and Phone App (new
_OrganizationSection) all mirror the server's gates rather than
re-deciding them. The Phone App cached its role at login and gates the
Users tab on it, so AuthService.adoptRole refreshes that from the profile
instead of making a freshly promoted admin sign in again.

Covered by orgs_test.go, which drives the real handler + middleware chain
against a stand-in PocketBase: promotion, the already-a-member refusal,
superadmin staying unattached, the rollback, own-org scoping, the
detach-and-demote, and the blocking-member 409.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tajniak81
2026-08-17 16:54:02 +02:00
co-authored by Claude Opus 5
parent 358ee68f94
commit cd16d4383f
30 changed files with 1193 additions and 63 deletions
+17 -1
View File
@@ -76,6 +76,22 @@ their own account. An organization cannot be deleted while it still has members.
membership. A superadmin spans all organizations; an admin is scoped by the
server to their own. Users may have no organization at all.
Creating one is self-service: any user who does not already belong to an
organization may `POST /api/orgs`, and becomes that organization's **admin** and
first member in the same request (if the promotion fails the new organization is
rolled back, so it is never left with nobody able to administer it). A user who
already belongs to one is refused — membership is a single relation, so creating
a second would mean silently abandoning the first.
A superadmin is the exception: they create organizations without joining them,
since they already span every tenant.
From there an admin manages **their own** organization — rename it, or delete it
once they are its only member. Deleting it detaches and demotes them back to a
plain `user` before the record is removed, so the organization is empty when it
goes. A superadmin may rename or delete any organization, but still only once it
has no members at all.
### Per-user car ownership + sharing
Cars are not a global list. `cars.owner` marks ownership and `car_shares` grants
@@ -139,7 +155,7 @@ POST /api/me/verify/request
GET /api/me/export POST /api/me/import
POST /api/me/delete POST /api/me/delete/cancel
# users + organizations (admin or superadmin; org writes are superadmin-only)
# users + organizations (admin or superadmin; POST /api/orgs is open to any user)
GET /api/users POST /api/users
PATCH /api/users/{id} DELETE /api/users/{id}
GET /api/orgs POST /api/orgs