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 {