diff --git a/Resources/Views/Admin/Form/event_form.leaf b/Resources/Views/Admin/Form/event_form.leaf index ddbdd1ed..ffac12fb 100644 --- a/Resources/Views/Admin/Form/event_form.leaf +++ b/Resources/Views/Admin/Form/event_form.leaf @@ -22,6 +22,13 @@
Please provide a location.
+ #if(event): +
+ + +
+ #endif +
diff --git a/Sources/App/Features/Auth/Models/Migrations/User+Migration+V2.swift b/Sources/App/Features/Auth/Models/Migrations/User+Migration+V2.swift new file mode 100644 index 00000000..b1bbe63f --- /dev/null +++ b/Sources/App/Features/Auth/Models/Migrations/User+Migration+V2.swift @@ -0,0 +1,15 @@ +import Fluent + +final class UserMigrationV2: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema(Schema.user) + .field("permissions", .array(of: .string), .required, .sql(.default("{}"))) + .update() + } + + func revert(on database: any Database) async throws { + try await database.schema(Schema.user) + .deleteField("permissions") + .update() + } +} diff --git a/Sources/App/Features/Auth/Models/Permissions.swift b/Sources/App/Features/Auth/Models/Permissions.swift new file mode 100644 index 00000000..fea0896d --- /dev/null +++ b/Sources/App/Features/Auth/Models/Permissions.swift @@ -0,0 +1,22 @@ +// +// Permissions.swift +// swift-leeds +// +// Created by James Sherlock on 12/10/2025. +// + +import Vapor + +enum Permission: String, CaseIterable { + case eventUpdate = "event.update" +} + +extension Request { + func requireUser(hasPermission permission: Permission) throws { + let allUserPermissions = user?.permissions.compactMap { Permission(rawValue: $0) } + + guard allUserPermissions?.contains(permission) == true else { + throw Abort(.unauthorized, reason: "user does not have permission `\(permission.rawValue)`") + } + } +} diff --git a/Sources/App/Features/Auth/Models/User.swift b/Sources/App/Features/Auth/Models/User.swift index 8e16ce75..648aaa7a 100644 --- a/Sources/App/Features/Auth/Models/User.swift +++ b/Sources/App/Features/Auth/Models/User.swift @@ -32,6 +32,9 @@ final class User: Authenticatable, ModelAuthenticatable, Content, ModelSessionAu @Field(key: "password_hash") var passwordHash: String + @Field(key: "permissions") + var permissions: [String] + @Field(key: "user_role") var role: User.Role diff --git a/Sources/App/Features/CheckIn/Controllers/CheckInAPIController.swift b/Sources/App/Features/CheckIn/Controllers/CheckInAPIController.swift index 23f08286..8f1d7d89 100644 --- a/Sources/App/Features/CheckIn/Controllers/CheckInAPIController.swift +++ b/Sources/App/Features/CheckIn/Controllers/CheckInAPIController.swift @@ -5,19 +5,19 @@ struct CheckInAPIController: RouteCollection { routes.get(":secret", use: onGet) } - @Sendable func onGet(request: Request) throws -> CheckIn { + @Sendable func onGet(request: Request) async throws -> CheckIn { // Verified that :secret (in the route /api/v1/checkin/:secret) is equal to `CHECKIN_SECRET` - // If it is, then return `CHECKIN_TAG` guard let secret = request.parameters.get("secret"), let checkinSecret = Environment.get("CHECKIN_SECRET"), - secret == checkinSecret, - let tag = Environment.get("CHECKIN_TAG") + secret == checkinSecret else { throw Abort(.notFound) } - - return CheckIn(tag: tag) + + // If it is, then return Event.checkinKey or `CHECKIN_TAG` + let event = try await Event.getCurrent(on: request.db) + return CheckIn(tag: event.checkinKey ?? Environment.get("CHECKIN_TAG") ?? "") } struct CheckIn: Content { diff --git a/Sources/App/Features/Events/Controllers/EventRouteController.swift b/Sources/App/Features/Events/Controllers/EventRouteController.swift index dd734e54..652c4552 100644 --- a/Sources/App/Features/Events/Controllers/EventRouteController.swift +++ b/Sources/App/Features/Events/Controllers/EventRouteController.swift @@ -45,7 +45,9 @@ struct EventRouteController: RouteCollection { let input = try request.content.decode(FormInput.self) let isCurrent = input.isCurrent ?? event?.isCurrent ?? false var eventID: Event.IDValue - + + try request.requireUser(hasPermission: .eventUpdate) + guard let date = Self.formDateFormatter().date(from: input.date) else { throw Abort(.badRequest, reason: "Invalid Date Format") } @@ -56,6 +58,7 @@ struct EventRouteController: RouteCollection { event.location = input.location event.isCurrent = isCurrent event.showSchedule = input.showSchedule ?? false + event.checkinKey = input.checkinKey ?? event.checkinKey eventID = try event.requireID() @@ -98,5 +101,6 @@ struct EventRouteController: RouteCollection { let location: String let isCurrent: Bool? let showSchedule: Bool? + let checkinKey: String? } } diff --git a/Sources/App/Features/Events/Models/Event.swift b/Sources/App/Features/Events/Models/Event.swift index 445e3eb6..8d2c5e04 100644 --- a/Sources/App/Features/Events/Models/Event.swift +++ b/Sources/App/Features/Events/Models/Event.swift @@ -31,6 +31,12 @@ final class Event: Model, Content, @unchecked Sendable { @Field(key: "show_schedule") var showSchedule: Bool + @Field(key: "checkin_key") + var checkinKey: String? + + @Field(key: "conference") + var conference: String + @Children(for: \.$event) var days: [EventDay] diff --git a/Sources/App/Features/Events/Models/Migrations/Event+Migration+V5.swift b/Sources/App/Features/Events/Models/Migrations/Event+Migration+V5.swift new file mode 100644 index 00000000..4256924e --- /dev/null +++ b/Sources/App/Features/Events/Models/Migrations/Event+Migration+V5.swift @@ -0,0 +1,15 @@ +import Fluent + +final class EventMigrationV5: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema(Schema.event) + .field("checkin_key", .string) + .update() + } + + func revert(on database: any Database) async throws { + try await database.schema(Schema.event) + .deleteField("checkin_key") + .update() + } +} diff --git a/Sources/App/Features/Events/Models/Migrations/Event+Migration+V6.swift b/Sources/App/Features/Events/Models/Migrations/Event+Migration+V6.swift new file mode 100644 index 00000000..9b1c3dc8 --- /dev/null +++ b/Sources/App/Features/Events/Models/Migrations/Event+Migration+V6.swift @@ -0,0 +1,15 @@ +import Fluent + +final class EventMigrationV6: AsyncMigration { + func prepare(on database: any Database) async throws { + try await database.schema(Schema.event) + .field("conference", .string, .required, .sql(.default("swiftleeds"))) + .update() + } + + func revert(on database: any Database) async throws { + try await database.schema(Schema.event) + .deleteField("conference") + .update() + } +} diff --git a/Sources/App/Migrations.swift b/Sources/App/Migrations.swift index 62151a23..035c33c2 100644 --- a/Sources/App/Migrations.swift +++ b/Sources/App/Migrations.swift @@ -84,6 +84,9 @@ class Migrations { app.migrations.add(SlotMigrationV5()) // Remove unneeded slot_id params app.migrations.add(PresentationMigrationV6()) // Add video visibility app.migrations.add(SlotMigrationV6()) // Remove legacy date and event_id fields + app.migrations.add(EventMigrationV5()) // Add `checkin_key` to event + app.migrations.add(UserMigrationV2()) // Add `permissions` to user + app.migrations.add(EventMigrationV6()) // Add `conference` to event ("swiftleeds" - default, or "kotlinleeds") do { guard let url = Environment.get("DATABASE_URL") else {