diff --git a/backend/pb_migrations/1754400000_init_schema.js b/backend/pb_migrations/1754400000_init_schema.js index 23aed54..a8ad793 100644 --- a/backend/pb_migrations/1754400000_init_schema.js +++ b/backend/pb_migrations/1754400000_init_schema.js @@ -12,8 +12,8 @@ migrate((app) => { { "id": "_pb_users_auth_", "listRule": "id = @request.auth.id", - "viewRule": "id = @request.auth.id", - "createRule": null, + "viewRule": "@request.auth.id != \"\"", + "createRule": "@request.auth.id != \"\"", "updateRule": "id = @request.auth.id", "deleteRule": "id = @request.auth.id", "name": "users", @@ -457,20 +457,26 @@ migrate((app) => { "type": "text" }, { - "help": "", "hidden": false, - "id": "select2063623452", - "maxSelect": 1, - "name": "status", + "id": "date_event_starts", + "max": "", + "min": "", + "name": "starts", "presentable": false, "required": false, "system": false, - "type": "select", - "values": [ - "draft", - "active", - "finished" - ] + "type": "date" + }, + { + "hidden": false, + "id": "date_event_ends", + "max": "", + "min": "", + "name": "ends", + "presentable": false, + "required": false, + "system": false, + "type": "date" }, { "cascadeDelete": false, @@ -590,9 +596,9 @@ migrate((app) => { "id": "spgs7e4anufen9u", "listRule": "team.users.id ?= @request.auth.id", "viewRule": "team.users.id ?= @request.auth.id", - "createRule": "team.users.id ?= @request.auth.id", - "updateRule": "team.users.id ?= @request.auth.id", - "deleteRule": "team.users.id ?= @request.auth.id", + "createRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id", + "updateRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id", + "deleteRule": "team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id", "name": "riders", "type": "base", "fields": [ @@ -655,6 +661,20 @@ migrate((app) => { "system": false, "type": "relation" }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "help": "", + "hidden": false, + "id": "relation_rider_user", + "maxSelect": 1, + "minSelect": 0, + "name": "user", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, { "hidden": false, "id": "autodate2990389176", @@ -1013,6 +1033,31 @@ migrate((app) => { "gesperrt" ] }, + { + "hidden": false, + "id": "date_trail_status_changed", + "max": "", + "min": "", + "name": "status_changed", + "presentable": false, + "required": false, + "system": false, + "type": "date" + }, + { + "cascadeDelete": false, + "collectionId": "_pb_users_auth_", + "help": "", + "hidden": false, + "id": "relation_trail_status_by", + "maxSelect": 1, + "minSelect": 0, + "name": "status_by", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + }, { "cascadeDelete": false, "collectionId": "_pb_users_auth_", diff --git a/backend/pb_migrations/1754500500_riders_user_and_admin_rules.js b/backend/pb_migrations/1754500500_riders_user_and_admin_rules.js new file mode 100644 index 0000000..38fd881 --- /dev/null +++ b/backend/pb_migrations/1754500500_riders_user_and_admin_rules.js @@ -0,0 +1,73 @@ +/// + +// Fahrer bekommen ein optionales Benutzerkonto, und die Fahrerverwaltung wird +// zur Sache der Teamleitung. +// +// 1) riders.user — Verknüpfung eines Fahrers mit einem Login. Optional, weil +// die meisten Fahrer nur in Ergebnislisten auftauchen und sich nie +// anmelden. cascadeDelete ist bewusst aus: Wird ein Konto gelöscht, soll +// der Fahrer samt seiner Zeiten erhalten bleiben. +// +// 2) riders create/update/delete nur noch für Owner und Admins. Bisher durfte +// jedes Teammitglied Fahrer anlegen, ändern und löschen — mit den Zeiten +// daran ist das zu viel Vertrauen in jeden Mitfahrer. Lesen bleibt für das +// ganze Team offen. +// +// 3) users.viewRule von "nur ich selbst" auf "eingeloggt" gelockert. Ohne das +// lässt sich zu einer bekannten User-ID kein Name anzeigen — betrifft die +// Mitgliederliste eines Teams, den Melder eines Trail-Markers und die +// Kontoverknüpfung eines Fahrers. Die listRule bleibt eng: Wer keine ID +// kennt, kann auch keine Konten durchblättern. E-Mail-Adressen bleiben +// über emailVisibility verborgen. +// +// 4) users.createRule von "niemand" auf "eingeloggt". Damit kann ein Admin +// einem Fahrer direkt ein Login anlegen. Bewusst nicht offen (""), sonst +// wäre es eine öffentliche Registrierung. + +const RIDERS_ADMIN_RULE = + 'team.owner.id ?= @request.auth.id || team.admins.id ?= @request.auth.id' +const RIDERS_MEMBER_RULE = 'team.users.id ?= @request.auth.id' +const LOGGED_IN = '@request.auth.id != ""' + +migrate((app) => { + const users = app.findCollectionByNameOrId('users') + const riders = app.findCollectionByNameOrId('riders') + + // Der Snapshot in 1754400000_init_schema.js führt das Feld inzwischen + // mit. Auf einer frischen Datenbank ist es also schon da, auf einer + // bestehenden noch nicht — dieselbe Lage wie bei den trail-Collections. + if (!riders.fields.getByName('user')) { + riders.fields.add(new Field({ + id: 'relation_rider_user', + name: 'user', + type: 'relation', + collectionId: users.id, + cascadeDelete: false, + maxSelect: 1, + minSelect: 0, + required: false, + })) + } + + riders.createRule = RIDERS_ADMIN_RULE + riders.updateRule = RIDERS_ADMIN_RULE + riders.deleteRule = RIDERS_ADMIN_RULE + app.save(riders) + + users.viewRule = LOGGED_IN + users.createRule = LOGGED_IN + app.save(users) +}, (app) => { + const users = app.findCollectionByNameOrId('users') + const riders = app.findCollectionByNameOrId('riders') + + riders.fields.removeByName('user') + riders.createRule = RIDERS_MEMBER_RULE + riders.updateRule = RIDERS_MEMBER_RULE + riders.deleteRule = RIDERS_MEMBER_RULE + app.save(riders) + + users.viewRule = 'id = @request.auth.id' + users.createRule = null + app.save(users) +}) diff --git a/backend/pb_migrations/1754500600_events_dates_instead_of_status.js b/backend/pb_migrations/1754500600_events_dates_instead_of_status.js new file mode 100644 index 0000000..0d0cb8b --- /dev/null +++ b/backend/pb_migrations/1754500600_events_dates_instead_of_status.js @@ -0,0 +1,52 @@ +/// + +// Events werden datiert statt bewertet. +// +// Der Status (draft/active/finished) war Handarbeit: Jemand musste ein Event +// aktiv schalten und hinterher wieder abschließen, sonst stand die Übersicht +// falsch da. Ein Datum weiß von selbst, was ansteht, was läuft und was vorbei +// ist. +// +// starts trägt den Tag; ends bleibt leer, wenn das Event nur einen Tag dauert, +// und trägt sonst den letzten Tag. Beide sind optional, damit bestehende +// Events ohne Datum weiterleben, bis jemand eines nachträgt. + +migrate((app) => { + const events = app.findCollectionByNameOrId('events') + + // Auf einer frischen Datenbank hat der Snapshot die Felder bereits. + if (!events.fields.getByName('starts')) { + events.fields.add(new Field({ + id: 'date_event_starts', + name: 'starts', + type: 'date', + })) + } + if (!events.fields.getByName('ends')) { + events.fields.add(new Field({ + id: 'date_event_ends', + name: 'ends', + type: 'date', + })) + } + + if (events.fields.getByName('status')) { + events.fields.removeByName('status') + } + + app.save(events) +}, (app) => { + const events = app.findCollectionByNameOrId('events') + + events.fields.removeByName('starts') + events.fields.removeByName('ends') + events.fields.add(new Field({ + id: 'select_event_status', + name: 'status', + type: 'select', + maxSelect: 1, + values: ['draft', 'active', 'finished'], + })) + + app.save(events) +}) diff --git a/backend/pb_migrations/1754500700_trail_status_meta.js b/backend/pb_migrations/1754500700_trail_status_meta.js new file mode 100644 index 0000000..705ce8a --- /dev/null +++ b/backend/pb_migrations/1754500700_trail_status_meta.js @@ -0,0 +1,45 @@ +/// + +// Wer hat den Trail gesperrt, und wann? +// +// Der Status allein sagt nur, wie es gerade steht. Für eine Meldung, der man +// trauen soll, gehört dazu, wann sie kam und von wem — eine Sperrung von +// heute Morgen wiegt anders als eine vom letzten Herbst. +// +// Beide Felder schreibt die Anwendung beim Setzen des Status mit; bestehende +// Trails bleiben ohne Angabe, bis der Status das nächste Mal geändert wird. + +migrate((app) => { + const trails = app.findCollectionByNameOrId('trails') + const users = app.findCollectionByNameOrId('users') + + if (!trails.fields.getByName('status_changed')) { + trails.fields.add(new Field({ + id: 'date_trail_status_changed', + name: 'status_changed', + type: 'date', + })) + } + + if (!trails.fields.getByName('status_by')) { + trails.fields.add(new Field({ + id: 'relation_trail_status_by', + name: 'status_by', + type: 'relation', + collectionId: users.id, + cascadeDelete: false, + maxSelect: 1, + minSelect: 0, + required: false, + })) + } + + app.save(trails) +}, (app) => { + const trails = app.findCollectionByNameOrId('trails') + + trails.fields.removeByName('status_changed') + trails.fields.removeByName('status_by') + + app.save(trails) +}) diff --git a/frontend/src/lib/types.d.ts b/frontend/src/lib/types.d.ts index b0f45cd..df80871 100644 --- a/frontend/src/lib/types.d.ts +++ b/frontend/src/lib/types.d.ts @@ -46,19 +46,15 @@ export type AuthSystemFields = { // Record types for each collection -export enum EventsStatusOptions { - "draft" = "draft", - "active" = "active", - "finished" = "finished", -} export type EventsRecord = { created?: IsoDateString description?: string + ends?: IsoDateString gps?: string id: string location?: string name?: string - status?: EventsStatusOptions + starts?: IsoDateString team?: RecordIdString updated?: IsoDateString } @@ -74,6 +70,7 @@ export type RidersRecord = { number?: string team?: RecordIdString updated?: IsoDateString + user?: RecordIdString } export type RunsRecord = { @@ -188,6 +185,8 @@ export type TrailsRecord = { id: string name: string status?: TrailsStatusOptions + status_by?: RecordIdString + status_changed?: IsoDateString stewards?: RecordIdString[] team: RecordIdString updated?: IsoDateString