Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Resources/Views/Admin/Form/event_form.leaf
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
<div class="invalid-feedback">Please provide a location.</div>
</div>

#if(event):
<div class="mb-3">
<label for="location" class="form-label">Tito Check-In Key</label>
<input class="form-control" type="text" id="checkinKey" name="checkinKey" value="#(event.checkinKey)" placeholder="chk_...">
</div>
#endif

<div class="form-check form-switch">
<input type="checkbox" class="form-check-input" id="showSchedule" name="showSchedule" #if(event.showSchedule):checked#endif>
<label class="form-check-label" for="showSchedule">Show schedule?</label>
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
22 changes: 22 additions & 0 deletions Sources/App/Features/Auth/Models/Permissions.swift
Original file line number Diff line number Diff line change
@@ -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)`")
}
}
}
3 changes: 3 additions & 0 deletions Sources/App/Features/Auth/Models/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -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()

Expand Down Expand Up @@ -98,5 +101,6 @@ struct EventRouteController: RouteCollection {
let location: String
let isCurrent: Bool?
let showSchedule: Bool?
let checkinKey: String?
}
}
6 changes: 6 additions & 0 deletions Sources/App/Features/Events/Models/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
3 changes: 3 additions & 0 deletions Sources/App/Migrations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down