~jan0sch/smederee

Showing details for patch 1aea4b407ef7db19bd35f23c06dfc89351dcee1b.
2023-11-13 (Mon), 2:18 PM - Jens Grassel - 1aea4b407ef7db19bd35f23c06dfc89351dcee1b

CODINGSTYLE: INDENTATION / MAJOR CODE REFORMAT

- adjust code style guide for Scala code
- add `.editorconfig` file in project root
- switch to 4 spaces deep indentation for Scala code
- adjust scalafmt configuration
- reformat code
Summary of changes
1 files added
  • .editorconfig
152 files modified with 17,658 lines added and 17,047 lines removed
  • .scalafmt.conf with 24 added and 4 removed lines
  • CODINGSTYLE.md with 14 added and 5 removed lines
  • build.sbt with 458 added and 458 removed lines
  • modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala with 203 added and 203 removed lines
  • modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala with 100 added and 98 removed lines
  • modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala with 31 added and 30 removed lines
  • modules/email/src/main/scala/de/smederee/email/EmailMiddleware.scala with 249 added and 249 removed lines
  • modules/email/src/main/scala/de/smederee/email/SimpleJavaMailMiddleware.scala with 53 added and 53 removed lines
  • modules/email/src/test/scala/de/smederee/email/EmailMiddlewareTest.scala with 55 added and 55 removed lines
  • modules/email/src/test/scala/de/smederee/email/Generators.scala with 45 added and 45 removed lines
  • modules/email/src/test/scala/de/smederee/email/SimpleJavaMailMiddlewareHelpersTest.scala with 26 added and 26 removed lines
  • modules/html-utils/src/main/scala/de/smederee/html/LinkTools.scala with 30 added and 30 removed lines
  • modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala with 140 added and 139 removed lines
  • modules/html-utils/src/main/scala/de/smederee/html/MetaTags.scala with 149 added and 148 removed lines
  • modules/html-utils/src/test/scala/de/smederee/html/LinkToolsTest.scala with 14 added and 14 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/Account.scala with 140 added and 140 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AccountManagementRepository.scala with 95 added and 95 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AccountManagementRoutes.scala with 377 added and 343 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AddPublicSshKeyForm.scala with 16 added and 14 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AuthenticationMiddleware.scala with 92 added and 92 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AuthenticationRepository.scala with 132 added and 132 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala with 182 added and 160 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/ChangePasswordForm.scala with 39 added and 38 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DatabaseMigrator.scala with 31 added and 31 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala with 46 added and 46 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala with 82 added and 81 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala with 51 added and 51 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieSignupRepository.scala with 14 added and 14 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala with 86 added and 86 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/EditVcsRepositoryForm.scala with 93 added and 89 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/HubServer.scala with 466 added and 417 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/LandingPageRoutes.scala with 120 added and 109 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala with 16 added and 16 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala with 50 added and 48 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/RelatedTypesConverter.scala with 29 added and 29 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/RequestHelpers.scala with 31 added and 31 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/ResetPasswordForm.scala with 11 added and 11 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRepository.scala with 65 added and 65 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRoutes.scala with 211 added and 177 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/Session.scala with 26 added and 26 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala with 54 added and 52 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala with 23 added and 23 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SignupRepository.scala with 30 added and 30 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala with 120 added and 118 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala with 118 added and 118 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala with 366 added and 365 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala with 1,598 added and 1,509 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/config/ConfigurationPath.scala with 19 added and 19 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala with 164 added and 163 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala with 13 added and 13 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/forms/types.scala with 80 added and 80 removed lines
  • modules/hub/src/main/scala/de/smederee/hub/types.scala with 65 added and 65 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala with 51 added and 48 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala with 257 added and 255 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala with 23 added and 23 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/NoLogin.scala with 33 added and 33 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala with 194 added and 194 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala with 26 added and 26 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala with 90 added and 69 removed lines
  • modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala with 87 added and 87 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/LabelForm.scala with 76 added and 70 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala with 438 added and 404 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/MilestoneForm.scala with 86 added and 78 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala with 682 added and 626 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/TicketForm.scala with 119 added and 117 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala with 542 added and 489 removed lines
  • modules/hub/src/main/scala/de/smederee/tickets/UsernamePathParameter.scala with 7 added and 7 removed lines
  • modules/hub/src/test/scala/de/smederee/TestTags.scala with 1 added and 1 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/AuthenticationMiddlewareTest.scala with 93 added and 93 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala with 272 added and 252 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala with 394 added and 393 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DatabaseMigratorTest.scala with 32 added and 32 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DoobieAccountManagementRepositoryTest.scala with 249 added and 249 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DoobieAuthenticationRepositoryTest.scala with 457 added and 457 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DoobieResetPasswordRepositoryTest.scala with 244 added and 240 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DoobieSignupRepositoryTest.scala with 124 added and 124 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala with 594 added and 574 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/Generators.scala with 172 added and 172 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala with 36 added and 36 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/SessionIdTest.scala with 24 added and 24 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/SessionTest.scala with 4 added and 4 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/TestAuthenticationRepository.scala with 62 added and 62 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/VcsRepositoryPatchMetadataTest.scala with 54 added and 54 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala with 55 added and 55 removed lines
  • modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala with 54 added and 54 removed lines
  • modules/hub/src/test/scala/de/smederee/ssh/DoobieSshAuthenticationRepositoryTest.scala with 19 added and 19 removed lines
  • modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala with 79 added and 79 removed lines
  • modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala with 59 added and 47 removed lines
  • modules/hub/src/test/scala/de/smederee/ssh/SshServerProviderTest.scala with 39 added and 39 removed lines
  • modules/hub/src/test/scala/de/smederee/ssh/SshUsernameTest.scala with 38 added and 38 removed lines
  • modules/i18n/src/main/scala/de/smederee/i18n/LanguageCode.scala with 34 added and 34 removed lines
  • modules/i18n/src/main/scala/de/smederee/i18n/Messages.scala with 70 added and 70 removed lines
  • modules/i18n/src/test/scala/de/smederee/i18n/LanguageCodeTest.scala with 19 added and 19 removed lines
  • modules/i18n/src/test/scala/de/smederee/i18n/MessagesTest.scala with 37 added and 37 removed lines
  • modules/security/src/main/scala/de/smederee/security/CsrfToken.scala with 21 added and 21 removed lines
  • modules/security/src/main/scala/de/smederee/security/Password.scala with 48 added and 48 removed lines
  • modules/security/src/main/scala/de/smederee/security/PasswordHash.scala with 23 added and 23 removed lines
  • modules/security/src/main/scala/de/smederee/security/PrivateKey.scala with 29 added and 29 removed lines
  • modules/security/src/main/scala/de/smederee/security/SignAndValidate.scala with 92 added and 92 removed lines
  • modules/security/src/main/scala/de/smederee/security/UserId.scala with 47 added and 47 removed lines
  • modules/security/src/main/scala/de/smederee/security/Username.scala with 56 added and 56 removed lines
  • modules/security/src/test/scala/de/smederee/security/PasswordTest.scala with 33 added and 33 removed lines
  • modules/security/src/test/scala/de/smederee/security/SignAndValidateTest.scala with 33 added and 33 removed lines
  • modules/security/src/test/scala/de/smederee/security/UserIdTest.scala with 21 added and 21 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Assignee.scala with 109 added and 109 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/DoobieLabelRepository.scala with 37 added and 37 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala with 79 added and 79 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/DoobieProjectRepository.scala with 33 added and 33 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketRepository.scala with 134 added and 134 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketServiceApi.scala with 13 added and 13 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Label.scala with 112 added and 112 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/LabelRepository.scala with 48 added and 48 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Milestone.scala with 86 added and 86 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala with 77 added and 77 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Project.scala with 249 added and 249 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/ProjectRepository.scala with 68 added and 68 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Slf4jLogHandler.scala with 37 added and 37 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Submitter.scala with 107 added and 107 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala with 293 added and 292 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/TicketRepository.scala with 177 added and 177 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/TicketServiceApi.scala with 17 added and 17 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/TicketsUser.scala with 1 added and 1 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/config/ConfigurationPath.scala with 19 added and 19 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseConfig.scala with 1 added and 1 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseMigrator.scala with 35 added and 31 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/config/SmedereeTicketsConfiguration.scala with 28 added and 28 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/forms/FormValidator.scala with 13 added and 13 removed lines
  • modules/tickets/src/main/scala/de/smederee/tickets/forms/types.scala with 80 added and 80 removed lines
  • modules/tickets/src/test/scala/de/smederee/TestTags.scala with 1 added and 1 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/BaseSpec.scala with 311 added and 307 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/ColourCodeTest.scala with 12 added and 12 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/DoobieLabelRepositoryTest.scala with 263 added and 263 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala with 394 added and 384 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/DoobieProjectRepositoryTest.scala with 186 added and 181 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketRepositoryTest.scala with 826 added and 786 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketServiceApiTest.scala with 75 added and 75 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/Generators.scala with 252 added and 249 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala with 17 added and 17 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/LabelNameTest.scala with 17 added and 17 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/LabelTest.scala with 6 added and 6 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala with 11 added and 11 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTest.scala with 6 added and 6 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala with 17 added and 17 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/TicketContentTest.scala with 7 added and 7 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/TicketFilterTest.scala with 98 added and 85 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/TicketNumberTest.scala with 7 added and 7 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/TicketResolutionTest.scala with 11 added and 11 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/TicketStatusTest.scala with 11 added and 11 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/TicketTitleTest.scala with 7 added and 7 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/config/DatabaseMigratorTest.scala with 38 added and 38 removed lines
  • modules/tickets/src/test/scala/de/smederee/tickets/config/SmedereeTicketsConfigurationTest.scala with 39 added and 39 removed lines
  • twirl/src/main/scala/org/http4s/twirl/TwirlInstances.scala with 22 added and 22 removed lines
diff -rN -u old-smederee/build.sbt new-smederee/build.sbt
--- old-smederee/build.sbt	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/build.sbt	2025-01-13 17:13:25.048470967 +0000
@@ -16,39 +16,39 @@
 Global / semanticdbEnabled := true
 
 inThisBuild(
-  Seq(
-    scalaVersion := "3.3.1",
-    organization := "de.smederee",
-    organizationName := "Contributors as noted in the AUTHORS.md file",
-    version := "0.9.0-SNAPSHOT",
-    scalacOptions ++= Seq(
-        "-deprecation",
-        "-explain",
-        "-explain-types",
-        "-feature",
-        "-language:higherKinds",
-        "-language:implicitConversions",
-        "-no-indent",  // Prevent usage of indent based syntax.
-        "-old-syntax", // Enforce classic syntax.
-        "-unchecked",
-        "-Wunused:imports",             // Warn on unused imports including given and wildcard imports.
-        "-Wunused:linted",              // TODO: Find out what this does!
-        "-Wunused:locals",              // Warn on unused local definitions.
-        "-Wunused:nowarn",              // Warn on unused (useless) `@nowarn` annotations.
-        "-Wunused:params",              // Warn on unused parameters.
-        "-Wunused:privates",            // Warn on unused private definitions.
-        "-Wunused:unsafe-warn-patvars", // TODO: Find out what this does!
-        "-Wvalue-discard",              // Warn on discarding computed values.
-        //"-Xfatal-warnings", // FIXME: Make this work despite of Twirl!
-        "-Ykind-projector",
-    ),
-    resolvers += "jitpack" at "https://jitpack.io", // for JANSI fork
-    Compile / console / scalacOptions --= Seq("-Xfatal-warnings"),
-    Test / console / scalacOptions --= Seq("-Xfatal-warnings"),
-    Test / fork := true,
-    Test / parallelExecution := false,
-    Test / testOptions += Tests.Argument(TestFrameworks.MUnit, "-b")
-  )
+    Seq(
+        scalaVersion     := "3.3.1",
+        organization     := "de.smederee",
+        organizationName := "Contributors as noted in the AUTHORS.md file",
+        version          := "0.9.0-SNAPSHOT",
+        scalacOptions ++= Seq(
+            "-deprecation",
+            "-explain",
+            "-explain-types",
+            "-feature",
+            "-language:higherKinds",
+            "-language:implicitConversions",
+            "-no-indent",  // Prevent usage of indent based syntax.
+            "-old-syntax", // Enforce classic syntax.
+            "-unchecked",
+            "-Wunused:imports",             // Warn on unused imports including given and wildcard imports.
+            "-Wunused:linted",              // TODO: Find out what this does!
+            "-Wunused:locals",              // Warn on unused local definitions.
+            "-Wunused:nowarn",              // Warn on unused (useless) `@nowarn` annotations.
+            "-Wunused:params",              // Warn on unused parameters.
+            "-Wunused:privates",            // Warn on unused private definitions.
+            "-Wunused:unsafe-warn-patvars", // TODO: Find out what this does!
+            "-Wvalue-discard",              // Warn on discarding computed values.
+            // "-Xfatal-warnings", // FIXME: Make this work despite of Twirl!
+            "-Ykind-projector"
+        ),
+        resolvers += "jitpack" at "https://jitpack.io", // for JANSI fork
+        Compile / console / scalacOptions --= Seq("-Xfatal-warnings"),
+        Test / console / scalacOptions --= Seq("-Xfatal-warnings"),
+        Test / fork              := true,
+        Test / parallelExecution := false,
+        Test / testOptions += Tests.Argument(TestFrameworks.MUnit, "-b")
+    )
 )
 
 // *****************************************************************************
@@ -56,470 +56,470 @@
 // *****************************************************************************
 
 lazy val smederee =
-  project
-    .in(file("."))
-    .settings(commonSettings)
-    .settings(
-      name := "smederee",
-      publish := {},
-      publishLocal := {}
-    )
-    .aggregate(darcs, email, htmlUtils, hub, i18n, security, tickets, twirl)
+    project
+        .in(file("."))
+        .settings(commonSettings)
+        .settings(
+            name         := "smederee",
+            publish      := {},
+            publishLocal := {}
+        )
+        .aggregate(darcs, email, htmlUtils, hub, i18n, security, tickets, twirl)
 
 lazy val darcs =
-  project
-    .in(file("modules/darcs"))
-    .enablePlugins(AutomateHeaderPlugin)
-    .settings(commonSettings)
-    .settings(
-      name := "darcs",
-      libraryDependencies ++= Seq(
-        library.catsCore,
-        library.catsEffect,
-        library.logback,
-        library.osLib,
-        library.munit           % Test,
-        library.munitCatsEffect % Test,
-        library.munitDiscipline % Test,
-        library.munitScalaCheck % Test,
-        library.scalaCheck      % Test
-      )
-    )
+    project
+        .in(file("modules/darcs"))
+        .enablePlugins(AutomateHeaderPlugin)
+        .settings(commonSettings)
+        .settings(
+            name := "darcs",
+            libraryDependencies ++= Seq(
+                library.catsCore,
+                library.catsEffect,
+                library.logback,
+                library.osLib,
+                library.munit           % Test,
+                library.munitCatsEffect % Test,
+                library.munitDiscipline % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            )
+        )
 
 lazy val email =
-  project
-    .in(file("modules/email"))
-    .enablePlugins(AutomateHeaderPlugin)
-    .settings(commonSettings)
-    .settings(
-      name := "email",
-      libraryDependencies ++= Seq(
-        library.catsCore,
-        library.catsEffect,
-        library.ip4sCore,
-        library.logback,
-        library.simpleJavaMail,
-        library.munit           % Test,
-        library.munitCatsEffect % Test,
-        library.munitDiscipline % Test,
-        library.munitScalaCheck % Test,
-        library.scalaCheck      % Test
-      )
-    )
+    project
+        .in(file("modules/email"))
+        .enablePlugins(AutomateHeaderPlugin)
+        .settings(commonSettings)
+        .settings(
+            name := "email",
+            libraryDependencies ++= Seq(
+                library.catsCore,
+                library.catsEffect,
+                library.ip4sCore,
+                library.logback,
+                library.simpleJavaMail,
+                library.munit           % Test,
+                library.munitCatsEffect % Test,
+                library.munitDiscipline % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            )
+        )
 
 lazy val htmlUtils =
-  project
-    .in(file("modules/html-utils"))
-    .enablePlugins(AutomateHeaderPlugin)
-    .settings(commonSettings)
-    .settings(
-      name := "html-utils",
-      libraryDependencies ++= Seq(
-        library.catsCore,
-        library.commonMark,
-        library.commonMarkExtHeadingAnchor,
-        library.commonMarkExtTables,
-        library.commonMarkExtTaskListItems,
-        library.http4sCore,
-        library.ip4sCore,
-        library.munit             % Test,
-        library.munitScalaCheck   % Test,
-        library.scalaCheck        % Test
-      )
-    )
+    project
+        .in(file("modules/html-utils"))
+        .enablePlugins(AutomateHeaderPlugin)
+        .settings(commonSettings)
+        .settings(
+            name := "html-utils",
+            libraryDependencies ++= Seq(
+                library.catsCore,
+                library.commonMark,
+                library.commonMarkExtHeadingAnchor,
+                library.commonMarkExtTables,
+                library.commonMarkExtTaskListItems,
+                library.http4sCore,
+                library.ip4sCore,
+                library.munit           % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            )
+        )
 
 lazy val hub =
-  project
-    .in(file("modules/hub"))
-    .dependsOn(darcs, email, htmlUtils, i18n, security, tickets, twirl)
-    .enablePlugins(
-      AutomateHeaderPlugin,
-      BuildInfoPlugin,
-      DebianPlugin,
-      JavaServerAppPackaging,
-      JDebPackaging,
-      RpmPlugin,
-      SbtTwirl,
-      SystemdPlugin
-    )
-    .settings(commonSettings)
-    .settings(
-      name := "smederee-hub",
-      buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion),
-      buildInfoPackage := "de.smederee.hub",
-      libraryDependencies ++= Seq(
-        library.apacheSshdCore,
-        library.apacheSshdSftp,
-        library.apacheSshdScp,
-        library.bouncyCastleProvider,
-        library.catsCore,
-        library.circeCore,
-        library.circeGeneric,
-        library.circeParser,
-        library.doobieCore,
-        library.doobieHikari,
-        library.doobiePostgres,
-        library.flywayCore,
-        library.http4sCirce,
-        library.http4sCore,
-        library.http4sDsl,
-        library.http4sEmberClient,
-        library.http4sEmberServer,
-        //library.http4sTwirl,
-        library.jansi,
-        library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
-        library.log4catsSlf4j,
-        library.logback,
-        library.osLib,
-        library.postgresql,
-        library.pureConfig,
-        library.springSecurityCrypto,
-        library.munit           % Test,
-        library.munitCatsEffect % Test,
-        library.munitDiscipline % Test,
-        library.munitScalaCheck % Test,
-        library.scalaCheck      % Test
-      ),
-      libraryDependencies := libraryDependencies.value.map {
-        case module if module.name == "twirl-api" =>
-          module.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
-        case module => module
-      } ++ Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0"),
-      TwirlKeys.templateImports ++= Seq(
-        "cats._",
-        "cats.data._",
-        "cats.syntax.all._",
-        "de.smederee.html._",
-        "de.smederee.i18n._",
-        "de.smederee.security.{ CsrfToken, UserId, Username }",
-        "org.http4s.Uri"
-      )
-    )
-    .settings(
-      Seq(
-        daemonUser := "smederee",
-        daemonGroup := "smederee",
-        Debian / debianPackageProvides += "smederee-hub",
-        Debian / debianPackageDependencies += "openjdk-17-jre-headless",
-        defaultLinuxInstallLocation := "/usr/local/share",
-        maintainer := "Wegtam GmbH <devops@wegtam.com>",
-        rpmLicense := Option("AGPL-3.0 or later"),
-        rpmVendor := "Wegtam GmbH <devops@wegtam.com>",
-        // Create an empty `conf/production.conf` file if it does not exist.
-        Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
-          DebianConstants.Postinst -> Seq(
-            s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
-            s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
-          ).mkString(" && ") // Chain both commands together in the shell.
-        ),
-        // Require a service restart after installation / update.
-        Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
-          DebianConstants.Postinst -> s"restartService ${normalizedName.value}"
-        ),
-        // Create an empty `conf/production.conf` file if it does not exist.
-        Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
-          RpmConstants.Post -> Seq(
-            s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
-            s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
-          ).mkString(" && ") // Chain both commands together in the shell.
-        ),
-        // Require a service restart after installation / update.
-        Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
-          RpmConstants.Post -> s"restartService ${normalizedName.value}"
-        ),
-        packageSummary := "Smederee Hub Service - Software collaboration platform.",
-        packageDescription := "Leverage the power of the darcs vcs to handle your projects with ease and confidence, this is the central hub service",
-        //Debian / requiredStartFacilities := Option("$local_fs $remote_fs $network $postgresql"),
-        // Do not package API docs.
-        Compile / packageDoc / publishArtifact := false,
-        Compile / doc / sources := Seq.empty,
-        // Prevent a customised local application.conf file to be packaged!
-        Compile / packageBin / mappings ~= { files =>
-          files.filterNot {
-            case (_, name) => name == "application.conf"
-          }
-        }
-      )
-    )
+    project
+        .in(file("modules/hub"))
+        .dependsOn(darcs, email, htmlUtils, i18n, security, tickets, twirl)
+        .enablePlugins(
+            AutomateHeaderPlugin,
+            BuildInfoPlugin,
+            DebianPlugin,
+            JavaServerAppPackaging,
+            JDebPackaging,
+            RpmPlugin,
+            SbtTwirl,
+            SystemdPlugin
+        )
+        .settings(commonSettings)
+        .settings(
+            name             := "smederee-hub",
+            buildInfoKeys    := Seq[BuildInfoKey](name, version, scalaVersion),
+            buildInfoPackage := "de.smederee.hub",
+            libraryDependencies ++= Seq(
+                library.apacheSshdCore,
+                library.apacheSshdSftp,
+                library.apacheSshdScp,
+                library.bouncyCastleProvider,
+                library.catsCore,
+                library.circeCore,
+                library.circeGeneric,
+                library.circeParser,
+                library.doobieCore,
+                library.doobieHikari,
+                library.doobiePostgres,
+                library.flywayCore,
+                library.http4sCirce,
+                library.http4sCore,
+                library.http4sDsl,
+                library.http4sEmberClient,
+                library.http4sEmberServer,
+                // library.http4sTwirl,
+                library.jansi,
+                library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
+                library.log4catsSlf4j,
+                library.logback,
+                library.osLib,
+                library.postgresql,
+                library.pureConfig,
+                library.springSecurityCrypto,
+                library.munit           % Test,
+                library.munitCatsEffect % Test,
+                library.munitDiscipline % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            ),
+            libraryDependencies := libraryDependencies.value.map {
+                case module if module.name == "twirl-api" =>
+                    module.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
+                case module => module
+            } ++ Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0"),
+            TwirlKeys.templateImports ++= Seq(
+                "cats._",
+                "cats.data._",
+                "cats.syntax.all._",
+                "de.smederee.html._",
+                "de.smederee.i18n._",
+                "de.smederee.security.{ CsrfToken, UserId, Username }",
+                "org.http4s.Uri"
+            )
+        )
+        .settings(
+            Seq(
+                daemonUser  := "smederee",
+                daemonGroup := "smederee",
+                Debian / debianPackageProvides += "smederee-hub",
+                Debian / debianPackageDependencies += "openjdk-17-jre-headless",
+                defaultLinuxInstallLocation := "/usr/local/share",
+                maintainer                  := "Wegtam GmbH <devops@wegtam.com>",
+                rpmLicense                  := Option("AGPL-3.0 or later"),
+                rpmVendor                   := "Wegtam GmbH <devops@wegtam.com>",
+                // Create an empty `conf/production.conf` file if it does not exist.
+                Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
+                    DebianConstants.Postinst -> Seq(
+                        s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
+                        s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
+                    ).mkString(" && ") // Chain both commands together in the shell.
+                ),
+                // Require a service restart after installation / update.
+                Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
+                    DebianConstants.Postinst -> s"restartService ${normalizedName.value}"
+                ),
+                // Create an empty `conf/production.conf` file if it does not exist.
+                Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
+                    RpmConstants.Post -> Seq(
+                        s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
+                        s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
+                    ).mkString(" && ") // Chain both commands together in the shell.
+                ),
+                // Require a service restart after installation / update.
+                Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
+                    RpmConstants.Post -> s"restartService ${normalizedName.value}"
+                ),
+                packageSummary := "Smederee Hub Service - Software collaboration platform.",
+                packageDescription := "Leverage the power of the darcs vcs to handle your projects with ease and confidence, this is the central hub service",
+                // Debian / requiredStartFacilities := Option("$local_fs $remote_fs $network $postgresql"),
+                // Do not package API docs.
+                Compile / packageDoc / publishArtifact := false,
+                Compile / doc / sources                := Seq.empty,
+                // Prevent a customised local application.conf file to be packaged!
+                Compile / packageBin / mappings ~= { files =>
+                    files.filterNot { case (_, name) =>
+                        name == "application.conf"
+                    }
+                }
+            )
+        )
 
 lazy val i18n =
-  project
-    .in(file("modules/i18n"))
-    .enablePlugins(AutomateHeaderPlugin)
-    .settings(commonSettings)
-    .settings(
-      name := "i18n",
-      libraryDependencies ++= Seq(
-        library.catsCore,
-        library.logback,
-        library.munit           % Test,
-        library.munitDiscipline % Test,
-        library.munitScalaCheck % Test,
-        library.scalaCheck      % Test
-      ),
-    )
+    project
+        .in(file("modules/i18n"))
+        .enablePlugins(AutomateHeaderPlugin)
+        .settings(commonSettings)
+        .settings(
+            name := "i18n",
+            libraryDependencies ++= Seq(
+                library.catsCore,
+                library.logback,
+                library.munit           % Test,
+                library.munitDiscipline % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            )
+        )
 
 lazy val security =
-  project
-    .in(file("modules/security"))
-    .enablePlugins(AutomateHeaderPlugin)
-    .settings(commonSettings)
-    .settings(
-      name := "security",
-      libraryDependencies ++= Seq(
-        library.bouncyCastleProvider % Runtime,
-        library.catsCore,
-        library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
-        library.logback,
-        library.springSecurityCrypto,
-        library.munit             % Test,
-        library.munitDiscipline   % Test,
-        library.munitScalaCheck   % Test,
-        library.scalaCheck        % Test
-      ),
-    )
+    project
+        .in(file("modules/security"))
+        .enablePlugins(AutomateHeaderPlugin)
+        .settings(commonSettings)
+        .settings(
+            name := "security",
+            libraryDependencies ++= Seq(
+                library.bouncyCastleProvider % Runtime,
+                library.catsCore,
+                library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
+                library.logback,
+                library.springSecurityCrypto,
+                library.munit           % Test,
+                library.munitDiscipline % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            )
+        )
 
 lazy val tickets =
-  project
-    .in(file("modules/tickets"))
-    .dependsOn(email, htmlUtils, i18n, security, twirl)
-    .enablePlugins(
-      AutomateHeaderPlugin,
-      BuildInfoPlugin,
-      DebianPlugin,
-      JavaServerAppPackaging,
-      JDebPackaging,
-      RpmPlugin,
-      SbtTwirl,
-      SystemdPlugin
-    )
-    .settings(commonSettings)
-    .settings(
-      name := "smederee-tickets",
-      buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion),
-      buildInfoPackage := "de.smederee.tickets",
-      libraryDependencies ++= Seq(
-        library.catsCore,
-        library.circeCore,
-        library.circeGeneric,
-        library.circeParser,
-        library.doobieCore,
-        library.doobieHikari,
-        library.doobiePostgres,
-        library.flywayCore,
-        library.http4sCirce,
-        library.http4sCore,
-        library.http4sDsl,
-        library.http4sEmberClient,
-        library.http4sEmberServer,
-        //library.http4sTwirl,
-        library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
-        library.log4catsSlf4j,
-        library.logback,
-        library.postgresql,
-        library.pureConfig,
-        library.springSecurityCrypto,
-        library.munit           % Test,
-        library.munitCatsEffect % Test,
-        library.munitDiscipline % Test,
-        library.munitScalaCheck % Test,
-        library.scalaCheck      % Test
-      )
-    )
-    .settings(
-      libraryDependencies := libraryDependencies.value.map {
-        case module if module.name == "twirl-api" =>
-          module.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
-        case module => module
-      } ++ Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0"),
-      TwirlKeys.templateImports ++= Seq(
-        "cats._",
-        "cats.data._",
-        "cats.syntax.all._",
-        "de.smederee.html._",
-        "de.smederee.i18n._",
-        "de.smederee.security.{ CsrfToken, UserId, Username }",
-        "org.http4s.Uri"
-      )
-    )
-    .settings(
-      Seq(
-        daemonUser := "smederee",
-        daemonGroup := "smederee",
-        Debian / debianPackageProvides += "smederee-tickets",
-        Debian / debianPackageDependencies += "openjdk-17-jre-headless",
-        defaultLinuxInstallLocation := "/usr/local/share",
-        maintainer := "Wegtam GmbH <devops@wegtam.com>",
-        rpmLicense := Option("AGPL-3.0 or later"),
-        rpmVendor := "Wegtam GmbH <devops@wegtam.com>",
-        // Create an empty `conf/production.conf` file if it does not exist.
-        Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
-          DebianConstants.Postinst -> Seq(
-            s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
-            s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
-          ).mkString(" && ") // Chain both commands together in the shell.
-        ),
-        // Require a service restart after installation / update.
-        Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
-          DebianConstants.Postinst -> s"restartService ${normalizedName.value}"
-        ),
-        // Create an empty `conf/production.conf` file if it does not exist.
-        Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
-          RpmConstants.Post -> Seq(
-            s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
-            s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
-          ).mkString(" && ") // Chain both commands together in the shell.
-        ),
-        // Require a service restart after installation / update.
-        Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
-          RpmConstants.Post -> s"restartService ${normalizedName.value}"
-        ),
-        packageSummary := "Smederee Ticket Service - Software collaboration platform.",
-        packageDescription := "Leverage the power of the darcs vcs to handle your projects with ease and confidence, this is the ticket service",
-        //Debian / requiredStartFacilities := Option("$local_fs $remote_fs $network $postgresql"),
-        // Do not package API docs.
-        Compile / packageDoc / publishArtifact := false,
-        Compile / doc / sources := Seq.empty,
-        // Require tests to be run before building a debian package.
-        Debian / packageBin := ((Debian / packageBin) dependsOn (Test / test)).value,
-        // Require tests to be run before building a RPM package.
-        Rpm / packageBin := ((Rpm / packageBin) dependsOn (Test / test)).value,
-        // Require tests to be run before building a universal package.
-        Universal / packageBin := ((Universal / packageBin) dependsOn (Test / test)).value,
-        Universal / packageOsxDmg := ((Universal / packageOsxDmg) dependsOn (Test / test)).value,
-        Universal / packageXzTarball := ((Universal / packageXzTarball) dependsOn (Test / test)).value,
-        Universal / packageZipTarball := ((Universal / packageZipTarball) dependsOn (Test / test)).value,
-        // Prevent a customised local application.conf file to be packaged!
-        Compile / packageBin / mappings ~= { files =>
-          files.filterNot {
-            case (_, name) => name == "application.conf"
-          }
-        }
-      )
-    )
+    project
+        .in(file("modules/tickets"))
+        .dependsOn(email, htmlUtils, i18n, security, twirl)
+        .enablePlugins(
+            AutomateHeaderPlugin,
+            BuildInfoPlugin,
+            DebianPlugin,
+            JavaServerAppPackaging,
+            JDebPackaging,
+            RpmPlugin,
+            SbtTwirl,
+            SystemdPlugin
+        )
+        .settings(commonSettings)
+        .settings(
+            name             := "smederee-tickets",
+            buildInfoKeys    := Seq[BuildInfoKey](name, version, scalaVersion),
+            buildInfoPackage := "de.smederee.tickets",
+            libraryDependencies ++= Seq(
+                library.catsCore,
+                library.circeCore,
+                library.circeGeneric,
+                library.circeParser,
+                library.doobieCore,
+                library.doobieHikari,
+                library.doobiePostgres,
+                library.flywayCore,
+                library.http4sCirce,
+                library.http4sCore,
+                library.http4sDsl,
+                library.http4sEmberClient,
+                library.http4sEmberServer,
+                // library.http4sTwirl,
+                library.jclOverSlf4j, // Bridge Java Commons Logging to SLF4J.
+                library.log4catsSlf4j,
+                library.logback,
+                library.postgresql,
+                library.pureConfig,
+                library.springSecurityCrypto,
+                library.munit           % Test,
+                library.munitCatsEffect % Test,
+                library.munitDiscipline % Test,
+                library.munitScalaCheck % Test,
+                library.scalaCheck      % Test
+            )
+        )
+        .settings(
+            libraryDependencies := libraryDependencies.value.map {
+                case module if module.name == "twirl-api" =>
+                    module.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
+                case module => module
+            } ++ Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0"),
+            TwirlKeys.templateImports ++= Seq(
+                "cats._",
+                "cats.data._",
+                "cats.syntax.all._",
+                "de.smederee.html._",
+                "de.smederee.i18n._",
+                "de.smederee.security.{ CsrfToken, UserId, Username }",
+                "org.http4s.Uri"
+            )
+        )
+        .settings(
+            Seq(
+                daemonUser  := "smederee",
+                daemonGroup := "smederee",
+                Debian / debianPackageProvides += "smederee-tickets",
+                Debian / debianPackageDependencies += "openjdk-17-jre-headless",
+                defaultLinuxInstallLocation := "/usr/local/share",
+                maintainer                  := "Wegtam GmbH <devops@wegtam.com>",
+                rpmLicense                  := Option("AGPL-3.0 or later"),
+                rpmVendor                   := "Wegtam GmbH <devops@wegtam.com>",
+                // Create an empty `conf/production.conf` file if it does not exist.
+                Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
+                    DebianConstants.Postinst -> Seq(
+                        s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
+                        s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
+                    ).mkString(" && ") // Chain both commands together in the shell.
+                ),
+                // Require a service restart after installation / update.
+                Debian / maintainerScripts := maintainerScriptsAppend((Debian / maintainerScripts).value)(
+                    DebianConstants.Postinst -> s"restartService ${normalizedName.value}"
+                ),
+                // Create an empty `conf/production.conf` file if it does not exist.
+                Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
+                    RpmConstants.Post -> Seq(
+                        s"touch ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf",
+                        s"chown ${daemonUser.value}:${daemonGroup.value} ${defaultLinuxInstallLocation.value}/${normalizedName.value}/conf/production.conf"
+                    ).mkString(" && ") // Chain both commands together in the shell.
+                ),
+                // Require a service restart after installation / update.
+                Rpm / maintainerScripts := maintainerScriptsAppend((Rpm / maintainerScripts).value)(
+                    RpmConstants.Post -> s"restartService ${normalizedName.value}"
+                ),
+                packageSummary := "Smederee Ticket Service - Software collaboration platform.",
+                packageDescription := "Leverage the power of the darcs vcs to handle your projects with ease and confidence, this is the ticket service",
+                // Debian / requiredStartFacilities := Option("$local_fs $remote_fs $network $postgresql"),
+                // Do not package API docs.
+                Compile / packageDoc / publishArtifact := false,
+                Compile / doc / sources                := Seq.empty,
+                // Require tests to be run before building a debian package.
+                Debian / packageBin := ((Debian / packageBin) dependsOn (Test / test)).value,
+                // Require tests to be run before building a RPM package.
+                Rpm / packageBin := ((Rpm / packageBin) dependsOn (Test / test)).value,
+                // Require tests to be run before building a universal package.
+                Universal / packageBin        := ((Universal / packageBin) dependsOn (Test / test)).value,
+                Universal / packageOsxDmg     := ((Universal / packageOsxDmg) dependsOn (Test / test)).value,
+                Universal / packageXzTarball  := ((Universal / packageXzTarball) dependsOn (Test / test)).value,
+                Universal / packageZipTarball := ((Universal / packageZipTarball) dependsOn (Test / test)).value,
+                // Prevent a customised local application.conf file to be packaged!
+                Compile / packageBin / mappings ~= { files =>
+                    files.filterNot { case (_, name) =>
+                        name == "application.conf"
+                    }
+                }
+            )
+        )
 
 // FIXME: This is a workaround until http4s-twirl gets published properly for Scala 3!
 lazy val twirl =
-  project
-    .in(file("twirl"))
-    .enablePlugins(SbtTwirl)
-    .settings(commonSettings)
-    .settings(
-      name := "twirl",
-      version := "0.1.0",
-      libraryDependencies += library.http4sCore,
-      libraryDependencies := libraryDependencies.value.map {
-        case module if module.name == "twirl-api" =>
-          module.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
-        case module => module
-      } ++ Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0"),
-    )
+    project
+        .in(file("twirl"))
+        .enablePlugins(SbtTwirl)
+        .settings(commonSettings)
+        .settings(
+            name    := "twirl",
+            version := "0.1.0",
+            libraryDependencies += library.http4sCore,
+            libraryDependencies := libraryDependencies.value.map {
+                case module if module.name == "twirl-api" =>
+                    module.cross(CrossVersion.for3Use2_13).exclude("org.scala-lang.modules", "scala-xml_2.13")
+                case module => module
+            } ++ Seq("org.scala-lang.modules" %% "scala-xml" % "2.2.0")
+        )
 
 // *****************************************************************************
 // Library dependencies
 // *****************************************************************************
 
 lazy val library =
-  new {
-    object Version {
-      val apacheSshd      = "2.11.0"
-      val bouncyCastle    = "1.76"
-      val cats            = "2.10.0"
-      val catsEffect      = "3.5.2"
-      val circe           = "0.14.6"
-      val commonMark      = "0.21.0"
-      val doobie          = "1.0.0-RC4"
-      val flyway          = "9.22.3"
-      val fs2             = "3.5.0"
-      val http4s          = "1.0.0-M40"
-      val ip4s            = "3.4.0"
-      val jansi           = "2.4.2"
-      val jclOverSlf4j    = "2.0.9"
-      val log4cats        = "2.6.0"
-      val logback         = "1.4.11"
-      val munit           = "0.7.29"
-      val munitCatsEffect = "1.0.7"
-      val munitDiscipline = "1.0.9"
-      val osLib           = "0.9.2"
-      val postgresql      = "42.6.0"
-      val pureConfig      = "0.17.4"
-      val scalaCheck      = "1.17.0"
-      val simpleJavaMail  = "8.3.1"
-      val springSecurity  = "6.1.5"
+    new {
+        object Version {
+            val apacheSshd      = "2.11.0"
+            val bouncyCastle    = "1.76"
+            val cats            = "2.10.0"
+            val catsEffect      = "3.5.2"
+            val circe           = "0.14.6"
+            val commonMark      = "0.21.0"
+            val doobie          = "1.0.0-RC4"
+            val flyway          = "9.22.3"
+            val fs2             = "3.5.0"
+            val http4s          = "1.0.0-M40"
+            val ip4s            = "3.4.0"
+            val jansi           = "2.4.2"
+            val jclOverSlf4j    = "2.0.9"
+            val log4cats        = "2.6.0"
+            val logback         = "1.4.11"
+            val munit           = "0.7.29"
+            val munitCatsEffect = "1.0.7"
+            val munitDiscipline = "1.0.9"
+            val osLib           = "0.9.2"
+            val postgresql      = "42.6.0"
+            val pureConfig      = "0.17.4"
+            val scalaCheck      = "1.17.0"
+            val simpleJavaMail  = "8.3.1"
+            val springSecurity  = "6.1.5"
+        }
+        val apacheSshdCore             = "org.apache.sshd"  % "sshd-core"                      % Version.apacheSshd
+        val apacheSshdSftp             = "org.apache.sshd"  % "sshd-sftp"                      % Version.apacheSshd
+        val apacheSshdScp              = "org.apache.sshd"  % "sshd-scp"                       % Version.apacheSshd
+        val bouncyCastleProvider       = "org.bouncycastle" % "bcprov-jdk15to18"               % Version.bouncyCastle
+        val catsCore                   = "org.typelevel"   %% "cats-core"                      % Version.cats
+        val catsEffect                 = "org.typelevel"   %% "cats-effect"                    % Version.catsEffect
+        val circeCore                  = "io.circe"        %% "circe-core"                     % Version.circe
+        val circeGeneric               = "io.circe"        %% "circe-generic"                  % Version.circe
+        val circeParser                = "io.circe"        %% "circe-parser"                   % Version.circe
+        val commonMark                 = "org.commonmark"   % "commonmark"                     % Version.commonMark
+        val commonMarkExtHeadingAnchor = "org.commonmark"   % "commonmark-ext-heading-anchor"  % Version.commonMark
+        val commonMarkExtTables        = "org.commonmark"   % "commonmark-ext-gfm-tables"      % Version.commonMark
+        val commonMarkExtTaskListItems = "org.commonmark"   % "commonmark-ext-task-list-items" % Version.commonMark
+        val doobieCore                 = "org.tpolecat"    %% "doobie-core"                    % Version.doobie
+        val doobieHikari               = "org.tpolecat"    %% "doobie-hikari"                  % Version.doobie
+        val doobiePostgres             = "org.tpolecat"    %% "doobie-postgres"                % Version.doobie
+        val doobieScalaTest            = "org.tpolecat"    %% "doobie-scalatest"               % Version.doobie
+        val flywayCore                 = "org.flywaydb"     % "flyway-core"                    % Version.flyway
+        val fs2Core                    = "co.fs2"          %% "fs2-core"                       % Version.fs2
+        val fs2IO                      = "co.fs2"          %% "fs2-io"                         % Version.fs2
+        val http4sCirce                = "org.http4s"      %% "http4s-circe"                   % Version.http4s
+        val http4sCore                 = "org.http4s"      %% "http4s-core"                    % Version.http4s
+        val http4sDsl                  = "org.http4s"      %% "http4s-dsl"                     % Version.http4s
+        val http4sEmberServer          = "org.http4s"      %% "http4s-ember-server"            % Version.http4s
+        val http4sEmberClient          = "org.http4s"      %% "http4s-ember-client"            % Version.http4s
+        // val http4sTwirl                = "org.http4s"                   %% "http4s-twirl"                   % Version.http4s
+        val ip4sCore             = "com.comcast"                 %% "ip4s-core"              % Version.ip4s
+        val jansi                = "com.github.Osiris-Team"       % "jansi"                  % Version.jansi
+        val jclOverSlf4j         = "org.slf4j"                    % "jcl-over-slf4j"         % Version.jclOverSlf4j
+        val log4catsSlf4j        = "org.typelevel"               %% "log4cats-slf4j"         % Version.log4cats
+        val logback              = "ch.qos.logback"               % "logback-classic"        % Version.logback
+        val munit                = "org.scalameta"               %% "munit"                  % Version.munit
+        val munitCatsEffect      = "org.typelevel"               %% "munit-cats-effect-3"    % Version.munitCatsEffect
+        val munitDiscipline      = "org.typelevel"               %% "discipline-munit"       % Version.munitDiscipline
+        val munitScalaCheck      = "org.scalameta"               %% "munit-scalacheck"       % Version.munit
+        val osLib                = "com.lihaoyi"                 %% "os-lib"                 % Version.osLib
+        val postgresql           = "org.postgresql"               % "postgresql"             % Version.postgresql
+        val pureConfig           = "com.github.pureconfig"       %% "pureconfig-core"        % Version.pureConfig
+        val scalaCheck           = "org.scalacheck"              %% "scalacheck"             % Version.scalaCheck
+        val simpleJavaMail       = "org.simplejavamail"           % "simple-java-mail"       % Version.simpleJavaMail
+        val springSecurityCrypto = "org.springframework.security" % "spring-security-crypto" % Version.springSecurity
     }
-    val apacheSshdCore             = "org.apache.sshd"              %  "sshd-core"                      % Version.apacheSshd
-    val apacheSshdSftp             = "org.apache.sshd"              %  "sshd-sftp"                      % Version.apacheSshd
-    val apacheSshdScp              = "org.apache.sshd"              %  "sshd-scp"                       % Version.apacheSshd
-    val bouncyCastleProvider       = "org.bouncycastle"             %  "bcprov-jdk15to18"               % Version.bouncyCastle
-    val catsCore                   = "org.typelevel"                %% "cats-core"                      % Version.cats
-    val catsEffect                 = "org.typelevel"                %% "cats-effect"                    % Version.catsEffect
-    val circeCore                  = "io.circe"                     %% "circe-core"                     % Version.circe
-    val circeGeneric               = "io.circe"                     %% "circe-generic"                  % Version.circe
-    val circeParser                = "io.circe"                     %% "circe-parser"                   % Version.circe
-    val commonMark                 = "org.commonmark"               %  "commonmark"                     % Version.commonMark
-    val commonMarkExtHeadingAnchor = "org.commonmark"               %  "commonmark-ext-heading-anchor"  % Version.commonMark
-    val commonMarkExtTables        = "org.commonmark"               %  "commonmark-ext-gfm-tables"      % Version.commonMark
-    val commonMarkExtTaskListItems = "org.commonmark"               %  "commonmark-ext-task-list-items" % Version.commonMark
-    val doobieCore                 = "org.tpolecat"                 %% "doobie-core"                    % Version.doobie
-    val doobieHikari               = "org.tpolecat"                 %% "doobie-hikari"                  % Version.doobie
-    val doobiePostgres             = "org.tpolecat"                 %% "doobie-postgres"                % Version.doobie
-    val doobieScalaTest            = "org.tpolecat"                 %% "doobie-scalatest"               % Version.doobie
-    val flywayCore                 = "org.flywaydb"                 %  "flyway-core"                    % Version.flyway
-    val fs2Core                    = "co.fs2"                       %% "fs2-core"                       % Version.fs2
-    val fs2IO                      = "co.fs2"                       %% "fs2-io"                         % Version.fs2
-    val http4sCirce                = "org.http4s"                   %% "http4s-circe"                   % Version.http4s
-    val http4sCore                 = "org.http4s"                   %% "http4s-core"                    % Version.http4s
-    val http4sDsl                  = "org.http4s"                   %% "http4s-dsl"                     % Version.http4s
-    val http4sEmberServer          = "org.http4s"                   %% "http4s-ember-server"            % Version.http4s
-    val http4sEmberClient          = "org.http4s"                   %% "http4s-ember-client"            % Version.http4s
-    //val http4sTwirl                = "org.http4s"                   %% "http4s-twirl"                   % Version.http4s
-    val ip4sCore                   = "com.comcast"                  %% "ip4s-core"                      % Version.ip4s
-    val jansi                      = "com.github.Osiris-Team"       %  "jansi"                          % Version.jansi
-    val jclOverSlf4j               = "org.slf4j"                    %  "jcl-over-slf4j"                 % Version.jclOverSlf4j
-    val log4catsSlf4j              = "org.typelevel"                %% "log4cats-slf4j"                    % Version.log4cats
-    val logback                    = "ch.qos.logback"               %  "logback-classic"                % Version.logback
-    val munit                      = "org.scalameta"                %% "munit"                          % Version.munit
-    val munitCatsEffect            = "org.typelevel"                %% "munit-cats-effect-3"            % Version.munitCatsEffect
-    val munitDiscipline            = "org.typelevel"                %% "discipline-munit"               % Version.munitDiscipline
-    val munitScalaCheck            = "org.scalameta"                %% "munit-scalacheck"               % Version.munit
-    val osLib                      = "com.lihaoyi"                  %% "os-lib"                         % Version.osLib
-    val postgresql                 = "org.postgresql"               %  "postgresql"                     % Version.postgresql
-    val pureConfig                 = "com.github.pureconfig"        %% "pureconfig-core"                % Version.pureConfig
-    val scalaCheck                 = "org.scalacheck"               %% "scalacheck"                     % Version.scalaCheck
-    val simpleJavaMail             = "org.simplejavamail"           %  "simple-java-mail"               % Version.simpleJavaMail
-    val springSecurityCrypto       = "org.springframework.security" %  "spring-security-crypto"         % Version.springSecurity
-  }
 
 // *****************************************************************************
 // Settings
 // *****************************************************************************
 
-/** Calculate the copyright year string e.g. xxxx or xxxx - yyyy depending
-  * on the given start year and the current year.
+/** Calculate the copyright year string e.g. xxxx or xxxx - yyyy depending on the given start year and the current year.
   *
   * @param start
   *   The start year of the project.
   * @param current
   *   The current year which must be equal or larger than the start year.
-  * @return A string containing either only the start year or "start year - current year"
+  * @return
+  *   A string containing either only the start year or "start year - current year"
   */
 def calculateCopyrightYears(start: Int, current: Int): String =
-  if (start < current)
-    s"$start - $current"
-  else
-    start.toString
+    if (start < current)
+        s"$start - $current"
+    else
+        start.toString
 
 lazy val commonSettings =
-  codeSettings ++
-  scalafmtSettings
+    codeSettings ++
+        scalafmtSettings
 
 lazy val codeSettings =
-  Seq(
-    startYear := Option(projectStartYear),
-    licenses += ("AGPL-3.0-or-later", new URL("https://www.gnu.org/licenses/agpl-3.0.txt"))
-  )
+    Seq(
+        startYear := Option(projectStartYear),
+        licenses += ("AGPL-3.0-or-later", new URL("https://www.gnu.org/licenses/agpl-3.0.txt"))
+    )
 
 lazy val scalafmtSettings =
-  Seq(
-    scalafmtOnCompile := false,
-  )
+    Seq(
+        scalafmtOnCompile := false
+    )
diff -rN -u old-smederee/CODINGSTYLE.md new-smederee/CODINGSTYLE.md
--- old-smederee/CODINGSTYLE.md	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/CODINGSTYLE.md	2025-01-13 17:13:25.048470967 +0000
@@ -12,22 +12,31 @@
 2. The configuration file for Scalafmt SHALL be the file [.scalafmt.conf](.scalafmt.conf) in the project root and SHALL be based upon the preset `defaultWithAlign` using the runner dialect `scala3` of Scalafmt with some additions.
 3. The configuration file for Scalafix SHALL be the file [.scalafix.conf](.scalafix.conf) in the project root and SHALL be configured to organize (rewrite) import rules.
 4. In the rare case of formatting interfering with code functionality, it is RECOMMENDED to disable automatic code formatting for the affected parts of the code only.
+5. An `.editorconfig` file in the project root MUST exist and SHALL contain the necessary settings.
 
 ### 2.1. Additions to the `defaultWithAlign` preset
 
 1. No unicode characters or literals are allowed in the source code e.g. `⇒` SHALL be written as `=>` or `←` as `<-` and so on.
 2. Rewrite rules SHOULD exist in the configuration to rewrite common unicode characters to their ASCII counterparts.
-3. The maximum line length in the source code SHOULD not be longer than 120 characters. There MAY be rare cases where exceptions are allowed.
-4. Imports SHOULD be written after the following guidelines and automatically applied by running Scalafix:
+3. Each level of indentation (for Scala code) MUST be 4 space characters.
+   ```scala
+   val foo = ???
+   val bar = foo match {
+       case something => ???
+       case _ => ???
+   }
+   ```
+4. The maximum line length in the source code SHOULD not be longer than 120 characters. There MAY be rare cases where exceptions are allowed.
+5. Imports SHOULD be written after the following guidelines and automatically applied by running Scalafix:
    1. Imports from Java core (e.g. `import java.time._`) MUST be written (grouped) first, followed by a blank line.
    2. Imports from the Scala core (e.g. `import scala.util.Failure`) MUST be written (grouped) last, prefixed by a blank line.
    3. Other imports SHOULD be grouped in between the Java and Scala core imports.
    4. Within the other imports test framework imports (e.g. `munit.` or `org.scalacheck.`) SHOULD be put last and grouped.
    5. Multiple imports from the same package in curly braces SHALL be rewritten as single import per line. This intends to reduce possible merge conflicts by import changes. The option `groupedImports = Explode` for Scalafix is intended to rewrite affected code automatically.
    6. Imports SHALL be sorted by their ASCII names. This SHOULD be enforced by the `importsOrder` setting of Scalafix.
-5. Redundant braces SHALL be removed (enforced by `rewrite.rules` including `RedundantBraces`).
-6. Redundant parentheses SHALL be removed (enforced by `rewrite.rules` including `RedundantParens`).
-7. Configuration files for sbt (e.g. `*.sbt`) MUST NOT be formatted via Scalafmt.
+6. Redundant braces SHALL be removed (enforced by `rewrite.rules` including `RedundantBraces`).
+7. Redundant parentheses SHALL be removed (enforced by `rewrite.rules` including `RedundantParens`).
+8. Configuration files for sbt (e.g. `*.sbt`) MUST NOT be formatted via Scalafmt.
 
 **TODO**: Describe `danglingParentheses.preset = true`
 
diff -rN -u old-smederee/.editorconfig new-smederee/.editorconfig
--- old-smederee/.editorconfig	1970-01-01 00:00:00.000000000 +0000
+++ new-smederee/.editorconfig	2025-01-13 17:13:25.048470967 +0000
@@ -0,0 +1,25 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Unix-style newlines with a newline ending every file
+[*]
+end_of_line = lf
+insert_final_newline = true
+
+# Matches multiple files with brace expansion notation
+
+# Scala
+[*.{scala,sbt,sc}]
+charset = utf-8
+indent_style = space
+indent_size = 4
+max_line_length = 120
+
+# Twirl
+[*.scala.{html,txt}]
+charset = utf-8
+indent_style = space
+indent_size = 2
+
diff -rN -u old-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala
--- old-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/modules/darcs/src/main/scala/de/smederee/darcs/DarcsCommands.scala	2025-01-13 17:13:25.048470967 +0000
@@ -29,25 +29,25 @@
 
 opaque type DarcsHash = String
 object DarcsHash {
-  val Format: Regex = "^[a-f0-9]{40}$".r
+    val Format: Regex = "^[a-f0-9]{40}$".r
 
-  /** Create an instance of DarcsHash from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a DarcsHash.
-    * @return
-    *   The appropriate instance of DarcsHash.
-    */
-  def apply(source: String): DarcsHash = source
-
-  /** Try to create an instance of DarcsHash from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a DarcsHash.
-    * @return
-    *   An option to the successfully converted DarcsHash.
-    */
-  def from(source: String): Option[DarcsHash] = Option(source).filter(s => Format.matches(s))
+    /** Create an instance of DarcsHash from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a DarcsHash.
+      * @return
+      *   The appropriate instance of DarcsHash.
+      */
+    def apply(source: String): DarcsHash = source
+
+    /** Try to create an instance of DarcsHash from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a DarcsHash.
+      * @return
+      *   An option to the successfully converted DarcsHash.
+      */
+    def from(source: String): Option[DarcsHash] = Option(source).filter(s => Format.matches(s))
 
 }
 
@@ -70,189 +70,189 @@
   *   A higher kinded type providing needed functionality, which is usually an IO monad like Async or Sync.
   */
 final class DarcsCommands[F[_]: Sync](val darcsBinary: Path) {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  /** Apply a patch to a darcs repository.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param patchFile
-    *   The path to the file containing the patch. This may also be a complete email. In the latter case the email will
-    *   be parsed for a MIME attachment with a darcs patch.
-    * @param options
-    *   Additional options for the initialize command. Please note that you should not specify options which need user
-    *   interaction and also you might want to set the `--dont-allow-conflicts` options.
-    * @return
-    *   The output of the darcs command.
-    */
-  def applyPatch(basePath: Path)(repositoryName: String)(patchFile: Path)(
-      options: Chain[String]
-  ): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary apply $patchFile for $repositoryName with $options")
-    val repositoryDirectory = basePath.resolve(repositoryName)
-    val darcsOptions        = List("apply") ::: options.toList ::: List(patchFile.toAbsolutePath().toString)
-    val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Clone a darcs repository from one directory into another one. This function is intended to be used for server side
-    * forking or repositories.
-    *
-    * @param sourceRepositoryPath
-    *   The path of the source repository.
-    * @param targetRespositoryPath
-    *   The path of the target repository.
-    * @param options
-    *   Additional options for the clone command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def clone(sourceRepositoryPath: Path, targetRespositoryPath: Path)(
-      options: Chain[String]
-  ): F[DarcsCommandOutput] = {
-    val source = sourceRepositoryPath.toAbsolutePath
-    val target = targetRespositoryPath.toAbsolutePath
-    log.trace(s"Execute $darcsBinary clone $sourceRepositoryPath to $targetRespositoryPath with $options")
-    val darcsOptions    = List("clone") ::: options.toList ::: List(source.toString, target.toString)
-    val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Return the diff of the specified patch from the given darcs repository.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param hash
-    *   The hash of the patch whose diff shall be generated.
-    * @param options
-    *   Additional options for the initialize command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def diff(basePath: Path)(repositoryName: String)(hash: DarcsHash)(options: Chain[String]): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary diff --hash $hash for $repositoryName with $options")
-    val repositoryDirectory = Paths.get(basePath.toString, repositoryName)
-    val darcsOptions        = List("diff", s"--hash=${hash.toString}") ::: options.toList
-    val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Create a distribution archive from the given darcs repository.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param options
-    *   Additional options for the initialize command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def dist(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary dist for $repositoryName")
-    val repositoryDirectory = Paths.get(basePath.toString, repositoryName)
-    val darcsOptions        = List("dist") ::: options.toList
-    val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Initialize a darcs repository under the given base path with the provided name. This is done by running the
-    * external darcs binary with the appropriate parameters.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param options
-    *   Additional options for the initialize command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def initialize(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary initialize $basePath/$repositoryName with $options")
-    val directory       = Paths.get(basePath.toString, repositoryName)
-    val darcsOptions    = List("initialize") ::: options.toList ::: List(directory.toString)
-    val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Run the darcs log command on the given repository and return the output.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param options
-    *   Additional options for the log command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def log(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary log in $basePath/$repositoryName with $options")
-    val directory       = Paths.get(basePath.toString, repositoryName)
-    val darcsOptions    = List("log") ::: options.toList
-    val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(directory), check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Run the darcs show dependencies command on the given repository and return the output.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param options
-    *   Additional options for the show command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def showDependencies(
-      basePath: Path
-  )(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary show dependencies in $basePath/$repositoryName with $options")
-    val directory       = Paths.get(basePath.toString, repositoryName)
-    val darcsOptions    = List("show", "dependencies") ::: options.toList
-    val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(directory), check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
-
-  /** Run the darcs whatsnew command on the given repository and return the output.
-    *
-    * @param basePath
-    *   The base path under which the repository is located.
-    * @param repositoryName
-    *   The name of the repository.
-    * @param options
-    *   Additional options for the whatsnew command.
-    * @return
-    *   The output of the darcs command.
-    */
-  def whatsnew(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
-    log.trace(s"Execute $darcsBinary whatsnew in $basePath/$repositoryName with $options")
-    val directory       = Paths.get(basePath.toString, repositoryName)
-    val darcsOptions    = List("whatsnew") ::: options.toList
-    val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
-    for {
-      process <- Sync[F].delay(externalCommand.call(cwd = os.Path(directory), check = false))
-    } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-  }
+    /** Apply a patch to a darcs repository.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param patchFile
+      *   The path to the file containing the patch. This may also be a complete email. In the latter case the email
+      *   will be parsed for a MIME attachment with a darcs patch.
+      * @param options
+      *   Additional options for the initialize command. Please note that you should not specify options which need user
+      *   interaction and also you might want to set the `--dont-allow-conflicts` options.
+      * @return
+      *   The output of the darcs command.
+      */
+    def applyPatch(basePath: Path)(repositoryName: String)(patchFile: Path)(
+        options: Chain[String]
+    ): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary apply $patchFile for $repositoryName with $options")
+        val repositoryDirectory = basePath.resolve(repositoryName)
+        val darcsOptions        = List("apply") ::: options.toList ::: List(patchFile.toAbsolutePath().toString)
+        val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Clone a darcs repository from one directory into another one. This function is intended to be used for server
+      * side forking or repositories.
+      *
+      * @param sourceRepositoryPath
+      *   The path of the source repository.
+      * @param targetRespositoryPath
+      *   The path of the target repository.
+      * @param options
+      *   Additional options for the clone command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def clone(sourceRepositoryPath: Path, targetRespositoryPath: Path)(
+        options: Chain[String]
+    ): F[DarcsCommandOutput] = {
+        val source = sourceRepositoryPath.toAbsolutePath
+        val target = targetRespositoryPath.toAbsolutePath
+        log.trace(s"Execute $darcsBinary clone $sourceRepositoryPath to $targetRespositoryPath with $options")
+        val darcsOptions    = List("clone") ::: options.toList ::: List(source.toString, target.toString)
+        val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Return the diff of the specified patch from the given darcs repository.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param hash
+      *   The hash of the patch whose diff shall be generated.
+      * @param options
+      *   Additional options for the initialize command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def diff(basePath: Path)(repositoryName: String)(hash: DarcsHash)(options: Chain[String]): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary diff --hash $hash for $repositoryName with $options")
+        val repositoryDirectory = Paths.get(basePath.toString, repositoryName)
+        val darcsOptions        = List("diff", s"--hash=${hash.toString}") ::: options.toList
+        val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Create a distribution archive from the given darcs repository.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param options
+      *   Additional options for the initialize command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def dist(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary dist for $repositoryName")
+        val repositoryDirectory = Paths.get(basePath.toString, repositoryName)
+        val darcsOptions        = List("dist") ::: options.toList
+        val externalCommand     = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(cwd = os.Path(repositoryDirectory), check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Initialize a darcs repository under the given base path with the provided name. This is done by running the
+      * external darcs binary with the appropriate parameters.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param options
+      *   Additional options for the initialize command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def initialize(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary initialize $basePath/$repositoryName with $options")
+        val directory       = Paths.get(basePath.toString, repositoryName)
+        val darcsOptions    = List("initialize") ::: options.toList ::: List(directory.toString)
+        val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Run the darcs log command on the given repository and return the output.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param options
+      *   Additional options for the log command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def log(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary log in $basePath/$repositoryName with $options")
+        val directory       = Paths.get(basePath.toString, repositoryName)
+        val darcsOptions    = List("log") ::: options.toList
+        val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(cwd = os.Path(directory), check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Run the darcs show dependencies command on the given repository and return the output.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param options
+      *   Additional options for the show command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def showDependencies(
+        basePath: Path
+    )(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary show dependencies in $basePath/$repositoryName with $options")
+        val directory       = Paths.get(basePath.toString, repositoryName)
+        val darcsOptions    = List("show", "dependencies") ::: options.toList
+        val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(cwd = os.Path(directory), check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
+
+    /** Run the darcs whatsnew command on the given repository and return the output.
+      *
+      * @param basePath
+      *   The base path under which the repository is located.
+      * @param repositoryName
+      *   The name of the repository.
+      * @param options
+      *   Additional options for the whatsnew command.
+      * @return
+      *   The output of the darcs command.
+      */
+    def whatsnew(basePath: Path)(repositoryName: String)(options: Chain[String]): F[DarcsCommandOutput] = {
+        log.trace(s"Execute $darcsBinary whatsnew in $basePath/$repositoryName with $options")
+        val directory       = Paths.get(basePath.toString, repositoryName)
+        val darcsOptions    = List("whatsnew") ::: options.toList
+        val externalCommand = os.proc(darcsBinary.toString, darcsOptions)
+        for {
+            process <- Sync[F].delay(externalCommand.call(cwd = os.Path(directory), check = false))
+        } yield DarcsCommandOutput(process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+    }
 
 }
diff -rN -u old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala
--- old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/DarcsCommandsTest.scala	2025-01-13 17:13:25.048470967 +0000
@@ -28,107 +28,109 @@
 import scala.annotation.nowarn
 
 final class DarcsCommandsTest extends CatsEffectSuite with TestHelpers {
-  val darcsBinary = Paths.get("darcs")
+    val darcsBinary = Paths.get("darcs")
 
-  @nowarn("msg=discarded non-Unit value.*")
-  val tempWorkingDirectory = ResourceFixture(
-    Resource.make(IO(Files.createTempDirectory("darcs-cmd-test-").toAbsolutePath()))(path => IO(deleteDirectory(path)))
-  )
-
-  tempWorkingDirectory.test("darcs apply must apply a proper patch") { workingDirectory =>
-    val cmd               = new DarcsCommands[IO](darcsBinary)
-    val repo              = "test-apply"
-    val expectedDirectory = workingDirectory.resolve(repo)
-    val testPatch         = Paths.get(getClass().getClassLoader().getResource("test-patch.dpatch").toURI())
-    val createRepository  = cmd.initialize(workingDirectory)(repo)(Chain.empty)
-    val test = for {
-      init  <- createRepository
-      patch <- cmd.applyPatch(workingDirectory)(repo)(testPatch)(Chain.empty)
-    } yield (init, patch)
-    test.map { output =>
-      val (init, patch) = output
-      assert(init.exitValue === 0, "darcs init did not finish with exit code 0!")
-      assert(patch.exitValue === 0, "darcs apply did not finish with exit code 0!")
-      assert(
-        Files.exists(expectedDirectory.resolve("README.md")),
-        "Expected file README.md missing after applying the patch!"
-      )
-    }
-  }
-
-  tempWorkingDirectory.test("darcs clone must create a proper copy") { workingDirectory =>
-    val cmd    = new DarcsCommands[IO](darcsBinary)
-    val source = workingDirectory.resolve("source")
-    val target = workingDirectory.resolve("target")
-    val test =
-      for {
-        init  <- cmd.initialize(workingDirectory)("source")(Chain.empty)
-        clone <- cmd.clone(source, target)(Chain.empty)
-      } yield (init, clone)
-    test.map { output =>
-      val (init, clone) = output
-      assert(init.exitValue === 0, "darcs init did not finish with exit code 0!")
-      assert(Files.exists(source.toAbsolutePath), "Source repository directory does not exist!")
-      assert(clone.exitValue === 0, "darcs clone did not finish with exit code 0!")
-      assert(Files.exists(target.toAbsolutePath), "Target repository directory does not exist!")
-    }
-  }
-
-  tempWorkingDirectory.test("darcs clone must fail if the source does not exist") { workingDirectory =>
-    val cmd    = new DarcsCommands[IO](darcsBinary)
-    val source = workingDirectory.resolve("source-should-not-exist")
-    val target = workingDirectory.resolve("target-should-not-be-created")
-    val test =
-      for {
-        clone <- cmd.clone(source, target)(Chain.empty)
-      } yield clone
-    test.map { clone =>
-      assert(!Files.exists(source.toAbsolutePath), "Source repository directory must not exist!")
-      assert(clone.exitValue =!= 0, "darcs clone must not finish with exit code 0!")
-      assert(!Files.exists(target.toAbsolutePath), "Target repository directory must not exist!")
-    }
-  }
-
-  tempWorkingDirectory.test("darcs initialize must create a new repository") { workingDirectory =>
-    val cmd               = new DarcsCommands[IO](darcsBinary)
-    val repo              = "test-repository"
-    val expectedDirectory = workingDirectory.resolve(repo)
-    val test              = cmd.initialize(workingDirectory)(repo)(Chain.empty)
-    test.map { output =>
-      assert(output.exitValue === 0, "Exit code of darcs command is expected to be 0!")
-      assert(Files.exists(expectedDirectory), "Expected directory does not exist!")
-      assert(Files.isDirectory(expectedDirectory), "Expected directory is not a directory!")
-      val darcsDirectory = Paths.get(expectedDirectory.toString, "_darcs")
-      assert(Files.exists(darcsDirectory), "The _darcs directory does not exist in the repository!")
-    }
-  }
-
-  tempWorkingDirectory.test("darcs initialize must create a new repository in an existing directory") {
-    workingDirectory =>
-      val cmd               = new DarcsCommands[IO](darcsBinary)
-      val repo              = "test-repository-in-directory"
-      val expectedDirectory = workingDirectory.resolve(repo)
-      val _                 = Files.createDirectories(expectedDirectory)
-      val test              = cmd.initialize(workingDirectory)(repo)(Chain.empty)
-      test.map { output =>
-        assert(output.exitValue === 0, "Exit code of darcs command is expected to be 0!")
-        val darcsDirectory = Paths.get(expectedDirectory.toString, "_darcs")
-        assert(Files.exists(darcsDirectory), "The _darcs directory does not exist in the repository!")
-      }
-  }
-
-  tempWorkingDirectory.test("darcs initialize must fail if the repository already exists") { workingDirectory =>
-    val cmd               = new DarcsCommands[IO](darcsBinary)
-    val repo              = "test-repository-existing"
-    val expectedDirectory = workingDirectory.resolve(repo)
-    val _                 = Files.createDirectories(expectedDirectory)
-    val test = for {
-      _      <- cmd.initialize(workingDirectory)(repo)(Chain.empty)
-      output <- cmd.initialize(workingDirectory)(repo)(Chain.empty)
-    } yield output
-    test.map { output =>
-      assert(output.exitValue =!= 0, "The initialize command is expected to fail here!")
-      assert(output.stderr.nonEmpty)
+    @nowarn("msg=discarded non-Unit value.*")
+    val tempWorkingDirectory = ResourceFixture(
+        Resource.make(IO(Files.createTempDirectory("darcs-cmd-test-").toAbsolutePath()))(path =>
+            IO(deleteDirectory(path))
+        )
+    )
+
+    tempWorkingDirectory.test("darcs apply must apply a proper patch") { workingDirectory =>
+        val cmd               = new DarcsCommands[IO](darcsBinary)
+        val repo              = "test-apply"
+        val expectedDirectory = workingDirectory.resolve(repo)
+        val testPatch         = Paths.get(getClass().getClassLoader().getResource("test-patch.dpatch").toURI())
+        val createRepository  = cmd.initialize(workingDirectory)(repo)(Chain.empty)
+        val test = for {
+            init  <- createRepository
+            patch <- cmd.applyPatch(workingDirectory)(repo)(testPatch)(Chain.empty)
+        } yield (init, patch)
+        test.map { output =>
+            val (init, patch) = output
+            assert(init.exitValue === 0, "darcs init did not finish with exit code 0!")
+            assert(patch.exitValue === 0, "darcs apply did not finish with exit code 0!")
+            assert(
+                Files.exists(expectedDirectory.resolve("README.md")),
+                "Expected file README.md missing after applying the patch!"
+            )
+        }
+    }
+
+    tempWorkingDirectory.test("darcs clone must create a proper copy") { workingDirectory =>
+        val cmd    = new DarcsCommands[IO](darcsBinary)
+        val source = workingDirectory.resolve("source")
+        val target = workingDirectory.resolve("target")
+        val test =
+            for {
+                init  <- cmd.initialize(workingDirectory)("source")(Chain.empty)
+                clone <- cmd.clone(source, target)(Chain.empty)
+            } yield (init, clone)
+        test.map { output =>
+            val (init, clone) = output
+            assert(init.exitValue === 0, "darcs init did not finish with exit code 0!")
+            assert(Files.exists(source.toAbsolutePath), "Source repository directory does not exist!")
+            assert(clone.exitValue === 0, "darcs clone did not finish with exit code 0!")
+            assert(Files.exists(target.toAbsolutePath), "Target repository directory does not exist!")
+        }
+    }
+
+    tempWorkingDirectory.test("darcs clone must fail if the source does not exist") { workingDirectory =>
+        val cmd    = new DarcsCommands[IO](darcsBinary)
+        val source = workingDirectory.resolve("source-should-not-exist")
+        val target = workingDirectory.resolve("target-should-not-be-created")
+        val test =
+            for {
+                clone <- cmd.clone(source, target)(Chain.empty)
+            } yield clone
+        test.map { clone =>
+            assert(!Files.exists(source.toAbsolutePath), "Source repository directory must not exist!")
+            assert(clone.exitValue =!= 0, "darcs clone must not finish with exit code 0!")
+            assert(!Files.exists(target.toAbsolutePath), "Target repository directory must not exist!")
+        }
+    }
+
+    tempWorkingDirectory.test("darcs initialize must create a new repository") { workingDirectory =>
+        val cmd               = new DarcsCommands[IO](darcsBinary)
+        val repo              = "test-repository"
+        val expectedDirectory = workingDirectory.resolve(repo)
+        val test              = cmd.initialize(workingDirectory)(repo)(Chain.empty)
+        test.map { output =>
+            assert(output.exitValue === 0, "Exit code of darcs command is expected to be 0!")
+            assert(Files.exists(expectedDirectory), "Expected directory does not exist!")
+            assert(Files.isDirectory(expectedDirectory), "Expected directory is not a directory!")
+            val darcsDirectory = Paths.get(expectedDirectory.toString, "_darcs")
+            assert(Files.exists(darcsDirectory), "The _darcs directory does not exist in the repository!")
+        }
+    }
+
+    tempWorkingDirectory.test("darcs initialize must create a new repository in an existing directory") {
+        workingDirectory =>
+            val cmd               = new DarcsCommands[IO](darcsBinary)
+            val repo              = "test-repository-in-directory"
+            val expectedDirectory = workingDirectory.resolve(repo)
+            val _                 = Files.createDirectories(expectedDirectory)
+            val test              = cmd.initialize(workingDirectory)(repo)(Chain.empty)
+            test.map { output =>
+                assert(output.exitValue === 0, "Exit code of darcs command is expected to be 0!")
+                val darcsDirectory = Paths.get(expectedDirectory.toString, "_darcs")
+                assert(Files.exists(darcsDirectory), "The _darcs directory does not exist in the repository!")
+            }
+    }
+
+    tempWorkingDirectory.test("darcs initialize must fail if the repository already exists") { workingDirectory =>
+        val cmd               = new DarcsCommands[IO](darcsBinary)
+        val repo              = "test-repository-existing"
+        val expectedDirectory = workingDirectory.resolve(repo)
+        val _                 = Files.createDirectories(expectedDirectory)
+        val test = for {
+            _      <- cmd.initialize(workingDirectory)(repo)(Chain.empty)
+            output <- cmd.initialize(workingDirectory)(repo)(Chain.empty)
+        } yield output
+        test.map { output =>
+            assert(output.exitValue =!= 0, "The initialize command is expected to fail here!")
+            assert(output.stderr.nonEmpty)
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala
--- old-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/modules/darcs/src/test/scala/de/smederee/darcs/TestHelpers.scala	2025-01-13 17:13:25.048470967 +0000
@@ -25,34 +25,35 @@
 
 trait TestHelpers {
 
-  /** Delete the given directory recursively.
-    *
-    * @param path
-    *   The path on the filesystem to the directory that shall be deleted.
-    * @return
-    *   `true` if the directory was deleted.
-    */
-  protected def deleteDirectory(path: Path): Boolean =
-    if (path.toString.trim =!= "/") {
-      Files.walkFileTree(
-        path,
-        new FileVisitor[Path] {
-          override def visitFileFailed(file: Path, exc: IOException): FileVisitResult = FileVisitResult.CONTINUE
+    /** Delete the given directory recursively.
+      *
+      * @param path
+      *   The path on the filesystem to the directory that shall be deleted.
+      * @return
+      *   `true` if the directory was deleted.
+      */
+    protected def deleteDirectory(path: Path): Boolean =
+        if (path.toString.trim =!= "/") {
+            Files.walkFileTree(
+                path,
+                new FileVisitor[Path] {
+                    override def visitFileFailed(file: Path, exc: IOException): FileVisitResult =
+                        FileVisitResult.CONTINUE
 
-          override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
-            Files.delete(file)
-            FileVisitResult.CONTINUE
-          }
+                    override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
+                        Files.delete(file)
+                        FileVisitResult.CONTINUE
+                    }
 
-          override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult =
-            FileVisitResult.CONTINUE
+                    override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult =
+                        FileVisitResult.CONTINUE
 
-          override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
-            Files.delete(dir)
-            FileVisitResult.CONTINUE
-          }
-        }
-      )
-      Files.deleteIfExists(path)
-    } else false
+                    override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
+                        Files.delete(dir)
+                        FileVisitResult.CONTINUE
+                    }
+                }
+            )
+            Files.deleteIfExists(path)
+        } else false
 }
diff -rN -u old-smederee/modules/email/src/main/scala/de/smederee/email/EmailMiddleware.scala new-smederee/modules/email/src/main/scala/de/smederee/email/EmailMiddleware.scala
--- old-smederee/modules/email/src/main/scala/de/smederee/email/EmailMiddleware.scala	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/modules/email/src/main/scala/de/smederee/email/EmailMiddleware.scala	2025-01-13 17:13:25.048470967 +0000
@@ -29,65 +29,65 @@
   */
 enum SmtpTransport {
 
-  /** Use plaintext communication with the server (NOT RECOMMENDED!).
-    */
-  case PLAIN
-
-  /** Encapsulate the entire communication within TLS.
-    */
-  case SMTPS
-
-  /** Initialise via plaintext SMTP and issue a mandatory STARTTLS command first.
-    */
-  case TLS
+    /** Use plaintext communication with the server (NOT RECOMMENDED!).
+      */
+    case PLAIN
+
+    /** Encapsulate the entire communication within TLS.
+      */
+    case SMTPS
+
+    /** Initialise via plaintext SMTP and issue a mandatory STARTTLS command first.
+      */
+    case TLS
 }
 
 opaque type EmailServerPassword = Array[Byte]
 object EmailServerPassword {
 
-  /** Create an instance of EmailServerPassword from the given Array[Byte] type.
-    *
-    * @param source
-    *   An instance of type Array[Byte] which will be returned as a EmailServerPassword.
-    * @return
-    *   The appropriate instance of EmailServerPassword.
-    */
-  def apply(source: Array[Byte]): EmailServerPassword = source
-
-  /** Try to create an instance of EmailServerPassword from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a EmailServerPassword.
-    * @return
-    *   An option to the successfully converted EmailServerPassword.
-    */
-  def from(source: String): Option[EmailServerPassword] = Option(source).map(_.getBytes(StandardCharsets.UTF_8))
+    /** Create an instance of EmailServerPassword from the given Array[Byte] type.
+      *
+      * @param source
+      *   An instance of type Array[Byte] which will be returned as a EmailServerPassword.
+      * @return
+      *   The appropriate instance of EmailServerPassword.
+      */
+    def apply(source: Array[Byte]): EmailServerPassword = source
+
+    /** Try to create an instance of EmailServerPassword from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a EmailServerPassword.
+      * @return
+      *   An option to the successfully converted EmailServerPassword.
+      */
+    def from(source: String): Option[EmailServerPassword] = Option(source).map(_.getBytes(StandardCharsets.UTF_8))
 }
 
 extension (password: EmailServerPassword) {
-  def toArray: Array[Byte] = password
+    def toArray: Array[Byte] = password
 }
 
 opaque type EmailServerUsername = String
 object EmailServerUsername {
 
-  /** Create an instance of EmailServerUsername from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a EmailServerUsername.
-    * @return
-    *   The appropriate instance of EmailServerUsername.
-    */
-  def apply(source: String): EmailServerUsername = source
-
-  /** Try to create an instance of EmailServerUsername from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a EmailServerUsername.
-    * @return
-    *   An option to the successfully converted EmailServerUsername.
-    */
-  def from(source: String): Option[EmailServerUsername] = Option(source)
+    /** Create an instance of EmailServerUsername from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a EmailServerUsername.
+      * @return
+      *   The appropriate instance of EmailServerUsername.
+      */
+    def apply(source: String): EmailServerUsername = source
+
+    /** Try to create an instance of EmailServerUsername from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a EmailServerUsername.
+      * @return
+      *   An option to the successfully converted EmailServerUsername.
+      */
+    def from(source: String): Option[EmailServerUsername] = Option(source)
 
 }
 
@@ -95,217 +95,217 @@
   */
 opaque type EmailAddress = String
 object EmailAddress {
-  given Eq[EmailAddress] = Eq.fromUniversalEquals
-
-  val validateString: Regex =
-    """^([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$""".r
-
-  /** Create an instance of EmailAddress from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a EmailAddress.
-    * @return
-    *   The appropriate instance of EmailAddress.
-    */
-  def apply(source: String): EmailAddress = source
-
-  /** Try to create an instance of EmailAddress from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a EmailAddress.
-    * @return
-    *   An option to the successfully converted EmailAddress.
-    */
-  def from(source: String): Option[EmailAddress] = {
-    // Must obviously not be null or empty.
-    val notEmpty = Option(source)
-    // Must have at least one '@' sign.
-    val hasAtChar = Option(source).filter(_.exists(_ === '@'))
-    // Must have at least one '.' character.
-    val hasDotChar = Option(source).filter(_.exists(_ === '.'))
-    // The maximum allowed length is 128 characters.
-    val belowMaxLength = Option(source).filter(_.length <= 128)
-    // The last part of the email address (usually the top level domain) must at least be 2 characters long.
-    val lastPartValid = Option(source) match {
-      case None => None
-      case Some(string) =>
-        val parts = string.split('.')
-        if (parts.lastOption.map(_.length >= 2).getOrElse(false))
-          Option(string)
-        else
-          None
-    }
-    // Check against a regular expression. TODO: Maybe we can skip the other tests alltogether?
-    val passesRegularExpression = Option(source).filter(validateString.matches)
-    // Validate pre-conditions
-    (notEmpty, hasAtChar, hasDotChar, belowMaxLength, lastPartValid, passesRegularExpression).mapN {
-      case (email, _, _, _, _, _) => email
-    }
-  }
+    given Eq[EmailAddress] = Eq.fromUniversalEquals
 
-  extension (email: EmailAddress) {
+    val validateString: Regex =
+        """^([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$""".r
 
-    /** Convert the email into a [[de.smederee.email.FromAddress]] to be useable within the email middleware.
+    /** Create an instance of EmailAddress from the given String type.
       *
+      * @param source
+      *   An instance of type String which will be returned as a EmailAddress.
       * @return
-      *   The email address to be used in a `From` header.
+      *   The appropriate instance of EmailAddress.
       */
-    def toFromAddress: FromAddress = FromAddress(email.toString)
+    def apply(source: String): EmailAddress = source
 
-    /** Convert the email into a [[de.smederee.email.ToAddress]] to be useable within the email middleware.
+    /** Try to create an instance of EmailAddress from the given String.
       *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a EmailAddress.
       * @return
-      *   The email address to be used in a `To` header.
+      *   An option to the successfully converted EmailAddress.
       */
-    def toToAddress: ToAddress = ToAddress(email.toString)
-  }
+    def from(source: String): Option[EmailAddress] = {
+        // Must obviously not be null or empty.
+        val notEmpty = Option(source)
+        // Must have at least one '@' sign.
+        val hasAtChar = Option(source).filter(_.exists(_ === '@'))
+        // Must have at least one '.' character.
+        val hasDotChar = Option(source).filter(_.exists(_ === '.'))
+        // The maximum allowed length is 128 characters.
+        val belowMaxLength = Option(source).filter(_.length <= 128)
+        // The last part of the email address (usually the top level domain) must at least be 2 characters long.
+        val lastPartValid = Option(source) match {
+            case None => None
+            case Some(string) =>
+                val parts = string.split('.')
+                if (parts.lastOption.map(_.length >= 2).getOrElse(false))
+                    Option(string)
+                else
+                    None
+        }
+        // Check against a regular expression. TODO: Maybe we can skip the other tests alltogether?
+        val passesRegularExpression = Option(source).filter(validateString.matches)
+        // Validate pre-conditions
+        (notEmpty, hasAtChar, hasDotChar, belowMaxLength, lastPartValid, passesRegularExpression).mapN {
+            case (email, _, _, _, _, _) => email
+        }
+    }
+
+    extension (email: EmailAddress) {
+
+        /** Convert the email into a [[de.smederee.email.FromAddress]] to be useable within the email middleware.
+          *
+          * @return
+          *   The email address to be used in a `From` header.
+          */
+        def toFromAddress: FromAddress = FromAddress(email.toString)
+
+        /** Convert the email into a [[de.smederee.email.ToAddress]] to be useable within the email middleware.
+          *
+          * @return
+          *   The email address to be used in a `To` header.
+          */
+        def toToAddress: ToAddress = ToAddress(email.toString)
+    }
 }
 
 opaque type FromAddress = String
 object FromAddress {
-  given Eq[FromAddress] = Eq.fromUniversalEquals
+    given Eq[FromAddress] = Eq.fromUniversalEquals
 
-  val validateString: Regex =
-    """^([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$""".r
+    val validateString: Regex =
+        """^([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$""".r
 
-  /** Create an instance of FromAddress from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a FromAddress.
-    * @return
-    *   The appropriate instance of FromAddress.
-    */
-  def apply(source: String): FromAddress = source
-
-  /** Try to create an instance of FromAddress from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a FromAddress.
-    * @return
-    *   An option to the successfully converted FromAddress.
-    */
-  def from(source: String): Option[FromAddress] = {
-    // Must obviously not be null or empty.
-    val notEmpty = Option(source)
-    // Must have at least one '@' sign.
-    val hasAtChar = Option(source).filter(_.exists(_ === '@'))
-    // Must have at least one '.' character.
-    val hasDotChar = Option(source).filter(_.exists(_ === '.'))
-    // The maximum allowed length is 128 characters.
-    val belowMaxLength = Option(source).filter(_.length <= 128)
-    // The last part of the email address (usually the top level domain) must at least be 2 characters long.
-    val lastPartValid = Option(source) match {
-      case None => None
-      case Some(string) =>
-        val parts = string.split('.')
-        if (parts.lastOption.map(_.length >= 2).getOrElse(false))
-          Option(string)
-        else
-          None
-    }
-    // Check against a regular expression. TODO: Maybe we can skip the other tests alltogether?
-    val passesRegularExpression = Option(source).filter(validateString.matches)
-    // Validate pre-conditions
-    (notEmpty, hasAtChar, hasDotChar, belowMaxLength, lastPartValid, passesRegularExpression).mapN {
-      case (email, _, _, _, _, _) => email
+    /** Create an instance of FromAddress from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a FromAddress.
+      * @return
+      *   The appropriate instance of FromAddress.
+      */
+    def apply(source: String): FromAddress = source
+
+    /** Try to create an instance of FromAddress from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a FromAddress.
+      * @return
+      *   An option to the successfully converted FromAddress.
+      */
+    def from(source: String): Option[FromAddress] = {
+        // Must obviously not be null or empty.
+        val notEmpty = Option(source)
+        // Must have at least one '@' sign.
+        val hasAtChar = Option(source).filter(_.exists(_ === '@'))
+        // Must have at least one '.' character.
+        val hasDotChar = Option(source).filter(_.exists(_ === '.'))
+        // The maximum allowed length is 128 characters.
+        val belowMaxLength = Option(source).filter(_.length <= 128)
+        // The last part of the email address (usually the top level domain) must at least be 2 characters long.
+        val lastPartValid = Option(source) match {
+            case None => None
+            case Some(string) =>
+                val parts = string.split('.')
+                if (parts.lastOption.map(_.length >= 2).getOrElse(false))
+                    Option(string)
+                else
+                    None
+        }
+        // Check against a regular expression. TODO: Maybe we can skip the other tests alltogether?
+        val passesRegularExpression = Option(source).filter(validateString.matches)
+        // Validate pre-conditions
+        (notEmpty, hasAtChar, hasDotChar, belowMaxLength, lastPartValid, passesRegularExpression).mapN {
+            case (email, _, _, _, _, _) => email
+        }
     }
-  }
 }
 
 opaque type SubjectLine = String
 object SubjectLine {
-  val MaxLength: Int = 78
+    val MaxLength: Int = 78
 
-  /** Create an instance of SubjectLine from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a SubjectLine.
-    * @return
-    *   The appropriate instance of SubjectLine.
-    */
-  def apply(source: String): SubjectLine = source
-
-  /** Try to create an instance of SubjectLine from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SubjectLine.
-    * @return
-    *   An option to the successfully converted SubjectLine.
-    */
-  def from(source: String): Option[SubjectLine] = Option(source).filter(_.length <= MaxLength)
+    /** Create an instance of SubjectLine from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a SubjectLine.
+      * @return
+      *   The appropriate instance of SubjectLine.
+      */
+    def apply(source: String): SubjectLine = source
+
+    /** Try to create an instance of SubjectLine from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SubjectLine.
+      * @return
+      *   An option to the successfully converted SubjectLine.
+      */
+    def from(source: String): Option[SubjectLine] = Option(source).filter(_.length <= MaxLength)
 }
 
 opaque type TextBody = String
 object TextBody {
 
-  /** Create an instance of TextBody from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a TextBody.
-    * @return
-    *   The appropriate instance of TextBody.
-    */
-  def apply(source: String): TextBody = source
-
-  /** Try to create an instance of TextBody from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a TextBody.
-    * @return
-    *   An option to the successfully converted TextBody.
-    */
-  def from(source: String): Option[TextBody] = Option(source).filter(_.nonEmpty)
+    /** Create an instance of TextBody from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a TextBody.
+      * @return
+      *   The appropriate instance of TextBody.
+      */
+    def apply(source: String): TextBody = source
+
+    /** Try to create an instance of TextBody from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a TextBody.
+      * @return
+      *   An option to the successfully converted TextBody.
+      */
+    def from(source: String): Option[TextBody] = Option(source).filter(_.nonEmpty)
 }
 
 opaque type ToAddress = String
 object ToAddress {
-  given Eq[ToAddress] = Eq.fromUniversalEquals
+    given Eq[ToAddress] = Eq.fromUniversalEquals
 
-  val validateString: Regex =
-    """^([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$""".r
+    val validateString: Regex =
+        """^([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)$""".r
 
-  /** Create an instance of ToAddress from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ToAddress.
-    * @return
-    *   The appropriate instance of ToAddress.
-    */
-  def apply(source: String): ToAddress = source
-
-  /** Try to create an instance of ToAddress from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ToAddress.
-    * @return
-    *   An option to the successfully converted ToAddress.
-    */
-  def from(source: String): Option[ToAddress] = {
-    // Must obviously not be null or empty.
-    val notEmpty = Option(source)
-    // Must have at least one '@' sign.
-    val hasAtChar = Option(source).filter(_.exists(_ === '@'))
-    // Must have at least one '.' character.
-    val hasDotChar = Option(source).filter(_.exists(_ === '.'))
-    // The maximum allowed length is 128 characters.
-    val belowMaxLength = Option(source).filter(_.length <= 128)
-    // The last part of the email address (usually the top level domain) must at least be 2 characters long.
-    val lastPartValid = Option(source) match {
-      case None => None
-      case Some(string) =>
-        val parts = string.split('.')
-        if (parts.lastOption.map(_.length >= 2).getOrElse(false))
-          Option(string)
-        else
-          None
-    }
-    // Check against a regular expression. TODO: Maybe we can skip the other tests alltogether?
-    val passesRegularExpression = Option(source).filter(validateString.matches)
-    // Validate pre-conditions
-    (notEmpty, hasAtChar, hasDotChar, belowMaxLength, lastPartValid, passesRegularExpression).mapN {
-      case (email, _, _, _, _, _) => email
+    /** Create an instance of ToAddress from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ToAddress.
+      * @return
+      *   The appropriate instance of ToAddress.
+      */
+    def apply(source: String): ToAddress = source
+
+    /** Try to create an instance of ToAddress from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ToAddress.
+      * @return
+      *   An option to the successfully converted ToAddress.
+      */
+    def from(source: String): Option[ToAddress] = {
+        // Must obviously not be null or empty.
+        val notEmpty = Option(source)
+        // Must have at least one '@' sign.
+        val hasAtChar = Option(source).filter(_.exists(_ === '@'))
+        // Must have at least one '.' character.
+        val hasDotChar = Option(source).filter(_.exists(_ === '.'))
+        // The maximum allowed length is 128 characters.
+        val belowMaxLength = Option(source).filter(_.length <= 128)
+        // The last part of the email address (usually the top level domain) must at least be 2 characters long.
+        val lastPartValid = Option(source) match {
+            case None => None
+            case Some(string) =>
+                val parts = string.split('.')
+                if (parts.lastOption.map(_.length >= 2).getOrElse(false))
+                    Option(string)
+                else
+                    None
+        }
+        // Check against a regular expression. TODO: Maybe we can skip the other tests alltogether?
+        val passesRegularExpression = Option(source).filter(validateString.matches)
+        // Validate pre-conditions
+        (notEmpty, hasAtChar, hasDotChar, belowMaxLength, lastPartValid, passesRegularExpression).mapN {
+            case (email, _, _, _, _, _) => email
+        }
     }
-  }
 }
 
 /** Configuration for the email middleware.
@@ -330,14 +330,14 @@
 )
 
 object EmailMiddlewareConfiguration {
-  // A default configuration for sending email via a local SMTP server without authentication.
-  val LocalSmtpServer: EmailMiddlewareConfiguration = EmailMiddlewareConfiguration(
-    host = host"localhost",
-    port = port"25",
-    transport = SmtpTransport.PLAIN,
-    username = None,
-    password = None
-  )
+    // A default configuration for sending email via a local SMTP server without authentication.
+    val LocalSmtpServer: EmailMiddlewareConfiguration = EmailMiddlewareConfiguration(
+        host = host"localhost",
+        port = port"25",
+        transport = SmtpTransport.PLAIN,
+        username = None,
+        password = None
+    )
 }
 
 /** An abstraction layer for functionality regarding the handling of email to ease testing.
@@ -347,14 +347,14 @@
   */
 trait EmailMiddleware[F[_]] {
 
-  /** Send the given message via the email protocol. Details depend on the configuration used for the underlying
-    * implementation.
-    *
-    * @param email
-    *   An email message containing all necessary information (headers and message body) to be sent.
-    * @return
-    *   Either an error message or Unit.
-    */
-  def send(email: EmailMessage): F[Either[String, Unit]]
+    /** Send the given message via the email protocol. Details depend on the configuration used for the underlying
+      * implementation.
+      *
+      * @param email
+      *   An email message containing all necessary information (headers and message body) to be sent.
+      * @return
+      *   Either an error message or Unit.
+      */
+    def send(email: EmailMessage): F[Either[String, Unit]]
 
 }
diff -rN -u old-smederee/modules/email/src/main/scala/de/smederee/email/SimpleJavaMailMiddleware.scala new-smederee/modules/email/src/main/scala/de/smederee/email/SimpleJavaMailMiddleware.scala
--- old-smederee/modules/email/src/main/scala/de/smederee/email/SimpleJavaMailMiddleware.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/email/src/main/scala/de/smederee/email/SimpleJavaMailMiddleware.scala	2025-01-13 17:13:25.048470967 +0000
@@ -34,63 +34,63 @@
   *   The configuration containing all needed information to connect to the email server and send emails.
   */
 final class SimpleJavaMailMiddleware(configuration: EmailMiddlewareConfiguration) extends EmailMiddleware[IO] {
-  import SimpleJavaMailMiddleware.helpers.*
+    import SimpleJavaMailMiddleware.helpers.*
 
-  private val cachedMailer: IO[Mailer] = for {
-    builder <- IO.delay {
-      val mailerBuilder = (configuration.username, configuration.password)
-        .mapN { case (username, password) =>
-          MailerBuilder.withSMTPServer(
-            configuration.host.toString,
-            configuration.port.toString.toInt,
-            username.toString,
-            new String(password.toArray, StandardCharsets.UTF_8)
-          )
+    private val cachedMailer: IO[Mailer] = for {
+        builder <- IO.delay {
+            val mailerBuilder = (configuration.username, configuration.password)
+                .mapN { case (username, password) =>
+                    MailerBuilder.withSMTPServer(
+                        configuration.host.toString,
+                        configuration.port.toString.toInt,
+                        username.toString,
+                        new String(password.toArray, StandardCharsets.UTF_8)
+                    )
+                }
+                .getOrElse(
+                    MailerBuilder.withSMTPServer(configuration.host.toString, configuration.port.toString.toInt)
+                )
+            // The mailer is a mutable construct from the Java library therefore we can omit the return value here.
+            val _ = configuration.transport match {
+                case SmtpTransport.PLAIN => mailerBuilder.withTransportStrategy(TransportStrategy.SMTP)
+                case SmtpTransport.SMTPS => mailerBuilder.withTransportStrategy(TransportStrategy.SMTPS)
+                case SmtpTransport.TLS   => mailerBuilder.withTransportStrategy(TransportStrategy.SMTP_TLS)
+            }
+            mailerBuilder
         }
-        .getOrElse(
-          MailerBuilder.withSMTPServer(configuration.host.toString, configuration.port.toString.toInt)
-        )
-      // The mailer is a mutable construct from the Java library therefore we can omit the return value here.
-      val _ = configuration.transport match {
-        case SmtpTransport.PLAIN => mailerBuilder.withTransportStrategy(TransportStrategy.SMTP)
-        case SmtpTransport.SMTPS => mailerBuilder.withTransportStrategy(TransportStrategy.SMTPS)
-        case SmtpTransport.TLS   => mailerBuilder.withTransportStrategy(TransportStrategy.SMTP_TLS)
-      }
-      mailerBuilder
-    }
-  } yield builder.async().buildMailer()
+    } yield builder.async().buildMailer()
 
-  override def send(email: EmailMessage): IO[Either[String, Unit]] = {
-    val sending = for {
-      mailer  <- cachedMailer
-      message <- IO.delay(email.toEmail)
-      // A bit more involving because we use the asynchronous mailer.
-      _ <- IO.fromCompletableFuture(IO.delay(mailer.sendMail(message)))
-    } yield ()
-    sending.attempt.map(_.leftMap(_.getMessage))
-  }
+    override def send(email: EmailMessage): IO[Either[String, Unit]] = {
+        val sending = for {
+            mailer  <- cachedMailer
+            message <- IO.delay(email.toEmail)
+            // A bit more involving because we use the asynchronous mailer.
+            _ <- IO.fromCompletableFuture(IO.delay(mailer.sendMail(message)))
+        } yield ()
+        sending.attempt.map(_.leftMap(_.getMessage))
+    }
 }
 
 object SimpleJavaMailMiddleware {
-  object helpers {
-    extension (email: EmailMessage) {
+    object helpers {
+        extension (email: EmailMessage) {
 
-      /** Convert our custom email message into an email instance which is understood by our used library.
-        *
-        * @return
-        *   An email useable by the Simple Java Mail library.
-        */
-      def toEmail: Email = {
-        val builder = EmailBuilder
-          .startingBlank()
-          .from(email.from.toString)
-          .withSubject(email.subject.toString)
-          .withPlainText(email.textBody.toString)
-        email.to.toList.foreach(address => builder.to(address.toString))
-        email.cc.foreach(address => builder.cc(address.toString))
-        email.bcc.foreach(address => builder.bcc(address.toString))
-        builder.buildEmail()
-      }
+            /** Convert our custom email message into an email instance which is understood by our used library.
+              *
+              * @return
+              *   An email useable by the Simple Java Mail library.
+              */
+            def toEmail: Email = {
+                val builder = EmailBuilder
+                    .startingBlank()
+                    .from(email.from.toString)
+                    .withSubject(email.subject.toString)
+                    .withPlainText(email.textBody.toString)
+                email.to.toList.foreach(address => builder.to(address.toString))
+                email.cc.foreach(address => builder.cc(address.toString))
+                email.bcc.foreach(address => builder.bcc(address.toString))
+                builder.buildEmail()
+            }
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/email/src/test/scala/de/smederee/email/EmailMiddlewareTest.scala new-smederee/modules/email/src/test/scala/de/smederee/email/EmailMiddlewareTest.scala
--- old-smederee/modules/email/src/test/scala/de/smederee/email/EmailMiddlewareTest.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/email/src/test/scala/de/smederee/email/EmailMiddlewareTest.scala	2025-01-13 17:13:25.052470972 +0000
@@ -27,67 +27,67 @@
 import org.scalacheck.*
 
 final class EmailMiddlewareTest extends ScalaCheckSuite {
-  given Arbitrary[FromAddress] = Arbitrary(genValidFromAddress)
-  given Arbitrary[ToAddress]   = Arbitrary(genValidToAddress)
+    given Arbitrary[FromAddress] = Arbitrary(genValidFromAddress)
+    given Arbitrary[ToAddress]   = Arbitrary(genValidToAddress)
 
-  property("EmailServerPassword.apply and .toArray must be reversible") {
-    forAll { (randomBytes: Array[Byte]) =>
-      val password = EmailServerPassword(randomBytes)
-      assert(java.util.Arrays.equals(password.toArray, randomBytes))
-    }
-  }
-
-  property("EmailServerPassword.from must create correct instances") {
-    forAll { (randomString: String) =>
-      EmailServerPassword.from(randomString) match {
-        case None => fail("Could not create EmailServerPassword!")
-        case Some(password) =>
-          assert(java.util.Arrays.equals(password.toArray, randomString.getBytes(StandardCharsets.UTF_8)))
-      }
-    }
-  }
-
-  property("FromAddress.from must not accept invalid input") {
-    forAll { (randomString: String) =>
-      assert(FromAddress.from(randomString).isEmpty)
-    }
-  }
-
-  property("FromAddress.from must accept valid input") {
-    forAll { (validFrom: FromAddress) =>
-      FromAddress.from(validFrom.toString) match {
-        case None          => fail("Could not create FromAddress!")
-        case Some(address) => assertEquals(address, validFrom)
-      }
-    }
-  }
-
-  property("SubjectLine.from must only accept invalid input") {
-    forAll { (randomString: String) =>
-      if (randomString.length <= SubjectLine.MaxLength)
-        assert(SubjectLine.from(randomString).nonEmpty)
-      else
-        assert(SubjectLine.from(randomString).isEmpty)
-    }
-  }
-
-  test("TextBody.from must not accept empty input") {
-    assert(TextBody.from("").isEmpty)
-  }
-
-  property("ToAddress.from must not accept invalid input") {
-    forAll { (randomString: String) =>
-      assert(ToAddress.from(randomString).isEmpty)
-    }
-  }
-
-  property("ToAddress.from must accept valid input") {
-    forAll { (validTo: ToAddress) =>
-      ToAddress.from(validTo.toString) match {
-        case None          => fail("Could not create ToAddress!")
-        case Some(address) => assertEquals(address, validTo)
-      }
+    property("EmailServerPassword.apply and .toArray must be reversible") {
+        forAll { (randomBytes: Array[Byte]) =>
+            val password = EmailServerPassword(randomBytes)
+            assert(java.util.Arrays.equals(password.toArray, randomBytes))
+        }
+    }
+
+    property("EmailServerPassword.from must create correct instances") {
+        forAll { (randomString: String) =>
+            EmailServerPassword.from(randomString) match {
+                case None => fail("Could not create EmailServerPassword!")
+                case Some(password) =>
+                    assert(java.util.Arrays.equals(password.toArray, randomString.getBytes(StandardCharsets.UTF_8)))
+            }
+        }
+    }
+
+    property("FromAddress.from must not accept invalid input") {
+        forAll { (randomString: String) =>
+            assert(FromAddress.from(randomString).isEmpty)
+        }
+    }
+
+    property("FromAddress.from must accept valid input") {
+        forAll { (validFrom: FromAddress) =>
+            FromAddress.from(validFrom.toString) match {
+                case None          => fail("Could not create FromAddress!")
+                case Some(address) => assertEquals(address, validFrom)
+            }
+        }
+    }
+
+    property("SubjectLine.from must only accept invalid input") {
+        forAll { (randomString: String) =>
+            if (randomString.length <= SubjectLine.MaxLength)
+                assert(SubjectLine.from(randomString).nonEmpty)
+            else
+                assert(SubjectLine.from(randomString).isEmpty)
+        }
+    }
+
+    test("TextBody.from must not accept empty input") {
+        assert(TextBody.from("").isEmpty)
+    }
+
+    property("ToAddress.from must not accept invalid input") {
+        forAll { (randomString: String) =>
+            assert(ToAddress.from(randomString).isEmpty)
+        }
+    }
+
+    property("ToAddress.from must accept valid input") {
+        forAll { (validTo: ToAddress) =>
+            ToAddress.from(validTo.toString) match {
+                case None          => fail("Could not create ToAddress!")
+                case Some(address) => assertEquals(address, validTo)
+            }
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/email/src/test/scala/de/smederee/email/Generators.scala new-smederee/modules/email/src/test/scala/de/smederee/email/Generators.scala
--- old-smederee/modules/email/src/test/scala/de/smederee/email/Generators.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/email/src/test/scala/de/smederee/email/Generators.scala	2025-01-13 17:13:25.052470972 +0000
@@ -24,49 +24,49 @@
 /** A container for ScalaCheck generators for our data types.
   */
 object Generators {
-  private val validEmailAddressPrefixChars =
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&’'*+/=?^_`{|}~-".toList
-  private val validDomainNameChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-".toList
+    private val validEmailAddressPrefixChars =
+        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&’'*+/=?^_`{|}~-".toList
+    private val validDomainNameChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-".toList
 
-  val genValidFromAddress: Gen[FromAddress] = for {
-    prefix <- Gen.nonEmptyListOf(Gen.oneOf(validEmailAddressPrefixChars)).map(_.take(64).mkString)
-    domain <- Gen.nonEmptyListOf(Gen.oneOf(validDomainNameChars)).map(_.take(32).mkString)
-    topLevelDomain <- Gen
-      .nonEmptyListOf(Gen.oneOf(validDomainNameChars))
-      .suchThat(_.length >= 2)
-      .map(_.take(24).mkString)
-    suffix = s"$domain.$topLevelDomain"
-  } yield FromAddress(s"$prefix@$suffix")
+    val genValidFromAddress: Gen[FromAddress] = for {
+        prefix <- Gen.nonEmptyListOf(Gen.oneOf(validEmailAddressPrefixChars)).map(_.take(64).mkString)
+        domain <- Gen.nonEmptyListOf(Gen.oneOf(validDomainNameChars)).map(_.take(32).mkString)
+        topLevelDomain <- Gen
+            .nonEmptyListOf(Gen.oneOf(validDomainNameChars))
+            .suchThat(_.length >= 2)
+            .map(_.take(24).mkString)
+        suffix = s"$domain.$topLevelDomain"
+    } yield FromAddress(s"$prefix@$suffix")
 
-  val genValidSubjectLine: Gen[SubjectLine] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(78).mkString).map(SubjectLine.apply)
+    val genValidSubjectLine: Gen[SubjectLine] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(78).mkString).map(SubjectLine.apply)
 
-  val genValidTextBody: Gen[TextBody] = Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString).map(TextBody.apply)
+    val genValidTextBody: Gen[TextBody] = Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString).map(TextBody.apply)
 
-  val genValidToAddress: Gen[ToAddress] = for {
-    prefix <- Gen.nonEmptyListOf(Gen.oneOf(validEmailAddressPrefixChars)).map(_.take(64).mkString)
-    domain <- Gen.nonEmptyListOf(Gen.oneOf(validDomainNameChars)).map(_.take(32).mkString)
-    topLevelDomain <- Gen
-      .nonEmptyListOf(Gen.oneOf(validDomainNameChars))
-      .suchThat(_.length >= 2)
-      .map(_.take(24).mkString)
-    suffix = s"$domain.$topLevelDomain"
-  } yield ToAddress(s"$prefix@$suffix")
+    val genValidToAddress: Gen[ToAddress] = for {
+        prefix <- Gen.nonEmptyListOf(Gen.oneOf(validEmailAddressPrefixChars)).map(_.take(64).mkString)
+        domain <- Gen.nonEmptyListOf(Gen.oneOf(validDomainNameChars)).map(_.take(32).mkString)
+        topLevelDomain <- Gen
+            .nonEmptyListOf(Gen.oneOf(validDomainNameChars))
+            .suchThat(_.length >= 2)
+            .map(_.take(24).mkString)
+        suffix = s"$domain.$topLevelDomain"
+    } yield ToAddress(s"$prefix@$suffix")
 
-  val genEmailMessage: Gen[EmailMessage] = for {
-    from     <- genValidFromAddress
-    to       <- genValidToAddress
-    too      <- Gen.listOf(genValidToAddress)
-    cc       <- Gen.listOf(genValidToAddress)
-    bcc      <- Gen.listOf(genValidToAddress)
-    subject  <- genValidSubjectLine
-    textBody <- genValidTextBody
-  } yield EmailMessage(
-    from,
-    NonEmptyList.of(to, too: _*),
-    cc,
-    bcc,
-    subject,
-    textBody
-  )
+    val genEmailMessage: Gen[EmailMessage] = for {
+        from     <- genValidFromAddress
+        to       <- genValidToAddress
+        too      <- Gen.listOf(genValidToAddress)
+        cc       <- Gen.listOf(genValidToAddress)
+        bcc      <- Gen.listOf(genValidToAddress)
+        subject  <- genValidSubjectLine
+        textBody <- genValidTextBody
+    } yield EmailMessage(
+        from,
+        NonEmptyList.of(to, too: _*),
+        cc,
+        bcc,
+        subject,
+        textBody
+    )
 }
diff -rN -u old-smederee/modules/email/src/test/scala/de/smederee/email/SimpleJavaMailMiddlewareHelpersTest.scala new-smederee/modules/email/src/test/scala/de/smederee/email/SimpleJavaMailMiddlewareHelpersTest.scala
--- old-smederee/modules/email/src/test/scala/de/smederee/email/SimpleJavaMailMiddlewareHelpersTest.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/email/src/test/scala/de/smederee/email/SimpleJavaMailMiddlewareHelpersTest.scala	2025-01-13 17:13:25.052470972 +0000
@@ -30,32 +30,32 @@
 import scala.jdk.CollectionConverters.*
 
 final class SimpleJavaMailMiddlewareHelpersTest extends ScalaCheckSuite {
-  import SimpleJavaMailMiddleware.helpers.*
+    import SimpleJavaMailMiddleware.helpers.*
 
-  given Eq[RecipientType] = Eq.fromUniversalEquals
+    given Eq[RecipientType] = Eq.fromUniversalEquals
 
-  given Arbitrary[EmailMessage] = Arbitrary(genEmailMessage)
+    given Arbitrary[EmailMessage] = Arbitrary(genEmailMessage)
 
-  property("EmailMessage.toEmail must create correct Email instances") {
-    forAll { (message: EmailMessage) =>
-      val email = message.toEmail
-      assertEquals(email.getFromRecipient.getAddress, message.from.toString)
-      assertEquals(email.getSubject, message.subject.toString)
-      assertEquals(email.getPlainText, message.textBody.toString)
-      val recipients = email.getRecipients.asScala.toList
-      message.to.toList.foreach { address =>
-        val r = recipients.find(r => r.getAddress === address.toString && r.getType === RecipientType.TO)
-        assert(r.nonEmpty)
-      }
-      message.cc.foreach { address =>
-        val r = recipients.find(r => r.getAddress === address.toString && r.getType === RecipientType.CC)
-        assert(r.nonEmpty)
-      }
-      message.bcc.foreach { address =>
-        val r = recipients.find(r => r.getAddress === address.toString && r.getType === RecipientType.BCC)
-        assert(r.nonEmpty)
-      }
+    property("EmailMessage.toEmail must create correct Email instances") {
+        forAll { (message: EmailMessage) =>
+            val email = message.toEmail
+            assertEquals(email.getFromRecipient.getAddress, message.from.toString)
+            assertEquals(email.getSubject, message.subject.toString)
+            assertEquals(email.getPlainText, message.textBody.toString)
+            val recipients = email.getRecipients.asScala.toList
+            message.to.toList.foreach { address =>
+                val r = recipients.find(r => r.getAddress === address.toString && r.getType === RecipientType.TO)
+                assert(r.nonEmpty)
+            }
+            message.cc.foreach { address =>
+                val r = recipients.find(r => r.getAddress === address.toString && r.getType === RecipientType.CC)
+                assert(r.nonEmpty)
+            }
+            message.bcc.foreach { address =>
+                val r = recipients.find(r => r.getAddress === address.toString && r.getType === RecipientType.BCC)
+                assert(r.nonEmpty)
+            }
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/html-utils/src/main/scala/de/smederee/html/LinkTools.scala new-smederee/modules/html-utils/src/main/scala/de/smederee/html/LinkTools.scala
--- old-smederee/modules/html-utils/src/main/scala/de/smederee/html/LinkTools.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/html-utils/src/main/scala/de/smederee/html/LinkTools.scala	2025-01-13 17:13:25.052470972 +0000
@@ -22,36 +22,36 @@
 
 object LinkTools {
 
-  extension (linkConfig: ExternalUrlConfiguration) {
+    extension (linkConfig: ExternalUrlConfiguration) {
 
-    /** Take the given URI path and create a full URI using the specified configuration with a possible path prefix and
-      * append the given path to it.
-      *
-      * @param path
-      *   An URI containing a path with possible URL fragment and query parameters which will be used to construct the
-      *   full URI.
-      * @return
-      *   A full URI created from the values of the ExternalUrlConfiguration (scheme, host, port, possible path prefix)
-      *   and the path data from the given URI.
-      */
-    def createFullUri(path: Uri): Uri = {
-      val completePath = linkConfig.path match {
-        case None             => path.path
-        case Some(pathPrefix) => pathPrefix.path |+| path.path
-      }
-      val baseUri = Uri(
-        scheme = Option(linkConfig.scheme),
-        authority = Option(
-          Uri.Authority(
-            userInfo = None,
-            host = Uri.Host.fromIp4sHost(linkConfig.host),
-            port = linkConfig.port.map(_.value)
-          )
-        ),
-        path = completePath
-      ).withQueryParams(path.params)
-      path.fragment.fold(baseUri)(fragment => baseUri.withFragment(fragment))
+        /** Take the given URI path and create a full URI using the specified configuration with a possible path prefix
+          * and append the given path to it.
+          *
+          * @param path
+          *   An URI containing a path with possible URL fragment and query parameters which will be used to construct
+          *   the full URI.
+          * @return
+          *   A full URI created from the values of the ExternalUrlConfiguration (scheme, host, port, possible path
+          *   prefix) and the path data from the given URI.
+          */
+        def createFullUri(path: Uri): Uri = {
+            val completePath = linkConfig.path match {
+                case None             => path.path
+                case Some(pathPrefix) => pathPrefix.path |+| path.path
+            }
+            val baseUri = Uri(
+                scheme = Option(linkConfig.scheme),
+                authority = Option(
+                    Uri.Authority(
+                        userInfo = None,
+                        host = Uri.Host.fromIp4sHost(linkConfig.host),
+                        port = linkConfig.port.map(_.value)
+                    )
+                ),
+                path = completePath
+            ).withQueryParams(path.params)
+            path.fragment.fold(baseUri)(fragment => baseUri.withFragment(fragment))
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala
--- old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MarkdownRenderer.scala	2025-01-13 17:13:25.052470972 +0000
@@ -36,150 +36,151 @@
 import scala.util.matching.Regex
 
 object MarkdownRenderer {
-  private val log = LoggerFactory.getLogger(getClass())
+    private val log = LoggerFactory.getLogger(getClass())
 
-  private val MarkdownExtensions =
-    List(TablesExtension.create(), HeadingAnchorExtension.create(), TaskListItemsExtension.create())
+    private val MarkdownExtensions =
+        List(TablesExtension.create(), HeadingAnchorExtension.create(), TaskListItemsExtension.create())
 
-  /** Render the given markdown content into HTML.
-    *
-    * @param markdownSource
-    *   Markdown source code that shall be rendered.
-    * @return
-    *   A string containing the rendered markdown (HTML).
-    */
-  def render(markdownSource: String): String = {
-    val parser   = Parser.builder().extensions(MarkdownExtensions.asJava).build()
-    val markdown = parser.parse(markdownSource)
-    val renderer = HtmlRenderer
-      .builder()
-      .escapeHtml(true)
-      .extensions(MarkdownExtensions.asJava)
-      .sanitizeUrls(true)
-      .build()
-    renderer.render(markdown)
-  }
-
-  /** Render the given markdown sources and adjust all relative links by prefixing them with the path for the repostiory
-    * file browsing (`repo-name/files`).
-    *
-    * @param repositoryName
-    *   The name of the repository which contains the markdown sources. If the option is empty then no URI path prefix
-    *   will be set.
-    * @param markdownSource
-    *   A string containing the markdown sources to be rendered (usually the content of the README.md file in the
-    *   repository root).
-    * @return
-    *   A string containing the rendered markdown (HTML).
-    */
-  def renderRepositoryMarkdownFile(repositoryName: Option[String])(markdownSource: String): String = {
-    val parser   = Parser.builder().extensions(MarkdownExtensions.asJava).build()
-    val markdown = parser.parse(markdownSource)
-    val renderer = HtmlRenderer
-      .builder()
-      .attributeProviderFactory(new AttributeProviderFactory {
-        override def create(context: AttributeProviderContext): AttributeProvider =
-          new LinkHrefCorrector(repositoryName)
-      })
-      .escapeHtml(true)
-      .extensions(MarkdownExtensions.asJava)
-      .nodeRendererFactory(new HtmlNodeRendererFactory {
-        override def create(context: HtmlNodeRendererContext): NodeRenderer = new ToDoTextRenderer(context)
-      })
-      .sanitizeUrls(true)
-      .build()
-    renderer.render(markdown)
-  }
-
-  /** A helper class used by the `renderRepositoryOverviewReadme` function to adjust the `href` attribute of links that
-    * are not absolute.
-    *
-    * @param repositoryName
-    *   The name of the repository which contains the markdown sources. If the option is empty then no URI path prefix
-    *   will be set.
-    */
-  @SuppressWarnings(
-    Array("scalafix:DisableSyntax.isInstanceOf")
-  ) // TODO: Find a way to get rid of the isInstanceOf below.
-  @nowarn("msg=discarded non-Unit value.*")
-  class LinkHrefCorrector(private val repositoryName: Option[String]) extends AttributeProvider {
-    override def setAttributes(node: Node, tagName: String, attributes: java.util.Map[String, String]): Unit =
-      if (node.isInstanceOf[Link]) {
-        (repositoryName, attributes.asScala.get("href").flatMap(href => Uri.fromString(href).toOption)).mapN {
-          case (repositoryName, uri) =>
-            if (uri.scheme.isEmpty) {
-              val pathPrefix = Uri.Path(Vector(Uri.Path.Segment(repositoryName), Uri.Path.Segment("files")))
-              val correctedUri =
-                if (uri.path.startsWith(pathPrefix))
-                  uri
-                else
-                  uri.copy(path = pathPrefix |+| uri.path)
-              log.debug(s"Corrected URI for repository overview README rendering: $uri -> $correctedUri")
-              attributes.put("href", correctedUri.toString)
+    /** Render the given markdown content into HTML.
+      *
+      * @param markdownSource
+      *   Markdown source code that shall be rendered.
+      * @return
+      *   A string containing the rendered markdown (HTML).
+      */
+    def render(markdownSource: String): String = {
+        val parser   = Parser.builder().extensions(MarkdownExtensions.asJava).build()
+        val markdown = parser.parse(markdownSource)
+        val renderer = HtmlRenderer
+            .builder()
+            .escapeHtml(true)
+            .extensions(MarkdownExtensions.asJava)
+            .sanitizeUrls(true)
+            .build()
+        renderer.render(markdown)
+    }
+
+    /** Render the given markdown sources and adjust all relative links by prefixing them with the path for the
+      * repostiory file browsing (`repo-name/files`).
+      *
+      * @param repositoryName
+      *   The name of the repository which contains the markdown sources. If the option is empty then no URI path prefix
+      *   will be set.
+      * @param markdownSource
+      *   A string containing the markdown sources to be rendered (usually the content of the README.md file in the
+      *   repository root).
+      * @return
+      *   A string containing the rendered markdown (HTML).
+      */
+    def renderRepositoryMarkdownFile(repositoryName: Option[String])(markdownSource: String): String = {
+        val parser   = Parser.builder().extensions(MarkdownExtensions.asJava).build()
+        val markdown = parser.parse(markdownSource)
+        val renderer = HtmlRenderer
+            .builder()
+            .attributeProviderFactory(new AttributeProviderFactory {
+                override def create(context: AttributeProviderContext): AttributeProvider =
+                    new LinkHrefCorrector(repositoryName)
+            })
+            .escapeHtml(true)
+            .extensions(MarkdownExtensions.asJava)
+            .nodeRendererFactory(new HtmlNodeRendererFactory {
+                override def create(context: HtmlNodeRendererContext): NodeRenderer = new ToDoTextRenderer(context)
+            })
+            .sanitizeUrls(true)
+            .build()
+        renderer.render(markdown)
+    }
+
+    /** A helper class used by the `renderRepositoryOverviewReadme` function to adjust the `href` attribute of links
+      * that are not absolute.
+      *
+      * @param repositoryName
+      *   The name of the repository which contains the markdown sources. If the option is empty then no URI path prefix
+      *   will be set.
+      */
+    @SuppressWarnings(
+        Array("scalafix:DisableSyntax.isInstanceOf")
+    ) // TODO: Find a way to get rid of the isInstanceOf below.
+    @nowarn("msg=discarded non-Unit value.*")
+    class LinkHrefCorrector(private val repositoryName: Option[String]) extends AttributeProvider {
+        override def setAttributes(node: Node, tagName: String, attributes: java.util.Map[String, String]): Unit =
+            if (node.isInstanceOf[Link]) {
+                (repositoryName, attributes.asScala.get("href").flatMap(href => Uri.fromString(href).toOption)).mapN {
+                    case (repositoryName, uri) =>
+                        if (uri.scheme.isEmpty) {
+                            val pathPrefix =
+                                Uri.Path(Vector(Uri.Path.Segment(repositoryName), Uri.Path.Segment("files")))
+                            val correctedUri =
+                                if (uri.path.startsWith(pathPrefix))
+                                    uri
+                                else
+                                    uri.copy(path = pathPrefix |+| uri.path)
+                            log.debug(s"Corrected URI for repository overview README rendering: $uri -> $correctedUri")
+                            attributes.put("href", correctedUri.toString)
+                        }
+                }
+            }
+    }
+
+    /** A custom text node renderer which is supposed to highlight several words which are considered "todo items". This
+      * is currently a very limited approach with a fixed list of matching words.
+      *
+      * @param context
+      *   A context for an html node renderer that is needed to extract the html writer from it.
+      */
+    class ToDoTextRenderer(context: HtmlNodeRendererContext) extends NodeRenderer {
+        import ToDoTextCssMapping.*
+
+        private final val htmlWriter: HtmlWriter = context.getWriter()
+        private final val log: Logger            = LoggerFactory.getLogger(getClass())
+
+        override def getNodeTypes(): java.util.Set[Class[? <: Node]] = Set(classOf[Text]).asJava
+
+        @SuppressWarnings(Array("scalafix:DisableSyntax.null", "scalafix:DisableSyntax.asInstanceOf"))
+        override def render(node: Node): Unit = {
+            val text = node.asInstanceOf[Text] // We only receive text nodes (see `getNodeTypes`).
+            isToDoItem.findFirstMatchIn(text.getLiteral()) match {
+                case Some(matchedItem) =>
+                    log.debug(s"Matched TODO item: ${text.getLiteral()} (${matchedItem.groupCount})")
+                    if (matchedItem.group(4) === null) {
+                        val prefix   = matchedItem.group(1)
+                        val item     = matchedItem.group(2)
+                        val suffix   = matchedItem.group(3)
+                        val cssClass = todoCssClasses(item.toLowerCase(Locale.ROOT))
+                        htmlWriter.text(prefix)
+                        htmlWriter.tag("span", Map("class" -> cssClass).asJava)
+                        htmlWriter.text(item + ":")
+                        htmlWriter.tag("/span")
+                        htmlWriter.text(suffix)
+                    } else {
+                        val item     = matchedItem.group(4)
+                        val cssClass = todoCssClasses(item.toLowerCase(Locale.ROOT))
+                        htmlWriter.tag("span", Map("class" -> cssClass).asJava)
+                        htmlWriter.text(item)
+                        htmlWriter.tag("/span")
+                    }
+                case _ =>
+                    htmlWriter.text(text.getLiteral())
             }
         }
-      }
-  }
 
-  /** A custom text node renderer which is supposed to highlight several words which are considered "todo items". This
-    * is currently a very limited approach with a fixed list of matching words.
-    *
-    * @param context
-    *   A context for an html node renderer that is needed to extract the html writer from it.
-    */
-  class ToDoTextRenderer(context: HtmlNodeRendererContext) extends NodeRenderer {
-    import ToDoTextCssMapping.*
-
-    private final val htmlWriter: HtmlWriter = context.getWriter()
-    private final val log: Logger            = LoggerFactory.getLogger(getClass())
-
-    override def getNodeTypes(): java.util.Set[Class[? <: Node]] = Set(classOf[Text]).asJava
-
-    @SuppressWarnings(Array("scalafix:DisableSyntax.null", "scalafix:DisableSyntax.asInstanceOf"))
-    override def render(node: Node): Unit = {
-      val text = node.asInstanceOf[Text] // We only receive text nodes (see `getNodeTypes`).
-      isToDoItem.findFirstMatchIn(text.getLiteral()) match {
-        case Some(matchedItem) =>
-          log.debug(s"Matched TODO item: ${text.getLiteral()} (${matchedItem.groupCount})")
-          if (matchedItem.group(4) === null) {
-            val prefix   = matchedItem.group(1)
-            val item     = matchedItem.group(2)
-            val suffix   = matchedItem.group(3)
-            val cssClass = todoCssClasses(item.toLowerCase(Locale.ROOT))
-            htmlWriter.text(prefix)
-            htmlWriter.tag("span", Map("class" -> cssClass).asJava)
-            htmlWriter.text(item + ":")
-            htmlWriter.tag("/span")
-            htmlWriter.text(suffix)
-          } else {
-            val item     = matchedItem.group(4)
-            val cssClass = todoCssClasses(item.toLowerCase(Locale.ROOT))
-            htmlWriter.tag("span", Map("class" -> cssClass).asJava)
-            htmlWriter.text(item)
-            htmlWriter.tag("/span")
-          }
-        case _ =>
-          htmlWriter.text(text.getLiteral())
-      }
     }
-
-  }
 }
 
 object ToDoTextCssMapping {
-  /* We want to match either on specific words followed by a colon (`:`) and also on complete text nodes
-   * which can be created if markdown rendering is involved.
-   * For example a `**WORD** some text` will produce two text nodes (`WORD` and ` some text`)
-   * while `WORD: some text` will only produce one.
-   */
-  val isToDoItem: Regex = "(?i)(.*)(DEBUG|FIXME|HACK|TODO):(.*)|^(DEBUG|FIXME|HACK|TODO)$".r
-
-  // Mapping of todo item (words) to css classes for highlighting.
-  val todoCssClasses: Map[String, String] = Map(
-    "todo"  -> "todo-default",
-    "fixme" -> "todo-error",
-    "debug" -> "todo-info",
-    "hack"  -> "todo-warning"
-  ).withDefaultValue("todo-default")
+    /* We want to match either on specific words followed by a colon (`:`) and also on complete text nodes
+     * which can be created if markdown rendering is involved.
+     * For example a `**WORD** some text` will produce two text nodes (`WORD` and ` some text`)
+     * while `WORD: some text` will only produce one.
+     */
+    val isToDoItem: Regex = "(?i)(.*)(DEBUG|FIXME|HACK|TODO):(.*)|^(DEBUG|FIXME|HACK|TODO)$".r
+
+    // Mapping of todo item (words) to css classes for highlighting.
+    val todoCssClasses: Map[String, String] = Map(
+        "todo"  -> "todo-default",
+        "fixme" -> "todo-error",
+        "debug" -> "todo-info",
+        "hack"  -> "todo-warning"
+    ).withDefaultValue("todo-default")
 
 }
diff -rN -u old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MetaTags.scala new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MetaTags.scala
--- old-smederee/modules/html-utils/src/main/scala/de/smederee/html/MetaTags.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/html-utils/src/main/scala/de/smederee/html/MetaTags.scala	2025-01-13 17:13:25.052470972 +0000
@@ -19,152 +19,152 @@
 
 enum MetaReferrerPolicy(val tag: String) {
 
-  /** The Referer header will be omitted: sent requests do not include any referrer information.
-    */
-  case NoReferrer extends MetaReferrerPolicy("no-referrer")
-
-  /** Send the origin, path, and querystring in Referer when the protocol security level stays the same or improves
-    * (HTTP -> HTTP, HTTP -> HTTPS, HTTPS -> HTTPS). Don't send the Referer header for requests to less secure
-    * destinations (HTTPS -> HTTP, HTTPS -> file).
-    */
-  case NoReferrerWhenDowngrade extends MetaReferrerPolicy("no-referrer-when-downgrade")
-
-  /** Send only the origin in the Referer header. For example, a document at https://example.com/page.html will send the
-    * referrer https://example.com/.
-    */
-  case Origin extends MetaReferrerPolicy("origin")
-
-  /** When performing a same-origin request to the same protocol level (HTTP -> HTTP, HTTPS -> HTTPS), send the origin,
-    * path, and query string. Send only the origin for cross origin requests and requests to less secure destinations
-    * (HTTPS -> HTTP).
-    */
-  case OriginWhenCrossOrigin extends MetaReferrerPolicy("origin-when-cross-origin")
-
-  /** Send the origin, path, and query string for same-origin requests. Don't send the Referer header for cross-origin
-    * requests.
-    */
-  case SameOrigin extends MetaReferrerPolicy("same-origin")
-
-  /** Send only the origin when the protocol security level stays the same (HTTPS -> HTTPS). Don't send the Referer
-    * header to less secure destinations (HTTPS -> HTTP).
-    */
-  case StrictOrigin extends MetaReferrerPolicy("strict-origin")
-
-  /** Send the origin, path, and querystring when performing a same-origin request. For cross-origin requests send the
-    * origin (only) when the protocol security level stays same (HTTPS -> HTTPS). Don't send the Referer header to less
-    * secure destinations (HTTPS -> HTTP).
-    */
-  case StrictOriginWhenCrossOrigin extends MetaReferrerPolicy("strict-origin-when-cross-origin")
-
-  /** Send the origin, path, and query string when performing any request, regardless of security.
-    */
-  case UnsafeUrl extends MetaReferrerPolicy("unsafe-url")
+    /** The Referer header will be omitted: sent requests do not include any referrer information.
+      */
+    case NoReferrer extends MetaReferrerPolicy("no-referrer")
+
+    /** Send the origin, path, and querystring in Referer when the protocol security level stays the same or improves
+      * (HTTP -> HTTP, HTTP -> HTTPS, HTTPS -> HTTPS). Don't send the Referer header for requests to less secure
+      * destinations (HTTPS -> HTTP, HTTPS -> file).
+      */
+    case NoReferrerWhenDowngrade extends MetaReferrerPolicy("no-referrer-when-downgrade")
+
+    /** Send only the origin in the Referer header. For example, a document at https://example.com/page.html will send
+      * the referrer https://example.com/.
+      */
+    case Origin extends MetaReferrerPolicy("origin")
+
+    /** When performing a same-origin request to the same protocol level (HTTP -> HTTP, HTTPS -> HTTPS), send the
+      * origin, path, and query string. Send only the origin for cross origin requests and requests to less secure
+      * destinations (HTTPS -> HTTP).
+      */
+    case OriginWhenCrossOrigin extends MetaReferrerPolicy("origin-when-cross-origin")
+
+    /** Send the origin, path, and query string for same-origin requests. Don't send the Referer header for cross-origin
+      * requests.
+      */
+    case SameOrigin extends MetaReferrerPolicy("same-origin")
+
+    /** Send only the origin when the protocol security level stays the same (HTTPS -> HTTPS). Don't send the Referer
+      * header to less secure destinations (HTTPS -> HTTP).
+      */
+    case StrictOrigin extends MetaReferrerPolicy("strict-origin")
+
+    /** Send the origin, path, and querystring when performing a same-origin request. For cross-origin requests send the
+      * origin (only) when the protocol security level stays same (HTTPS -> HTTPS). Don't send the Referer header to
+      * less secure destinations (HTTPS -> HTTP).
+      */
+    case StrictOriginWhenCrossOrigin extends MetaReferrerPolicy("strict-origin-when-cross-origin")
+
+    /** Send the origin, path, and query string when performing any request, regardless of security.
+      */
+    case UnsafeUrl extends MetaReferrerPolicy("unsafe-url")
 }
 
 enum MetaRobotsDirective(val tag: String) {
 
-  /** Equivalent to index, follow. Used by Google
-    */
-  case All extends MetaRobotsDirective("all")
-
-  /** Allows the robot to follow the links on the page (default). Used by all.
-    */
-  case Follow extends MetaRobotsDirective("follow")
-
-  /** Allows the robot to index the page (default). Used by all.
-    */
-  case Index extends MetaRobotsDirective("index")
-
-  /** Requests the search engine not to cache the page content. Used by Bing, Google, Yahoo.
-    */
-  case NoArchive extends MetaRobotsDirective("noarchive")
-
-  /** Synonym of noarchive. Used by Bing.
-    */
-  case NoCache extends MetaRobotsDirective("nocache")
-
-  /** Requests the robot to not follow the links on the page. Used by all.
-    */
-  case NoFollow extends MetaRobotsDirective("nofollow")
-
-  /** Requests this page not to appear as the referring page of an indexed image. Used by Google.
-    */
-  case NoImageIndex extends MetaRobotsDirective("noimageindex")
-
-  /** Requests the robot to not index the page. Used by all.
-    */
-  case NoIndex extends MetaRobotsDirective("noindex")
-
-  /** Prevents displaying any description of the page in search engine results. Used by Bing, Google.
-    */
-  case NoSnippet extends MetaRobotsDirective("nosnippet")
-
-  /** Equivalent to noindex, nofollow. Used by Google.
-    */
-  case None extends MetaRobotsDirective("none")
+    /** Equivalent to index, follow. Used by Google
+      */
+    case All extends MetaRobotsDirective("all")
+
+    /** Allows the robot to follow the links on the page (default). Used by all.
+      */
+    case Follow extends MetaRobotsDirective("follow")
+
+    /** Allows the robot to index the page (default). Used by all.
+      */
+    case Index extends MetaRobotsDirective("index")
+
+    /** Requests the search engine not to cache the page content. Used by Bing, Google, Yahoo.
+      */
+    case NoArchive extends MetaRobotsDirective("noarchive")
+
+    /** Synonym of noarchive. Used by Bing.
+      */
+    case NoCache extends MetaRobotsDirective("nocache")
+
+    /** Requests the robot to not follow the links on the page. Used by all.
+      */
+    case NoFollow extends MetaRobotsDirective("nofollow")
+
+    /** Requests this page not to appear as the referring page of an indexed image. Used by Google.
+      */
+    case NoImageIndex extends MetaRobotsDirective("noimageindex")
+
+    /** Requests the robot to not index the page. Used by all.
+      */
+    case NoIndex extends MetaRobotsDirective("noindex")
+
+    /** Prevents displaying any description of the page in search engine results. Used by Bing, Google.
+      */
+    case NoSnippet extends MetaRobotsDirective("nosnippet")
+
+    /** Equivalent to noindex, nofollow. Used by Google.
+      */
+    case None extends MetaRobotsDirective("none")
 }
 
 opaque type MetaDescription = String
 object MetaDescription {
 
-  /** Create an instance of MetaDescription from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a MetaDescription.
-    * @return
-    *   The appropriate instance of MetaDescription.
-    */
-  def apply(source: String): MetaDescription = source
-
-  /** Try to create an instance of MetaDescription from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a MetaDescription.
-    * @return
-    *   An option to the successfully converted MetaDescription.
-    */
-  def from(source: String): Option[MetaDescription] = Option(source)
+    /** Create an instance of MetaDescription from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a MetaDescription.
+      * @return
+      *   The appropriate instance of MetaDescription.
+      */
+    def apply(source: String): MetaDescription = source
+
+    /** Try to create an instance of MetaDescription from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a MetaDescription.
+      * @return
+      *   An option to the successfully converted MetaDescription.
+      */
+    def from(source: String): Option[MetaDescription] = Option(source)
 
 }
 
 opaque type MetaKeyWords = List[String]
 object MetaKeyWords {
 
-  /** Create an instance of MetaKeyWords from the given List[String] type.
-    *
-    * @param source
-    *   An instance of type List[String] which will be returned as a MetaKeyWords.
-    * @return
-    *   The appropriate instance of MetaKeyWords.
-    */
-  def apply(source: List[String]): MetaKeyWords = source
-
-  /** Return an empty instance of MetaKeyWords.
-    *
-    * @return
-    *   An empty list.
-    */
-  def empty: MetaKeyWords = List.empty
-
-  /** Try to create an instance of MetaKeyWords from the given List[String].
-    *
-    * @param source
-    *   A List[String] that should fulfil the requirements to be converted into a MetaKeyWords.
-    * @return
-    *   An option to the successfully converted MetaKeyWords.
-    */
-  def from(source: List[String]): Option[MetaKeyWords] =
-    source.flatMap(string => Option(string)) match {
-      case Nil      => None
-      case keywords => Option(keywords)
+    /** Create an instance of MetaKeyWords from the given List[String] type.
+      *
+      * @param source
+      *   An instance of type List[String] which will be returned as a MetaKeyWords.
+      * @return
+      *   The appropriate instance of MetaKeyWords.
+      */
+    def apply(source: List[String]): MetaKeyWords = source
+
+    /** Return an empty instance of MetaKeyWords.
+      *
+      * @return
+      *   An empty list.
+      */
+    def empty: MetaKeyWords = List.empty
+
+    /** Try to create an instance of MetaKeyWords from the given List[String].
+      *
+      * @param source
+      *   A List[String] that should fulfil the requirements to be converted into a MetaKeyWords.
+      * @return
+      *   An option to the successfully converted MetaKeyWords.
+      */
+    def from(source: List[String]): Option[MetaKeyWords] =
+        source.flatMap(string => Option(string)) match {
+            case Nil      => None
+            case keywords => Option(keywords)
+        }
+
+    extension (keywords: MetaKeyWords) {
+        def isEmpty: Boolean  = keywords.isEmpty
+        def mkString: String  = keywords.toList.mkString(", ")
+        def nonEmpty: Boolean = keywords.nonEmpty
     }
 
-  extension (keywords: MetaKeyWords) {
-    def isEmpty: Boolean  = keywords.isEmpty
-    def mkString: String  = keywords.toList.mkString(", ")
-    def nonEmpty: Boolean = keywords.nonEmpty
-  }
-
 }
 
 /** HTML meta attributes which can be written into the header part of an HTML page.
@@ -188,23 +188,24 @@
 
 object MetaTags {
 
-  /** Return a default instance of meta tags which is empty and has the nofollow and noindex flags for robots set.
-    *
-    * @return
-    *   An instance of meta tags containing only nofollow and noindex flags for robots.
-    */
-  def default: MetaTags =
-    MetaTags(
-      description = None,
-      keywords = MetaKeyWords.empty,
-      referrer = None,
-      robots = Set(MetaRobotsDirective.NoFollow, MetaRobotsDirective.NoIndex)
-    )
-
-  /** Return an empty meta tags instance.
-    *
-    * @return
-    *   An instance of meta tags containing no values, resulting in no tags being rendered.
-    */
-  def empty: MetaTags = MetaTags(description = None, keywords = MetaKeyWords.empty, referrer = None, robots = Set.empty)
+    /** Return a default instance of meta tags which is empty and has the nofollow and noindex flags for robots set.
+      *
+      * @return
+      *   An instance of meta tags containing only nofollow and noindex flags for robots.
+      */
+    def default: MetaTags =
+        MetaTags(
+            description = None,
+            keywords = MetaKeyWords.empty,
+            referrer = None,
+            robots = Set(MetaRobotsDirective.NoFollow, MetaRobotsDirective.NoIndex)
+        )
+
+    /** Return an empty meta tags instance.
+      *
+      * @return
+      *   An instance of meta tags containing no values, resulting in no tags being rendered.
+      */
+    def empty: MetaTags =
+        MetaTags(description = None, keywords = MetaKeyWords.empty, referrer = None, robots = Set.empty)
 }
diff -rN -u old-smederee/modules/html-utils/src/test/scala/de/smederee/html/LinkToolsTest.scala new-smederee/modules/html-utils/src/test/scala/de/smederee/html/LinkToolsTest.scala
--- old-smederee/modules/html-utils/src/test/scala/de/smederee/html/LinkToolsTest.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/html-utils/src/test/scala/de/smederee/html/LinkToolsTest.scala	2025-01-13 17:13:25.052470972 +0000
@@ -26,19 +26,19 @@
 
 final class LinkToolsTest extends ScalaCheckSuite {
 
-  val externalUrlConfig = ExternalUrlConfiguration(
-    host"proxy.example.com",
-    Uri.fromString("sub-path/with/children").toOption,
-    None,
-    Uri.Scheme.https
-  )
-
-  test("createFullUri must create a corrent full Uri") {
-    val uri     = uri"/another/path#fragment?parameter=value&another=one"
-    val fullUri = externalUrlConfig.createFullUri(uri)
-    assertEquals(
-      fullUri.renderString,
-      "https://proxy.example.com/sub-path/with/children/another/path#fragment?parameter=value&another=one"
+    val externalUrlConfig = ExternalUrlConfiguration(
+        host"proxy.example.com",
+        Uri.fromString("sub-path/with/children").toOption,
+        None,
+        Uri.Scheme.https
     )
-  }
+
+    test("createFullUri must create a corrent full Uri") {
+        val uri     = uri"/another/path#fragment?parameter=value&another=one"
+        val fullUri = externalUrlConfig.createFullUri(uri)
+        assertEquals(
+            fullUri.renderString,
+            "https://proxy.example.com/sub-path/with/children/another/path#fragment?parameter=value&another=one"
+        )
+    }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -31,100 +31,100 @@
   */
 abstract class AccountManagementRepository[F[_]] {
 
-  /** Add the given ssh key to the database. The database MUST ensure that a key is unique across the system! Usually by
-    * having a unique constraint on the fingerprint of the key.
-    *
-    * @param key
-    *   The public ssh key to be saved to the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def addSshKey(key: PublicSshKey): F[Int]
-
-  /** Delete the account with the given user id from the database.
-    *
-    * The internal database logic (foreign keys, cascading) SHALL ensure that everything related to the user is deleted
-    * from the database.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteAccount(uid: UserId): F[Int]
-
-  /** Delete the ssh key with the given id and owner from the database.
-    *
-    * @param keyId
-    *   The unique id of the public ssh key.
-    * @param ownerId
-    *   The unique user id of the owner of the ssh key.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteSshKey(keyId: UUID, ownerId: UserId): F[Int]
-
-  /** Find the account with the given validation token.
-    *
-    * @param token
-    *   A validation token.
-    * @return
-    *   An option to the account if it exists.
-    */
-  def findByValidationToken(token: ValidationToken): F[Option[Account]]
-
-  /** Retrieve the password hash from the database.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   An option to the password hash.
-    */
-  def findPasswordHash(uid: UserId): F[Option[PasswordHash]]
-
-  /** Return all public ssh keys for the given user.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   A stream of public ssh keys that may be empty.
-    */
-  def listSshKeys(uid: UserId): Stream[F, PublicSshKey]
-
-  /** Mark the account with the given user id as validated. This includes the following operations in the database:
-    *
-    * {{{
-    * 1. set the ´validated_email` column to `true`
-    * 2. clear the `validation_token` column
-    * }}}
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def markAsValidated(uid: UserId): F[Int]
-
-  /** Set the language for the user account.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @param language
-    *   An option to the preferred language that shall be set for the account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def setLanguage(uid: UserId, language: Option[LanguageCode]): F[Int]
-
-  /** Set the validation token for the account with the given user id.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @param token
-    *   The validation token to be stored in the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def setValidationToken(uid: UserId, token: ValidationToken): F[Int]
+    /** Add the given ssh key to the database. The database MUST ensure that a key is unique across the system! Usually
+      * by having a unique constraint on the fingerprint of the key.
+      *
+      * @param key
+      *   The public ssh key to be saved to the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def addSshKey(key: PublicSshKey): F[Int]
+
+    /** Delete the account with the given user id from the database.
+      *
+      * The internal database logic (foreign keys, cascading) SHALL ensure that everything related to the user is
+      * deleted from the database.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteAccount(uid: UserId): F[Int]
+
+    /** Delete the ssh key with the given id and owner from the database.
+      *
+      * @param keyId
+      *   The unique id of the public ssh key.
+      * @param ownerId
+      *   The unique user id of the owner of the ssh key.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteSshKey(keyId: UUID, ownerId: UserId): F[Int]
+
+    /** Find the account with the given validation token.
+      *
+      * @param token
+      *   A validation token.
+      * @return
+      *   An option to the account if it exists.
+      */
+    def findByValidationToken(token: ValidationToken): F[Option[Account]]
+
+    /** Retrieve the password hash from the database.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   An option to the password hash.
+      */
+    def findPasswordHash(uid: UserId): F[Option[PasswordHash]]
+
+    /** Return all public ssh keys for the given user.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   A stream of public ssh keys that may be empty.
+      */
+    def listSshKeys(uid: UserId): Stream[F, PublicSshKey]
+
+    /** Mark the account with the given user id as validated. This includes the following operations in the database:
+      *
+      * {{{
+      * 1. set the ´validated_email` column to `true`
+      * 2. clear the `validation_token` column
+      * }}}
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def markAsValidated(uid: UserId): F[Int]
+
+    /** Set the language for the user account.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @param language
+      *   An option to the preferred language that shall be set for the account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def setLanguage(uid: UserId, language: Option[LanguageCode]): F[Int]
+
+    /** Set the validation token for the account with the given user id.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @param token
+      *   The validation token to be stored in the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def setValidationToken(uid: UserId, token: ValidationToken): F[Int]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRoutes.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AccountManagementRoutes.scala	2025-01-13 17:13:25.052470972 +0000
@@ -71,357 +71,391 @@
     signAndValidate: SignAndValidate,
     ticketServiceApi: TicketServiceApi[F]
 ) extends Http4sDsl[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  /** Delete the given directory recursively. It is checked if the given directory is a direct sub directory of the
-    * `repositoriesDirectory` and only if this is the case is the directory removed.
-    *
-    * @param userDirectory
-    *   The path on the filesystem to the directory that shall be deleted.
-    * @return
-    *   `true` if the directory was deleted.
-    */
-  protected def deleteUserDirectory(userDirectory: java.nio.file.Path): F[Boolean] =
-    for {
-      _            <- Sync[F].delay(log.debug(s"Request to delete user dir: $userDirectory"))
-      reposDirPath <- Sync[F].delay(configuration.darcs.repositoriesDirectory.toPath)
-      isSubDir     <- Sync[F].delay(reposDirPath.equals(userDirectory.getParent()))
-      deleted <-
-        Sync[F].delay {
-          if (isSubDir) {
-            Files.walkFileTree(
-              userDirectory,
-              new FileVisitor[java.nio.file.Path] {
-                override def visitFileFailed(file: java.nio.file.Path, exc: IOException): FileVisitResult =
-                  FileVisitResult.CONTINUE
-
-                override def visitFile(
-                    file: java.nio.file.Path,
-                    attrs: java.nio.file.attribute.BasicFileAttributes
-                ): FileVisitResult = {
-                  Files.delete(file)
-                  FileVisitResult.CONTINUE
+    /** Delete the given directory recursively. It is checked if the given directory is a direct sub directory of the
+      * `repositoriesDirectory` and only if this is the case is the directory removed.
+      *
+      * @param userDirectory
+      *   The path on the filesystem to the directory that shall be deleted.
+      * @return
+      *   `true` if the directory was deleted.
+      */
+    protected def deleteUserDirectory(userDirectory: java.nio.file.Path): F[Boolean] =
+        for {
+            _            <- Sync[F].delay(log.debug(s"Request to delete user dir: $userDirectory"))
+            reposDirPath <- Sync[F].delay(configuration.darcs.repositoriesDirectory.toPath)
+            isSubDir     <- Sync[F].delay(reposDirPath.equals(userDirectory.getParent()))
+            deleted <-
+                Sync[F].delay {
+                    if (isSubDir) {
+                        Files.walkFileTree(
+                            userDirectory,
+                            new FileVisitor[java.nio.file.Path] {
+                                override def visitFileFailed(
+                                    file: java.nio.file.Path,
+                                    exc: IOException
+                                ): FileVisitResult = FileVisitResult.CONTINUE
+
+                                override def visitFile(
+                                    file: java.nio.file.Path,
+                                    attrs: java.nio.file.attribute.BasicFileAttributes
+                                ): FileVisitResult = {
+                                    Files.delete(file)
+                                    FileVisitResult.CONTINUE
+                                }
+
+                                override def preVisitDirectory(
+                                    dir: java.nio.file.Path,
+                                    attrs: java.nio.file.attribute.BasicFileAttributes
+                                ): FileVisitResult = FileVisitResult.CONTINUE
+
+                                override def postVisitDirectory(
+                                    dir: java.nio.file.Path,
+                                    exc: IOException
+                                ): FileVisitResult = {
+                                    Files.delete(dir)
+                                    FileVisitResult.CONTINUE
+                                }
+                            }
+                        )
+                        Files.deleteIfExists(userDirectory)
+                    } else {
+                        log.warn(
+                            s"Refused requested removal of directory $userDirectory which is not a direct sub directory of the configured repositories directory!"
+                        )
+                        false
+                    }
                 }
+        } yield deleted
 
-                override def preVisitDirectory(
-                    dir: java.nio.file.Path,
-                    attrs: java.nio.file.attribute.BasicFileAttributes
-                ): FileVisitResult = FileVisitResult.CONTINUE
-
-                override def postVisitDirectory(
-                    dir: java.nio.file.Path,
-                    exc: IOException
-                ): FileVisitResult = {
-                  Files.delete(dir)
-                  FileVisitResult.CONTINUE
-                }
-              }
-            )
-            Files.deleteIfExists(userDirectory)
-          } else {
-            log.warn(
-              s"Refused requested removal of directory $userDirectory which is not a direct sub directory of the configured repositories directory!"
-            )
-            false
-          }
-        }
-    } yield deleted
-
-  private val addSshKey: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / "user" / "settings" / "ssh" / "add" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
-            }
-          }
-          form          <- Sync[F].delay(AddPublicSshKeyForm.validate(formData))
-          actionBaseUri <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
-          addAction     <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/add"))
-          deleteAction  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/delete"))
-          keys          <- accountManagementRepo.listSshKeys(user.uid).compile.toList
-          resp <- form match {
-            case Validated.Invalid(errors) =>
-              BadRequest(
-                views.html.account
-                  .sshSettings(lang = language)(csrf, Option(s"Smederee/~${user.name} - SSH Settings"), user)(
-                    actionBaseUri,
-                    addAction,
-                    deleteAction,
-                    keys
-                  )(formData, FormErrors.fromNec(errors))
-              )
-            case Validated.Valid(validSshKeyForm) =>
-              for {
-                id           <- Sync[F].delay(UUID.randomUUID())
-                createdAt    <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
-                convertedKey <- Sync[F].delay(PublicSshKey.from(id)(user.uid)(createdAt)(validSshKeyForm.keyString))
-                key <- Sync[F].delay(
-                  convertedKey.map(key => validSshKeyForm.name.fold(key)(name => key.copy(comment = Option(name))))
-                ) // Override comment with the one from the form if given.
-                written <- key.traverse(key => accountManagementRepo.addSshKey(key).recoverWith(_ => Sync[F].pure(0)))
-                resp <- written match {
-                  case None =>
-                    // There has been no write at all, implying that the conversion failed.
-                    BadRequest(
-                      views.html.account
-                        .sshSettings(lang = language)(csrf, Option(s"Smederee/~${user.name} - SSH Settings"), user)(
-                          actionBaseUri,
-                          addAction,
-                          deleteAction,
-                          keys
-                        )(
-                          formData,
-                          Map(
-                            AddPublicSshKeyForm.fieldGlobal -> List(
-                              FormFieldError("The key could not be properly converted!")
+    private val addSshKey: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / "user" / "settings" / "ssh" / "add" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    form          <- Sync[F].delay(AddPublicSshKeyForm.validate(formData))
+                    actionBaseUri <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
+                    addAction     <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/add"))
+                    deleteAction  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/delete"))
+                    keys          <- accountManagementRepo.listSshKeys(user.uid).compile.toList
+                    resp <- form match {
+                        case Validated.Invalid(errors) =>
+                            BadRequest(
+                                views.html.account
+                                    .sshSettings(lang = language)(
+                                        csrf,
+                                        Option(s"Smederee/~${user.name} - SSH Settings"),
+                                        user
+                                    )(
+                                        actionBaseUri,
+                                        addAction,
+                                        deleteAction,
+                                        keys
+                                    )(formData, FormErrors.fromNec(errors))
                             )
-                          )
-                        )
+                        case Validated.Valid(validSshKeyForm) =>
+                            for {
+                                id        <- Sync[F].delay(UUID.randomUUID())
+                                createdAt <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
+                                convertedKey <- Sync[F].delay(
+                                    PublicSshKey.from(id)(user.uid)(createdAt)(validSshKeyForm.keyString)
+                                )
+                                key <- Sync[F].delay(
+                                    convertedKey.map(key =>
+                                        validSshKeyForm.name.fold(key)(name => key.copy(comment = Option(name)))
+                                    )
+                                ) // Override comment with the one from the form if given.
+                                written <- key.traverse(key =>
+                                    accountManagementRepo.addSshKey(key).recoverWith(_ => Sync[F].pure(0))
+                                )
+                                resp <- written match {
+                                    case None =>
+                                        // There has been no write at all, implying that the conversion failed.
+                                        BadRequest(
+                                            views.html.account
+                                                .sshSettings(lang = language)(
+                                                    csrf,
+                                                    Option(s"Smederee/~${user.name} - SSH Settings"),
+                                                    user
+                                                )(
+                                                    actionBaseUri,
+                                                    addAction,
+                                                    deleteAction,
+                                                    keys
+                                                )(
+                                                    formData,
+                                                    Map(
+                                                        AddPublicSshKeyForm.fieldGlobal -> List(
+                                                            FormFieldError("The key could not be properly converted!")
+                                                        )
+                                                    )
+                                                )
+                                        )
+                                    case Some(1) =>
+                                        // One row was written to the database implying that everything went well.
+                                        SeeOther(Location(actionBaseUri.addSegment("ssh")))
+                                    case Some(_) =>
+                                        // Any other result implies that there has been an error.
+                                        accountManagementRepo.listSshKeys(user.uid).compile.toList.flatMap { keys =>
+                                            BadRequest(
+                                                views.html.account
+                                                    .sshSettings(lang = language)(
+                                                        csrf,
+                                                        Option(s"Smederee/~${user.name} - SSH Settings"),
+                                                        user
+                                                    )(
+                                                        actionBaseUri,
+                                                        addAction,
+                                                        deleteAction,
+                                                        keys
+                                                    )(
+                                                        formData,
+                                                        Map(
+                                                            AddPublicSshKeyForm.fieldGlobal -> List(
+                                                                FormFieldError(
+                                                                    "An error occured while saving the key to the database!"
+                                                                )
+                                                            )
+                                                        )
+                                                    )
+                                            )
+                                        }
+                                }
+                            } yield resp
+                    }
+                } yield resp
+            }
+    }
+
+    private val deleteAccount: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / "user" / "settings" / "delete" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    passwordField  <- Sync[F].delay(formData.get("password").flatMap(Password.from))
+                    userIsSure     <- Sync[F].delay(formData.get("i-am-sure").exists(_ === "yes"))
+                    rootUri        <- Sync[F].delay(configuration.external.createFullUri(uri"/"))
+                    actionBaseUri  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
+                    deleteAction   <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/delete"))
+                    languageAction <- Sync[F].delay(configuration.external.createFullUri(uri"/user/settings/language"))
+                    validateAction <- Sync[F].delay(
+                        configuration.external.createFullUri(uri"user/settings/email/validate")
                     )
-                  case Some(1) =>
-                    // One row was written to the database implying that everything went well.
-                    SeeOther(Location(actionBaseUri.addSegment("ssh")))
-                  case Some(_) =>
-                    // Any other result implies that there has been an error.
-                    accountManagementRepo.listSshKeys(user.uid).compile.toList.flatMap { keys =>
-                      BadRequest(
-                        views.html.account
-                          .sshSettings(lang = language)(csrf, Option(s"Smederee/~${user.name} - SSH Settings"), user)(
-                            actionBaseUri,
-                            addAction,
-                            deleteAction,
-                            keys
-                          )(
-                            formData,
-                            Map(
-                              AddPublicSshKeyForm.fieldGlobal -> List(
-                                FormFieldError("An error occured while saving the key to the database!")
-                              )
+                    passwordHash <- accountManagementRepo.findPasswordHash(user.uid)
+                    passwordCorrect <- Sync[F].delay(
+                        (passwordField, passwordHash)
+                            .mapN { case (enteredPassword, hashFromDatabase) =>
+                                enteredPassword.matches(hashFromDatabase)
+                            }
+                            .getOrElse(false)
+                    )
+                    response <-
+                        if (passwordCorrect && userIsSure) {
+                            // First we delete the user directory and all sub directories.
+                            // Afterwards we delete the user account from the database and redirect the user
+                            // to the start page, removing the authentication cookie in the process.
+                            for {
+                                _ <- Sync[F].delay(
+                                    log.info(s"Going to delete account ${user.name} as requested by the user.")
+                                )
+                                userDir <- Sync[F].delay(
+                                    java.nio.file.Paths
+                                        .get(
+                                            configuration.darcs.repositoriesDirectory.toPath.toString,
+                                            user.name.toString
+                                        )
+                                )
+                                _ <- deleteUserDirectory(userDir)
+                                response <- ticketServiceApi.deleteUser(user.uid) *> accountManagementRepo
+                                    .deleteAccount(
+                                        user.uid
+                                    ) *> SeeOther(Location(rootUri)).map(
+                                        _.removeCookie(Constants.authenticationCookieName.toString)
+                                    )
+                            } yield response
+                        } else
+                            BadRequest(
+                                views.html.account.settings(lang = language)(
+                                    csrf,
+                                    Option(s"Smederee/~${user.name} - Settings"),
+                                    user
+                                )(
+                                    actionBaseUri,
+                                    deleteAction,
+                                    languageAction,
+                                    validateAction
+                                )
                             )
-                          )
-                      )
+                } yield response
+            }
+    }
+
+    private val deleteSshKey: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / "user" / "settings" / "ssh" / "delete" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    actionBaseUri <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
+                    userIsSure    <- Sync[F].delay(formData.get("i-am-sure").exists(_ === "yes"))
+                    sshKeyId <- Sync[F].delay {
+                        Try(formData.get("ssh-key-id").map(UUID.fromString)) match {
+                            case scala.util.Failure(error) =>
+                                log.error("Error while parsing ssh key id upon key delete request.", error)
+                                None
+                            case scala.util.Success(Some(keyId)) => Option(keyId)
+                            case _                               => None
+                        }
+                    }
+                    _ <- userIsSure match {
+                        case false => Sync[F].pure(None)
+                        case true  => sshKeyId.traverse(id => accountManagementRepo.deleteSshKey(id, user.uid))
                     }
+                    resp <- SeeOther(Location(actionBaseUri.addSegment("ssh")))
+                } yield resp
+            }
+    }
+
+    private val sendValidationEmail: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / "user" / "settings" / "email" / "validate" as user =>
+            for {
+                csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                token    <- Sync[F].delay(ValidationToken.generate)
+                _        <- accountManagementRepo.setValidationToken(user.uid, token)
+                from     <- Sync[F].delay(FromAddress("noreply@smeder.ee"))
+                to       <- Sync[F].delay(NonEmptyList.of(user.email.toToAddress))
+                uri      <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/email/validate"))
+                subject  <- Sync[F].delay(SubjectLine("Smederee - Please validate your email address."))
+                body <- Sync[F].delay(
+                    TextBody(views.txt.emails.validate(user, token, uri).toString)
+                ) // TODO: extension method?
+                message <- Sync[F].delay(EmailMessage(from, to, List.empty, List.empty, subject, body))
+                result  <- emailMiddleware.send(message)
+                response <- result match {
+                    case Left(error) => InternalServerError(s"An error occured: $error")
+                    case Right(_)    => SeeOther(Location(configuration.external.createFullUri(uri"user/settings")))
                 }
-              } yield resp
-          }
-        } yield resp
-      }
-  }
-
-  private val deleteAccount: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / "user" / "settings" / "delete" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
+            } yield response
+    }
+
+    private val setLanguage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / "user" / "settings" / "language" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    languageCode   <- Sync[F].delay(formData.get("language").flatMap(LanguageCode.from))
+                    rootUri        <- Sync[F].delay(configuration.external.createFullUri(uri"/"))
+                    actionBaseUri  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
+                    deleteAction   <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/delete"))
+                    languageAction <- Sync[F].delay(configuration.external.createFullUri(uri"/user/settings/language"))
+                    validateAction <- Sync[F].delay(
+                        configuration.external.createFullUri(uri"user/settings/email/validate")
+                    )
+                    _ <- accountManagementRepo.setLanguage(user.uid, languageCode)
+                    _ <- ticketServiceApi.createOrUpdateUser(TicketsUser(user.uid, user.name, user.email, languageCode))
+                    resp <- SeeOther(Location(actionBaseUri))
+                } yield resp
             }
-          }
-          passwordField  <- Sync[F].delay(formData.get("password").flatMap(Password.from))
-          userIsSure     <- Sync[F].delay(formData.get("i-am-sure").exists(_ === "yes"))
-          rootUri        <- Sync[F].delay(configuration.external.createFullUri(uri"/"))
-          actionBaseUri  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
-          deleteAction   <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/delete"))
-          languageAction <- Sync[F].delay(configuration.external.createFullUri(uri"/user/settings/language"))
-          validateAction <- Sync[F].delay(
-            configuration.external.createFullUri(uri"user/settings/email/validate")
-          )
-          passwordHash <- accountManagementRepo.findPasswordHash(user.uid)
-          passwordCorrect <- Sync[F].delay(
-            (passwordField, passwordHash)
-              .mapN { case (enteredPassword, hashFromDatabase) =>
-                enteredPassword.matches(hashFromDatabase)
-              }
-              .getOrElse(false)
-          )
-          response <-
-            if (passwordCorrect && userIsSure) {
-              // First we delete the user directory and all sub directories.
-              // Afterwards we delete the user account from the database and redirect the user
-              // to the start page, removing the authentication cookie in the process.
-              for {
-                _ <- Sync[F].delay(
-                  log.info(s"Going to delete account ${user.name} as requested by the user.")
+    }
+
+    private val showAccountSettings: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "user" / "settings" as user =>
+            for {
+                csrf           <- Sync[F].delay(ar.req.getCsrfToken)
+                language       <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                actionBaseUri  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
+                deleteAction   <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/delete"))
+                languageAction <- Sync[F].delay(configuration.external.createFullUri(uri"/user/settings/language"))
+                validateAction <- Sync[F].delay(
+                    configuration.external.createFullUri(uri"user/settings/email/validate")
                 )
-                userDir <- Sync[F].delay(
-                  java.nio.file.Paths
-                    .get(configuration.darcs.repositoriesDirectory.toPath.toString, user.name.toString)
+                resp <- Ok(
+                    views.html.account
+                        .settings(lang = language)(csrf, Option(s"Smederee/~${user.name} - Settings"), user)(
+                            actionBaseUri,
+                            deleteAction,
+                            languageAction,
+                            validateAction
+                        )
                 )
-                _ <- deleteUserDirectory(userDir)
-                response <- ticketServiceApi.deleteUser(user.uid) *> accountManagementRepo.deleteAccount(
-                  user.uid
-                ) *> SeeOther(Location(rootUri)).map(_.removeCookie(Constants.authenticationCookieName.toString))
-              } yield response
-            } else
-              BadRequest(
-                views.html.account.settings(lang = language)(csrf, Option(s"Smederee/~${user.name} - Settings"), user)(
-                  actionBaseUri,
-                  deleteAction,
-                  languageAction,
-                  validateAction
+            } yield resp
+    }
+
+    private val showAccountSshSettings: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "user" / "settings" / "ssh" as user =>
+            for {
+                csrf          <- Sync[F].delay(ar.req.getCsrfToken)
+                language      <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                actionBaseUri <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
+                addAction     <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/add"))
+                deleteAction  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/delete"))
+                keys          <- accountManagementRepo.listSshKeys(user.uid).compile.toList
+                resp <- Ok(
+                    views.html.account
+                        .sshSettings(lang = language)(csrf, Option(s"Smederee/~${user.name} - SSH Settings"), user)(
+                            actionBaseUri,
+                            addAction,
+                            deleteAction,
+                            keys
+                        )()
                 )
-              )
-        } yield response
-      }
-  }
-
-  private val deleteSshKey: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / "user" / "settings" / "ssh" / "delete" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf <- Sync[F].delay(ar.req.getCsrfToken)
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
-            }
-          }
-          actionBaseUri <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
-          userIsSure    <- Sync[F].delay(formData.get("i-am-sure").exists(_ === "yes"))
-          sshKeyId <- Sync[F].delay {
-            Try(formData.get("ssh-key-id").map(UUID.fromString)) match {
-              case scala.util.Failure(error) =>
-                log.error("Error while parsing ssh key id upon key delete request.", error)
-                None
-              case scala.util.Success(Some(keyId)) => Option(keyId)
-              case _                               => None
-            }
-          }
-          _ <- userIsSure match {
-            case false => Sync[F].pure(None)
-            case true  => sshKeyId.traverse(id => accountManagementRepo.deleteSshKey(id, user.uid))
-          }
-          resp <- SeeOther(Location(actionBaseUri.addSegment("ssh")))
-        } yield resp
-      }
-  }
-
-  private val sendValidationEmail: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / "user" / "settings" / "email" / "validate" as user =>
-      for {
-        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        token    <- Sync[F].delay(ValidationToken.generate)
-        _        <- accountManagementRepo.setValidationToken(user.uid, token)
-        from     <- Sync[F].delay(FromAddress("noreply@smeder.ee"))
-        to       <- Sync[F].delay(NonEmptyList.of(user.email.toToAddress))
-        uri      <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/email/validate"))
-        subject  <- Sync[F].delay(SubjectLine("Smederee - Please validate your email address."))
-        body <- Sync[F].delay(
-          TextBody(views.txt.emails.validate(user, token, uri).toString)
-        ) // TODO: extension method?
-        message <- Sync[F].delay(EmailMessage(from, to, List.empty, List.empty, subject, body))
-        result  <- emailMiddleware.send(message)
-        response <- result match {
-          case Left(error) => InternalServerError(s"An error occured: $error")
-          case Right(_)    => SeeOther(Location(configuration.external.createFullUri(uri"user/settings")))
-        }
-      } yield response
-  }
-
-  private val setLanguage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / "user" / "settings" / "language" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf <- Sync[F].delay(ar.req.getCsrfToken)
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
-            }
-          }
-          languageCode   <- Sync[F].delay(formData.get("language").flatMap(LanguageCode.from))
-          rootUri        <- Sync[F].delay(configuration.external.createFullUri(uri"/"))
-          actionBaseUri  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
-          deleteAction   <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/delete"))
-          languageAction <- Sync[F].delay(configuration.external.createFullUri(uri"/user/settings/language"))
-          validateAction <- Sync[F].delay(
-            configuration.external.createFullUri(uri"user/settings/email/validate")
-          )
-          _    <- accountManagementRepo.setLanguage(user.uid, languageCode)
-          _    <- ticketServiceApi.createOrUpdateUser(TicketsUser(user.uid, user.name, user.email, languageCode))
-          resp <- SeeOther(Location(actionBaseUri))
-        } yield resp
-      }
-  }
-
-  private val showAccountSettings: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / "user" / "settings" as user =>
-      for {
-        csrf           <- Sync[F].delay(ar.req.getCsrfToken)
-        language       <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        actionBaseUri  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
-        deleteAction   <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/delete"))
-        languageAction <- Sync[F].delay(configuration.external.createFullUri(uri"/user/settings/language"))
-        validateAction <- Sync[F].delay(
-          configuration.external.createFullUri(uri"user/settings/email/validate")
-        )
-        resp <- Ok(
-          views.html.account.settings(lang = language)(csrf, Option(s"Smederee/~${user.name} - Settings"), user)(
-            actionBaseUri,
-            deleteAction,
-            languageAction,
-            validateAction
-          )
-        )
-      } yield resp
-  }
-
-  private val showAccountSshSettings: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / "user" / "settings" / "ssh" as user =>
-      for {
-        csrf          <- Sync[F].delay(ar.req.getCsrfToken)
-        language      <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        actionBaseUri <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings"))
-        addAction     <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/add"))
-        deleteAction  <- Sync[F].delay(configuration.external.createFullUri(uri"user/settings/ssh/delete"))
-        keys          <- accountManagementRepo.listSshKeys(user.uid).compile.toList
-        resp <- Ok(
-          views.html.account.sshSettings(lang = language)(csrf, Option(s"Smederee/~${user.name} - SSH Settings"), user)(
-            actionBaseUri,
-            addAction,
-            deleteAction,
-            keys
-          )()
-        )
-      } yield resp
-  }
-
-  private val validateEmailAddress: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / "user" / "settings" / "email" / "validate" / ValidationTokenPathParameter(
-          token
-        ) =>
-      for {
-        csrf    <- Sync[F].delay(req.getCsrfToken)
-        account <- accountManagementRepo.findByValidationToken(token)
-        _       <- account.traverse(user => accountManagementRepo.markAsValidated(user.uid))
-        resp    <- SeeOther(Location(configuration.external.createFullUri(uri"/")))
-      } yield resp
-  }
+            } yield resp
+    }
+
+    private val validateEmailAddress: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / "user" / "settings" / "email" / "validate" / ValidationTokenPathParameter(
+                token
+            ) =>
+            for {
+                csrf    <- Sync[F].delay(req.getCsrfToken)
+                account <- accountManagementRepo.findByValidationToken(token)
+                _       <- account.traverse(user => accountManagementRepo.markAsValidated(user.uid))
+                resp    <- SeeOther(Location(configuration.external.createFullUri(uri"/")))
+            } yield resp
+    }
 
-  val protectedRoutes =
-    addSshKey <+> deleteAccount <+> deleteSshKey <+> sendValidationEmail <+> setLanguage <+> showAccountSettings <+> showAccountSshSettings
+    val protectedRoutes =
+        addSshKey <+> deleteAccount <+> deleteSshKey <+> sendValidationEmail <+> setLanguage <+> showAccountSettings <+> showAccountSshSettings
 
-  val routes = validateEmailAddress
+    val routes = validateEmailAddress
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/Account.scala	2025-01-13 17:13:25.052470972 +0000
@@ -37,173 +37,173 @@
   */
 extension (p: Password) {
 
-  /** Encode the password using the argon2 algorithm.
-    *
-    * @return
-    *   The hash value for the password generated by the underlying Argon2 implementation.
-    */
-  def encode: PasswordHash = {
-    val encoder = PasswordEncoder.Argon2
-    PasswordHash(encoder.encode(new String(p.toArray, StandardCharsets.UTF_8)))
-  }
-
-  /** Verify if the password matches the given `PasswordHash`.
-    *
-    * @param hash
-    *   A password hash generated by the Argon2 algorithm.
-    * @return
-    *   `true` if the hash was generated from the given password and `false` otherwise.
-    */
-  def matches(hash: PasswordHash): Boolean = {
-    val encoder = PasswordEncoder.Argon2
-    encoder.matches(new String(p.toArray, StandardCharsets.UTF_8), hash.toString)
-  }
+    /** Encode the password using the argon2 algorithm.
+      *
+      * @return
+      *   The hash value for the password generated by the underlying Argon2 implementation.
+      */
+    def encode: PasswordHash = {
+        val encoder = PasswordEncoder.Argon2
+        PasswordHash(encoder.encode(new String(p.toArray, StandardCharsets.UTF_8)))
+    }
+
+    /** Verify if the password matches the given `PasswordHash`.
+      *
+      * @param hash
+      *   A password hash generated by the Argon2 algorithm.
+      * @return
+      *   `true` if the hash was generated from the given password and `false` otherwise.
+      */
+    def matches(hash: PasswordHash): Boolean = {
+        val encoder = PasswordEncoder.Argon2
+        encoder.matches(new String(p.toArray, StandardCharsets.UTF_8), hash.toString)
+    }
 }
 
 /** Initialises and holds our preferred password encoding algorithm.
   */
 object PasswordEncoder {
-  // Initialisation parameters for the argon2 algorithm (see whitepaper for details).
-  private val saltLength  = 16    // 128 bits
-  private val hashLength  = 32    // 256 bits
-  private val parallelism = 1
-  private val memoryUsage = 12288 // 12 MB
-  private val iterations  = 10
+    // Initialisation parameters for the argon2 algorithm (see whitepaper for details).
+    private val saltLength  = 16    // 128 bits
+    private val hashLength  = 32    // 256 bits
+    private val parallelism = 1
+    private val memoryUsage = 12288 // 12 MB
+    private val iterations  = 10
 
-  def Argon2 = new Argon2PasswordEncoder(saltLength, hashLength, parallelism, memoryUsage, iterations)
+    def Argon2 = new Argon2PasswordEncoder(saltLength, hashLength, parallelism, memoryUsage, iterations)
 
 }
 
 opaque type ResetToken = String
 object ResetToken {
-  val Format: Regex = "^[a-zA-z0-9]+".r
-  val Length: Int   = 128
+    val Format: Regex = "^[a-zA-z0-9]+".r
+    val Length: Int   = 128
+
+    given Eq[ResetToken] = Eq.fromUniversalEquals
+
+    /** Create an instance of ResetToken from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ResetToken.
+      * @return
+      *   The appropriate instance of ResetToken.
+      */
+    def apply(source: String): ResetToken = source
 
-  given Eq[ResetToken] = Eq.fromUniversalEquals
+    /** Try to create an instance of ResetToken from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ResetToken.
+      * @return
+      *   An option to the successfully converted ResetToken.
+      */
+    def from(source: String): Option[ResetToken] = Option(source).filter(s => s.length === Length && Format.matches(s))
 
-  /** Create an instance of ResetToken from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ResetToken.
-    * @return
-    *   The appropriate instance of ResetToken.
-    */
-  def apply(source: String): ResetToken = source
-
-  /** Try to create an instance of ResetToken from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ResetToken.
-    * @return
-    *   An option to the successfully converted ResetToken.
-    */
-  def from(source: String): Option[ResetToken] = Option(source).filter(s => s.length === Length && Format.matches(s))
-
-  /** Generate a new reset token.
-    *
-    * @return
-    *   A randomly generated reset token.
-    */
-  def generate: ResetToken = scala.util.Random.alphanumeric.take(Length).mkString
+    /** Generate a new reset token.
+      *
+      * @return
+      *   A randomly generated reset token.
+      */
+    def generate: ResetToken = scala.util.Random.alphanumeric.take(Length).mkString
 }
 
 /** Extractor to retrieve a ResetToken from a path parameter.
   */
 object ResetTokenPathParameter {
-  def unapply(str: String): Option[ResetToken] = Option(str).flatMap(ResetToken.from)
+    def unapply(str: String): Option[ResetToken] = Option(str).flatMap(ResetToken.from)
 }
 
 opaque type UnlockToken = String
 object UnlockToken {
-  val Format: Regex = "^[a-zA-z0-9]+".r
-  val Length: Int   = 128
+    val Format: Regex = "^[a-zA-z0-9]+".r
+    val Length: Int   = 128
+
+    given Eq[UnlockToken] = Eq.fromUniversalEquals
+
+    /** Create an instance of UnlockToken from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a UnlockToken.
+      * @return
+      *   The appropriate instance of UnlockToken.
+      */
+    def apply(source: String): UnlockToken = source
 
-  given Eq[UnlockToken] = Eq.fromUniversalEquals
+    /** Try to create an instance of UnlockToken from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a UnlockToken.
+      * @return
+      *   An option to the successfully converted UnlockToken.
+      */
+    def from(source: String): Option[UnlockToken] = Option(source).filter(s => s.length === Length && Format.matches(s))
 
-  /** Create an instance of UnlockToken from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a UnlockToken.
-    * @return
-    *   The appropriate instance of UnlockToken.
-    */
-  def apply(source: String): UnlockToken = source
-
-  /** Try to create an instance of UnlockToken from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a UnlockToken.
-    * @return
-    *   An option to the successfully converted UnlockToken.
-    */
-  def from(source: String): Option[UnlockToken] = Option(source).filter(s => s.length === Length && Format.matches(s))
-
-  /** Generate a new unlock token.
-    *
-    * @return
-    *   A randomly generated unlock token.
-    */
-  def generate: UnlockToken = scala.util.Random.alphanumeric.take(Length).mkString
+    /** Generate a new unlock token.
+      *
+      * @return
+      *   A randomly generated unlock token.
+      */
+    def generate: UnlockToken = scala.util.Random.alphanumeric.take(Length).mkString
 
 }
 
 /** Extractor to retrieve an UnlockToken from a path parameter.
   */
 object UnlockTokenPathParameter {
-  def unapply(str: String): Option[UnlockToken] = UnlockToken.from(str)
+    def unapply(str: String): Option[UnlockToken] = UnlockToken.from(str)
 }
 
 /** Extractor to retrieve an Username from a path parameter.
   */
 object UsernamePathParameter {
-  def unapply(str: String): Option[Username] =
-    Option(str).flatMap { string =>
-      if (string.startsWith("~"))
-        Username.from(string.drop(1))
-      else
-        None
-    }
+    def unapply(str: String): Option[Username] =
+        Option(str).flatMap { string =>
+            if (string.startsWith("~"))
+                Username.from(string.drop(1))
+            else
+                None
+        }
 }
 
 opaque type ValidationToken = String
 object ValidationToken {
-  val Format: Regex = "^[a-zA-z0-9]+".r
-  val Length: Int   = 64
+    val Format: Regex = "^[a-zA-z0-9]+".r
+    val Length: Int   = 64
+
+    given Eq[ValidationToken] = Eq.fromUniversalEquals
+
+    /** Create an instance of ValidationToken from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ValidationToken.
+      * @return
+      *   The appropriate instance of ValidationToken.
+      */
+    def apply(source: String): ValidationToken = source
 
-  given Eq[ValidationToken] = Eq.fromUniversalEquals
+    /** Try to create an instance of ValidationToken from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ValidationToken.
+      * @return
+      *   An option to the successfully converted ValidationToken.
+      */
+    def from(source: String): Option[ValidationToken] =
+        Option(source).filter(s => s.length === Length && Format.matches(s))
 
-  /** Create an instance of ValidationToken from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ValidationToken.
-    * @return
-    *   The appropriate instance of ValidationToken.
-    */
-  def apply(source: String): ValidationToken = source
-
-  /** Try to create an instance of ValidationToken from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ValidationToken.
-    * @return
-    *   An option to the successfully converted ValidationToken.
-    */
-  def from(source: String): Option[ValidationToken] =
-    Option(source).filter(s => s.length === Length && Format.matches(s))
-
-  /** Generate a new unlock token.
-    *
-    * @return
-    *   A randomly generated unlock token.
-    */
-  def generate: ValidationToken = scala.util.Random.alphanumeric.take(Length).mkString
+    /** Generate a new unlock token.
+      *
+      * @return
+      *   A randomly generated unlock token.
+      */
+    def generate: ValidationToken = scala.util.Random.alphanumeric.take(Length).mkString
 
 }
 
 /** Extractor to retrieve a validation token from a path parameter.
   */
 object ValidationTokenPathParameter {
-  def unapply(str: String): Option[ValidationToken] = ValidationToken.from(str)
+    def unapply(str: String): Option[ValidationToken] = ValidationToken.from(str)
 }
 
 /** A user account.
@@ -228,18 +228,18 @@
 )
 
 object Account {
-  given Eq[Account] =
-    Eq.instance { (a, b) =>
-      a.uid === b.uid && a.name === b.name && a.email === b.email
+    given Eq[Account] =
+        Eq.instance { (a, b) =>
+            a.uid === b.uid && a.name === b.name && a.email === b.email
+        }
+
+    extension (account: Account) {
+
+        /** Create vcs repository owner metadata from the account.
+          *
+          * @return
+          *   Descriptive information about the owner of a vcs repository based on the account.
+          */
+        def toVcsRepositoryOwner: VcsRepositoryOwner = VcsRepositoryOwner(account.uid, account.name, account.email)
     }
-
-  extension (account: Account) {
-
-    /** Create vcs repository owner metadata from the account.
-      *
-      * @return
-      *   Descriptive information about the owner of a vcs repository based on the account.
-      */
-    def toVcsRepositoryOwner: VcsRepositoryOwner = VcsRepositoryOwner(account.uid, account.name, account.email)
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AddPublicSshKeyForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AddPublicSshKeyForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AddPublicSshKeyForm.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AddPublicSshKeyForm.scala	2025-01-13 17:13:25.052470972 +0000
@@ -33,20 +33,22 @@
 final case class AddPublicSshKeyForm(name: Option[KeyComment], keyString: SshPublicKeyString)
 
 object AddPublicSshKeyForm extends FormValidator[AddPublicSshKeyForm] {
-  val fieldName: FormField = FormField("name")
-  val fieldKey: FormField  = FormField("key")
+    val fieldName: FormField = FormField("name")
+    val fieldKey: FormField  = FormField("key")
 
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, AddPublicSshKeyForm] = {
-    val name = data.get(fieldName).fold(None.validNec)(name => KeyComment.from(name).validNec)
-    val key = data
-      .get(fieldKey)
-      .fold(FormFieldError("No ssh public key given!").invalidNec)(string =>
-        SshPublicKeyString.from(string.trim).fold(FormFieldError("Invalid ssh public key!").invalidNec)(_.validNec)
-      )
-      .leftMap(errors => NonEmptyChain.of(Map(fieldKey -> errors.toList)))
-    (name, key).mapN { case (validName, validKey) =>
-      AddPublicSshKeyForm(validName, validKey)
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, AddPublicSshKeyForm] = {
+        val name = data.get(fieldName).fold(None.validNec)(name => KeyComment.from(name).validNec)
+        val key = data
+            .get(fieldKey)
+            .fold(FormFieldError("No ssh public key given!").invalidNec)(string =>
+                SshPublicKeyString
+                    .from(string.trim)
+                    .fold(FormFieldError("Invalid ssh public key!").invalidNec)(_.validNec)
+            )
+            .leftMap(errors => NonEmptyChain.of(Map(fieldKey -> errors.toList)))
+        (name, key).mapN { case (validName, validKey) =>
+            AddPublicSshKeyForm(validName, validKey)
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationMiddleware.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationMiddleware.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationMiddleware.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationMiddleware.scala	2025-01-13 17:13:25.052470972 +0000
@@ -32,96 +32,96 @@
   */
 trait AuthenticationMiddleware {
 
-  /** Helper function to find and validate the authentication token from a request and extract the session id from it.
-    *
-    * @param request
-    *   A http4s request.
-    * @param signAndValidate
-    *   A class providing functionality to validate the authentication token.
-    * @return
-    *   Either an error message or the extracted session id.
-    */
-  protected def extractSessionId[F[_]: Sync](
-      request: Request[F],
-      signAndValidate: SignAndValidate
-  ): Either[ErrorMessageKey, SessionId] =
-    for {
-      token  <- request.getAuthenticationToken.toRight(ErrorMessageKey("error.auth.token.not-found"))
-      authId <- signAndValidate.validate(token).toRight(ErrorMessageKey("error.auth.token.invalid"))
-      id     <- SessionId.from(authId).toRight(ErrorMessageKey("error.auth.token.invalid"))
-    } yield id
+    /** Helper function to find and validate the authentication token from a request and extract the session id from it.
+      *
+      * @param request
+      *   A http4s request.
+      * @param signAndValidate
+      *   A class providing functionality to validate the authentication token.
+      * @return
+      *   Either an error message or the extracted session id.
+      */
+    protected def extractSessionId[F[_]: Sync](
+        request: Request[F],
+        signAndValidate: SignAndValidate
+    ): Either[ErrorMessageKey, SessionId] =
+        for {
+            token  <- request.getAuthenticationToken.toRight(ErrorMessageKey("error.auth.token.not-found"))
+            authId <- signAndValidate.validate(token).toRight(ErrorMessageKey("error.auth.token.invalid"))
+            id     <- SessionId.from(authId).toRight(ErrorMessageKey("error.auth.token.invalid"))
+        } yield id
 
-  /** Extract an authentication token from a request, validate it and try to load the corresponding user account from
-    * the database repository if the token is valid. The functionality is wrapped into a [[cats.data.Kleisli]] to be
-    * composable within the http4s middleware.
-    *
-    * @param repo
-    *   A database repository providing the functionality to find a user account in the database.
-    * @param signAndValidate
-    *   A class providing functionality to validate the authentication token.
-    * @param timeouts
-    *   Timeouts related to the authentication and session management.
-    * @return
-    *   Either an error or the loaded user account.
-    */
-  def authenticateUser[F[_]: Sync](
-      repo: AuthenticationRepository[F],
-      signAndValidate: SignAndValidate,
-      timeouts: AuthenticationTimeouts
-  ): Kleisli[F, Request[F], Either[ErrorMessageKey, Account]] =
-    Kleisli { request =>
-      val sessionId = extractSessionId(request, signAndValidate)
-      sessionId.traverse(resolveUser(repo)(timeouts).run).map {
-        case Left(error)          => error.asLeft
-        case Right(None)          => ErrorMessageKey("error.auth.unauthorized.message").asLeft
-        case Right(Some(account)) => account.asRight
-      }
-    }
+    /** Extract an authentication token from a request, validate it and try to load the corresponding user account from
+      * the database repository if the token is valid. The functionality is wrapped into a [[cats.data.Kleisli]] to be
+      * composable within the http4s middleware.
+      *
+      * @param repo
+      *   A database repository providing the functionality to find a user account in the database.
+      * @param signAndValidate
+      *   A class providing functionality to validate the authentication token.
+      * @param timeouts
+      *   Timeouts related to the authentication and session management.
+      * @return
+      *   Either an error or the loaded user account.
+      */
+    def authenticateUser[F[_]: Sync](
+        repo: AuthenticationRepository[F],
+        signAndValidate: SignAndValidate,
+        timeouts: AuthenticationTimeouts
+    ): Kleisli[F, Request[F], Either[ErrorMessageKey, Account]] =
+        Kleisli { request =>
+            val sessionId = extractSessionId(request, signAndValidate)
+            sessionId.traverse(resolveUser(repo)(timeouts).run).map {
+                case Left(error)          => error.asLeft
+                case Right(None)          => ErrorMessageKey("error.auth.unauthorized.message").asLeft
+                case Right(Some(account)) => account.asRight
+            }
+        }
 
-  /** This is an Implementation of [[authenticateUser]] which in addition to [[cats.data.Kleisli]] also uses
-    * [[cats.data.OptionT]] to make it useable with the fall through functionality of the http4s authentication
-    * middleware. The function also extracts an authentication token from a requests, validates it and tries to load the
-    * corresponding user account from the database repository if the token is valid.
-    *
-    * @param repo
-    *   A database repository providing the functionality to find a user account in the database.
-    * @param signAndValidate
-    *   A class providing functionality to validate the authentication token.
-    * @param timeouts
-    *   Timeouts related to the authentication and session management.
-    * @return
-    *   An option to the account of a logged in user.
-    */
-  def authenticateUserWithFallThrough[F[_]: Sync](
-      repo: AuthenticationRepository[F],
-      signAndValidate: SignAndValidate,
-      timeouts: AuthenticationTimeouts
-  ): Kleisli[OptionT[F, *], Request[F], Account] =
-    Kleisli { request =>
-      val sessionId = extractSessionId(request, signAndValidate)
-      OptionT(sessionId.traverse(resolveUser(repo)(timeouts).run).map(_.getOrElse(None)))
-    }
+    /** This is an Implementation of [[authenticateUser]] which in addition to [[cats.data.Kleisli]] also uses
+      * [[cats.data.OptionT]] to make it useable with the fall through functionality of the http4s authentication
+      * middleware. The function also extracts an authentication token from a requests, validates it and tries to load
+      * the corresponding user account from the database repository if the token is valid.
+      *
+      * @param repo
+      *   A database repository providing the functionality to find a user account in the database.
+      * @param signAndValidate
+      *   A class providing functionality to validate the authentication token.
+      * @param timeouts
+      *   Timeouts related to the authentication and session management.
+      * @return
+      *   An option to the account of a logged in user.
+      */
+    def authenticateUserWithFallThrough[F[_]: Sync](
+        repo: AuthenticationRepository[F],
+        signAndValidate: SignAndValidate,
+        timeouts: AuthenticationTimeouts
+    ): Kleisli[OptionT[F, *], Request[F], Account] =
+        Kleisli { request =>
+            val sessionId = extractSessionId(request, signAndValidate)
+            OptionT(sessionId.traverse(resolveUser(repo)(timeouts).run).map(_.getOrElse(None)))
+        }
 
-  /** Try to resolve a user account using the provided database repository. It is wrapped into a [[cats.data.Kleisli]]
-    * to be composable within the http4s middleware. The function expects a [[SessionId]] to be passed to it which is
-    * used to obtain a session and the related user account.
-    *
-    * @param repo
-    *   A database repository providing the functionality to find a user account in the database.
-    * @param timeouts
-    *   Timeouts related to the authentication and session management.
-    * @return
-    *   An option to the user account if it exists.
-    */
-  def resolveUser[F[_]: Sync](
-      repo: AuthenticationRepository[F]
-  )(timeouts: AuthenticationTimeouts): Kleisli[F, SessionId, Option[Account]] =
-    Kleisli { sessionId =>
-      for {
-        session      <- repo.findUserSession(sessionId)
-        currentTime  <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
-        validSession <- Sync[F].delay(session.filterNot(_.hasReachedAbsoluteTimeout(timeouts)(currentTime)))
-        account      <- session.traverse(s => repo.findAccount(s.uid)).map(_.getOrElse(None))
-      } yield account
-    }
+    /** Try to resolve a user account using the provided database repository. It is wrapped into a [[cats.data.Kleisli]]
+      * to be composable within the http4s middleware. The function expects a [[SessionId]] to be passed to it which is
+      * used to obtain a session and the related user account.
+      *
+      * @param repo
+      *   A database repository providing the functionality to find a user account in the database.
+      * @param timeouts
+      *   Timeouts related to the authentication and session management.
+      * @return
+      *   An option to the user account if it exists.
+      */
+    def resolveUser[F[_]: Sync](
+        repo: AuthenticationRepository[F]
+    )(timeouts: AuthenticationTimeouts): Kleisli[F, SessionId, Option[Account]] =
+        Kleisli { sessionId =>
+            for {
+                session      <- repo.findUserSession(sessionId)
+                currentTime  <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
+                validSession <- Sync[F].delay(session.filterNot(_.hasReachedAbsoluteTimeout(timeouts)(currentTime)))
+                account      <- session.traverse(s => repo.findAccount(s.uid)).map(_.getOrElse(None))
+            } yield account
+        }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -37,137 +37,137 @@
   */
 abstract class AuthenticationRepository[F[_]] {
 
-  /** Return all accounts found in the database sorted ascending by username.
-    *
-    * @return
-    *   A stream containing all accounts sorted ascending by username which may be empty.
-    */
-  def allAccounts(): Stream[F, Account]
-
-  /** Create a user session in the database.
-    *
-    * @param session
-    *   The session information that shall be persisted.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createUserSession(session: Session): F[Int]
-
-  /** Delete all user sessions of the given user from the database.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteAllUserSessions(uid: UserId): F[Int]
-
-  /** Delete the user session with the given ID from the database.
-    *
-    * @param id
-    *   The globally unique ID of the session.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteUserSession(id: SessionId): F[Int]
-
-  /** Search for the unlocked account with the given unique id from the database.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   An option to the loaded account if it exists.
-    */
-  def findAccount(uid: UserId): F[Option[Account]]
-
-  /** Search for the unlocked account with the given email address in the database and return the first found result.
-    *
-    * @param email
-    *   The email address of the user account which must be unique according to our requirements.
-    * @return
-    *   An option to the found account if it exists.
-    */
-  def findAccountByEmail(email: EmailAddress): F[Option[Account]]
-
-  /** Search for the unlocked account with the given name in the database and return the first found result.
-    *
-    * @param name
-    *   The username which must be unique according to our requirements.
-    * @return
-    *   An option to the found account if it exists.
-    */
-  def findAccountByName(name: Username): F[Option[Account]]
-
-  /** Search for a locked account with the given unlock token and return the first found result.
-    *
-    * An account is considered *locked* **NOT** by the presence of an unlock token **BUT** by the presence of the
-    * `locked_at` date!
-    *
-    * @param name
-    *   The username which must be unique according to our requirements.
-    * @param token
-    *   An unlock token which must be present if given.
-    * @return
-    *   An option to the account if it exists and is locked using the given token.
-    */
-  def findLockedAccount(name: Username)(token: Option[UnlockToken]): F[Option[Account]]
-
-  /** Retrieve the password hash and the number of failed login attempts from the database.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   An option to a tuple containing the password hash and the number of failed login attempts.
-    */
-  def findPasswordHashAndAttempts(uid: UserId): F[Option[(PasswordHash, Int)]]
-
-  /** Find the session with the given ID in the database.
-    *
-    * @param id
-    *   A session id which is supposed to be globally unique.
-    * @return
-    *   An option to the desired session if it exists.
-    */
-  def findUserSession(id: SessionId): F[Option[Session]]
-
-  /** Increment the number of failed login attempts for the user with the given name.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def incrementFailedAttempts(uid: UserId): F[Int]
-
-  /** Lock the account with the given id and set the value of the `unlock_token` column to the given token which might
-    * be `None` (`null` in the database).
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @param token
-    *   An unlock token which must be present.
-    * @return
-    *   The number of affected database rows.
-    */
-  def lockAccount(uid: UserId)(token: Option[UnlockToken]): F[Int]
-
-  /** Reset the number of failed login attempts (set it to zero) for the user account with the given id.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def resetFailedAttempts(uid: UserId): F[Int]
-
-  /** Unlock the user account with the given id. The process of unlocking clears both columns related to locking:
-    * `locked_at` and `unlock_token`!
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def unlockAccount(uid: UserId): F[Int]
+    /** Return all accounts found in the database sorted ascending by username.
+      *
+      * @return
+      *   A stream containing all accounts sorted ascending by username which may be empty.
+      */
+    def allAccounts(): Stream[F, Account]
+
+    /** Create a user session in the database.
+      *
+      * @param session
+      *   The session information that shall be persisted.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createUserSession(session: Session): F[Int]
+
+    /** Delete all user sessions of the given user from the database.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteAllUserSessions(uid: UserId): F[Int]
+
+    /** Delete the user session with the given ID from the database.
+      *
+      * @param id
+      *   The globally unique ID of the session.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteUserSession(id: SessionId): F[Int]
+
+    /** Search for the unlocked account with the given unique id from the database.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   An option to the loaded account if it exists.
+      */
+    def findAccount(uid: UserId): F[Option[Account]]
+
+    /** Search for the unlocked account with the given email address in the database and return the first found result.
+      *
+      * @param email
+      *   The email address of the user account which must be unique according to our requirements.
+      * @return
+      *   An option to the found account if it exists.
+      */
+    def findAccountByEmail(email: EmailAddress): F[Option[Account]]
+
+    /** Search for the unlocked account with the given name in the database and return the first found result.
+      *
+      * @param name
+      *   The username which must be unique according to our requirements.
+      * @return
+      *   An option to the found account if it exists.
+      */
+    def findAccountByName(name: Username): F[Option[Account]]
+
+    /** Search for a locked account with the given unlock token and return the first found result.
+      *
+      * An account is considered *locked* **NOT** by the presence of an unlock token **BUT** by the presence of the
+      * `locked_at` date!
+      *
+      * @param name
+      *   The username which must be unique according to our requirements.
+      * @param token
+      *   An unlock token which must be present if given.
+      * @return
+      *   An option to the account if it exists and is locked using the given token.
+      */
+    def findLockedAccount(name: Username)(token: Option[UnlockToken]): F[Option[Account]]
+
+    /** Retrieve the password hash and the number of failed login attempts from the database.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   An option to a tuple containing the password hash and the number of failed login attempts.
+      */
+    def findPasswordHashAndAttempts(uid: UserId): F[Option[(PasswordHash, Int)]]
+
+    /** Find the session with the given ID in the database.
+      *
+      * @param id
+      *   A session id which is supposed to be globally unique.
+      * @return
+      *   An option to the desired session if it exists.
+      */
+    def findUserSession(id: SessionId): F[Option[Session]]
+
+    /** Increment the number of failed login attempts for the user with the given name.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def incrementFailedAttempts(uid: UserId): F[Int]
+
+    /** Lock the account with the given id and set the value of the `unlock_token` column to the given token which might
+      * be `None` (`null` in the database).
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @param token
+      *   An unlock token which must be present.
+      * @return
+      *   The number of affected database rows.
+      */
+    def lockAccount(uid: UserId)(token: Option[UnlockToken]): F[Int]
+
+    /** Reset the number of failed login attempts (set it to zero) for the user account with the given id.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def resetFailedAttempts(uid: UserId): F[Int]
+
+    /** Unlock the user account with the given id. The process of unlocking clears both columns related to locking:
+      * `locked_at` and `unlock_token`!
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def unlockAccount(uid: UserId): F[Int]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/AuthenticationRoutes.scala	2025-01-13 17:13:25.052470972 +0000
@@ -44,17 +44,17 @@
   */
 enum AuthenticationFailure {
 
-  /** The account was not found in the database.
-    */
-  case AccountNotFound extends AuthenticationFailure
-
-  /** The account is locked.
-    */
-  case AccountLocked extends AuthenticationFailure
-
-  /** The provided password was wrong.
-    */
-  case WrongPassword extends AuthenticationFailure
+    /** The account was not found in the database.
+      */
+    case AccountNotFound extends AuthenticationFailure
+
+    /** The account is locked.
+      */
+    case AccountLocked extends AuthenticationFailure
+
+    /** The provided password was wrong.
+      */
+    case WrongPassword extends AuthenticationFailure
 }
 
 /** The routes for handling the authentication related tasks like login, logout, password reset, unlocking etc.
@@ -80,159 +80,181 @@
     repo: AuthenticationRepository[F],
     signAndValidate: SignAndValidate
 ) extends Http4sDsl[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  private val loginPath = uri"/login"
-  private val resetPath = uri"/forgot-password"
+    private val loginPath = uri"/login"
+    private val resetPath = uri"/forgot-password"
 
-  private val parseLoginForm: HttpRoutes[F] = HttpRoutes.of[F] { case request @ POST -> Root / "login" =>
-    request.decodeStrict[F, UrlForm] { urlForm =>
-      for {
-        csrf <- Sync[F].delay(request.getCsrfToken)
-        formData <- Sync[F].delay {
-          urlForm.values.map { t =>
-            val (key, values) = t
-            (
-              key,
-              values.headOption.getOrElse("")
-            ) // Pick the first value (a field might get submitted multiple times)!
-          }
-        }
-        form <- Sync[F].delay(LoginForm.validate(formData))
-        response <- form match {
-          case Validated.Invalid(es) =>
-            BadRequest(
-              views.html.login()(loginPath, csrf, resetPath, title = "Smederee - Login to your account".some)(
-                formData,
-                FormErrors.fromNec(es)
-              )
-            )
-          case Validated.Valid(loginForm) =>
+    private val parseLoginForm: HttpRoutes[F] = HttpRoutes.of[F] { case request @ POST -> Root / "login" =>
+        request.decodeStrict[F, UrlForm] { urlForm =>
             for {
-              user <- repo.findAccountByName(loginForm.name)
-              auth <- user.map(_.uid).traverse(repo.findPasswordHashAndAttempts).map(_.getOrElse(None))
-              check = auth.map { tuple =>
-                val (hash, failedAttempts) = tuple
-                (
-                  loginForm.password.matches(hash),
-                  failedAttempts <= authenticationConfig.lockAfter.toInt,
-                  failedAttempts
-                )
-              }
-              source <- Sync[F].delay(request.from.map(_.toString).getOrElse("UNKNOWN_ADDRESS"))
-              login <- check match {
-                case None =>
-                  // No account was found!
-                  for {
-                    delay <- Sync[F]
-                      .delay(scala.util.Random.nextInt(2) + 1) // Prevent fast response to avoid account guessing.
-                    _ <- Sync[F].sleep(FiniteDuration(delay, SECONDS))
-                  } yield Left(AuthenticationFailure.AccountNotFound)
-                case Some((true, true, _)) =>
-                  // The password is valid and the attempts are below or equal to the limit.
-                  Sync[F].delay(user.toRight(AuthenticationFailure.AccountNotFound))
-                case Some((false, true, attempts)) =>
-                  // The password is wrong and the attempts are below or equal to the limit.
-                  for {
-                    _ <- user.map(_.uid).traverse(repo.incrementFailedAttempts)
-                    _ <- user.traverse(user => Sync[F].delay(log.warn(s"Login failure for ${user.name} from $source.")))
-                    // We increase the delay for processing failed login attempts to discourage brute forcing.
-                    delay <- Sync[F].delay((attempts - 1) * 5) // TODO: Extract the base delay into the configuration.
-                    _     <- Sync[F].sleep(FiniteDuration(delay, SECONDS))
-                  } yield Left(AuthenticationFailure.WrongPassword)
-                case Some((_, false, _)) =>
-                  // The login attempts are above the limit.
-                  for {
-                    _ <- user.map(_.uid).traverse(uid => repo.lockAccount(uid)(UnlockToken.generate.some))
-                    _ <- user.traverse(user =>
-                      Sync[F].delay(
-                        log.warn(
-                          s"Locked account ${user.name} (too many authentication failures), latest request from $source!"
+                csrf <- Sync[F].delay(request.getCsrfToken)
+                formData <- Sync[F].delay {
+                    urlForm.values.map { t =>
+                        val (key, values) = t
+                        (
+                            key,
+                            values.headOption.getOrElse("")
+                        ) // Pick the first value (a field might get submitted multiple times)!
+                    }
+                }
+                form <- Sync[F].delay(LoginForm.validate(formData))
+                response <- form match {
+                    case Validated.Invalid(es) =>
+                        BadRequest(
+                            views.html
+                                .login()(loginPath, csrf, resetPath, title = "Smederee - Login to your account".some)(
+                                    formData,
+                                    FormErrors.fromNec(es)
+                                )
                         )
-                      )
-                    )
-                  } yield Left(AuthenticationFailure.AccountLocked)
-              }
-              response <- login match {
-                case Left(_) =>
-                  BadRequest(
-                    views.html.login()(loginPath, csrf, resetPath, title = "Smederee - Login to your account".some)(
-                      formData,
-                      Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
-                    )
-                  )
-                case Right(account) =>
-                  for {
-                    session <- Sync[F].delay(
-                      Session.create(
-                        account.uid,
-                        OffsetDateTime.now(ZoneOffset.UTC),
-                        OffsetDateTime.now(ZoneOffset.UTC)
-                      )
-                    )
-                    _ <- repo.createUserSession(session)
-                    _ <- repo.resetFailedAttempts(account.uid)
-                    token <- Sync[F].delay(
-                      signAndValidate.signToken(session.id.toString)(clock.millis.toString)
-                    )
-                    response <- SeeOther(Location(Uri(path = Uri.Path.Root)))
-                      .map(_.addCookie(token.toAuthenticationCookie(external.host.toString.some)(None)(secure = true)))
-                  } yield response
-              }
+                    case Validated.Valid(loginForm) =>
+                        for {
+                            user <- repo.findAccountByName(loginForm.name)
+                            auth <- user.map(_.uid).traverse(repo.findPasswordHashAndAttempts).map(_.getOrElse(None))
+                            check = auth.map { tuple =>
+                                val (hash, failedAttempts) = tuple
+                                (
+                                    loginForm.password.matches(hash),
+                                    failedAttempts <= authenticationConfig.lockAfter.toInt,
+                                    failedAttempts
+                                )
+                            }
+                            source <- Sync[F].delay(request.from.map(_.toString).getOrElse("UNKNOWN_ADDRESS"))
+                            login <- check match {
+                                case None =>
+                                    // No account was found!
+                                    for {
+                                        delay <- Sync[F]
+                                            .delay(
+                                                scala.util.Random.nextInt(2) + 1
+                                            ) // Prevent fast response to avoid account guessing.
+                                        _ <- Sync[F].sleep(FiniteDuration(delay, SECONDS))
+                                    } yield Left(AuthenticationFailure.AccountNotFound)
+                                case Some((true, true, _)) =>
+                                    // The password is valid and the attempts are below or equal to the limit.
+                                    Sync[F].delay(user.toRight(AuthenticationFailure.AccountNotFound))
+                                case Some((false, true, attempts)) =>
+                                    // The password is wrong and the attempts are below or equal to the limit.
+                                    for {
+                                        _ <- user.map(_.uid).traverse(repo.incrementFailedAttempts)
+                                        _ <- user.traverse(user =>
+                                            Sync[F].delay(log.warn(s"Login failure for ${user.name} from $source."))
+                                        )
+                                        // We increase the delay for processing failed login attempts to discourage brute forcing.
+                                        delay <- Sync[F].delay(
+                                            (attempts - 1) * 5
+                                        ) // TODO: Extract the base delay into the configuration.
+                                        _ <- Sync[F].sleep(FiniteDuration(delay, SECONDS))
+                                    } yield Left(AuthenticationFailure.WrongPassword)
+                                case Some((_, false, _)) =>
+                                    // The login attempts are above the limit.
+                                    for {
+                                        _ <- user
+                                            .map(_.uid)
+                                            .traverse(uid => repo.lockAccount(uid)(UnlockToken.generate.some))
+                                        _ <- user.traverse(user =>
+                                            Sync[F].delay(
+                                                log.warn(
+                                                    s"Locked account ${user.name} (too many authentication failures), latest request from $source!"
+                                                )
+                                            )
+                                        )
+                                    } yield Left(AuthenticationFailure.AccountLocked)
+                            }
+                            response <- login match {
+                                case Left(_) =>
+                                    BadRequest(
+                                        views.html.login()(
+                                            loginPath,
+                                            csrf,
+                                            resetPath,
+                                            title = "Smederee - Login to your account".some
+                                        )(
+                                            formData,
+                                            Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
+                                        )
+                                    )
+                                case Right(account) =>
+                                    for {
+                                        session <- Sync[F].delay(
+                                            Session.create(
+                                                account.uid,
+                                                OffsetDateTime.now(ZoneOffset.UTC),
+                                                OffsetDateTime.now(ZoneOffset.UTC)
+                                            )
+                                        )
+                                        _ <- repo.createUserSession(session)
+                                        _ <- repo.resetFailedAttempts(account.uid)
+                                        token <- Sync[F].delay(
+                                            signAndValidate.signToken(session.id.toString)(clock.millis.toString)
+                                        )
+                                        response <- SeeOther(Location(Uri(path = Uri.Path.Root)))
+                                            .map(
+                                                _.addCookie(
+                                                    token.toAuthenticationCookie(external.host.toString.some)(None)(
+                                                        secure = true
+                                                    )
+                                                )
+                                            )
+                                    } yield response
+                            }
+                        } yield response
+                }
             } yield response
         }
-      } yield response
     }
-  }
 
-  private val parseLoginFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case _ @POST -> Root / "login" as _ =>
-      SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
-  }
-
-  private val logout: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ POST -> Root / "logout" as _ =>
-    for {
-      authToken <- Sync[F].delay(ar.req.getAuthenticationToken)
-      sessionId <- Sync[F].delay(authToken.flatMap(signAndValidate.validate).flatMap(SessionId.from))
-      _         <- sessionId.traverse(repo.deleteUserSession)
-      response  <- SeeOther(Location(loginPath)).map(_.removeCookie(Constants.authenticationCookieName.toString))
-    } yield response
-  }
-
-  private val showLoginForm: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "login" =>
-    for {
-      csrf     <- Sync[F].delay(req.getCsrfToken)
-      response <- Ok(views.html.login()(loginPath, csrf, resetPath, title = "Smederee - Login to your account".some)())
-    } yield response
-  }
-
-  private val showLoginFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case _ @GET -> Root / "login" as _ =>
-      SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
-  }
-
-  private val unlockAccount: HttpRoutes[F] = HttpRoutes.of {
-    case request @ GET -> Root / "unlock" / UsernamePathParameter(name) / UnlockTokenPathParameter(token) =>
-      for {
-        csrf     <- Sync[F].delay(request.getCsrfToken)
-        user     <- repo.findLockedAccount(name)(token.some)
-        _        <- user.map(_.uid).traverse(repo.unlockAccount)
-        _        <- user.map(_.uid).traverse(repo.resetFailedAttempts)
-        _        <- user.traverse(user => Sync[F].delay(log.info(s"Unlocked account for ${user.name}.")))
-        response <- SeeOther(Location(loginPath))
-      } yield response
-  }
-
-  val protectedRoutes =
-    if (authenticationConfig.enabled)
-      logout <+> showLoginFormForLoggedInUsers <+> parseLoginFormForLoggedInUsers
-    else
-      AuthedRoutes.empty[Account, F]
-
-  val routes =
-    if (authenticationConfig.enabled)
-      showLoginForm <+> parseLoginForm <+> unlockAccount
-    else
-      HttpRoutes.empty[F]
+    private val parseLoginFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case _ @POST -> Root / "login" as _ =>
+            SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
+    }
+
+    private val logout: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ POST -> Root / "logout" as _ =>
+        for {
+            authToken <- Sync[F].delay(ar.req.getAuthenticationToken)
+            sessionId <- Sync[F].delay(authToken.flatMap(signAndValidate.validate).flatMap(SessionId.from))
+            _         <- sessionId.traverse(repo.deleteUserSession)
+            response  <- SeeOther(Location(loginPath)).map(_.removeCookie(Constants.authenticationCookieName.toString))
+        } yield response
+    }
+
+    private val showLoginForm: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "login" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            response <- Ok(
+                views.html.login()(loginPath, csrf, resetPath, title = "Smederee - Login to your account".some)()
+            )
+        } yield response
+    }
+
+    private val showLoginFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case _ @GET -> Root / "login" as _ =>
+            SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
+    }
+
+    private val unlockAccount: HttpRoutes[F] = HttpRoutes.of {
+        case request @ GET -> Root / "unlock" / UsernamePathParameter(name) / UnlockTokenPathParameter(token) =>
+            for {
+                csrf     <- Sync[F].delay(request.getCsrfToken)
+                user     <- repo.findLockedAccount(name)(token.some)
+                _        <- user.map(_.uid).traverse(repo.unlockAccount)
+                _        <- user.map(_.uid).traverse(repo.resetFailedAttempts)
+                _        <- user.traverse(user => Sync[F].delay(log.info(s"Unlocked account for ${user.name}.")))
+                response <- SeeOther(Location(loginPath))
+            } yield response
+    }
+
+    val protectedRoutes =
+        if (authenticationConfig.enabled)
+            logout <+> showLoginFormForLoggedInUsers <+> parseLoginFormForLoggedInUsers
+        else
+            AuthedRoutes.empty[Account, F]
+
+    val routes =
+        if (authenticationConfig.enabled)
+            showLoginForm <+> parseLoginForm <+> unlockAccount
+        else
+            HttpRoutes.empty[F]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/ChangePasswordForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/ChangePasswordForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/ChangePasswordForm.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/ChangePasswordForm.scala	2025-01-13 17:13:25.052470972 +0000
@@ -42,43 +42,44 @@
 )
 
 object ChangePasswordForm extends FormValidator[ChangePasswordForm] {
-  val fieldName: FormField                 = FormField("name")
-  val fieldPassword: FormField             = FormField("password")
-  val fieldPasswordConfirmation: FormField = FormField("password_confirmation")
-  val fieldResetToken: FormField           = FormField("token")
+    val fieldName: FormField                 = FormField("name")
+    val fieldPassword: FormField             = FormField("password")
+    val fieldPasswordConfirmation: FormField = FormField("password_confirmation")
+    val fieldResetToken: FormField           = FormField("token")
 
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, ChangePasswordForm] = {
-    val name: ValidatedNec[FormErrors, Username] = data
-      .get(fieldName)
-      .fold(FormFieldError("No username given!").invalidNec)(s =>
-        Username.from(s).fold(FormFieldError("Invalid username!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
-    val password: ValidatedNec[FormErrors, Password] = data
-      .get(fieldPassword)
-      .fold("No password given!".invalidNec)(Password.validate)
-      .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList.map(FormFieldError.apply))))
-    val passwordConfirmation: ValidatedNec[FormErrors, Password] = data
-      .get(fieldPasswordConfirmation)
-      .fold("No password given!".invalidNec)(Password.validate)
-      .leftMap(es => NonEmptyChain.of(Map(fieldPasswordConfirmation -> es.toList.map(FormFieldError.apply))))
-    val passwordsMatching: ValidatedNec[FormErrors, Password] = (password.toOption, passwordConfirmation.toOption)
-      .mapN { case (pw, pwc) =>
-        (pw, pwc)
-      }
-      .filter(tuple => tuple._1.matches(tuple._2.encode)) match {
-      case None          => Map(fieldPasswordConfirmation -> List(FormFieldError("Passwords do not match!"))).invalidNec
-      case Some((pw, _)) => pw.validNec
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, ChangePasswordForm] = {
+        val name: ValidatedNec[FormErrors, Username] = data
+            .get(fieldName)
+            .fold(FormFieldError("No username given!").invalidNec)(s =>
+                Username.from(s).fold(FormFieldError("Invalid username!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+        val password: ValidatedNec[FormErrors, Password] = data
+            .get(fieldPassword)
+            .fold("No password given!".invalidNec)(Password.validate)
+            .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList.map(FormFieldError.apply))))
+        val passwordConfirmation: ValidatedNec[FormErrors, Password] = data
+            .get(fieldPasswordConfirmation)
+            .fold("No password given!".invalidNec)(Password.validate)
+            .leftMap(es => NonEmptyChain.of(Map(fieldPasswordConfirmation -> es.toList.map(FormFieldError.apply))))
+        val passwordsMatching: ValidatedNec[FormErrors, Password] = (password.toOption, passwordConfirmation.toOption)
+            .mapN { case (pw, pwc) =>
+                (pw, pwc)
+            }
+            .filter(tuple => tuple._1.matches(tuple._2.encode)) match {
+                case None =>
+                    Map(fieldPasswordConfirmation -> List(FormFieldError("Passwords do not match!"))).invalidNec
+                case Some((pw, _)) => pw.validNec
+            }
+        val token: ValidatedNec[FormErrors, ResetToken] = data
+            .get(fieldResetToken)
+            .fold(FormFieldError("No reset token!").invalidNec)(s =>
+                ResetToken.from(s).fold(FormFieldError("Invalid reset token!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldResetToken -> es.toList)))
+        (name, password, passwordConfirmation, passwordsMatching, token).mapN {
+            case (validName, validPassword, validPasswordConfirmation, _, validResetToken) =>
+                ChangePasswordForm(validName, validPassword, validPasswordConfirmation, validResetToken)
+        }
     }
-    val token: ValidatedNec[FormErrors, ResetToken] = data
-      .get(fieldResetToken)
-      .fold(FormFieldError("No reset token!").invalidNec)(s =>
-        ResetToken.from(s).fold(FormFieldError("Invalid reset token!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldResetToken -> es.toList)))
-    (name, password, passwordConfirmation, passwordsMatching, token).mapN {
-      case (validName, validPassword, validPasswordConfirmation, _, validResetToken) =>
-        ChangePasswordForm(validName, validPassword, validPasswordConfirmation, validResetToken)
-    }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/ConfigurationPath.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/ConfigurationPath.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/ConfigurationPath.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/ConfigurationPath.scala	2025-01-13 17:13:25.056470978 +0000
@@ -23,23 +23,23 @@
 opaque type ConfigurationPath = String
 object ConfigurationPath {
 
-  given Conversion[ConfigurationPath, String] = _.toString
+    given Conversion[ConfigurationPath, String] = _.toString
 
-  /** Create an instance of ConfigurationPath from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ConfigurationPath.
-    * @return
-    *   The appropriate instance of ConfigurationPath.
-    */
-  def apply(source: String): ConfigurationPath = source
+    /** Create an instance of ConfigurationPath from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ConfigurationPath.
+      * @return
+      *   The appropriate instance of ConfigurationPath.
+      */
+    def apply(source: String): ConfigurationPath = source
 
-  /** Try to create an instance of ConfigurationPath from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ConfigurationPath.
-    * @return
-    *   An option to the successfully converted ConfigurationPath.
-    */
-  def from(source: String): Option[ConfigurationPath] = Option(source).map(_.trim).filter(_.nonEmpty)
+    /** Try to create an instance of ConfigurationPath from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ConfigurationPath.
+      * @return
+      *   An option to the successfully converted ConfigurationPath.
+      */
+    def from(source: String): Option[ConfigurationPath] = Option(source).map(_.trim).filter(_.nonEmpty)
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala	2025-01-13 17:13:25.056470978 +0000
@@ -36,102 +36,102 @@
 opaque type CookieName = String
 object CookieName {
 
-  /** Create an instance of CookieName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a CookieName.
-    * @return
-    *   The appropriate instance of CookieName.
-    */
-  def apply(source: String): CookieName = source
-
-  /** Try to create an instance of CookieName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a CookieName.
-    * @return
-    *   An option to the successfully converted CookieName.
-    */
-  def from(source: String): Option[CookieName] =
-    Option(source).map(_.trim.nonEmpty) match {
-      case Some(true) => Option(source.trim)
-      case _          => None
-    }
+    /** Create an instance of CookieName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a CookieName.
+      * @return
+      *   The appropriate instance of CookieName.
+      */
+    def apply(source: String): CookieName = source
+
+    /** Try to create an instance of CookieName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a CookieName.
+      * @return
+      *   An option to the successfully converted CookieName.
+      */
+    def from(source: String): Option[CookieName] =
+        Option(source).map(_.trim.nonEmpty) match {
+            case Some(true) => Option(source.trim)
+            case _          => None
+        }
 }
 
 opaque type DirectoryPath = Path
 object DirectoryPath {
 
-  /** Create an instance of DirectoryPath from the given Path type.
-    *
-    * @param source
-    *   An instance of type Path which will be returned as a DirectoryPath.
-    * @return
-    *   The appropriate instance of DirectoryPath.
-    */
-  def apply(source: Path): DirectoryPath = source
-
-  /** Try to create an instance of DirectoryPath from the given Path.
-    *
-    * @param source
-    *   A Path that should fulfil the requirements to be converted into a DirectoryPath.
-    * @return
-    *   An option to the successfully converted DirectoryPath.
-    */
-  def from(source: Path): Option[DirectoryPath] = Option(source)
-
-  /** Try to create an instance of DirectoryPath from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a DirectoryPath.
-    * @return
-    *   An option to the successfully converted DirectoryPath.
-    */
-  def fromString(source: String): Option[DirectoryPath] = Try(Paths.get(source)).toOption
-
-  extension (dir: DirectoryPath) {
-    def toPath: Path = dir
-  }
+    /** Create an instance of DirectoryPath from the given Path type.
+      *
+      * @param source
+      *   An instance of type Path which will be returned as a DirectoryPath.
+      * @return
+      *   The appropriate instance of DirectoryPath.
+      */
+    def apply(source: Path): DirectoryPath = source
+
+    /** Try to create an instance of DirectoryPath from the given Path.
+      *
+      * @param source
+      *   A Path that should fulfil the requirements to be converted into a DirectoryPath.
+      * @return
+      *   An option to the successfully converted DirectoryPath.
+      */
+    def from(source: Path): Option[DirectoryPath] = Option(source)
+
+    /** Try to create an instance of DirectoryPath from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a DirectoryPath.
+      * @return
+      *   An option to the successfully converted DirectoryPath.
+      */
+    def fromString(source: String): Option[DirectoryPath] = Try(Paths.get(source)).toOption
+
+    extension (dir: DirectoryPath) {
+        def toPath: Path = dir
+    }
 
 }
 
 opaque type FailedAttempts = Int
 object FailedAttempts {
 
-  /** Create an instance of FailedAttempts from the given Int type.
-    *
-    * @param source
-    *   An instance of type Int which will be returned as a FailedAttempts.
-    * @return
-    *   The appropriate instance of FailedAttempts.
-    */
-  def apply(source: Int): FailedAttempts = source
-
-  /** Try to create an instance of FailedAttempts from the given Int.
-    *
-    * @param source
-    *   A Int that should fulfil the requirements to be converted into a FailedAttempts.
-    * @return
-    *   An option to the successfully converted FailedAttempts.
-    */
-  def from(source: Int): Option[FailedAttempts] =
-    if (source >= 0)
-      Option(source)
-    else
-      None
-
-  extension (attempts: FailedAttempts) {
-    def toInt: Int = attempts
-  }
+    /** Create an instance of FailedAttempts from the given Int type.
+      *
+      * @param source
+      *   An instance of type Int which will be returned as a FailedAttempts.
+      * @return
+      *   The appropriate instance of FailedAttempts.
+      */
+    def apply(source: Int): FailedAttempts = source
+
+    /** Try to create an instance of FailedAttempts from the given Int.
+      *
+      * @param source
+      *   A Int that should fulfil the requirements to be converted into a FailedAttempts.
+      * @return
+      *   An option to the successfully converted FailedAttempts.
+      */
+    def from(source: Int): Option[FailedAttempts] =
+        if (source >= 0)
+            Option(source)
+        else
+            None
+
+    extension (attempts: FailedAttempts) {
+        def toInt: Int = attempts
+    }
 
 }
 
 /** Global constants which are used throughout the code.
   */
 object Constants {
-  val assetsPath: Uri                      = Uri(path = Uri.Path(Vector(Uri.Path.Segment("assets"))))
-  val authenticationCookieName: CookieName = CookieName("sloetel")
-  val csrfCookieName: CookieName           = CookieName("csrf-token")
+    val assetsPath: Uri                      = Uri(path = Uri.Path(Vector(Uri.Path.Segment("assets"))))
+    val authenticationCookieName: CookieName = CookieName("sloetel")
+    val csrfCookieName: CookieName           = CookieName("csrf-token")
 }
 
 /** Configuration for the authentication service.
@@ -144,11 +144,11 @@
 final case class SmedereeHubConfig(database: DatabaseConfig, service: ServiceConfig)
 
 object SmedereeHubConfig {
-  val location: ConfigurationPath = ConfigurationPath("hub")
+    val location: ConfigurationPath = ConfigurationPath("hub")
 
-  given Eq[SmedereeHubConfig] = Eq.fromUniversalEquals
+    given Eq[SmedereeHubConfig] = Eq.fromUniversalEquals
 
-  given ConfigReader[SmedereeHubConfig] = ConfigReader.forProduct2("database", "service")(SmedereeHubConfig.apply)
+    given ConfigReader[SmedereeHubConfig] = ConfigReader.forProduct2("database", "service")(SmedereeHubConfig.apply)
 }
 
 /** Configuration specifying the database access.
@@ -165,8 +165,8 @@
 final case class DatabaseConfig(driver: String, url: String, user: String, pass: String)
 
 object DatabaseConfig {
-  given Eq[DatabaseConfig]           = Eq.fromUniversalEquals
-  given ConfigReader[DatabaseConfig] = ConfigReader.forProduct4("driver", "url", "user", "pass")(DatabaseConfig.apply)
+    given Eq[DatabaseConfig]           = Eq.fromUniversalEquals
+    given ConfigReader[DatabaseConfig] = ConfigReader.forProduct4("driver", "url", "user", "pass")(DatabaseConfig.apply)
 }
 
 /** The general service configuration which describes how the service will be run.
@@ -218,44 +218,44 @@
 )
 
 object ServiceConfig {
-  given Eq[ServiceConfig] = Eq.fromUniversalEquals
+    given Eq[ServiceConfig] = Eq.fromUniversalEquals
 
-  given ConfigReader[Host]       = ConfigReader.fromStringOpt[Host](Host.fromString)
-  given ConfigReader[Port]       = ConfigReader.fromStringOpt[Port](Port.fromString)
-  given ConfigReader[Uri]        = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
-  given ConfigReader[Uri.Scheme] = ConfigReader.fromStringOpt(s => Uri.Scheme.fromString(s).toOption)
-
-  given ConfigReader[DirectoryPath] = ConfigReader.fromStringOpt(DirectoryPath.fromString)
-
-  given ConfigReader[EmailServerUsername] = ConfigReader.fromStringOpt[EmailServerUsername](EmailServerUsername.from)
-  given ConfigReader[EmailServerPassword] = ConfigReader.fromStringOpt[EmailServerPassword](EmailServerPassword.from)
-  given ConfigReader[SmtpTransport] =
-    ConfigReader.fromStringOpt[SmtpTransport](string => Try(SmtpTransport.valueOf(string)).toOption)
-
-  given ConfigReader[EmailMiddlewareConfiguration] =
-    ConfigReader.forProduct5("host", "port", "transport", "username", "password")(
-      EmailMiddlewareConfiguration.apply
-    )
-
-  given ConfigReader[ExternalUrlConfiguration] =
-    ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply)
-
-  given ConfigReader[ServiceConfig] =
-    ConfigReader.forProduct13(
-      "host",
-      "port",
-      "csrf-key-file",
-      "download-directory",
-      "render-maximum-file-size",
-      "authentication",
-      "billing",
-      "darcs",
-      "email",
-      "external",
-      "signup",
-      "ssh",
-      "ticket-integration"
-    )(ServiceConfig.apply)
+    given ConfigReader[Host]       = ConfigReader.fromStringOpt[Host](Host.fromString)
+    given ConfigReader[Port]       = ConfigReader.fromStringOpt[Port](Port.fromString)
+    given ConfigReader[Uri]        = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+    given ConfigReader[Uri.Scheme] = ConfigReader.fromStringOpt(s => Uri.Scheme.fromString(s).toOption)
+
+    given ConfigReader[DirectoryPath] = ConfigReader.fromStringOpt(DirectoryPath.fromString)
+
+    given ConfigReader[EmailServerUsername] = ConfigReader.fromStringOpt[EmailServerUsername](EmailServerUsername.from)
+    given ConfigReader[EmailServerPassword] = ConfigReader.fromStringOpt[EmailServerPassword](EmailServerPassword.from)
+    given ConfigReader[SmtpTransport] =
+        ConfigReader.fromStringOpt[SmtpTransport](string => Try(SmtpTransport.valueOf(string)).toOption)
+
+    given ConfigReader[EmailMiddlewareConfiguration] =
+        ConfigReader.forProduct5("host", "port", "transport", "username", "password")(
+            EmailMiddlewareConfiguration.apply
+        )
+
+    given ConfigReader[ExternalUrlConfiguration] =
+        ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply)
+
+    given ConfigReader[ServiceConfig] =
+        ConfigReader.forProduct13(
+            "host",
+            "port",
+            "csrf-key-file",
+            "download-directory",
+            "render-maximum-file-size",
+            "authentication",
+            "billing",
+            "darcs",
+            "email",
+            "external",
+            "signup",
+            "ssh",
+            "ticket-integration"
+        )(ServiceConfig.apply)
 }
 
 /** Timeouts related to the authentication and session management.
@@ -275,10 +275,10 @@
 )
 
 object AuthenticationTimeouts {
-  given ConfigReader[AuthenticationTimeouts] =
-    ConfigReader.forProduct3("absolute-timeout", "idle-timeout", "renewal-timeout")(
-      AuthenticationTimeouts.apply
-    )
+    given ConfigReader[AuthenticationTimeouts] =
+        ConfigReader.forProduct3("absolute-timeout", "idle-timeout", "renewal-timeout")(
+            AuthenticationTimeouts.apply
+        )
 }
 
 /** Configuration for the authentication feature.
@@ -300,24 +300,25 @@
 )
 
 object AuthenticationConfiguration {
-  given Eq[AuthenticationConfiguration] = Eq.fromUniversalEquals
-
-  given ConfigReader[FailedAttempts] =
-    ConfigReader.fromStringOpt { s =>
-      if (s.forall(_.isDigit))
-        FailedAttempts.from(s.toInt)
-      else
-        None
-    }
-
-  given ConfigReader[PrivateKey] = ConfigReader.fromStringOpt(s => PrivateKey.from(s.getBytes(StandardCharsets.UTF_8)))
-
-  given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+    given Eq[AuthenticationConfiguration] = Eq.fromUniversalEquals
 
-  given ConfigReader[AuthenticationConfiguration] =
-    ConfigReader.forProduct4("enabled", "cookie-secret", "lock-after", "timeouts")(
-      AuthenticationConfiguration.apply
-    )
+    given ConfigReader[FailedAttempts] =
+        ConfigReader.fromStringOpt { s =>
+            if (s.forall(_.isDigit))
+                FailedAttempts.from(s.toInt)
+            else
+                None
+        }
+
+    given ConfigReader[PrivateKey] =
+        ConfigReader.fromStringOpt(s => PrivateKey.from(s.getBytes(StandardCharsets.UTF_8)))
+
+    given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+
+    given ConfigReader[AuthenticationConfiguration] =
+        ConfigReader.forProduct4("enabled", "cookie-secret", "lock-after", "timeouts")(
+            AuthenticationConfiguration.apply
+        )
 }
 
 /** Billing configuration which has a general enabled flag and further more specific settings.
@@ -328,9 +329,9 @@
 final case class BillingConfiguration(enabled: Boolean)
 
 object BillingConfiguration {
-  given Eq[BillingConfiguration] = Eq.fromUniversalEquals
+    given Eq[BillingConfiguration] = Eq.fromUniversalEquals
 
-  given ConfigReader[BillingConfiguration] = ConfigReader.forProduct1("enabled")(BillingConfiguration.apply)
+    given ConfigReader[BillingConfiguration] = ConfigReader.forProduct1("enabled")(BillingConfiguration.apply)
 }
 
 /** Configuration for the darcs module for vcs related operations via darcs.
@@ -352,12 +353,12 @@
 final case class DarcsConfiguration(executable: Path, repositoriesDirectory: DirectoryPath)
 
 object DarcsConfiguration {
-  given Eq[DarcsConfiguration] = Eq.fromUniversalEquals
+    given Eq[DarcsConfiguration] = Eq.fromUniversalEquals
 
-  given ConfigReader[DirectoryPath] = ConfigReader.fromStringOpt(DirectoryPath.fromString)
+    given ConfigReader[DirectoryPath] = ConfigReader.fromStringOpt(DirectoryPath.fromString)
 
-  given ConfigReader[DarcsConfiguration] =
-    ConfigReader.forProduct2("executable", "repositories-directory")(DarcsConfiguration.apply)
+    given ConfigReader[DarcsConfiguration] =
+        ConfigReader.forProduct2("executable", "repositories-directory")(DarcsConfiguration.apply)
 }
 
 /** Configuration for the signup feature.
@@ -368,11 +369,11 @@
 final case class SignupConfiguration(enabled: Boolean)
 
 object SignupConfiguration {
-  given Eq[SignupConfiguration] = Eq.fromUniversalEquals
+    given Eq[SignupConfiguration] = Eq.fromUniversalEquals
 
-  given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+    given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
 
-  given ConfigReader[SignupConfiguration] = ConfigReader.forProduct1("enabled")(SignupConfiguration.apply)
+    given ConfigReader[SignupConfiguration] = ConfigReader.forProduct1("enabled")(SignupConfiguration.apply)
 
 }
 
@@ -386,7 +387,7 @@
 final case class TicketIntegrationConfiguration(baseUri: Uri, enabled: Boolean)
 
 object TicketIntegrationConfiguration {
-  given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
-  given ConfigReader[TicketIntegrationConfiguration] =
-    ConfigReader.forProduct2("base-uri", "enabled")(TicketIntegrationConfiguration.apply)
+    given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+    given ConfigReader[TicketIntegrationConfiguration] =
+        ConfigReader.forProduct2("base-uri", "enabled")(TicketIntegrationConfiguration.apply)
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DatabaseMigrator.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DatabaseMigrator.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DatabaseMigrator.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DatabaseMigrator.scala	2025-01-13 17:13:25.052470972 +0000
@@ -27,40 +27,40 @@
   */
 final class DatabaseMigrator[F[_]: Sync] {
 
-  /** Apply pending migrations to the database if needed using the underlying Flyway library.
-    *
-    * @param url
-    *   The JDBC connection URL **without** username and password.
-    * @param user
-    *   The username (login) needed to authenticate against the database.
-    * @param pass
-    *   The password needed to authenticate against the database.
-    * @return
-    *   A migrate result object holding information about executed migrations and the schema. See the Java-Doc of Flyway
-    *   for details.
-    */
-  def migrate(url: String, user: String, pass: String): F[MigrateResult] =
-    for {
-      flyway <- Sync[F].delay(DatabaseMigrator.configureFlyway(url, user, pass).load())
-      result <- Sync[F].delay(flyway.migrate())
-    } yield result
+    /** Apply pending migrations to the database if needed using the underlying Flyway library.
+      *
+      * @param url
+      *   The JDBC connection URL **without** username and password.
+      * @param user
+      *   The username (login) needed to authenticate against the database.
+      * @param pass
+      *   The password needed to authenticate against the database.
+      * @return
+      *   A migrate result object holding information about executed migrations and the schema. See the Java-Doc of
+      *   Flyway for details.
+      */
+    def migrate(url: String, user: String, pass: String): F[MigrateResult] =
+        for {
+            flyway <- Sync[F].delay(DatabaseMigrator.configureFlyway(url, user, pass).load())
+            result <- Sync[F].delay(flyway.migrate())
+        } yield result
 }
 
 object DatabaseMigrator {
 
-  /** Configure an instance of flyway with our defaults and the given credentials for a database connection. The
-    * returned instance must be activated by calling the `.load()` method.
-    *
-    * @param url
-    *   The JDBC connection URL **without** username and password.
-    * @param user
-    *   The username (login) needed to authenticate against the database.
-    * @param pass
-    *   The password needed to authenticate against the database.
-    * @return
-    *   An instance configuration which can be turned into a flyway database migrator by calling the `.load()` method.
-    */
-  def configureFlyway(url: String, user: String, pass: String): FluentConfiguration =
-    Flyway.configure().defaultSchema("hub").locations("classpath:db/migration/hub").dataSource(url, user, pass)
+    /** Configure an instance of flyway with our defaults and the given credentials for a database connection. The
+      * returned instance must be activated by calling the `.load()` method.
+      *
+      * @param url
+      *   The JDBC connection URL **without** username and password.
+      * @param user
+      *   The username (login) needed to authenticate against the database.
+      * @param pass
+      *   The password needed to authenticate against the database.
+      * @return
+      *   An instance configuration which can be turned into a flyway database migrator by calling the `.load()` method.
+      */
+    def configureFlyway(url: String, user: String, pass: String): FluentConfiguration =
+        Flyway.configure().defaultSchema("hub").locations("classpath:db/migration/hub").dataSource(url, user, pass)
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAccountManagementRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -30,52 +30,52 @@
 import fs2.Stream
 
 final class DoobieAccountManagementRepository[F[_]: Sync](tx: Transactor[F]) extends AccountManagementRepository[F] {
-  given Meta[EmailAddress]    = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[EncodedKeyBytes] = Meta[String].timap(EncodedKeyBytes.unsafeFrom)(_.toString)
-  given Meta[KeyComment]      = Meta[String].timap(KeyComment.apply)(_.toString)
-  given Meta[KeyFingerprint]  = Meta[String].timap(KeyFingerprint.apply)(_.toString)
-  given Meta[LanguageCode]    = Meta[String].timap(LanguageCode.apply)(_.toString)
-  given Meta[PasswordHash]    = Meta[String].timap(PasswordHash.apply)(_.toString)
-  given Meta[SshKeyType]      = Meta[String].timap(id => SshKeyType.Mappings(id))(_.identifier)
-  given Meta[UserId]          = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]        = Meta[String].timap(Username.apply)(_.toString)
-  given Meta[ValidationToken] = Meta[String].timap(ValidationToken.apply)(_.toString)
+    given Meta[EmailAddress]    = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[EncodedKeyBytes] = Meta[String].timap(EncodedKeyBytes.unsafeFrom)(_.toString)
+    given Meta[KeyComment]      = Meta[String].timap(KeyComment.apply)(_.toString)
+    given Meta[KeyFingerprint]  = Meta[String].timap(KeyFingerprint.apply)(_.toString)
+    given Meta[LanguageCode]    = Meta[String].timap(LanguageCode.apply)(_.toString)
+    given Meta[PasswordHash]    = Meta[String].timap(PasswordHash.apply)(_.toString)
+    given Meta[SshKeyType]      = Meta[String].timap(id => SshKeyType.Mappings(id))(_.identifier)
+    given Meta[UserId]          = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]        = Meta[String].timap(Username.apply)(_.toString)
+    given Meta[ValidationToken] = Meta[String].timap(ValidationToken.apply)(_.toString)
 
-  private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
+    private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
 
-  override def addSshKey(key: PublicSshKey): F[Int] =
-    sql"""INSERT INTO "hub"."ssh_keys" (id, uid, key_type, key, fingerprint, comment, created_at)
+    override def addSshKey(key: PublicSshKey): F[Int] =
+        sql"""INSERT INTO "hub"."ssh_keys" (id, uid, key_type, key, fingerprint, comment, created_at)
       VALUES(${key.id}, ${key.ownerId}, ${key.keyType}, ${key.keyBytes}, ${key.fingerprint}, ${key.comment}, NOW())""".update.run
-      .transact(tx)
+            .transact(tx)
 
-  override def deleteAccount(uid: UserId): F[Int] =
-    sql"""DELETE FROM "hub"."accounts" WHERE uid = $uid""".update.run.transact(tx)
+    override def deleteAccount(uid: UserId): F[Int] =
+        sql"""DELETE FROM "hub"."accounts" WHERE uid = $uid""".update.run.transact(tx)
 
-  override def deleteSshKey(keyId: UUID, ownerId: UserId): F[Int] =
-    sql"""DELETE FROM "hub"."ssh_keys" WHERE id = $keyId AND uid = $ownerId""".update.run.transact(tx)
+    override def deleteSshKey(keyId: UUID, ownerId: UserId): F[Int] =
+        sql"""DELETE FROM "hub"."ssh_keys" WHERE id = $keyId AND uid = $ownerId""".update.run.transact(tx)
 
-  override def findByValidationToken(token: ValidationToken): F[Option[Account]] = {
-    val query = selectAccountColumns ++ fr"""WHERE validation_token = $token""" ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
+    override def findByValidationToken(token: ValidationToken): F[Option[Account]] = {
+        val query = selectAccountColumns ++ fr"""WHERE validation_token = $token""" ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
 
-  override def findPasswordHash(uid: UserId): F[Option[PasswordHash]] =
-    sql"""SELECT password FROM "hub"."accounts" WHERE uid = $uid LIMIT 1""".query[PasswordHash].option.transact(tx)
+    override def findPasswordHash(uid: UserId): F[Option[PasswordHash]] =
+        sql"""SELECT password FROM "hub"."accounts" WHERE uid = $uid LIMIT 1""".query[PasswordHash].option.transact(tx)
 
-  override def listSshKeys(uid: UserId): Stream[F, PublicSshKey] =
-    sql"""SELECT id, uid, key_type, key, fingerprint, comment, created_at, last_used_at FROM "hub"."ssh_keys" WHERE uid = $uid"""
-      .query[PublicSshKey]
-      .stream
-      .transact(tx)
+    override def listSshKeys(uid: UserId): Stream[F, PublicSshKey] =
+        sql"""SELECT id, uid, key_type, key, fingerprint, comment, created_at, last_used_at FROM "hub"."ssh_keys" WHERE uid = $uid"""
+            .query[PublicSshKey]
+            .stream
+            .transact(tx)
 
-  override def markAsValidated(uid: UserId): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET validated_email = TRUE, validation_token = NULL WHERE uid = $uid""".update.run
-      .transact(tx)
+    override def markAsValidated(uid: UserId): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET validated_email = TRUE, validation_token = NULL WHERE uid = $uid""".update.run
+            .transact(tx)
 
-  override def setLanguage(uid: UserId, language: Option[LanguageCode]): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET language = $language WHERE uid = $uid""".update.run.transact(tx)
+    override def setLanguage(uid: UserId, language: Option[LanguageCode]): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET language = $language WHERE uid = $uid""".update.run.transact(tx)
 
-  override def setValidationToken(uid: UserId, token: ValidationToken): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET validation_token = $token WHERE uid = $uid""".update.run.transact(tx)
+    override def setValidationToken(uid: UserId, token: ValidationToken): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET validation_token = $token WHERE uid = $uid""".update.run.transact(tx)
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieAuthenticationRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -31,85 +31,86 @@
 import fs2.Stream
 
 final class DoobieAuthenticationRepository[F[_]: Sync](tx: Transactor[F]) extends AuthenticationRepository[F] {
-  given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
-  given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
-  given Meta[SessionId]    = Meta[String].timap(SessionId.apply)(_.toString)
-  given Meta[UnlockToken]  = Meta[String].timap(UnlockToken.apply)(_.toString)
-  given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
-
-  private val lockedFilter         = fr"""locked_at IS NOT NULL"""
-  private val notLockedFilter      = fr"""locked_at IS NULL"""
-  private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
-
-  override def allAccounts(): Stream[F, Account] = {
-    val query = selectAccountColumns ++ fr"""ORDER BY "name" ASC"""
-    query.query[Account].stream.transact(tx)
-  }
-
-  override def createUserSession(session: Session): F[Int] =
-    sql"""INSERT INTO "hub"."sessions" (id, uid, created_at, updated_at) VALUES (${session.id}, ${session.uid}, ${session.createdAt}, ${session.updatedAt})""".update.run
-      .transact(tx)
-
-  override def deleteAllUserSessions(uid: UserId): F[Int] =
-    sql"""DELETE FROM "hub"."sessions" WHERE uid = $uid""".update.run.transact(tx)
-
-  override def deleteUserSession(id: SessionId): F[Int] =
-    sql"""DELETE FROM "hub"."sessions" WHERE id = $id""".update.run.transact(tx)
-
-  override def findAccount(uid: UserId): F[Option[Account]] = {
-    val uidFilter = fr"""uid = $uid"""
-    val query     = selectAccountColumns ++ whereAnd(notLockedFilter, uidFilter) ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
-
-  override def findAccountByEmail(email: EmailAddress): F[Option[Account]] = {
-    val emailFilter = fr"""email = $email"""
-    val query       = selectAccountColumns ++ whereAnd(notLockedFilter, emailFilter) ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
-
-  override def findAccountByName(name: Username): F[Option[Account]] = {
-    val nameFilter = fr"""name = $name"""
-    val query      = selectAccountColumns ++ whereAnd(notLockedFilter, nameFilter) ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
-
-  override def findLockedAccount(name: Username)(token: Option[UnlockToken]): F[Option[Account]] = {
-    val nameFilter  = fr"""name = $name"""
-    val tokenFilter = token.map(token => fr"""unlock_token = $token""")
-    val query = selectAccountColumns ++ whereAndOpt(lockedFilter.some, nameFilter.some, tokenFilter) ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
-
-  override def findPasswordHashAndAttempts(uid: UserId): F[Option[(PasswordHash, Int)]] = {
-    val uidFilter = fr"""uid = $uid"""
-    val query = fr"""SELECT password, failed_attempts FROM "hub"."accounts"""" ++ whereAnd(
-      notLockedFilter,
-      uidFilter
-    ) ++ fr"""LIMIT 1"""
-    query.query[(PasswordHash, Int)].option.transact(tx)
-  }
-
-  override def findUserSession(id: SessionId): F[Option[Session]] =
-    sql"""SELECT id, uid, created_at, updated_at FROM "hub"."sessions" WHERE id = $id LIMIT 1"""
-      .query[Session]
-      .option
-      .transact(tx)
-
-  override def incrementFailedAttempts(uid: UserId): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET failed_attempts = failed_attempts + 1 WHERE uid = $uid""".update.run
-      .transact(tx)
-
-  override def lockAccount(uid: UserId)(token: Option[UnlockToken]): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET locked_at = NOW(), unlock_token = $token WHERE uid = $uid""".update.run
-      .transact(tx)
-
-  override def resetFailedAttempts(uid: UserId): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET failed_attempts = 0 WHERE uid = $uid""".update.run.transact(tx)
-
-  override def unlockAccount(uid: UserId): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET locked_at = NULL, unlock_token = NULL WHERE uid = $uid""".update.run
-      .transact(tx)
+    given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
+    given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
+    given Meta[SessionId]    = Meta[String].timap(SessionId.apply)(_.toString)
+    given Meta[UnlockToken]  = Meta[String].timap(UnlockToken.apply)(_.toString)
+    given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
+
+    private val lockedFilter         = fr"""locked_at IS NOT NULL"""
+    private val notLockedFilter      = fr"""locked_at IS NULL"""
+    private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
+
+    override def allAccounts(): Stream[F, Account] = {
+        val query = selectAccountColumns ++ fr"""ORDER BY "name" ASC"""
+        query.query[Account].stream.transact(tx)
+    }
+
+    override def createUserSession(session: Session): F[Int] =
+        sql"""INSERT INTO "hub"."sessions" (id, uid, created_at, updated_at) VALUES (${session.id}, ${session.uid}, ${session.createdAt}, ${session.updatedAt})""".update.run
+            .transact(tx)
+
+    override def deleteAllUserSessions(uid: UserId): F[Int] =
+        sql"""DELETE FROM "hub"."sessions" WHERE uid = $uid""".update.run.transact(tx)
+
+    override def deleteUserSession(id: SessionId): F[Int] =
+        sql"""DELETE FROM "hub"."sessions" WHERE id = $id""".update.run.transact(tx)
+
+    override def findAccount(uid: UserId): F[Option[Account]] = {
+        val uidFilter = fr"""uid = $uid"""
+        val query     = selectAccountColumns ++ whereAnd(notLockedFilter, uidFilter) ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
+
+    override def findAccountByEmail(email: EmailAddress): F[Option[Account]] = {
+        val emailFilter = fr"""email = $email"""
+        val query       = selectAccountColumns ++ whereAnd(notLockedFilter, emailFilter) ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
+
+    override def findAccountByName(name: Username): F[Option[Account]] = {
+        val nameFilter = fr"""name = $name"""
+        val query      = selectAccountColumns ++ whereAnd(notLockedFilter, nameFilter) ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
+
+    override def findLockedAccount(name: Username)(token: Option[UnlockToken]): F[Option[Account]] = {
+        val nameFilter  = fr"""name = $name"""
+        val tokenFilter = token.map(token => fr"""unlock_token = $token""")
+        val query =
+            selectAccountColumns ++ whereAndOpt(lockedFilter.some, nameFilter.some, tokenFilter) ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
+
+    override def findPasswordHashAndAttempts(uid: UserId): F[Option[(PasswordHash, Int)]] = {
+        val uidFilter = fr"""uid = $uid"""
+        val query = fr"""SELECT password, failed_attempts FROM "hub"."accounts"""" ++ whereAnd(
+                notLockedFilter,
+                uidFilter
+            ) ++ fr"""LIMIT 1"""
+        query.query[(PasswordHash, Int)].option.transact(tx)
+    }
+
+    override def findUserSession(id: SessionId): F[Option[Session]] =
+        sql"""SELECT id, uid, created_at, updated_at FROM "hub"."sessions" WHERE id = $id LIMIT 1"""
+            .query[Session]
+            .option
+            .transact(tx)
+
+    override def incrementFailedAttempts(uid: UserId): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET failed_attempts = failed_attempts + 1 WHERE uid = $uid""".update.run
+            .transact(tx)
+
+    override def lockAccount(uid: UserId)(token: Option[UnlockToken]): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET locked_at = NOW(), unlock_token = $token WHERE uid = $uid""".update.run
+            .transact(tx)
+
+    override def resetFailedAttempts(uid: UserId): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET failed_attempts = 0 WHERE uid = $uid""".update.run.transact(tx)
+
+    override def unlockAccount(uid: UserId): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET locked_at = NULL, unlock_token = NULL WHERE uid = $uid""".update.run
+            .transact(tx)
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieResetPasswordRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -30,55 +30,55 @@
 import doobie.postgres.implicits.*
 
 final class DoobieResetPasswordRepository[F[_]: Sync](tx: Transactor[F]) extends ResetPasswordRepository[F] {
-  given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
-  given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
-  given Meta[ResetToken]   = Meta[String].timap(ResetToken.apply)(_.toString)
-  given Meta[SessionId]    = Meta[String].timap(SessionId.apply)(_.toString)
-  given Meta[UnlockToken]  = Meta[String].timap(UnlockToken.apply)(_.toString)
-  given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
-
-  private val notLockedFilter              = fr"""locked_at IS NULL"""
-  private val resetTokenExpiryNotSetFilter = fr"""reset_expiry IS NULL"""
-  private val resetTokenExpirySetFilter    = fr"""reset_expiry IS NOT NULL"""
-  private val resetTokenNotExpiredFilter   = fr"""reset_expiry > NOW()"""
-  private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
-
-  override def findByNameAndResetPasswordToken(name: Username, token: ResetToken): F[Option[Account]] = {
-    val nameFilter       = fr"""name = $name"""
-    val resetTokenFilter = fr"""reset_token = $token"""
-    val query = selectAccountColumns ++ whereAnd(
-      notLockedFilter,
-      nameFilter,
-      resetTokenExpiryNotSetFilter,
-      resetTokenFilter
-    ) ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
-
-  override def findByResetPasswordToken(token: ResetToken): F[Option[Account]] = {
-    val resetTokenFilter = fr"""reset_token = $token"""
-    val query = selectAccountColumns ++ whereAnd(
-      notLockedFilter,
-      resetTokenExpirySetFilter,
-      resetTokenNotExpiredFilter,
-      resetTokenFilter
-    ) ++ fr"""LIMIT 1"""
-    query.query[Account].option.transact(tx)
-  }
-
-  override def removeResetPasswordExpirationDate(uid: UserId): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET reset_expiry = NULL WHERE uid = $uid""".update.run.transact(tx)
-
-  override def removeResetPasswordToken(uid: UserId): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET reset_expiry = NULL, reset_token = NULL WHERE uid = $uid""".update.run
-      .transact(tx)
-
-  override def setPassword(uid: UserId)(hash: PasswordHash): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET password = $hash WHERE uid = $uid""".update.run.transact(tx)
-
-  override def setResetPasswordToken(uid: UserId)(token: ResetToken, tokenExpiration: OffsetDateTime): F[Int] =
-    sql"""UPDATE "hub"."accounts" SET  reset_expiry = $tokenExpiration, reset_token = $token WHERE uid = $uid""".update.run
-      .transact(tx)
+    given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
+    given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
+    given Meta[ResetToken]   = Meta[String].timap(ResetToken.apply)(_.toString)
+    given Meta[SessionId]    = Meta[String].timap(SessionId.apply)(_.toString)
+    given Meta[UnlockToken]  = Meta[String].timap(UnlockToken.apply)(_.toString)
+    given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
+
+    private val notLockedFilter              = fr"""locked_at IS NULL"""
+    private val resetTokenExpiryNotSetFilter = fr"""reset_expiry IS NULL"""
+    private val resetTokenExpirySetFilter    = fr"""reset_expiry IS NOT NULL"""
+    private val resetTokenNotExpiredFilter   = fr"""reset_expiry > NOW()"""
+    private val selectAccountColumns = fr"""SELECT uid, name, email, validated_email, language FROM "hub"."accounts""""
+
+    override def findByNameAndResetPasswordToken(name: Username, token: ResetToken): F[Option[Account]] = {
+        val nameFilter       = fr"""name = $name"""
+        val resetTokenFilter = fr"""reset_token = $token"""
+        val query = selectAccountColumns ++ whereAnd(
+            notLockedFilter,
+            nameFilter,
+            resetTokenExpiryNotSetFilter,
+            resetTokenFilter
+        ) ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
+
+    override def findByResetPasswordToken(token: ResetToken): F[Option[Account]] = {
+        val resetTokenFilter = fr"""reset_token = $token"""
+        val query = selectAccountColumns ++ whereAnd(
+            notLockedFilter,
+            resetTokenExpirySetFilter,
+            resetTokenNotExpiredFilter,
+            resetTokenFilter
+        ) ++ fr"""LIMIT 1"""
+        query.query[Account].option.transact(tx)
+    }
+
+    override def removeResetPasswordExpirationDate(uid: UserId): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET reset_expiry = NULL WHERE uid = $uid""".update.run.transact(tx)
+
+    override def removeResetPasswordToken(uid: UserId): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET reset_expiry = NULL, reset_token = NULL WHERE uid = $uid""".update.run
+            .transact(tx)
+
+    override def setPassword(uid: UserId)(hash: PasswordHash): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET password = $hash WHERE uid = $uid""".update.run.transact(tx)
+
+    override def setResetPasswordToken(uid: UserId)(token: ResetToken, tokenExpiration: OffsetDateTime): F[Int] =
+        sql"""UPDATE "hub"."accounts" SET  reset_expiry = $tokenExpiration, reset_token = $token WHERE uid = $uid""".update.run
+            .transact(tx)
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieSignupRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieSignupRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieSignupRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieSignupRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -28,19 +28,19 @@
 
 final class DoobieSignupRepository[F[_]: Sync](tx: Transactor[F]) extends SignupRepository[F] {
 
-  given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
-  given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
-
-  override def createAccount(account: Account, hash: PasswordHash): F[Int] =
-    sql"""INSERT INTO "hub"."accounts" (uid, name, email, password, created_at, updated_at, validated_email) VALUES(${account.uid}, ${account.name}, ${account.email}, $hash, NOW(), NOW(), ${account.validatedEmail})""".update.run
-      .transact(tx)
+    given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[PasswordHash] = Meta[String].timap(PasswordHash.apply)(_.toString)
+    given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
+
+    override def createAccount(account: Account, hash: PasswordHash): F[Int] =
+        sql"""INSERT INTO "hub"."accounts" (uid, name, email, password, created_at, updated_at, validated_email) VALUES(${account.uid}, ${account.name}, ${account.email}, $hash, NOW(), NOW(), ${account.validatedEmail})""".update.run
+            .transact(tx)
 
-  override def findEmail(address: EmailAddress): F[Option[EmailAddress]] =
-    sql"""SELECT email FROM "hub"."accounts" WHERE email = $address""".query[EmailAddress].option.transact(tx)
+    override def findEmail(address: EmailAddress): F[Option[EmailAddress]] =
+        sql"""SELECT email FROM "hub"."accounts" WHERE email = $address""".query[EmailAddress].option.transact(tx)
 
-  override def findUsername(name: Username): F[Option[Username]] =
-    sql"""SELECT name FROM "hub"."accounts" WHERE name = $name""".query[Username].option.transact(tx)
+    override def findUsername(name: Username): F[Option[Username]] =
+        sql"""SELECT name FROM "hub"."accounts" WHERE name = $name""".query[Username].option.transact(tx)
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -34,17 +34,17 @@
 
 final class DoobieVcsMetadataRepository[F[_]: Sync](tx: Transactor[F]) extends VcsMetadataRepository[F] {
 
-  given Meta[EmailAddress]             = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[Uri]                      = Meta[String].timap(Uri.unsafeFromString)(_.toString)
-  given Meta[UserId]                   = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]                 = Meta[String].timap(Username.apply)(_.toString)
-  given Meta[VcsRepositoryDescription] = Meta[String].timap(VcsRepositoryDescription.apply)(_.toString)
-  given Meta[VcsRepositoryId]          = Meta[Long].timap(VcsRepositoryId.apply)(_.toLong)
-  given Meta[VcsRepositoryName]        = Meta[String].timap(VcsRepositoryName.apply)(_.toString)
-  given Meta[VcsType]                  = Meta[String].timap(VcsType.valueOf)(_.toString)
+    given Meta[EmailAddress]             = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[Uri]                      = Meta[String].timap(Uri.unsafeFromString)(_.toString)
+    given Meta[UserId]                   = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]                 = Meta[String].timap(Username.apply)(_.toString)
+    given Meta[VcsRepositoryDescription] = Meta[String].timap(VcsRepositoryDescription.apply)(_.toString)
+    given Meta[VcsRepositoryId]          = Meta[Long].timap(VcsRepositoryId.apply)(_.toLong)
+    given Meta[VcsRepositoryName]        = Meta[String].timap(VcsRepositoryName.apply)(_.toString)
+    given Meta[VcsType]                  = Meta[String].timap(VcsType.valueOf)(_.toString)
 
-  private val selectRepositoryColumns =
-    fr"""SELECT
+    private val selectRepositoryColumns =
+        fr"""SELECT
           "repos".name AS name,
           "accounts".uid AS owner_id,
           "accounts".name AS owner_name,
@@ -58,8 +58,8 @@
         JOIN "hub"."accounts" AS "accounts"
         ON "repos".owner = "accounts".uid"""
 
-  override def createFork(source: VcsRepositoryId, target: VcsRepositoryId): F[Int] =
-    sql"""INSERT INTO "hub"."forks" (
+    override def createFork(source: VcsRepositoryId, target: VcsRepositoryId): F[Int] =
+        sql"""INSERT INTO "hub"."forks" (
             original_repo,
             forked_repo
           ) VALUES (
@@ -67,8 +67,8 @@
             $target
           )""".update.run.transact(tx)
 
-  override def createVcsRepository(repository: VcsRepository): F[Int] =
-    sql"""INSERT INTO "hub"."repositories" (
+    override def createVcsRepository(repository: VcsRepository): F[Int] =
+        sql"""INSERT INTO "hub"."repositories" (
             name,
             owner,
             is_private,
@@ -90,25 +90,25 @@
             NOW()
           )""".update.run.transact(tx)
 
-  override def deleteVcsRepository(repository: VcsRepository): F[Int] =
-    sql"""DELETE FROM "hub"."repositories"
+    override def deleteVcsRepository(repository: VcsRepository): F[Int] =
+        sql"""DELETE FROM "hub"."repositories"
           WHERE owner = ${repository.owner.uid}
           AND name = ${repository.name}""".update.run.transact(tx)
 
-  override def findVcsRepository(
-      owner: VcsRepositoryOwner,
-      name: VcsRepositoryName
-  ): F[Option[VcsRepository]] = {
-    val nameFilter  = fr""""repos".name = $name"""
-    val ownerFilter = fr""""repos".owner = ${owner.uid}"""
-    val query       = selectRepositoryColumns ++ whereAnd(ownerFilter, nameFilter) ++ fr"""LIMIT 1"""
-    query.query[VcsRepository].option.transact(tx)
-  }
-
-  override def findVcsRepositoryBranches(
-      originalRepositoryId: VcsRepositoryId
-  ): Stream[F, (Username, VcsRepositoryName)] = {
-    val query = sql"""SELECT
+    override def findVcsRepository(
+        owner: VcsRepositoryOwner,
+        name: VcsRepositoryName
+    ): F[Option[VcsRepository]] = {
+        val nameFilter  = fr""""repos".name = $name"""
+        val ownerFilter = fr""""repos".owner = ${owner.uid}"""
+        val query       = selectRepositoryColumns ++ whereAnd(ownerFilter, nameFilter) ++ fr"""LIMIT 1"""
+        query.query[VcsRepository].option.transact(tx)
+    }
+
+    override def findVcsRepositoryBranches(
+        originalRepositoryId: VcsRepositoryId
+    ): Stream[F, (Username, VcsRepositoryName)] = {
+        val query = sql"""SELECT
                         "accounts"."name" AS "owner_name",
                         "repos"."name" AS "repository_name"
                       FROM "hub"."forks" AS "forks"
@@ -117,63 +117,63 @@
                       JOIN "hub"."accounts" AS "accounts"
                       ON "repos"."owner" = "accounts"."uid"
                       WHERE "forks"."original_repo" = $originalRepositoryId"""
-    query.query[(Username, VcsRepositoryName)].stream.transact(tx)
-  }
+        query.query[(Username, VcsRepositoryName)].stream.transact(tx)
+    }
+
+    override def findVcsRepositoryId(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepositoryId]] = {
+        val nameFilter  = fr"""name = $name"""
+        val ownerFilter = fr"""owner = ${owner.uid}"""
+        val query = fr"""SELECT id FROM "hub"."repositories"""" ++ whereAnd(ownerFilter, nameFilter) ++ fr"""LIMIT 1"""
+        query.query[VcsRepositoryId].option.transact(tx)
+    }
 
-  override def findVcsRepositoryId(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepositoryId]] = {
-    val nameFilter  = fr"""name = $name"""
-    val ownerFilter = fr"""owner = ${owner.uid}"""
-    val query = fr"""SELECT id FROM "hub"."repositories"""" ++ whereAnd(ownerFilter, nameFilter) ++ fr"""LIMIT 1"""
-    query.query[VcsRepositoryId].option.transact(tx)
-  }
-
-  override def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]] =
-    sql"""SELECT uid, name, email FROM "hub"."accounts" WHERE name = $name LIMIT 1"""
-      .query[VcsRepositoryOwner]
-      .option
-      .transact(tx)
-
-  override def findVcsRepositoryParentFork(
-      owner: VcsRepositoryOwner,
-      name: VcsRepositoryName
-  ): F[Option[VcsRepository]] = {
-    val query =
-      selectRepositoryColumns ++ fr"""WHERE "repos".id = (SELECT original_repo FROM "hub"."forks" WHERE forked_repo = (SELECT id FROM "hub"."repositories" WHERE name = $name AND owner = ${owner.uid}))"""
-    query.query[VcsRepository].option.transact(tx)
-  }
-
-  override def listAllRepositories(
-      requester: Option[Account]
-  )(ordering: VcsMetadataRepositoriesOrdering): Stream[F, VcsRepository] = {
-    val orderClause = ordering match {
-      case NameAscending  => fr"""ORDER BY name ASC"""
-      case NameDescending => fr"""ORDER BY name DESC"""
-    }
-    // We use a SQL UNION here which first queries all others repos and then (potentially) the repos of the requester.
-    val query = requester.fold(selectRepositoryColumns ++ fr"""WHERE is_private = FALSE""")(account =>
-      selectRepositoryColumns ++ fr"""WHERE is_private = FALSE AND owner != ${account.uid}""" ++ fr"""UNION""" ++ selectRepositoryColumns ++ fr"""WHERE owner = ${account.uid}"""
-    ) ++ orderClause
-    query.query[VcsRepository].stream.transact(tx)
-  }
-
-  override def listRepositories(
-      requester: Option[Account]
-  )(owner: VcsRepositoryOwner): Stream[F, VcsRepository] = {
-    val ownerFilter = fr""""repos".owner = ${owner.uid}"""
-    val whereClause = requester match {
-      case None => whereAnd(ownerFilter, fr""""repos".is_private = FALSE""") // Guest only see public repos.
-      case Some(account) =>
-        if (account.uid === owner.uid)
-          whereAnd(ownerFilter) // The user asks for their own repositories.
-        else
-          whereAnd(ownerFilter, fr""""repos".is_private = FALSE""") // TODO: More logic later (groups, perms).
-    }
-    val query = selectRepositoryColumns ++ whereClause ++ fr"""ORDER BY name ASC"""
-    query.query[VcsRepository].stream.transact(tx)
-  }
+    override def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]] =
+        sql"""SELECT uid, name, email FROM "hub"."accounts" WHERE name = $name LIMIT 1"""
+            .query[VcsRepositoryOwner]
+            .option
+            .transact(tx)
+
+    override def findVcsRepositoryParentFork(
+        owner: VcsRepositoryOwner,
+        name: VcsRepositoryName
+    ): F[Option[VcsRepository]] = {
+        val query =
+            selectRepositoryColumns ++ fr"""WHERE "repos".id = (SELECT original_repo FROM "hub"."forks" WHERE forked_repo = (SELECT id FROM "hub"."repositories" WHERE name = $name AND owner = ${owner.uid}))"""
+        query.query[VcsRepository].option.transact(tx)
+    }
+
+    override def listAllRepositories(
+        requester: Option[Account]
+    )(ordering: VcsMetadataRepositoriesOrdering): Stream[F, VcsRepository] = {
+        val orderClause = ordering match {
+            case NameAscending  => fr"""ORDER BY name ASC"""
+            case NameDescending => fr"""ORDER BY name DESC"""
+        }
+        // We use a SQL UNION here which first queries all others repos and then (potentially) the repos of the requester.
+        val query = requester.fold(selectRepositoryColumns ++ fr"""WHERE is_private = FALSE""")(account =>
+            selectRepositoryColumns ++ fr"""WHERE is_private = FALSE AND owner != ${account.uid}""" ++ fr"""UNION""" ++ selectRepositoryColumns ++ fr"""WHERE owner = ${account.uid}"""
+        ) ++ orderClause
+        query.query[VcsRepository].stream.transact(tx)
+    }
+
+    override def listRepositories(
+        requester: Option[Account]
+    )(owner: VcsRepositoryOwner): Stream[F, VcsRepository] = {
+        val ownerFilter = fr""""repos".owner = ${owner.uid}"""
+        val whereClause = requester match {
+            case None => whereAnd(ownerFilter, fr""""repos".is_private = FALSE""") // Guest only see public repos.
+            case Some(account) =>
+                if (account.uid === owner.uid)
+                    whereAnd(ownerFilter) // The user asks for their own repositories.
+                else
+                    whereAnd(ownerFilter, fr""""repos".is_private = FALSE""") // TODO: More logic later (groups, perms).
+        }
+        val query = selectRepositoryColumns ++ whereClause ++ fr"""ORDER BY name ASC"""
+        query.query[VcsRepository].stream.transact(tx)
+    }
 
-  override def updateVcsRepository(repository: VcsRepository): F[Int] =
-    sql"""UPDATE "hub"."repositories"
+    override def updateVcsRepository(repository: VcsRepository): F[Int] =
+        sql"""UPDATE "hub"."repositories"
             SET is_private = ${repository.isPrivate},
             description = ${repository.description},
             tickets_enabled = ${repository.ticketsEnabled},
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/EditVcsRepositoryForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/EditVcsRepositoryForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/EditVcsRepositoryForm.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/EditVcsRepositoryForm.scala	2025-01-13 17:13:25.052470972 +0000
@@ -47,97 +47,101 @@
 )
 
 object EditVcsRepositoryForm extends FormValidator[EditVcsRepositoryForm] {
-  val fieldDescription: FormField    = FormField("description")
-  val fieldIsPrivate: FormField      = FormField("is_private")
-  val fieldName: FormField           = FormField("name")
-  val fieldTicketsEnabled: FormField = FormField("tickets_enabled")
-  val fieldWebsite: FormField        = FormField("website")
+    val fieldDescription: FormField    = FormField("description")
+    val fieldIsPrivate: FormField      = FormField("is_private")
+    val fieldName: FormField           = FormField("name")
+    val fieldTicketsEnabled: FormField = FormField("tickets_enabled")
+    val fieldWebsite: FormField        = FormField("website")
 
-  /** Create a form for editing a vcs repository filled with the data from the given repository.
-    *
-    * @param repo
-    *   A VCS repository object containing the data that shall be used to fill the form.
-    * @return
-    *   A edit vcs repository form filled with the data from the repository.
-    */
-  def fromVcsRepository(repo: VcsRepository): EditVcsRepositoryForm =
-    EditVcsRepositoryForm(repo.name, repo.isPrivate, repo.description, repo.ticketsEnabled, repo.website)
+    /** Create a form for editing a vcs repository filled with the data from the given repository.
+      *
+      * @param repo
+      *   A VCS repository object containing the data that shall be used to fill the form.
+      * @return
+      *   A edit vcs repository form filled with the data from the repository.
+      */
+    def fromVcsRepository(repo: VcsRepository): EditVcsRepositoryForm =
+        EditVcsRepositoryForm(repo.name, repo.isPrivate, repo.description, repo.ticketsEnabled, repo.website)
 
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, EditVcsRepositoryForm] = {
-    val name = data
-      .get(fieldName)
-      .fold(FormFieldError("No repository name given!").invalidNec)(s =>
-        VcsRepositoryName.from(s).fold(FormFieldError("Invalid repository name!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
-    val privateFlag: ValidatedNec[FormErrors, Boolean] =
-      data.get(fieldIsPrivate).fold(false.validNec)(s => s.matches("true").validNec)
-    val description = data
-      .get(fieldDescription)
-      .fold(Option.empty[VcsRepositoryDescription].validNec) { s =>
-        if (s.trim.isEmpty)
-          Option.empty[VcsRepositoryDescription].validNec // Sometimes "empty" strings are sent.
-        else
-          VcsRepositoryDescription
-            .from(s)
-            .fold(FormFieldError("Invalid repository description!").invalidNec)(descr => Option(descr).validNec)
-      }
-      .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
-    val ticketsEnabledFlag: ValidatedNec[FormErrors, Boolean] =
-      data.get(fieldTicketsEnabled).fold(false.validNec)(s => s.matches("true").validNec)
-    val website = data
-      .get(fieldWebsite)
-      .fold(Option.empty[Uri].validNec) { s =>
-        if (s.trim.isEmpty)
-          Option.empty[Uri].validNec // Sometimes "empty" strings are sent.
-        else
-          Uri
-            .fromString(s)
-            .toOption
-            .fold(FormFieldError("Invalid website URI!").invalidNec) { uri =>
-              uri.scheme match {
-                case Some(Uri.Scheme.http) | Some(Uri.Scheme.https) => Option(uri).validNec
-                case _                                              => FormFieldError("Invalid website URI!").invalidNec
-              }
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, EditVcsRepositoryForm] = {
+        val name = data
+            .get(fieldName)
+            .fold(FormFieldError("No repository name given!").invalidNec)(s =>
+                VcsRepositoryName.from(s).fold(FormFieldError("Invalid repository name!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+        val privateFlag: ValidatedNec[FormErrors, Boolean] =
+            data.get(fieldIsPrivate).fold(false.validNec)(s => s.matches("true").validNec)
+        val description = data
+            .get(fieldDescription)
+            .fold(Option.empty[VcsRepositoryDescription].validNec) { s =>
+                if (s.trim.isEmpty)
+                    Option.empty[VcsRepositoryDescription].validNec // Sometimes "empty" strings are sent.
+                else
+                    VcsRepositoryDescription
+                        .from(s)
+                        .fold(FormFieldError("Invalid repository description!").invalidNec)(descr =>
+                            Option(descr).validNec
+                        )
+            }
+            .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
+        val ticketsEnabledFlag: ValidatedNec[FormErrors, Boolean] =
+            data.get(fieldTicketsEnabled).fold(false.validNec)(s => s.matches("true").validNec)
+        val website = data
+            .get(fieldWebsite)
+            .fold(Option.empty[Uri].validNec) { s =>
+                if (s.trim.isEmpty)
+                    Option.empty[Uri].validNec // Sometimes "empty" strings are sent.
+                else
+                    Uri
+                        .fromString(s)
+                        .toOption
+                        .fold(FormFieldError("Invalid website URI!").invalidNec) { uri =>
+                            uri.scheme match {
+                                case Some(Uri.Scheme.http) | Some(Uri.Scheme.https) => Option(uri).validNec
+                                case _ => FormFieldError("Invalid website URI!").invalidNec
+                            }
+                        }
             }
-      }
-      .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList)))
-    (name, privateFlag, description, ticketsEnabledFlag, website).mapN {
-      case (validName, isPrivate, validDescription, ticketsEnabled, validWebsite) =>
-        EditVcsRepositoryForm(validName, isPrivate, validDescription, ticketsEnabled, validWebsite)
+            .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList)))
+        (name, privateFlag, description, ticketsEnabledFlag, website).mapN {
+            case (validName, isPrivate, validDescription, ticketsEnabled, validWebsite) =>
+                EditVcsRepositoryForm(validName, isPrivate, validDescription, ticketsEnabled, validWebsite)
+        }
     }
-  }
 
-  extension (form: EditVcsRepositoryForm) {
+    extension (form: EditVcsRepositoryForm) {
 
-    /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
-      * twirl templating library.
-      *
-      * @return
-      *   A stringified map containing the data of the form.
-      */
-    def toMap: Map[String, String] = {
-      val isPrivate =
-        if (form.isPrivate)
-          "true"
-        else
-          "false"
-      val ticketsEnabled =
-        if (form.ticketsEnabled)
-          "true"
-        else
-          "false"
-      val formData = Map(
-        EditVcsRepositoryForm.fieldName.toString           -> form.name.toString,
-        EditVcsRepositoryForm.fieldIsPrivate.toString      -> isPrivate,
-        EditVcsRepositoryForm.fieldTicketsEnabled.toString -> ticketsEnabled
-      )
-      val description = form.description.fold(Map.empty)(description =>
-        Map(EditVcsRepositoryForm.fieldDescription.toString -> description.toString)
-      )
-      val website =
-        form.website.fold(Map.empty)(website => Map(EditVcsRepositoryForm.fieldWebsite.toString -> website.toString))
-      formData ++ description ++ website
+        /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
+          * twirl templating library.
+          *
+          * @return
+          *   A stringified map containing the data of the form.
+          */
+        def toMap: Map[String, String] = {
+            val isPrivate =
+                if (form.isPrivate)
+                    "true"
+                else
+                    "false"
+            val ticketsEnabled =
+                if (form.ticketsEnabled)
+                    "true"
+                else
+                    "false"
+            val formData = Map(
+                EditVcsRepositoryForm.fieldName.toString           -> form.name.toString,
+                EditVcsRepositoryForm.fieldIsPrivate.toString      -> isPrivate,
+                EditVcsRepositoryForm.fieldTicketsEnabled.toString -> ticketsEnabled
+            )
+            val description = form.description.fold(Map.empty)(description =>
+                Map(EditVcsRepositoryForm.fieldDescription.toString -> description.toString)
+            )
+            val website =
+                form.website.fold(Map.empty)(website =>
+                    Map(EditVcsRepositoryForm.fieldWebsite.toString -> website.toString)
+                )
+            formData ++ description ++ website
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/FormValidator.scala	2025-01-13 17:13:25.056470978 +0000
@@ -34,21 +34,21 @@
   *   The concrete type of the validated form output.
   */
 abstract class FormValidator[T] {
-  final val fieldGlobal: FormField = FormValidator.fieldGlobal
+    final val fieldGlobal: FormField = FormValidator.fieldGlobal
 
-  /** Validate the submitted form data which is received as stringified map and return either a validated `T` or a list
-    * of [[de.smederee.hub.forms.types.FormErrors]].
-    *
-    * @param data
-    *   The stringified map which was submitted.
-    * @return
-    *   Either the validated form as concrete type T or a list of form errors.
-    */
-  def validate(data: Map[String, String]): ValidatedNec[FormErrors, T]
+    /** Validate the submitted form data which is received as stringified map and return either a validated `T` or a
+      * list of [[de.smederee.hub.forms.types.FormErrors]].
+      *
+      * @param data
+      *   The stringified map which was submitted.
+      * @return
+      *   Either the validated form as concrete type T or a list of form errors.
+      */
+    def validate(data: Map[String, String]): ValidatedNec[FormErrors, T]
 
 }
 
 object FormValidator {
-  // A constant for the field name used for global errors.
-  val fieldGlobal: FormField = FormField("global")
+    // A constant for the field name used for global errors.
+    val fieldGlobal: FormField = FormField("global")
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/forms/types.scala	2025-01-13 17:13:25.056470978 +0000
@@ -22,85 +22,85 @@
 
 object types {
 
-  type FormErrors = Map[FormField, List[FormFieldError]]
-  object FormErrors {
-    val empty: FormErrors = Map.empty[FormField, List[FormFieldError]]
-
-    /** Create a single FormErrors instance from a given non empty chain of FormErrors which is usually returned from
-      * validators.
-      *
-      * @param errors
-      *   A non empty chain of FormErrors.
-      * @return
-      *   A single FormErrors instance containing all the errors.
-      */
-    def fromNec(errors: NonEmptyChain[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
-
-    /** Create a single FormErrors instance from a given non empty list of FormErrors which is usually returned from
-      * validators.
-      *
-      * @param errors
-      *   A non empty list of FormErrors.
-      * @return
-      *   A single FormErrors instance containing all the errors.
-      */
-    def fromNel(errors: NonEmptyList[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
-  }
-
-  opaque type FormField = String
-  object FormField {
-
-    /** Create an instance of FormField from the given String type.
-      *
-      * @param source
-      *   An instance of type String which will be returned as a FormField.
-      * @return
-      *   The appropriate instance of FormField.
-      */
-    def apply(source: String): FormField = source
-
-    /** Try to create an instance of FormField from the given String.
-      *
-      * @param source
-      *   A String that should fulfil the requirements to be converted into a FormField.
-      * @return
-      *   An option to the successfully converted FormField.
-      */
-    def from(source: String): Option[FormField] =
-      Option(source).map(_.trim.nonEmpty) match {
-        case Some(true) => Option(source.trim)
-        case _          => None
-      }
-  }
-
-  given Conversion[FormField, String] = _.toString
-
-  opaque type FormFieldError = String
-  object FormFieldError {
-
-    /** Create an instance of FormFieldError from the given String type.
-      *
-      * @param source
-      *   An instance of type String which will be returned as a FormFieldError.
-      * @return
-      *   The appropriate instance of FormFieldError.
-      */
-    def apply(source: String): FormFieldError = source
-
-    /** Try to create an instance of FormFieldError from the given String.
-      *
-      * @param source
-      *   A String that should fulfil the requirements to be converted into a FormFieldError.
-      * @return
-      *   An option to the successfully converted FormFieldError.
-      */
-    def from(source: String): Option[FormFieldError] =
-      Option(source).map(_.trim.nonEmpty) match {
-        case Some(true) => Option(source.trim)
-        case _          => None
-      }
-  }
+    type FormErrors = Map[FormField, List[FormFieldError]]
+    object FormErrors {
+        val empty: FormErrors = Map.empty[FormField, List[FormFieldError]]
+
+        /** Create a single FormErrors instance from a given non empty chain of FormErrors which is usually returned
+          * from validators.
+          *
+          * @param errors
+          *   A non empty chain of FormErrors.
+          * @return
+          *   A single FormErrors instance containing all the errors.
+          */
+        def fromNec(errors: NonEmptyChain[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
+
+        /** Create a single FormErrors instance from a given non empty list of FormErrors which is usually returned from
+          * validators.
+          *
+          * @param errors
+          *   A non empty list of FormErrors.
+          * @return
+          *   A single FormErrors instance containing all the errors.
+          */
+        def fromNel(errors: NonEmptyList[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
+    }
+
+    opaque type FormField = String
+    object FormField {
+
+        /** Create an instance of FormField from the given String type.
+          *
+          * @param source
+          *   An instance of type String which will be returned as a FormField.
+          * @return
+          *   The appropriate instance of FormField.
+          */
+        def apply(source: String): FormField = source
+
+        /** Try to create an instance of FormField from the given String.
+          *
+          * @param source
+          *   A String that should fulfil the requirements to be converted into a FormField.
+          * @return
+          *   An option to the successfully converted FormField.
+          */
+        def from(source: String): Option[FormField] =
+            Option(source).map(_.trim.nonEmpty) match {
+                case Some(true) => Option(source.trim)
+                case _          => None
+            }
+    }
+
+    given Conversion[FormField, String] = _.toString
+
+    opaque type FormFieldError = String
+    object FormFieldError {
+
+        /** Create an instance of FormFieldError from the given String type.
+          *
+          * @param source
+          *   An instance of type String which will be returned as a FormFieldError.
+          * @return
+          *   The appropriate instance of FormFieldError.
+          */
+        def apply(source: String): FormFieldError = source
+
+        /** Try to create an instance of FormFieldError from the given String.
+          *
+          * @param source
+          *   A String that should fulfil the requirements to be converted into a FormFieldError.
+          * @return
+          *   An option to the successfully converted FormFieldError.
+          */
+        def from(source: String): Option[FormFieldError] =
+            Option(source).map(_.trim.nonEmpty) match {
+                case Some(true) => Option(source.trim)
+                case _          => None
+            }
+    }
 
-  given Conversion[FormFieldError, String] = _.toString
+    given Conversion[FormFieldError, String] = _.toString
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/HubServer.scala	2025-01-13 17:13:25.052470972 +0000
@@ -55,7 +55,7 @@
   *   A list of subcommands available.
   */
 enum HubCommand(val help: String, val subcommands: List[HubSubcommand]) {
-  case user extends HubCommand(help = "Manage user accounts.", subcommands = List(HubSubcommand.list))
+    case user extends HubCommand(help = "Manage user accounts.", subcommands = List(HubSubcommand.list))
 }
 
 /** Subcommands that are grouped below a [[HubCommand]].
@@ -66,27 +66,27 @@
   *   A string containing basic help information for the subcommand.
   */
 enum HubSubcommand(val arguments: List[HubArgument], val help: String) {
-  case delete
-      extends HubSubcommand(
-        arguments = List(HubArgument.DryRun, HubArgument.Username),
-        help = "Delete the account from the database. This is not reversible!"
-      )
-  case find
-      extends HubSubcommand(
-        arguments = List(HubArgument.Email, HubArgument.Username),
-        help = "Find a user account in the database."
-      )
-  case list extends HubSubcommand(arguments = Nil, help = "List all accounts in the database.")
-  case lock
-      extends HubSubcommand(
-        arguments = List(HubArgument.DryRun, HubArgument.Username),
-        help = "Lock the account in the database."
-      )
-  case unlock
-      extends HubSubcommand(
-        arguments = List(HubArgument.DryRun, HubArgument.Username),
-        help = "Unlock the account in the database."
-      )
+    case delete
+        extends HubSubcommand(
+            arguments = List(HubArgument.DryRun, HubArgument.Username),
+            help = "Delete the account from the database. This is not reversible!"
+        )
+    case find
+        extends HubSubcommand(
+            arguments = List(HubArgument.Email, HubArgument.Username),
+            help = "Find a user account in the database."
+        )
+    case list extends HubSubcommand(arguments = Nil, help = "List all accounts in the database.")
+    case lock
+        extends HubSubcommand(
+            arguments = List(HubArgument.DryRun, HubArgument.Username),
+            help = "Lock the account in the database."
+        )
+    case unlock
+        extends HubSubcommand(
+            arguments = List(HubArgument.DryRun, HubArgument.Username),
+            help = "Unlock the account in the database."
+        )
 }
 
 /** Possible arguments for commands and subcommands.
@@ -95,9 +95,9 @@
   *   A string containing basic help information for the subcommand.
   */
 enum HubArgument(val help: String) {
-  case DryRun   extends HubArgument(help = "--dry-run")
-  case Email    extends HubArgument(help = "<email>")
-  case Username extends HubArgument(help = "<username>")
+    case DryRun   extends HubArgument(help = "--dry-run")
+    case Email    extends HubArgument(help = "<email>")
+    case Username extends HubArgument(help = "<username>")
 }
 
 /** This is the main entry point for the hub service.
@@ -105,82 +105,83 @@
   * It initialises the application (configuration parsing, database migrations) and starts the HTTP service eventually.
   */
 object HubServer extends IOApp with AuthenticationMiddleware {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given org.typelevel.log4cats.LoggerFactory[IO] = org.typelevel.log4cats.slf4j.Slf4jFactory.create[IO]
+    given org.typelevel.log4cats.LoggerFactory[IO] = org.typelevel.log4cats.slf4j.Slf4jFactory.create[IO]
 
-  /** Create a function to perform the origin checks for the CSRF protection middleware using the configuration.
-    *
-    * @param allowedOrigins
-    *   A list of configuration settings describing the external url configuration for a service. These settings are
-    *   affecting how a service will communicate several information to the "outside world" e.g. if it runs behind a
-    *   reverse proxy. These settings are needed to validate the correct values for expected hostname, port and
-    *   transport scheme of the checked requests.
-    * @return
-    *   A function which will check the correct origin of requests / cookies inside the CSRF middleware.
-    */
-  private def createCsrfOriginCheck(allowedOrigins: NonEmptyList[ExternalUrlConfiguration]): Request[IO] => Boolean = {
-    request =>
-      allowedOrigins.exists { linkConfig =>
-        CSRF.defaultOriginCheck(request, linkConfig.host.toString, linkConfig.scheme, linkConfig.port.map(_.value))
-      }
-  }
+    /** Create a function to perform the origin checks for the CSRF protection middleware using the configuration.
+      *
+      * @param allowedOrigins
+      *   A list of configuration settings describing the external url configuration for a service. These settings are
+      *   affecting how a service will communicate several information to the "outside world" e.g. if it runs behind a
+      *   reverse proxy. These settings are needed to validate the correct values for expected hostname, port and
+      *   transport scheme of the checked requests.
+      * @return
+      *   A function which will check the correct origin of requests / cookies inside the CSRF middleware.
+      */
+    private def createCsrfOriginCheck(
+        allowedOrigins: NonEmptyList[ExternalUrlConfiguration]
+    ): Request[IO] => Boolean = { request =>
+        allowedOrigins.exists { linkConfig =>
+            CSRF.defaultOriginCheck(request, linkConfig.host.toString, linkConfig.scheme, linkConfig.port.map(_.value))
+        }
+    }
 
-  /** Try to load the CSRF key from the given path. If it doesn't exist or fails then a new key is generated and stored
-    * in the file.
-    *
-    * @param csrfKeyFile
-    *   A file which contains the key used to build the CSRF protection.
-    * @return
-    *   A byte vector containing the key used for CSRF protection.
-    */
-  private def loadOrCreateCsrfKey(csrfKeyFile: java.nio.file.Path): IO[ByteVector] =
-    for {
-      _ <- IO(
-        Files.createDirectories(
-          csrfKeyFile.getParent,
-          PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x"))
-        )
-      )
-      key <- Files.exists(csrfKeyFile) match {
-        case false => CSRF.generateSigningKey[IO]()
-        case true  => IO(ByteVector(Files.readAllBytes(csrfKeyFile)))
-      }
-      _ <- IO(Files.write(csrfKeyFile, key.toArray))
-      _ <- IO(Files.setPosixFilePermissions(csrfKeyFile, PosixFilePermissions.fromString("rw-------")))
-    } yield key
+    /** Try to load the CSRF key from the given path. If it doesn't exist or fails then a new key is generated and
+      * stored in the file.
+      *
+      * @param csrfKeyFile
+      *   A file which contains the key used to build the CSRF protection.
+      * @return
+      *   A byte vector containing the key used for CSRF protection.
+      */
+    private def loadOrCreateCsrfKey(csrfKeyFile: java.nio.file.Path): IO[ByteVector] =
+        for {
+            _ <- IO(
+                Files.createDirectories(
+                    csrfKeyFile.getParent,
+                    PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-xr-x"))
+                )
+            )
+            key <- Files.exists(csrfKeyFile) match {
+                case false => CSRF.generateSigningKey[IO]()
+                case true  => IO(ByteVector(Files.readAllBytes(csrfKeyFile)))
+            }
+            _ <- IO(Files.write(csrfKeyFile, key.toArray))
+            _ <- IO(Files.setPosixFilePermissions(csrfKeyFile, PosixFilePermissions.fromString("rw-------")))
+        } yield key
 
-  /** Run migrations on the databases in the given configurations and return their results.
-    *
-    * @param hubConfiguration
-    *   Configuration for the Smederee hub service.
-    * @param ticketsConfiguration
-    *   Configuration for the Smederee ticket service.
-    * @return
-    *   A tuple holding the migration results.
-    */
-  private def migrateDatabases(
-      hubConfiguration: SmedereeHubConfig,
-      ticketsConfiguration: SmedereeTicketsConfiguration
-  ) = {
-    val hubDatabaseMigrator     = new de.smederee.hub.DatabaseMigrator[IO]
-    val ticketsDatabaseMigrator = new de.smederee.tickets.config.DatabaseMigrator[IO]
-    for {
-      hubMigrations <- hubDatabaseMigrator.migrate(
-        hubConfiguration.database.url,
-        hubConfiguration.database.user,
-        hubConfiguration.database.pass
-      )
-      ticketMigrations <- ticketsDatabaseMigrator.migrate(
-        ticketsConfiguration.database.url,
-        ticketsConfiguration.database.user,
-        ticketsConfiguration.database.pass
-      )
-    } yield (hubMigrations, ticketMigrations)
-  }
+    /** Run migrations on the databases in the given configurations and return their results.
+      *
+      * @param hubConfiguration
+      *   Configuration for the Smederee hub service.
+      * @param ticketsConfiguration
+      *   Configuration for the Smederee ticket service.
+      * @return
+      *   A tuple holding the migration results.
+      */
+    private def migrateDatabases(
+        hubConfiguration: SmedereeHubConfig,
+        ticketsConfiguration: SmedereeTicketsConfiguration
+    ) = {
+        val hubDatabaseMigrator     = new de.smederee.hub.DatabaseMigrator[IO]
+        val ticketsDatabaseMigrator = new de.smederee.tickets.config.DatabaseMigrator[IO]
+        for {
+            hubMigrations <- hubDatabaseMigrator.migrate(
+                hubConfiguration.database.url,
+                hubConfiguration.database.user,
+                hubConfiguration.database.pass
+            )
+            ticketMigrations <- ticketsDatabaseMigrator.migrate(
+                ticketsConfiguration.database.url,
+                ticketsConfiguration.database.user,
+                ticketsConfiguration.database.pass
+            )
+        } yield (hubMigrations, ticketMigrations)
+    }
 
-  private def generateHelp(): String =
-    s"""
+    private def generateHelp(): String =
+        s"""
        |user delete\t[--dry-run] <name>\t- Delete a locked(!) user account from the database.
        |user find\t<email> || <name>\t- Find a user account with the given email or name.
        |user list\t\t\t\t- List all unlocked(!) user accounts in the database.
@@ -188,323 +189,371 @@
        |user unlock\t[--dry-run] <name>\t- Unlock the user account.
        |""".stripMargin
 
-  def run(args: List[String]): IO[ExitCode] = {
-    val _ = Locale.setDefault(Locale.ENGLISH) // TODO: Make this configurable.
-    val hubConfiguration = ConfigSource
-      .fromConfig(ConfigFactory.load(getClass.getClassLoader))
-      .at(SmedereeHubConfig.location)
-      .loadOrThrow[SmedereeHubConfig]
-    args match {
-      case List("help") | List("--help") | List("-h") =>
-        // TODO: Add help information!
-        for {
-          _ <- IO(println("Welcome to the Smederee Hub Server."))
-          _ <- IO(println("==================================="))
-          _ <- IO(println("To simply start the server just run this command without arguments."))
-          _ <- IO(println(generateHelp()))
-        } yield ExitCode.Success
-      case commandString :: parameters =>
-        val hubTransactor = Transactor.fromDriverManager[IO](
-          driver = hubConfiguration.database.driver,
-          url = hubConfiguration.database.url,
-          user = hubConfiguration.database.user,
-          password = hubConfiguration.database.pass,
-          logHandler = None
-        )
-        val authenticationRepo = new DoobieAuthenticationRepository[IO](hubTransactor)
-        HubCommand.valueOf(commandString.toLowerCase(Locale.ROOT)) match {
-          case HubCommand.user =>
-            parameters match {
-              case subcommandString :: arguments =>
-                HubSubcommand.valueOf(subcommandString.toLowerCase(Locale.ROOT)) match {
-                  case HubSubcommand.delete =>
-                    val accountManagementRepo = new DoobieAccountManagementRepository[IO](hubTransactor)
-                    val dryRun                = arguments.headOption.exists(_ === "--dry-run")
-                    val name =
-                      if (dryRun)
-                        arguments.drop(1).headOption.flatMap(Username.from)
-                      else
-                        arguments.headOption.flatMap(Username.from)
-                    for {
-                      user <- name.traverse(name => authenticationRepo.findLockedAccount(name)(None))
-                      _ <- user.flatten.fold(IO(println("No such locked user account found!")))(account =>
-                        IO(println(s"Going to delete user ${account.name} (${account.email})!"))
-                      )
-                      deleted <-
-                        if (dryRun)
-                          IO.pure(0.some)
-                        else
-                          user.flatten.traverse(account => accountManagementRepo.deleteAccount(account.uid))
-                      result = deleted.getOrElse(0) match {
-                        case 0 => ExitCode.Error // If not database rows were updated we assume an error.
-                        case _ => ExitCode.Success
-                      }
-                    } yield result
-                  case HubSubcommand.find =>
-                    val email = arguments.headOption.flatMap(EmailAddress.from)
-                    val name  = arguments.headOption.flatMap(Username.from)
-                    for {
-                      userByEmail <- email.traverse(authenticationRepo.findAccountByEmail)
-                      userByName  <- name.traverse(authenticationRepo.findAccountByName)
-                      users = List(userByEmail.flatten, userByName.flatten).collect { case Some(user) =>
-                        user
-                      }
-                      _ <- IO(
-                        users.map(user =>
-                          println(s"${user.name},${user.email},${user.validatedEmail},${user.language}")
-                        )
-                      )
-                    } yield ExitCode.Success
-                  case HubSubcommand.list =>
-                    val users = authenticationRepo.allAccounts()
-                    users
-                      .foreach(user =>
-                        IO(println(s"${user.name},${user.email},${user.validatedEmail},${user.language}"))
-                      )
-                      .compile
-                      .drain
-                      .as(ExitCode.Success)
-                  case HubSubcommand.lock =>
-                    val dryRun = arguments.headOption.exists(_ === "--dry-run")
-                    val name =
-                      if (dryRun)
-                        arguments.drop(1).headOption.flatMap(Username.from)
-                      else
-                        arguments.headOption.flatMap(Username.from)
-                    for {
-                      user <- name.traverse(authenticationRepo.findAccountByName)
-                      _ <- user.flatten.fold(IO(println("No such unlocked user account found!")))(account =>
-                        IO(println(s"Going to lock user ${account.name} (${account.email})!"))
-                      )
-                      locked <-
-                        if (dryRun)
-                          IO.pure(0.some)
-                        else
-                          user.flatten.traverse(account =>
-                            authenticationRepo.lockAccount(account.uid)(UnlockToken.generate.some)
-                          )
-                      result = locked.getOrElse(0) match {
-                        case 0 => ExitCode.Error // If not database rows were updated we assume an error.
-                        case _ => ExitCode.Success
-                      }
-                    } yield result
-                  case HubSubcommand.unlock =>
-                    val dryRun = arguments.headOption.exists(_ === "--dry-run")
-                    val name =
-                      if (dryRun)
-                        arguments.drop(1).headOption.flatMap(Username.from)
-                      else
-                        arguments.headOption.flatMap(Username.from)
-                    for {
-                      user <- name.traverse(name => authenticationRepo.findLockedAccount(name)(None))
-                      _ <- user.flatten.fold(IO(println("No such locked user account found!")))(account =>
-                        IO(println(s"Going to unlock user ${account.name} (${account.email})!"))
-                      )
-                      unlocked <-
-                        if (dryRun)
-                          IO.pure(0.some)
-                        else
-                          user.flatten.traverse(account => authenticationRepo.unlockAccount(account.uid))
-                      result = unlocked.getOrElse(0) match {
-                        case 0 => ExitCode.Error // If not database rows were updated we assume an error.
-                        case _ => ExitCode.Success
-                      }
-                    } yield result
+    def run(args: List[String]): IO[ExitCode] = {
+        val _ = Locale.setDefault(Locale.ENGLISH) // TODO: Make this configurable.
+        val hubConfiguration = ConfigSource
+            .fromConfig(ConfigFactory.load(getClass.getClassLoader))
+            .at(SmedereeHubConfig.location)
+            .loadOrThrow[SmedereeHubConfig]
+        args match {
+            case List("help") | List("--help") | List("-h") =>
+                // TODO: Add help information!
+                for {
+                    _ <- IO(println("Welcome to the Smederee Hub Server."))
+                    _ <- IO(println("==================================="))
+                    _ <- IO(println("To simply start the server just run this command without arguments."))
+                    _ <- IO(println(generateHelp()))
+                } yield ExitCode.Success
+            case commandString :: parameters =>
+                val hubTransactor = Transactor.fromDriverManager[IO](
+                    driver = hubConfiguration.database.driver,
+                    url = hubConfiguration.database.url,
+                    user = hubConfiguration.database.user,
+                    password = hubConfiguration.database.pass,
+                    logHandler = None
+                )
+                val authenticationRepo = new DoobieAuthenticationRepository[IO](hubTransactor)
+                HubCommand.valueOf(commandString.toLowerCase(Locale.ROOT)) match {
+                    case HubCommand.user =>
+                        parameters match {
+                            case subcommandString :: arguments =>
+                                HubSubcommand.valueOf(subcommandString.toLowerCase(Locale.ROOT)) match {
+                                    case HubSubcommand.delete =>
+                                        val accountManagementRepo =
+                                            new DoobieAccountManagementRepository[IO](hubTransactor)
+                                        val dryRun = arguments.headOption.exists(_ === "--dry-run")
+                                        val name =
+                                            if (dryRun)
+                                                arguments.drop(1).headOption.flatMap(Username.from)
+                                            else
+                                                arguments.headOption.flatMap(Username.from)
+                                        for {
+                                            user <- name.traverse(name =>
+                                                authenticationRepo.findLockedAccount(name)(None)
+                                            )
+                                            _ <- user.flatten.fold(IO(println("No such locked user account found!")))(
+                                                account =>
+                                                    IO(
+                                                        println(
+                                                            s"Going to delete user ${account.name} (${account.email})!"
+                                                        )
+                                                    )
+                                            )
+                                            deleted <-
+                                                if (dryRun)
+                                                    IO.pure(0.some)
+                                                else
+                                                    user.flatten.traverse(account =>
+                                                        accountManagementRepo.deleteAccount(account.uid)
+                                                    )
+                                            result = deleted.getOrElse(0) match {
+                                                case 0 =>
+                                                    ExitCode.Error // If not database rows were updated we assume an error.
+                                                case _ => ExitCode.Success
+                                            }
+                                        } yield result
+                                    case HubSubcommand.find =>
+                                        val email = arguments.headOption.flatMap(EmailAddress.from)
+                                        val name  = arguments.headOption.flatMap(Username.from)
+                                        for {
+                                            userByEmail <- email.traverse(authenticationRepo.findAccountByEmail)
+                                            userByName  <- name.traverse(authenticationRepo.findAccountByName)
+                                            users = List(userByEmail.flatten, userByName.flatten).collect {
+                                                case Some(user) =>
+                                                    user
+                                            }
+                                            _ <- IO(
+                                                users.map(user =>
+                                                    println(
+                                                        s"${user.name},${user.email},${user.validatedEmail},${user.language}"
+                                                    )
+                                                )
+                                            )
+                                        } yield ExitCode.Success
+                                    case HubSubcommand.list =>
+                                        val users = authenticationRepo.allAccounts()
+                                        users
+                                            .foreach(user =>
+                                                IO(
+                                                    println(
+                                                        s"${user.name},${user.email},${user.validatedEmail},${user.language}"
+                                                    )
+                                                )
+                                            )
+                                            .compile
+                                            .drain
+                                            .as(ExitCode.Success)
+                                    case HubSubcommand.lock =>
+                                        val dryRun = arguments.headOption.exists(_ === "--dry-run")
+                                        val name =
+                                            if (dryRun)
+                                                arguments.drop(1).headOption.flatMap(Username.from)
+                                            else
+                                                arguments.headOption.flatMap(Username.from)
+                                        for {
+                                            user <- name.traverse(authenticationRepo.findAccountByName)
+                                            _ <- user.flatten.fold(IO(println("No such unlocked user account found!")))(
+                                                account =>
+                                                    IO(
+                                                        println(
+                                                            s"Going to lock user ${account.name} (${account.email})!"
+                                                        )
+                                                    )
+                                            )
+                                            locked <-
+                                                if (dryRun)
+                                                    IO.pure(0.some)
+                                                else
+                                                    user.flatten.traverse(account =>
+                                                        authenticationRepo.lockAccount(account.uid)(
+                                                            UnlockToken.generate.some
+                                                        )
+                                                    )
+                                            result = locked.getOrElse(0) match {
+                                                case 0 =>
+                                                    ExitCode.Error // If not database rows were updated we assume an error.
+                                                case _ => ExitCode.Success
+                                            }
+                                        } yield result
+                                    case HubSubcommand.unlock =>
+                                        val dryRun = arguments.headOption.exists(_ === "--dry-run")
+                                        val name =
+                                            if (dryRun)
+                                                arguments.drop(1).headOption.flatMap(Username.from)
+                                            else
+                                                arguments.headOption.flatMap(Username.from)
+                                        for {
+                                            user <- name.traverse(name =>
+                                                authenticationRepo.findLockedAccount(name)(None)
+                                            )
+                                            _ <- user.flatten.fold(IO(println("No such locked user account found!")))(
+                                                account =>
+                                                    IO(
+                                                        println(
+                                                            s"Going to unlock user ${account.name} (${account.email})!"
+                                                        )
+                                                    )
+                                            )
+                                            unlocked <-
+                                                if (dryRun)
+                                                    IO.pure(0.some)
+                                                else
+                                                    user.flatten.traverse(account =>
+                                                        authenticationRepo.unlockAccount(account.uid)
+                                                    )
+                                            result = unlocked.getOrElse(0) match {
+                                                case 0 =>
+                                                    ExitCode.Error // If not database rows were updated we assume an error.
+                                                case _ => ExitCode.Success
+                                            }
+                                        } yield result
+                                }
+                            case _ => IO.raiseError(new IllegalArgumentException("Missing subcommand/options!"))
+                        }
                 }
-              case _ => IO.raiseError(new IllegalArgumentException("Missing subcommand/options!"))
-            }
+            case _ =>
+                for {
+                    ticketsConfiguration <- IO(
+                        ConfigSource
+                            .fromConfig(ConfigFactory.load(getClass.getClassLoader))
+                            .at(SmedereeTicketsConfiguration.location)
+                            .loadOrThrow[SmedereeTicketsConfiguration]
+                    )
+                    _ <- migrateDatabases(hubConfiguration, ticketsConfiguration)
+                    _ <- IO {
+                        val defaultSecret = "CHANGEME".getBytes(StandardCharsets.UTF_8)
+                        if (
+                            java.util.Arrays.equals(
+                                defaultSecret,
+                                hubConfiguration.service.authentication.cookieSecret.toArray
+                            )
+                        )
+                            log.warn("SERVICE IS USING DEFAULT COOKIE SECRET! PLEASE CONFIGURE A SECURE ONE!")
+                    }
+                    repoCheck <- IO {
+                        val repositoriesDirectory = hubConfiguration.service.darcs.repositoriesDirectory.toPath
+                        if (Files.exists(repositoriesDirectory)) {
+                            if (Files.isDirectory(repositoriesDirectory)) {
+                                Right(s"Using repositories directory at: $repositoriesDirectory")
+                            } else {
+                                Left(
+                                    s"Path to repositories directory exists but is not a directory: $repositoriesDirectory"
+                                )
+                            }
+                        } else {
+                            log.warn(
+                                s"Repositories directory does not exist, trying to create it: $repositoriesDirectory"
+                            )
+                            Files.createDirectories(
+                                repositoriesDirectory,
+                                PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-x---"))
+                            )
+                            Right(s"Using repositories directory at: $repositoriesDirectory")
+                        }
+                    }
+                    _ <- repoCheck match {
+                        case Left(error)    => IO.raiseError(new RuntimeException(error))
+                        case Right(message) => IO(log.info(message))
+                    }
+                    hubTransactor = Transactor.fromDriverManager[IO](
+                        driver = hubConfiguration.database.driver,
+                        url = hubConfiguration.database.url,
+                        user = hubConfiguration.database.user,
+                        password = hubConfiguration.database.pass,
+                        logHandler = None
+                    )
+                    ticketsTransactor = Transactor.fromDriverManager[IO](
+                        driver = ticketsConfiguration.database.driver,
+                        url = ticketsConfiguration.database.url,
+                        user = ticketsConfiguration.database.user,
+                        password = ticketsConfiguration.database.pass,
+                        logHandler = None
+                    )
+                    ticketServiceApi     = new DoobieTicketServiceApi[IO](ticketsTransactor)
+                    ticketLabelsRepo     = new DoobieLabelRepository[IO](ticketsTransactor)
+                    ticketMilestonesRepo = new DoobieMilestoneRepository[IO](ticketsTransactor)
+                    ticketProjectsRepo   = new DoobieProjectRepository[IO](ticketsTransactor)
+                    ticketsRepo          = new DoobieTicketRepository[IO](ticketsTransactor)
+                    ticketRoutes = new TicketRoutes[IO](
+                        ticketsConfiguration,
+                        ticketLabelsRepo,
+                        ticketMilestonesRepo,
+                        ticketProjectsRepo,
+                        ticketsRepo
+                    )
+                    ticketLabelRoutes = new LabelRoutes[IO](ticketsConfiguration, ticketLabelsRepo, ticketProjectsRepo)
+                    ticketMilestoneRoutes = new MilestoneRoutes[IO](
+                        ticketsConfiguration,
+                        ticketMilestonesRepo,
+                        ticketProjectsRepo
+                    )
+                    cryptoClock = java.time.Clock.systemUTC
+                    csrfKey <- loadOrCreateCsrfKey(hubConfiguration.service.csrfKeyFile)
+                    csrfOriginCheck = createCsrfOriginCheck(
+                        NonEmptyList(hubConfiguration.service.external, List(ticketsConfiguration.externalUrl))
+                    )
+                    csrfBuilder = CSRF[IO, IO](csrfKey, csrfOriginCheck)
+                    /* The idea behind the `onFailure` part of the CSRF protection middleware is
+                     * that we simply remove the CSRF cookie and redirect the user to the frontpage.
+                     * This is done to avoid frustration for users after a server restart because
+                     * the CSRF secret key will change then and thus all requests are invalid.
+                     */
+                    csrfMiddleware = csrfBuilder
+                        .withCookieDomain(Option(hubConfiguration.service.external.host.toString))
+                        .withCookieName(Constants.csrfCookieName.toString)
+                        .withCookiePath(Option("/"))
+                        .withCSRFCheck(
+                            CSRF.checkCSRFinHeaderAndForm[IO, IO](Constants.csrfCookieName.toString, FunctionK.id)
+                        )
+                        .withOnFailure(
+                            Response[IO](
+                                Status.Forbidden,
+                                entity = Entity.utf8String(de.smederee.hub.views.html.errors.csrfFailed().body)
+                            )
+                        )
+                        .build
+                    signAndValidate = SignAndValidate(hubConfiguration.service.authentication.cookieSecret)
+                    assetsRoutes <- resourceServiceBuilder[IO](Constants.assetsPath.path.toAbsolute.toString).toRoutes
+                    authenticationRepo = new DoobieAuthenticationRepository[IO](hubTransactor)
+                    authenticationWithFallThrough = AuthMiddleware.withFallThrough(
+                        authenticateUserWithFallThrough(
+                            authenticationRepo,
+                            signAndValidate,
+                            hubConfiguration.service.authentication.timeouts
+                        )
+                    )
+                    darcsWrapper          = new DarcsCommands[IO](hubConfiguration.service.darcs.executable)
+                    emailMiddleware       = new SimpleJavaMailMiddleware(hubConfiguration.service.email)
+                    accountManagementRepo = new DoobieAccountManagementRepository[IO](hubTransactor)
+                    accountManagementRoutes = new AccountManagementRoutes[IO](
+                        accountManagementRepo,
+                        hubConfiguration.service,
+                        emailMiddleware,
+                        signAndValidate,
+                        ticketServiceApi
+                    )
+                    authenticationRoutes = new AuthenticationRoutes[IO](
+                        cryptoClock,
+                        hubConfiguration.service.authentication,
+                        hubConfiguration.service.external,
+                        authenticationRepo,
+                        signAndValidate
+                    )
+                    resetPasswordRepo = new DoobieResetPasswordRepository[IO](hubTransactor)
+                    resetPasswordRoutes = new ResetPasswordRoutes[IO](
+                        hubConfiguration.service.authentication,
+                        authenticationRepo,
+                        emailMiddleware,
+                        hubConfiguration.service.external,
+                        resetPasswordRepo
+                    )
+                    signUpRepo      = new DoobieSignupRepository[IO](hubTransactor)
+                    signUpRoutes    = new SignupRoutes[IO](hubConfiguration.service, signUpRepo)
+                    landingPages    = new LandingPageRoutes[IO](hubConfiguration.service)
+                    vcsMetadataRepo = new DoobieVcsMetadataRepository[IO](hubTransactor)
+                    vcsRepoRoutes = new VcsRepositoryRoutes[IO](
+                        hubConfiguration.service,
+                        darcsWrapper,
+                        vcsMetadataRepo,
+                        ticketProjectsRepo
+                    )
+                    protectedRoutesWithFallThrough = authenticationWithFallThrough(
+                        authenticationRoutes.protectedRoutes <+>
+                            accountManagementRoutes.protectedRoutes <+>
+                            resetPasswordRoutes.protectedRoutes <+>
+                            signUpRoutes.protectedRoutes <+>
+                            ticketLabelRoutes.protectedRoutes <+>
+                            ticketMilestoneRoutes.protectedRoutes <+>
+                            ticketRoutes.protectedRoutes <+>
+                            vcsRepoRoutes.protectedRoutes <+>
+                            landingPages.protectedRoutes
+                    )
+                    hubWebService = Router(
+                        Constants.assetsPath.path.toAbsolute.toString -> assetsRoutes,
+                        "/" -> (protectedRoutesWithFallThrough <+>
+                            authenticationRoutes.routes <+>
+                            resetPasswordRoutes.routes <+>
+                            accountManagementRoutes.routes <+>
+                            signUpRoutes.routes <+>
+                            ticketLabelRoutes.routes <+>
+                            ticketMilestoneRoutes.routes <+>
+                            ticketRoutes.routes <+>
+                            vcsRepoRoutes.routes <+>
+                            landingPages.routes)
+                    ).orNotFound
+                    // Create our ssh server fiber (or a dummy one if disabled).
+                    sshServerProvider = hubConfiguration.service.ssh.enabled match {
+                        case false => None
+                        case true =>
+                            Option(
+                                new SshServerProvider(
+                                    hubConfiguration.service.darcs,
+                                    hubConfiguration.database,
+                                    hubConfiguration.service.ssh
+                                )
+                            )
+                    }
+                    sshServer = sshServerProvider.fold(IO.unit.as(ExitCode.Success))(
+                        _.run().use(server =>
+                            IO(log.info(s"SSH-Server started at ${server.getHost()}:${server.getPort()}.")) >> IO.never
+                                .as(
+                                    ExitCode.Success
+                                )
+                        )
+                    )
+                    // Create our webserver fiber.
+                    resource = EmberServerBuilder
+                        .default[IO]
+                        .withHost(hubConfiguration.service.host)
+                        .withPort(hubConfiguration.service.port)
+                        .withHttpApp(csrfMiddleware.validate()(hubWebService))
+                        .build
+                    webServer = resource.use(server =>
+                        IO(log.info("Server started at {}", server.address)) >> IO.never.as(ExitCode.Success)
+                    )
+                    executeFibers <- (webServer, sshServer).parTupled // We run both fibers.
+                    (exitCode, _) = executeFibers
+                } yield exitCode
         }
-      case _ =>
-        for {
-          ticketsConfiguration <- IO(
-            ConfigSource
-              .fromConfig(ConfigFactory.load(getClass.getClassLoader))
-              .at(SmedereeTicketsConfiguration.location)
-              .loadOrThrow[SmedereeTicketsConfiguration]
-          )
-          _ <- migrateDatabases(hubConfiguration, ticketsConfiguration)
-          _ <- IO {
-            val defaultSecret = "CHANGEME".getBytes(StandardCharsets.UTF_8)
-            if (java.util.Arrays.equals(defaultSecret, hubConfiguration.service.authentication.cookieSecret.toArray))
-              log.warn("SERVICE IS USING DEFAULT COOKIE SECRET! PLEASE CONFIGURE A SECURE ONE!")
-          }
-          repoCheck <- IO {
-            val repositoriesDirectory = hubConfiguration.service.darcs.repositoriesDirectory.toPath
-            if (Files.exists(repositoriesDirectory)) {
-              if (Files.isDirectory(repositoriesDirectory)) {
-                Right(s"Using repositories directory at: $repositoriesDirectory")
-              } else {
-                Left(s"Path to repositories directory exists but is not a directory: $repositoriesDirectory")
-              }
-            } else {
-              log.warn(s"Repositories directory does not exist, trying to create it: $repositoriesDirectory")
-              Files.createDirectories(
-                repositoriesDirectory,
-                PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwxr-x---"))
-              )
-              Right(s"Using repositories directory at: $repositoriesDirectory")
-            }
-          }
-          _ <- repoCheck match {
-            case Left(error)    => IO.raiseError(new RuntimeException(error))
-            case Right(message) => IO(log.info(message))
-          }
-          hubTransactor = Transactor.fromDriverManager[IO](
-            driver = hubConfiguration.database.driver,
-            url = hubConfiguration.database.url,
-            user = hubConfiguration.database.user,
-            password = hubConfiguration.database.pass,
-            logHandler = None
-          )
-          ticketsTransactor = Transactor.fromDriverManager[IO](
-            driver = ticketsConfiguration.database.driver,
-            url = ticketsConfiguration.database.url,
-            user = ticketsConfiguration.database.user,
-            password = ticketsConfiguration.database.pass,
-            logHandler = None
-          )
-          ticketServiceApi     = new DoobieTicketServiceApi[IO](ticketsTransactor)
-          ticketLabelsRepo     = new DoobieLabelRepository[IO](ticketsTransactor)
-          ticketMilestonesRepo = new DoobieMilestoneRepository[IO](ticketsTransactor)
-          ticketProjectsRepo   = new DoobieProjectRepository[IO](ticketsTransactor)
-          ticketsRepo          = new DoobieTicketRepository[IO](ticketsTransactor)
-          ticketRoutes = new TicketRoutes[IO](
-            ticketsConfiguration,
-            ticketLabelsRepo,
-            ticketMilestonesRepo,
-            ticketProjectsRepo,
-            ticketsRepo
-          )
-          ticketLabelRoutes = new LabelRoutes[IO](ticketsConfiguration, ticketLabelsRepo, ticketProjectsRepo)
-          ticketMilestoneRoutes = new MilestoneRoutes[IO](
-            ticketsConfiguration,
-            ticketMilestonesRepo,
-            ticketProjectsRepo
-          )
-          cryptoClock = java.time.Clock.systemUTC
-          csrfKey <- loadOrCreateCsrfKey(hubConfiguration.service.csrfKeyFile)
-          csrfOriginCheck = createCsrfOriginCheck(
-            NonEmptyList(hubConfiguration.service.external, List(ticketsConfiguration.externalUrl))
-          )
-          csrfBuilder = CSRF[IO, IO](csrfKey, csrfOriginCheck)
-          /* The idea behind the `onFailure` part of the CSRF protection middleware is
-           * that we simply remove the CSRF cookie and redirect the user to the frontpage.
-           * This is done to avoid frustration for users after a server restart because
-           * the CSRF secret key will change then and thus all requests are invalid.
-           */
-          csrfMiddleware = csrfBuilder
-            .withCookieDomain(Option(hubConfiguration.service.external.host.toString))
-            .withCookieName(Constants.csrfCookieName.toString)
-            .withCookiePath(Option("/"))
-            .withCSRFCheck(CSRF.checkCSRFinHeaderAndForm[IO, IO](Constants.csrfCookieName.toString, FunctionK.id))
-            .withOnFailure(
-              Response[IO](
-                Status.Forbidden,
-                entity = Entity.utf8String(de.smederee.hub.views.html.errors.csrfFailed().body)
-              )
-            )
-            .build
-          signAndValidate = SignAndValidate(hubConfiguration.service.authentication.cookieSecret)
-          assetsRoutes <- resourceServiceBuilder[IO](Constants.assetsPath.path.toAbsolute.toString).toRoutes
-          authenticationRepo = new DoobieAuthenticationRepository[IO](hubTransactor)
-          authenticationWithFallThrough = AuthMiddleware.withFallThrough(
-            authenticateUserWithFallThrough(
-              authenticationRepo,
-              signAndValidate,
-              hubConfiguration.service.authentication.timeouts
-            )
-          )
-          darcsWrapper          = new DarcsCommands[IO](hubConfiguration.service.darcs.executable)
-          emailMiddleware       = new SimpleJavaMailMiddleware(hubConfiguration.service.email)
-          accountManagementRepo = new DoobieAccountManagementRepository[IO](hubTransactor)
-          accountManagementRoutes = new AccountManagementRoutes[IO](
-            accountManagementRepo,
-            hubConfiguration.service,
-            emailMiddleware,
-            signAndValidate,
-            ticketServiceApi
-          )
-          authenticationRoutes = new AuthenticationRoutes[IO](
-            cryptoClock,
-            hubConfiguration.service.authentication,
-            hubConfiguration.service.external,
-            authenticationRepo,
-            signAndValidate
-          )
-          resetPasswordRepo = new DoobieResetPasswordRepository[IO](hubTransactor)
-          resetPasswordRoutes = new ResetPasswordRoutes[IO](
-            hubConfiguration.service.authentication,
-            authenticationRepo,
-            emailMiddleware,
-            hubConfiguration.service.external,
-            resetPasswordRepo
-          )
-          signUpRepo      = new DoobieSignupRepository[IO](hubTransactor)
-          signUpRoutes    = new SignupRoutes[IO](hubConfiguration.service, signUpRepo)
-          landingPages    = new LandingPageRoutes[IO](hubConfiguration.service)
-          vcsMetadataRepo = new DoobieVcsMetadataRepository[IO](hubTransactor)
-          vcsRepoRoutes = new VcsRepositoryRoutes[IO](
-            hubConfiguration.service,
-            darcsWrapper,
-            vcsMetadataRepo,
-            ticketProjectsRepo
-          )
-          protectedRoutesWithFallThrough = authenticationWithFallThrough(
-            authenticationRoutes.protectedRoutes <+>
-              accountManagementRoutes.protectedRoutes <+>
-              resetPasswordRoutes.protectedRoutes <+>
-              signUpRoutes.protectedRoutes <+>
-              ticketLabelRoutes.protectedRoutes <+>
-              ticketMilestoneRoutes.protectedRoutes <+>
-              ticketRoutes.protectedRoutes <+>
-              vcsRepoRoutes.protectedRoutes <+>
-              landingPages.protectedRoutes
-          )
-          hubWebService = Router(
-            Constants.assetsPath.path.toAbsolute.toString -> assetsRoutes,
-            "/" -> (protectedRoutesWithFallThrough <+>
-              authenticationRoutes.routes <+>
-              resetPasswordRoutes.routes <+>
-              accountManagementRoutes.routes <+>
-              signUpRoutes.routes <+>
-              ticketLabelRoutes.routes <+>
-              ticketMilestoneRoutes.routes <+>
-              ticketRoutes.routes <+>
-              vcsRepoRoutes.routes <+>
-              landingPages.routes)
-          ).orNotFound
-          // Create our ssh server fiber (or a dummy one if disabled).
-          sshServerProvider = hubConfiguration.service.ssh.enabled match {
-            case false => None
-            case true =>
-              Option(
-                new SshServerProvider(
-                  hubConfiguration.service.darcs,
-                  hubConfiguration.database,
-                  hubConfiguration.service.ssh
-                )
-              )
-          }
-          sshServer = sshServerProvider.fold(IO.unit.as(ExitCode.Success))(
-            _.run().use(server =>
-              IO(log.info(s"SSH-Server started at ${server.getHost()}:${server.getPort()}.")) >> IO.never.as(
-                ExitCode.Success
-              )
-            )
-          )
-          // Create our webserver fiber.
-          resource = EmberServerBuilder
-            .default[IO]
-            .withHost(hubConfiguration.service.host)
-            .withPort(hubConfiguration.service.port)
-            .withHttpApp(csrfMiddleware.validate()(hubWebService))
-            .build
-          webServer = resource.use(server =>
-            IO(log.info("Server started at {}", server.address)) >> IO.never.as(ExitCode.Success)
-          )
-          executeFibers <- (webServer, sshServer).parTupled // We run both fibers.
-          (exitCode, _) = executeFibers
-        } yield exitCode
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/LandingPageRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/LandingPageRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/LandingPageRoutes.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/LandingPageRoutes.scala	2025-01-13 17:13:25.052470972 +0000
@@ -39,114 +39,125 @@
   *   A higher kinded type providing needed functionality, which is usually an IO monad like Async or Sync.
   */
 final class LandingPageRoutes[F[_]: Async](configuration: ServiceConfig) extends Http4sDsl[F] {
-  private val linkConfig = configuration.external
-  // The base URI for our site which that be passed into some templates which create links themselfes.
-  private val baseUri = linkConfig.createFullUri(Uri())
-  // The URL that shall be used in the `action` field of the form.
-  private val signupUri = linkConfig.createFullUri(uri"/signup")
-
-  private val contact: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / "contact" as user =>
-    for {
-      csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-      language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-      resp     <- Ok(views.html.contact(baseUri, lang = language)(csrf, "Smederee - Contact".some, user.some))
-    } yield resp
-  }
-
-  private val contactForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "contact" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.contact(baseUri)(csrf, "Smederee - Contact".some))
-    } yield resp
-  }
-
-  private val imprint: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / "imprint" as user =>
-    for {
-      csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-      language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-      resp <- Ok(views.html.imprint(baseUri, lang = language)(csrf, "Smederee - Imprint / Impressum".some, user.some))
-    } yield resp
-  }
-
-  private val imprintForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "imprint" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.imprint(baseUri)(csrf, "Smederee - Imprint / Impressum".some))
-    } yield resp
-  }
-
-  private val mainSite: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root as user =>
-    for {
-      csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-      language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-      resp <- Ok(
-        views.html.index(baseUri, lang = language)()(signupUri, csrf, "Welcome to the Smederee!".some, user.some)
-      )
-    } yield resp
-  }
-
-  private val mainSiteForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.index(baseUri)()(signupUri, csrf, "Welcome to the Smederee!".some))
-    } yield resp
-  }
-
-  private val privacyPolicy: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / "privacy-policy" as user =>
-      for {
-        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        resp <- Ok(
-          views.html.privacyPolicy(baseUri, lang = language)(csrf, "Smederee - Privacy Policy".some, user.some)
-        )
-      } yield resp
-  }
-
-  private val privacyPolicyForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "privacy-policy" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.privacyPolicy(baseUri)(csrf, "Smederee - Privacy Policy".some))
-    } yield resp
-  }
-
-  private val publicAlpha: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / "public-alpha" as user =>
-      for {
-        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        resp <- Ok(
-          views.html
-            .publicAlpha(baseUri, lang = language)(csrf, "Smederee - Details about our public alpha.".some, user.some)
-        )
-      } yield resp
-  }
-
-  private val publicAlphaForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "public-alpha" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.publicAlpha(baseUri)(csrf, "Smederee - Details about our public alpha.".some))
-    } yield resp
-  }
-
-  private val termsOfUse: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / "terms-of-use" as user =>
-    for {
-      csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-      language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-      resp     <- Ok(views.html.termsOfUse(baseUri, lang = language)(csrf, "Smederee - Terms of Use".some, user.some))
-    } yield resp
-  }
-
-  private val termsOfUseForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "terms-of-use" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.termsOfUse(baseUri)(csrf, "Smederee - Terms of Use".some))
-    } yield resp
-  }
+    private val linkConfig = configuration.external
+    // The base URI for our site which that be passed into some templates which create links themselfes.
+    private val baseUri = linkConfig.createFullUri(Uri())
+    // The URL that shall be used in the `action` field of the form.
+    private val signupUri = linkConfig.createFullUri(uri"/signup")
+
+    private val contact: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / "contact" as user =>
+        for {
+            csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+            language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+            resp     <- Ok(views.html.contact(baseUri, lang = language)(csrf, "Smederee - Contact".some, user.some))
+        } yield resp
+    }
+
+    private val contactForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "contact" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.contact(baseUri)(csrf, "Smederee - Contact".some))
+        } yield resp
+    }
+
+    private val imprint: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root / "imprint" as user =>
+        for {
+            csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+            language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+            resp <- Ok(
+                views.html.imprint(baseUri, lang = language)(csrf, "Smederee - Imprint / Impressum".some, user.some)
+            )
+        } yield resp
+    }
+
+    private val imprintForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "imprint" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.imprint(baseUri)(csrf, "Smederee - Imprint / Impressum".some))
+        } yield resp
+    }
+
+    private val mainSite: AuthedRoutes[Account, F] = AuthedRoutes.of { case ar @ GET -> Root as user =>
+        for {
+            csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+            language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+            resp <- Ok(
+                views.html
+                    .index(baseUri, lang = language)()(signupUri, csrf, "Welcome to the Smederee!".some, user.some)
+            )
+        } yield resp
+    }
+
+    private val mainSiteForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.index(baseUri)()(signupUri, csrf, "Welcome to the Smederee!".some))
+        } yield resp
+    }
+
+    private val privacyPolicy: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "privacy-policy" as user =>
+            for {
+                csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                resp <- Ok(
+                    views.html
+                        .privacyPolicy(baseUri, lang = language)(csrf, "Smederee - Privacy Policy".some, user.some)
+                )
+            } yield resp
+    }
+
+    private val privacyPolicyForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "privacy-policy" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.privacyPolicy(baseUri)(csrf, "Smederee - Privacy Policy".some))
+        } yield resp
+    }
+
+    private val publicAlpha: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "public-alpha" as user =>
+            for {
+                csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                resp <- Ok(
+                    views.html
+                        .publicAlpha(baseUri, lang = language)(
+                            csrf,
+                            "Smederee - Details about our public alpha.".some,
+                            user.some
+                        )
+                )
+            } yield resp
+    }
+
+    private val publicAlphaForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "public-alpha" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.publicAlpha(baseUri)(csrf, "Smederee - Details about our public alpha.".some))
+        } yield resp
+    }
+
+    private val termsOfUse: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "terms-of-use" as user =>
+            for {
+                csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                resp <- Ok(
+                    views.html.termsOfUse(baseUri, lang = language)(csrf, "Smederee - Terms of Use".some, user.some)
+                )
+            } yield resp
+    }
+
+    private val termsOfUseForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "terms-of-use" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.termsOfUse(baseUri)(csrf, "Smederee - Terms of Use".some))
+        } yield resp
+    }
 
-  val protectedRoutes = contact <+> imprint <+> privacyPolicy <+> publicAlpha <+> termsOfUse <+> mainSite
+    val protectedRoutes = contact <+> imprint <+> privacyPolicy <+> publicAlpha <+> termsOfUse <+> mainSite
 
-  val routes =
-    contactForGuests <+> imprintForGuests <+> privacyPolicyForGuests <+> publicAlphaForGuests <+> termsOfUseForGuests <+> mainSiteForGuests
+    val routes =
+        contactForGuests <+> imprintForGuests <+> privacyPolicyForGuests <+> publicAlphaForGuests <+> termsOfUseForGuests <+> mainSiteForGuests
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/LoginForm.scala	2025-01-13 17:13:25.052470972 +0000
@@ -33,21 +33,21 @@
 final case class LoginForm(name: Username, password: Password)
 
 object LoginForm extends FormValidator[LoginForm] {
-  val fieldName: FormField     = FormField("name")
-  val fieldPassword: FormField = FormField("password")
+    val fieldName: FormField     = FormField("name")
+    val fieldPassword: FormField = FormField("password")
 
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, LoginForm] = {
-    val genericError = FormFieldError("Invalid credentials!") // We just return a generic error.
-    val name: ValidatedNec[FormErrors, Username] = data
-      .get(fieldName)
-      .fold(genericError.invalidNec)(s => Username.from(s).fold(genericError.invalidNec)(_.validNec))
-      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
-    val password: ValidatedNec[FormErrors, Password] = data
-      .get(fieldPassword)
-      .fold(genericError.invalidNec)(s => Password.from(s).fold(genericError.invalidNec)(_.validNec))
-      .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList)))
-    (name, password).mapN { case (n, pw) =>
-      LoginForm(n, pw)
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, LoginForm] = {
+        val genericError = FormFieldError("Invalid credentials!") // We just return a generic error.
+        val name: ValidatedNec[FormErrors, Username] = data
+            .get(fieldName)
+            .fold(genericError.invalidNec)(s => Username.from(s).fold(genericError.invalidNec)(_.validNec))
+            .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+        val password: ValidatedNec[FormErrors, Password] = data
+            .get(fieldPassword)
+            .fold(genericError.invalidNec)(s => Password.from(s).fold(genericError.invalidNec)(_.validNec))
+            .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList)))
+        (name, password).mapN { case (n, pw) =>
+            LoginForm(n, pw)
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala	2025-01-13 17:13:25.052470972 +0000
@@ -47,54 +47,56 @@
 )
 
 object NewVcsRepositoryForm extends FormValidator[NewVcsRepositoryForm] {
-  val fieldDescription: FormField    = FormField("description")
-  val fieldIsPrivate: FormField      = FormField("is_private")
-  val fieldName: FormField           = FormField("name")
-  val fieldTicketsEnabled: FormField = FormField("tickets_enabled")
-  val fieldWebsite: FormField        = FormField("website")
+    val fieldDescription: FormField    = FormField("description")
+    val fieldIsPrivate: FormField      = FormField("is_private")
+    val fieldName: FormField           = FormField("name")
+    val fieldTicketsEnabled: FormField = FormField("tickets_enabled")
+    val fieldWebsite: FormField        = FormField("website")
 
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, NewVcsRepositoryForm] = {
-    val name = data
-      .get(fieldName)
-      .fold(FormFieldError("No repository name given!").invalidNec)(s =>
-        VcsRepositoryName.from(s).fold(FormFieldError("Invalid repository name!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
-    val privateFlag: ValidatedNec[FormErrors, Boolean] =
-      data.get(fieldIsPrivate).fold(false.validNec)(s => s.matches("true").validNec)
-    val description = data
-      .get(fieldDescription)
-      .fold(Option.empty[VcsRepositoryDescription].validNec) { s =>
-        if (s.trim.isEmpty)
-          Option.empty[VcsRepositoryDescription].validNec // Sometimes "empty" strings are sent.
-        else
-          VcsRepositoryDescription
-            .from(s)
-            .fold(FormFieldError("Invalid repository description!").invalidNec)(descr => Option(descr).validNec)
-      }
-      .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
-    val ticketsEnabledFlag: ValidatedNec[FormErrors, Boolean] =
-      data.get(fieldTicketsEnabled).fold(false.validNec)(s => s.matches("true").validNec)
-    val website = data
-      .get(fieldWebsite)
-      .fold(Option.empty[Uri].validNec) { s =>
-        if (s.trim.isEmpty)
-          Option.empty[Uri].validNec // Sometimes "empty" strings are sent.
-        else
-          Uri
-            .fromString(s)
-            .toOption
-            .fold(FormFieldError("Invalid website URI!").invalidNec) { uri =>
-              uri.scheme match {
-                case Some(Uri.Scheme.http) | Some(Uri.Scheme.https) => Option(uri).validNec
-                case _                                              => FormFieldError("Invalid website URI!").invalidNec
-              }
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, NewVcsRepositoryForm] = {
+        val name = data
+            .get(fieldName)
+            .fold(FormFieldError("No repository name given!").invalidNec)(s =>
+                VcsRepositoryName.from(s).fold(FormFieldError("Invalid repository name!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+        val privateFlag: ValidatedNec[FormErrors, Boolean] =
+            data.get(fieldIsPrivate).fold(false.validNec)(s => s.matches("true").validNec)
+        val description = data
+            .get(fieldDescription)
+            .fold(Option.empty[VcsRepositoryDescription].validNec) { s =>
+                if (s.trim.isEmpty)
+                    Option.empty[VcsRepositoryDescription].validNec // Sometimes "empty" strings are sent.
+                else
+                    VcsRepositoryDescription
+                        .from(s)
+                        .fold(FormFieldError("Invalid repository description!").invalidNec)(descr =>
+                            Option(descr).validNec
+                        )
             }
-      }
-      .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList)))
-    (name, privateFlag, description, ticketsEnabledFlag, website).mapN {
-      case (validName, isPrivate, validDescription, ticketsEnabled, validWebsite) =>
-        NewVcsRepositoryForm(validName, isPrivate, validDescription, ticketsEnabled, validWebsite)
+            .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
+        val ticketsEnabledFlag: ValidatedNec[FormErrors, Boolean] =
+            data.get(fieldTicketsEnabled).fold(false.validNec)(s => s.matches("true").validNec)
+        val website = data
+            .get(fieldWebsite)
+            .fold(Option.empty[Uri].validNec) { s =>
+                if (s.trim.isEmpty)
+                    Option.empty[Uri].validNec // Sometimes "empty" strings are sent.
+                else
+                    Uri
+                        .fromString(s)
+                        .toOption
+                        .fold(FormFieldError("Invalid website URI!").invalidNec) { uri =>
+                            uri.scheme match {
+                                case Some(Uri.Scheme.http) | Some(Uri.Scheme.https) => Option(uri).validNec
+                                case _ => FormFieldError("Invalid website URI!").invalidNec
+                            }
+                        }
+            }
+            .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList)))
+        (name, privateFlag, description, ticketsEnabledFlag, website).mapN {
+            case (validName, isPrivate, validDescription, ticketsEnabled, validWebsite) =>
+                NewVcsRepositoryForm(validName, isPrivate, validDescription, ticketsEnabled, validWebsite)
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/RelatedTypesConverter.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/RelatedTypesConverter.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/RelatedTypesConverter.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/RelatedTypesConverter.scala	2025-01-13 17:13:25.052470972 +0000
@@ -32,38 +32,38 @@
   *   The target type of the conversion.
   */
 trait RelatedTypesConverter[FROM, TO] {
-  extension (from: FROM) {
+    extension (from: FROM) {
 
-    /** Create an instance of the desired type.
-      *
-      * @return
-      *   An instance of the desired type based on the data of the source type.
-      */
-    def convert: TO
-  }
+        /** Create an instance of the desired type.
+          *
+          * @return
+          *   An instance of the desired type based on the data of the source type.
+          */
+        def convert: TO
+    }
 }
 
 object RelatedTypesConverter {
-  given RelatedTypesConverter[Account, ProjectOwner] with {
-    extension (from: Account) {
-      override def convert: ProjectOwner =
-        ProjectOwner(
-          uid = ProjectOwnerId.fromUserId(from.uid),
-          name = ProjectOwnerName.fromUsername(from.name),
-          email = from.email
-        )
+    given RelatedTypesConverter[Account, ProjectOwner] with {
+        extension (from: Account) {
+            override def convert: ProjectOwner =
+                ProjectOwner(
+                    uid = ProjectOwnerId.fromUserId(from.uid),
+                    name = ProjectOwnerName.fromUsername(from.name),
+                    email = from.email
+                )
+        }
     }
-  }
 
-  given RelatedTypesConverter[VcsRepository, Project] with {
-    extension (from: VcsRepository) {
-      override def convert: Project =
-        Project(
-          owner = ProjectOwner(ProjectOwnerId.fromUserId(from.owner.uid), from.owner.name, from.owner.email),
-          name = ProjectName(from.name.toString),
-          description = from.description.map(descr => ProjectDescription(descr.toString)),
-          isPrivate = from.isPrivate
-        )
+    given RelatedTypesConverter[VcsRepository, Project] with {
+        extension (from: VcsRepository) {
+            override def convert: Project =
+                Project(
+                    owner = ProjectOwner(ProjectOwnerId.fromUserId(from.owner.uid), from.owner.name, from.owner.email),
+                    name = ProjectName(from.name.toString),
+                    description = from.description.map(descr => ProjectDescription(descr.toString)),
+                    isPrivate = from.isPrivate
+                )
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/RequestHelpers.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/RequestHelpers.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/RequestHelpers.scala	2025-01-13 17:13:25.028470938 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/RequestHelpers.scala	2025-01-13 17:13:25.052470972 +0000
@@ -24,39 +24,39 @@
 import org.http4s.*
 
 trait RequestHelpers[A] {
-  extension (a: A) {
+    extension (a: A) {
 
-    /** Parse the request cookies and extract the authentication token if it can be found.
-      *
-      * @return
-      *   An option to the authentication token.
-      */
-    def getAuthenticationToken: Option[SignedToken]
-
-    /** Parse the request cookies and extract the CSRF token if it can be found.
-      *
-      * @return
-      *   An option to the CSRF cookie.
-      */
-    def getCsrfToken: Option[CsrfToken]
-  }
+        /** Parse the request cookies and extract the authentication token if it can be found.
+          *
+          * @return
+          *   An option to the authentication token.
+          */
+        def getAuthenticationToken: Option[SignedToken]
+
+        /** Parse the request cookies and extract the CSRF token if it can be found.
+          *
+          * @return
+          *   An option to the CSRF cookie.
+          */
+        def getCsrfToken: Option[CsrfToken]
+    }
 }
 
 object RequestHelpers {
-  object instances {
-    given RequestHelpers[Request[_]] with {
-      extension (r: Request[_]) {
-
-        def getAuthenticationToken: Option[SignedToken] =
-          r.cookies
-            .find(_.name === Constants.authenticationCookieName.toString)
-            .flatMap(cookie => SignedToken.from(cookie.content))
-
-        def getCsrfToken: Option[CsrfToken] =
-          r.cookies
-            .find(_.name === Constants.csrfCookieName.toString)
-            .flatMap(cookie => CsrfToken.from(cookie.content))
-      }
+    object instances {
+        given RequestHelpers[Request[_]] with {
+            extension (r: Request[_]) {
+
+                def getAuthenticationToken: Option[SignedToken] =
+                    r.cookies
+                        .find(_.name === Constants.authenticationCookieName.toString)
+                        .flatMap(cookie => SignedToken.from(cookie.content))
+
+                def getCsrfToken: Option[CsrfToken] =
+                    r.cookies
+                        .find(_.name === Constants.csrfCookieName.toString)
+                        .flatMap(cookie => CsrfToken.from(cookie.content))
+            }
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordForm.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordForm.scala	2025-01-13 17:13:25.052470972 +0000
@@ -31,15 +31,15 @@
 final case class ResetPasswordForm(email: EmailAddress)
 
 object ResetPasswordForm extends FormValidator[ResetPasswordForm] {
-  val fieldEmail: FormField = FormField("email")
+    val fieldEmail: FormField = FormField("email")
 
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, ResetPasswordForm] = {
-    val email: ValidatedNec[FormErrors, EmailAddress] = data
-      .get(fieldEmail)
-      .fold(FormFieldError("No email address given!").invalidNec)(s =>
-        EmailAddress.from(s).fold(FormFieldError("Invalid email address!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldEmail -> es.toList)))
-    email.map(ResetPasswordForm.apply)
-  }
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, ResetPasswordForm] = {
+        val email: ValidatedNec[FormErrors, EmailAddress] = data
+            .get(fieldEmail)
+            .fold(FormFieldError("No email address given!").invalidNec)(s =>
+                EmailAddress.from(s).fold(FormFieldError("Invalid email address!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldEmail -> es.toList)))
+        email.map(ResetPasswordForm.apply)
+    }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRepository.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRepository.scala	2025-01-13 17:13:25.052470972 +0000
@@ -37,70 +37,70 @@
   */
 abstract class ResetPasswordRepository[F[_]] {
 
-  /** Find a user account using the given name and reset token. This function shall not check for the expiration date
-    * but for the expiration date being NULL and for the given combination of username and reset token.
-    *
-    * The nulled out expiration date is considered proof that the link with the reset url has been called.
-    *
-    * @param name
-    *   The username which must be unique according to our requirements.
-    * @param token
-    *   A token that must be present in the reset token column.
-    * @return
-    *   An option to the found user account.
-    */
-  def findByNameAndResetPasswordToken(name: Username, token: ResetToken): F[Option[Account]]
-
-  /** Find a user account via the given password reset token which must not be expired.
-    *
-    * @param token
-    *   A token that must be present in the reset token column.
-    * @return
-    *   An option to the found user account.
-    */
-  def findByResetPasswordToken(token: ResetToken): F[Option[Account]]
-
-  /** Remove just the expiration date for the reset password token from the database. This is used to prevent opening
-    * the reset password link a second time while still being able to perform a password change.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def removeResetPasswordExpirationDate(uid: UserId): F[Int]
-
-  /** Remove the reset password token and the expiration date from the database.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def removeResetPasswordToken(uid: UserId): F[Int]
-
-  /** Set the password hash for the account with the given user id.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @param hash
-    *   The password hash for the account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def setPassword(uid: UserId)(hash: PasswordHash): F[Int]
-
-  /** Set the password reset token for the given user and the related expiration time.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @param token
-    *   A token that is written into the reset token column and must be present in the reset request uri.
-    * @param tokenExpiration
-    *   The timestamp when the token shall expire.
-    * @return
-    *   The number of affected database rows.
-    */
-  def setResetPasswordToken(uid: UserId)(token: ResetToken, tokenExpiration: OffsetDateTime): F[Int]
+    /** Find a user account using the given name and reset token. This function shall not check for the expiration date
+      * but for the expiration date being NULL and for the given combination of username and reset token.
+      *
+      * The nulled out expiration date is considered proof that the link with the reset url has been called.
+      *
+      * @param name
+      *   The username which must be unique according to our requirements.
+      * @param token
+      *   A token that must be present in the reset token column.
+      * @return
+      *   An option to the found user account.
+      */
+    def findByNameAndResetPasswordToken(name: Username, token: ResetToken): F[Option[Account]]
+
+    /** Find a user account via the given password reset token which must not be expired.
+      *
+      * @param token
+      *   A token that must be present in the reset token column.
+      * @return
+      *   An option to the found user account.
+      */
+    def findByResetPasswordToken(token: ResetToken): F[Option[Account]]
+
+    /** Remove just the expiration date for the reset password token from the database. This is used to prevent opening
+      * the reset password link a second time while still being able to perform a password change.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def removeResetPasswordExpirationDate(uid: UserId): F[Int]
+
+    /** Remove the reset password token and the expiration date from the database.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def removeResetPasswordToken(uid: UserId): F[Int]
+
+    /** Set the password hash for the account with the given user id.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @param hash
+      *   The password hash for the account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def setPassword(uid: UserId)(hash: PasswordHash): F[Int]
+
+    /** Set the password reset token for the given user and the related expiration time.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @param token
+      *   A token that is written into the reset token column and must be present in the reset request uri.
+      * @param tokenExpiration
+      *   The timestamp when the token shall expire.
+      * @return
+      *   The number of affected database rows.
+      */
+    def setResetPasswordToken(uid: UserId)(token: ResetToken, tokenExpiration: OffsetDateTime): F[Int]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRoutes.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/ResetPasswordRoutes.scala	2025-01-13 17:13:25.056470978 +0000
@@ -48,187 +48,221 @@
     external: ExternalUrlConfiguration,
     resetPasswordRepo: ResetPasswordRepository[F]
 ) extends Http4sDsl[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  private val loginPath               = uri"/login"
-  private val resetPath               = uri"/forgot-password"
-  private val resetChangePasswordPath = uri"/forgot-password/change-password"
-  private val resetRequestPath        = uri"/forgot-password/request-email"
-  private val resetSentPath           = uri"/forgot-password/email-sent"
-
-  private val passwordResetRequestForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case _ @POST -> Root / "forgot-password" / "request-email" as _ =>
-      SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
-  }
-
-  private val passwordResetRequest: HttpRoutes[F] = HttpRoutes.of {
-    case req @ POST -> Root / "forgot-password" / "request-email" =>
-      req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf <- Sync[F].delay(req.getCsrfToken)
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
+    private val loginPath               = uri"/login"
+    private val resetPath               = uri"/forgot-password"
+    private val resetChangePasswordPath = uri"/forgot-password/change-password"
+    private val resetRequestPath        = uri"/forgot-password/request-email"
+    private val resetSentPath           = uri"/forgot-password/email-sent"
+
+    private val passwordResetRequestForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case _ @POST -> Root / "forgot-password" / "request-email" as _ =>
+            SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
+    }
+
+    private val passwordResetRequest: HttpRoutes[F] = HttpRoutes.of {
+        case req @ POST -> Root / "forgot-password" / "request-email" =>
+            req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf <- Sync[F].delay(req.getCsrfToken)
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    form <- Sync[F].delay(ResetPasswordForm.validate(formData))
+                    response <- form match {
+                        case Validated.Invalid(es) =>
+                            BadRequest(
+                                views.html
+                                    .reset()(resetPath, csrf, title = "Smederee - Reset your account password".some)(
+                                        formData,
+                                        FormErrors.fromNec(es)
+                                    )
+                            )
+                        case Validated.Valid(resetForm) =>
+                            authenticationRepo.findAccountByEmail(resetForm.email).flatMap {
+                                case None =>
+                                    for {
+                                        delay <- Sync[F]
+                                            .delay(
+                                                scala.util.Random.nextInt(2) + 1
+                                            ) // Prevent fast response to avoid email guessing.
+                                        _        <- Sync[F].sleep(FiniteDuration(delay, SECONDS))
+                                        response <- SeeOther(Location(resetSentPath))
+                                    } yield response
+                                case Some(user) =>
+                                    for {
+                                        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                                        token    <- Sync[F].delay(ResetToken.generate)
+                                        _ <- resetPasswordRepo
+                                            .setResetPasswordToken(user.uid)(
+                                                token,
+                                                OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
+                                            )
+                                        from <- Sync[F].delay(FromAddress("noreply@smeder.ee"))
+                                        to   <- Sync[F].delay(NonEmptyList.of(user.email.toToAddress))
+                                        uri <- Sync[F].delay(external.createFullUri(uri"reset").addPath(token.toString))
+                                        subject <- Sync[F]
+                                            .delay(
+                                                SubjectLine(
+                                                    "Smederee - Someone has requested a password reset for your email address."
+                                                )
+                                            )
+                                        body <- Sync[F].delay(
+                                            TextBody(views.txt.emails.reset(user, uri).toString)
+                                        ) // TODO: extension method?
+                                        message <- Sync[F]
+                                            .delay(EmailMessage(from, to, List.empty, List.empty, subject, body))
+                                        result   <- emailMiddleware.send(message)
+                                        _        <- Sync[F].delay(result.leftMap(error => log.error(error)))
+                                        response <- SeeOther(Location(resetSentPath))
+                                    } yield response
+                            }
+                    }
+                } yield response
             }
-          }
-          form <- Sync[F].delay(ResetPasswordForm.validate(formData))
-          response <- form match {
-            case Validated.Invalid(es) =>
-              BadRequest(
-                views.html.reset()(resetPath, csrf, title = "Smederee - Reset your account password".some)(
-                  formData,
-                  FormErrors.fromNec(es)
-                )
-              )
-            case Validated.Valid(resetForm) =>
-              authenticationRepo.findAccountByEmail(resetForm.email).flatMap {
-                case None =>
-                  for {
-                    delay <- Sync[F]
-                      .delay(scala.util.Random.nextInt(2) + 1) // Prevent fast response to avoid email guessing.
-                    _        <- Sync[F].sleep(FiniteDuration(delay, SECONDS))
-                    response <- SeeOther(Location(resetSentPath))
-                  } yield response
-                case Some(user) =>
-                  for {
-                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-                    token    <- Sync[F].delay(ResetToken.generate)
-                    _ <- resetPasswordRepo
-                      .setResetPasswordToken(user.uid)(token, OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L))
-                    from <- Sync[F].delay(FromAddress("noreply@smeder.ee"))
-                    to   <- Sync[F].delay(NonEmptyList.of(user.email.toToAddress))
-                    uri  <- Sync[F].delay(external.createFullUri(uri"reset").addPath(token.toString))
-                    subject <- Sync[F]
-                      .delay(SubjectLine("Smederee - Someone has requested a password reset for your email address."))
-                    body <- Sync[F].delay(
-                      TextBody(views.txt.emails.reset(user, uri).toString)
-                    ) // TODO: extension method?
-                    message  <- Sync[F].delay(EmailMessage(from, to, List.empty, List.empty, subject, body))
-                    result   <- emailMiddleware.send(message)
-                    _        <- Sync[F].delay(result.leftMap(error => log.error(error)))
-                    response <- SeeOther(Location(resetSentPath))
-                  } yield response
-              }
-          }
-        } yield response
-      }
-  }
+    }
 
-  private val requestPasswordReset: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "forgot-password" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      response <- Ok(
-        views.html.reset()(resetRequestPath, csrf, title = "Smederee - Reset your account password".some)()
-      )
-    } yield response
-  }
-
-  private val requestPasswordResetForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case _ @GET -> Root / "forgot-password" as _ =>
-      SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
-  }
-
-  private val passwordResetEmailSent: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / "forgot-password" / "email-sent" =>
-      for {
-        csrf     <- Sync[F].delay(req.getCsrfToken)
-        response <- Ok(views.html.resetSent()(csrf, title = "Reset password email sent.".some))
-      } yield response
-  }
-
-  private val changePassword: HttpRoutes[F] = HttpRoutes.of {
-    case req @ POST -> Root / "forgot-password" / "change-password" / ResetTokenPathParameter(token) =>
-      req.decodeStrict[F, UrlForm] { urlForm =>
+    private val requestPasswordReset: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "forgot-password" =>
         for {
-          csrf <- Sync[F].delay(req.getCsrfToken)
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
-            }
-          }
-          form <- Sync[F].delay(
-            ChangePasswordForm
-              .validate(formData)
-              .andThen(form =>
-                if (form.token === token) form.validNec
-                else
-                  Map(
-                    ChangePasswordForm.fieldGlobal -> List(FormFieldError("Invalid password reset token!"))
-                  ).invalidNec
-              )
-          )
-          changePasswordUri = resetChangePasswordPath.addSegment(token.toString)
-          response <- form match {
-            case Validated.Invalid(es) =>
-              BadRequest(
-                views.html
-                  .changePassword()(changePasswordUri, csrf, title = "Smederee - Change your password.".some, token)(
-                    formData,
-                    FormErrors.fromNec(es)
-                  )
-              )
-            case Validated.Valid(changePasswordForm) =>
-              for {
-                user <- resetPasswordRepo.findByNameAndResetPasswordToken(changePasswordForm.name, token)
-                _ <- user match {
-                  case None =>
-                    Sync[F].delay(log.info(s"Password reset form: No user named ${changePasswordForm.name} found!"))
-                  case Some(user) =>
-                    Sync[F].delay(log.info(s"Password reset form: Changing password for ${user.name}."))
-                }
-                _ <- user.traverse(user => resetPasswordRepo.setPassword(user.uid)(changePasswordForm.password.encode))
-                _ <- user.traverse(user => resetPasswordRepo.removeResetPasswordToken(user.uid))
-                _ <- user.traverse(user => authenticationRepo.deleteAllUserSessions(user.uid)) // Close all sessions.
-                response <- SeeOther(Location(loginPath))
-              } yield response
-          }
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            response <- Ok(
+                views.html.reset()(resetRequestPath, csrf, title = "Smederee - Reset your account password".some)()
+            )
         } yield response
-      }
-  }
+    }
+
+    private val requestPasswordResetForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case _ @GET -> Root / "forgot-password" as _ =>
+            SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
+    }
+
+    private val passwordResetEmailSent: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / "forgot-password" / "email-sent" =>
+            for {
+                csrf     <- Sync[F].delay(req.getCsrfToken)
+                response <- Ok(views.html.resetSent()(csrf, title = "Reset password email sent.".some))
+            } yield response
+    }
+
+    private val changePassword: HttpRoutes[F] = HttpRoutes.of {
+        case req @ POST -> Root / "forgot-password" / "change-password" / ResetTokenPathParameter(token) =>
+            req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf <- Sync[F].delay(req.getCsrfToken)
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    form <- Sync[F].delay(
+                        ChangePasswordForm
+                            .validate(formData)
+                            .andThen(form =>
+                                if (form.token === token) form.validNec
+                                else
+                                    Map(
+                                        ChangePasswordForm.fieldGlobal -> List(
+                                            FormFieldError("Invalid password reset token!")
+                                        )
+                                    ).invalidNec
+                            )
+                    )
+                    changePasswordUri = resetChangePasswordPath.addSegment(token.toString)
+                    response <- form match {
+                        case Validated.Invalid(es) =>
+                            BadRequest(
+                                views.html
+                                    .changePassword()(
+                                        changePasswordUri,
+                                        csrf,
+                                        title = "Smederee - Change your password.".some,
+                                        token
+                                    )(
+                                        formData,
+                                        FormErrors.fromNec(es)
+                                    )
+                            )
+                        case Validated.Valid(changePasswordForm) =>
+                            for {
+                                user <- resetPasswordRepo.findByNameAndResetPasswordToken(
+                                    changePasswordForm.name,
+                                    token
+                                )
+                                _ <- user match {
+                                    case None =>
+                                        Sync[F].delay(
+                                            log.info(
+                                                s"Password reset form: No user named ${changePasswordForm.name} found!"
+                                            )
+                                        )
+                                    case Some(user) =>
+                                        Sync[F].delay(
+                                            log.info(s"Password reset form: Changing password for ${user.name}.")
+                                        )
+                                }
+                                _ <- user.traverse(user =>
+                                    resetPasswordRepo.setPassword(user.uid)(changePasswordForm.password.encode)
+                                )
+                                _ <- user.traverse(user => resetPasswordRepo.removeResetPasswordToken(user.uid))
+                                _ <- user.traverse(user =>
+                                    authenticationRepo.deleteAllUserSessions(user.uid)
+                                ) // Close all sessions.
+                                response <- SeeOther(Location(loginPath))
+                            } yield response
+                    }
+                } yield response
+            }
+    }
 
-  private val changePasswordForm: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / "forgot-password" / "change-password" / ResetTokenPathParameter(token) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        user <- resetPasswordRepo.findByResetPasswordToken(token)
-        response <- user match {
-          case None => Sync[F].delay(log.debug(s"Requested password reset token $token was not found!")) *> NotFound()
-          case Some(user) =>
+    private val changePasswordForm: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / "forgot-password" / "change-password" / ResetTokenPathParameter(token) =>
             for {
-              _ <- resetPasswordRepo.removeResetPasswordExpirationDate(user.uid) // The URL shall only work once!
-              _ <- Sync[F].delay(log.debug(s"Password reset uri called for ${user.email}."))
-              changePasswordUri = resetChangePasswordPath.addSegment(token.toString)
-              response <- Ok(
-                views.html.changePassword()(
-                  changePasswordUri,
-                  csrf,
-                  title = "Smederee - Change your password.".some,
-                  token
-                )()
-              )
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                user <- resetPasswordRepo.findByResetPasswordToken(token)
+                response <- user match {
+                    case None =>
+                        Sync[F].delay(log.debug(s"Requested password reset token $token was not found!")) *> NotFound()
+                    case Some(user) =>
+                        for {
+                            _ <- resetPasswordRepo.removeResetPasswordExpirationDate(
+                                user.uid
+                            ) // The URL shall only work once!
+                            _ <- Sync[F].delay(log.debug(s"Password reset uri called for ${user.email}."))
+                            changePasswordUri = resetChangePasswordPath.addSegment(token.toString)
+                            response <- Ok(
+                                views.html.changePassword()(
+                                    changePasswordUri,
+                                    csrf,
+                                    title = "Smederee - Change your password.".some,
+                                    token
+                                )()
+                            )
+                        } yield response
+                }
             } yield response
-        }
-      } yield response
-  }
-
-  val protectedRoutes =
-    if (authenticationConfig.enabled)
-      requestPasswordResetForLoggedInUsers <+> passwordResetRequestForLoggedInUsers
-    else
-      AuthedRoutes.empty[Account, F]
-
-  val routes =
-    if (authenticationConfig.enabled)
-      changePassword <+> changePasswordForm <+> requestPasswordReset <+> passwordResetRequest <+> passwordResetEmailSent
-    else
-      HttpRoutes.empty[F]
+    }
+
+    val protectedRoutes =
+        if (authenticationConfig.enabled)
+            requestPasswordResetForLoggedInUsers <+> passwordResetRequestForLoggedInUsers
+        else
+            AuthedRoutes.empty[Account, F]
+
+    val routes =
+        if (authenticationConfig.enabled)
+            changePassword <+> changePasswordForm <+> requestPasswordReset <+> passwordResetRequest <+> passwordResetEmailSent
+        else
+            HttpRoutes.empty[F]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SessionHelpers.scala	2025-01-13 17:13:25.056470978 +0000
@@ -30,57 +30,59 @@
 import scala.concurrent.duration.FiniteDuration
 
 object SessionHelpers {
-  object instances {
-    extension (session: Session) {
+    object instances {
+        extension (session: Session) {
 
-      /** Check if the session has reached the absolute timeout defined by the given timeouts configuration in regard to
-        * the given point in time passed as `currentTime`.
-        *
-        * @param timeouts
-        *   Timeouts related to the authentication and session management.
-        * @param currentTime
-        *   The current time to compare the `createdAt` field of the session to.
-        * @return
-        *   Return `true` if the session was created at a point in time that is out of the allowed timeout range.
-        */
-      def hasReachedAbsoluteTimeout(timeouts: AuthenticationTimeouts)(currentTime: OffsetDateTime): Boolean =
-        session.createdAt.compareTo(currentTime.minusSeconds(timeouts.absoluteTimeout.toSeconds)) < 0
-    }
+            /** Check if the session has reached the absolute timeout defined by the given timeouts configuration in
+              * regard to the given point in time passed as `currentTime`.
+              *
+              * @param timeouts
+              *   Timeouts related to the authentication and session management.
+              * @param currentTime
+              *   The current time to compare the `createdAt` field of the session to.
+              * @return
+              *   Return `true` if the session was created at a point in time that is out of the allowed timeout range.
+              */
+            def hasReachedAbsoluteTimeout(timeouts: AuthenticationTimeouts)(currentTime: OffsetDateTime): Boolean =
+                session.createdAt.compareTo(currentTime.minusSeconds(timeouts.absoluteTimeout.toSeconds)) < 0
+        }
 
-    extension (signedToken: SignedToken) {
+        extension (signedToken: SignedToken) {
 
-      /** Create an authentication cookie from the signed token. The mentioned cookie will be named after the related
-        * constant from [[de.smederee.hub.config.Constants]] and contain the signed token as value. Furthermore the
-        * cookie will be using a strict same-site policy, be http-only and respect the given values for expiration and
-        * the secure flag.
-        *
-        * @param domain
-        *   An optional domain that shall be set for the cookie. If it is not set then a "host only" cookie will be
-        *   created which is in most cases what you want. Setting a specific domain can be necessary if you want to
-        *   share cookies between a domain and possible existing sub domains.
-        * @param expires
-        *   A duration in which the cookie shall expire. It is optional resulting in the cookie being a non-persistent
-        *   "session cookie" if not set.
-        * @param secure
-        *   A flag indication if the secure flag on the cookie shall be set to allow it only to be transmitted via
-        *   encrypted channels (i.e. HTTPS).
-        * @return
-        *   A response cookie ready to be used as an authentication cookie.
-        */
-      def toAuthenticationCookie(
-          domain: Option[String]
-      )(expires: Option[FiniteDuration])(secure: Boolean): ResponseCookie =
-        ResponseCookie(
-          name = Constants.authenticationCookieName.toString,
-          content = signedToken.toString,
-          expires = expires.flatMap(duration =>
-            HttpDate.fromZonedDateTime(ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(duration.toSeconds)).toOption
-          ),
-          domain = domain,
-          sameSite = Option(SameSite.Strict),
-          secure = secure,
-          httpOnly = true
-        )
+            /** Create an authentication cookie from the signed token. The mentioned cookie will be named after the
+              * related constant from [[de.smederee.hub.config.Constants]] and contain the signed token as value.
+              * Furthermore the cookie will be using a strict same-site policy, be http-only and respect the given
+              * values for expiration and the secure flag.
+              *
+              * @param domain
+              *   An optional domain that shall be set for the cookie. If it is not set then a "host only" cookie will
+              *   be created which is in most cases what you want. Setting a specific domain can be necessary if you
+              *   want to share cookies between a domain and possible existing sub domains.
+              * @param expires
+              *   A duration in which the cookie shall expire. It is optional resulting in the cookie being a
+              *   non-persistent "session cookie" if not set.
+              * @param secure
+              *   A flag indication if the secure flag on the cookie shall be set to allow it only to be transmitted via
+              *   encrypted channels (i.e. HTTPS).
+              * @return
+              *   A response cookie ready to be used as an authentication cookie.
+              */
+            def toAuthenticationCookie(
+                domain: Option[String]
+            )(expires: Option[FiniteDuration])(secure: Boolean): ResponseCookie =
+                ResponseCookie(
+                    name = Constants.authenticationCookieName.toString,
+                    content = signedToken.toString,
+                    expires = expires.flatMap(duration =>
+                        HttpDate
+                            .fromZonedDateTime(ZonedDateTime.now(ZoneOffset.UTC).plusSeconds(duration.toSeconds))
+                            .toOption
+                    ),
+                    domain = domain,
+                    sameSite = Option(SameSite.Strict),
+                    secure = secure,
+                    httpOnly = true
+                )
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/Session.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/Session.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/Session.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/Session.scala	2025-01-13 17:13:25.056470978 +0000
@@ -38,30 +38,30 @@
 final case class Session(id: SessionId, uid: UserId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime)
 
 object Session {
-  given Eq[Session] =
-    Eq.instance[Session] { (a, b) =>
-      a.id === b.id && a.uid === b.uid && a.createdAt.compareTo(b.createdAt) === 0 && a.updatedAt.compareTo(
-        b.updatedAt
-      ) === 0
-    }
+    given Eq[Session] =
+        Eq.instance[Session] { (a, b) =>
+            a.id === b.id && a.uid === b.uid && a.createdAt.compareTo(b.createdAt) === 0 && a.updatedAt.compareTo(
+                b.updatedAt
+            ) === 0
+        }
 
-  /** Create a new session object for the given user and generate a new session id.
-    *
-    * @param uid
-    *   The unique ID of the user account related to the session.
-    * @param createdAt
-    *   The creation timestamp to be used for the session.
-    * @param updatedAt
-    *   The session ID should be re-generated in regular intervals resulting in a copy of the old session entry with a
-    *   new ID and the corresponding timestamp in this field.
-    * @return
-    *   A new session with a generated id for the given user and timestamp.
-    */
-  def create(uid: UserId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime): Session =
-    Session(
-      id = SessionId.generate,
-      uid = uid,
-      createdAt = createdAt,
-      updatedAt = updatedAt
-    )
+    /** Create a new session object for the given user and generate a new session id.
+      *
+      * @param uid
+      *   The unique ID of the user account related to the session.
+      * @param createdAt
+      *   The creation timestamp to be used for the session.
+      * @param updatedAt
+      *   The session ID should be re-generated in regular intervals resulting in a copy of the old session entry with a
+      *   new ID and the corresponding timestamp in this field.
+      * @return
+      *   A new session with a generated id for the given user and timestamp.
+      */
+    def create(uid: UserId, createdAt: OffsetDateTime, updatedAt: OffsetDateTime): Session =
+        Session(
+            id = SessionId.generate,
+            uid = uid,
+            createdAt = createdAt,
+            updatedAt = updatedAt
+        )
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupForm.scala	2025-01-13 17:13:25.056470978 +0000
@@ -38,28 +38,28 @@
 final case class SignupForm(name: Username, email: EmailAddress, password: Password)
 
 object SignupForm extends FormValidator[SignupForm] {
-  val fieldName: FormField     = FormField("name")
-  val fieldEmail: FormField    = FormField("email")
-  val fieldPassword: FormField = FormField("password")
-  override def validate(data: Map[String, String]): ValidatedNec[FormErrors, SignupForm] = {
-    val email: ValidatedNec[FormErrors, EmailAddress] = data
-      .get(fieldEmail)
-      .fold(FormFieldError("No email address given!").invalidNec)(s =>
-        EmailAddress.from(s).fold(FormFieldError("Invalid email address!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldEmail -> es.toList)))
-    val name: ValidatedNec[FormErrors, Username] = data
-      .get(fieldName)
-      .fold(FormFieldError("No username given!").invalidNec)(s =>
-        Username.from(s).fold(FormFieldError("Invalid username!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
-    val password: ValidatedNec[FormErrors, Password] = data
-      .get(fieldPassword)
-      .fold("No password given!".invalidNec)(Password.validate)
-      .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList.map(FormFieldError.apply))))
-    (email, name, password).mapN { case (validEmail, validName, validPassword) =>
-      SignupForm(validName, validEmail, validPassword)
+    val fieldName: FormField     = FormField("name")
+    val fieldEmail: FormField    = FormField("email")
+    val fieldPassword: FormField = FormField("password")
+    override def validate(data: Map[String, String]): ValidatedNec[FormErrors, SignupForm] = {
+        val email: ValidatedNec[FormErrors, EmailAddress] = data
+            .get(fieldEmail)
+            .fold(FormFieldError("No email address given!").invalidNec)(s =>
+                EmailAddress.from(s).fold(FormFieldError("Invalid email address!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldEmail -> es.toList)))
+        val name: ValidatedNec[FormErrors, Username] = data
+            .get(fieldName)
+            .fold(FormFieldError("No username given!").invalidNec)(s =>
+                Username.from(s).fold(FormFieldError("Invalid username!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+        val password: ValidatedNec[FormErrors, Password] = data
+            .get(fieldPassword)
+            .fold("No password given!".invalidNec)(Password.validate)
+            .leftMap(es => NonEmptyChain.of(Map(fieldPassword -> es.toList.map(FormFieldError.apply))))
+        (email, name, password).mapN { case (validEmail, validName, validPassword) =>
+            SignupForm(validName, validEmail, validPassword)
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRepository.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRepository.scala	2025-01-13 17:13:25.056470978 +0000
@@ -27,35 +27,35 @@
   */
 abstract class SignupRepository[F[_]] {
 
-  /** Create the given user account in the database.
-    *
-    * @param account
-    *   The account to be created.
-    * @param hash
-    *   The password hash for the account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createAccount(account: Account, hash: PasswordHash): F[Int]
+    /** Create the given user account in the database.
+      *
+      * @param account
+      *   The account to be created.
+      * @param hash
+      *   The password hash for the account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createAccount(account: Account, hash: PasswordHash): F[Int]
 
-  /** Find the given email address in the database accounts table. This function can be used to check if an email
-    * address has already been taken.
-    *
-    * @param address
-    *   An email address which shall be searched for.
-    * @return
-    *   An option which is either empty or contains the email address if it exists in the database.
-    */
-  def findEmail(address: EmailAddress): F[Option[EmailAddress]]
+    /** Find the given email address in the database accounts table. This function can be used to check if an email
+      * address has already been taken.
+      *
+      * @param address
+      *   An email address which shall be searched for.
+      * @return
+      *   An option which is either empty or contains the email address if it exists in the database.
+      */
+    def findEmail(address: EmailAddress): F[Option[EmailAddress]]
 
-  /** Find the given username in the database accounts table. This function can be used to check if a username has
-    * already been taken.
-    *
-    * @param name
-    *   The username which shall be searched for.
-    * @return
-    *   An option which is either empty or contains the username if it exists in the database.
-    */
-  def findUsername(name: Username): F[Option[Username]]
+    /** Find the given username in the database accounts table. This function can be used to check if a username has
+      * already been taken.
+      *
+      * @param name
+      *   The username which shall be searched for.
+      * @return
+      *   An option which is either empty or contains the username if it exists in the database.
+      */
+    def findUsername(name: Username): F[Option[Username]]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/SignupRoutes.scala	2025-01-13 17:13:25.056470978 +0000
@@ -43,125 +43,127 @@
   *   A higher kinded type providing needed functionality, which is usually an IO monad like Async or Sync.
   */
 final class SignupRoutes[F[_]: Async](configuration: ServiceConfig, repo: SignupRepository[F]) extends Http4sDsl[F] {
-  private val log          = LoggerFactory.getLogger(getClass)
-  private val linkConfig   = configuration.external
-  private val signupConfig = configuration.signup
-  // The base URI for our site which that be passed into some templates which create links themselfes.
-  private val baseUri = linkConfig.createFullUri(Uri())
-  // The URL path that shall be used in the `action` field of the form.
-  private val signupUri = linkConfig.createFullUri(uri"/signup")
-
-  // Parse the signup form and take appropriate actions like returning errors or creating an account.
-  private val parseSignUpForm = HttpRoutes.of[F] { case request @ POST -> Root / "signup" =>
-    request.decodeStrict[F, UrlForm] { urlForm =>
-      for {
-        csrf <- Sync[F].delay(request.getCsrfToken)
-        formData <- Sync[F].delay {
-          urlForm.values.map { t =>
-            val (key, values) = t
-            (
-              key,
-              values.headOption.getOrElse("")
-            ) // Pick the first value (a field might get submitted multiple times)!
-          }
+    private val log          = LoggerFactory.getLogger(getClass)
+    private val linkConfig   = configuration.external
+    private val signupConfig = configuration.signup
+    // The base URI for our site which that be passed into some templates which create links themselfes.
+    private val baseUri = linkConfig.createFullUri(Uri())
+    // The URL path that shall be used in the `action` field of the form.
+    private val signupUri = linkConfig.createFullUri(uri"/signup")
+
+    // Parse the signup form and take appropriate actions like returning errors or creating an account.
+    private val parseSignUpForm = HttpRoutes.of[F] { case request @ POST -> Root / "signup" =>
+        request.decodeStrict[F, UrlForm] { urlForm =>
+            for {
+                csrf <- Sync[F].delay(request.getCsrfToken)
+                formData <- Sync[F].delay {
+                    urlForm.values.map { t =>
+                        val (key, values) = t
+                        (
+                            key,
+                            values.headOption.getOrElse("")
+                        ) // Pick the first value (a field might get submitted multiple times)!
+                    }
+                }
+                form <- Sync[F].delay(SignupForm.validate(formData))
+                // Only check for existing email or username if the first validation was successful.
+                // TODO: Find a way to remove the nesting of `Validated[Validated[...]]` to also remove the nested pattern match later on.
+                checkExisting <- form.traverse { signupForm =>
+                    for {
+                        duplicateEmail <- repo.findEmail(signupForm.email)
+                        duplicateName  <- repo.findUsername(signupForm.name)
+                        validatedEmail = duplicateEmail.fold(signupForm.email.validNec[FormErrors])(_ =>
+                            Map(
+                                SignupForm.fieldEmail -> List(FormFieldError("This email address is already in use!"))
+                            ).invalidNec
+                        )
+                        validatedName = duplicateName.fold(signupForm.name.validNec[FormErrors])(_ =>
+                            Map(
+                                SignupForm.fieldName -> List(FormFieldError("This username is already in use!"))
+                            ).invalidNec
+                        )
+                        validatedEmailAndName = (validatedEmail, validatedName).mapN { case (_, _) =>
+                            signupForm: SignupForm
+                        }
+                    } yield validatedEmailAndName
+                }
+                resp <- checkExisting match {
+                    case Validated.Invalid(es) =>
+                        BadRequest(
+                            views.html
+                                .signup()(signupUri, csrf, "Smederee - Sign up for an account".some)(
+                                    formData,
+                                    FormErrors.fromNec(es)
+                                )
+                        )
+                    case Validated.Valid(innerValidation) =>
+                        innerValidation match {
+                            case Validated.Invalid(es) =>
+                                BadRequest(
+                                    views.html
+                                        .signup()(signupUri, csrf, "Smederee - Sign up for an account".some)(
+                                            formData,
+                                            FormErrors.fromNec(es)
+                                        )
+                                )
+                            case Validated.Valid(signupForm) =>
+                                for {
+                                    uid <- Sync[F].delay(UserId.randomUserId)
+                                    account <- Sync[F].delay(
+                                        Account(
+                                            uid = uid,
+                                            name = signupForm.name,
+                                            email = signupForm.email,
+                                            validatedEmail = false,
+                                            language = None
+                                        )
+                                    )
+                                    hash     <- Sync[F].delay(signupForm.password.encode)
+                                    _        <- Sync[F].delay(log.info(s"Going to create account for ${account.name}."))
+                                    _        <- repo.createAccount(account, hash)
+                                    redirect <- SeeOther(Location(signupUri.addPath("welcome")))
+                                } yield redirect
+                        }
+                }
+            } yield resp
         }
-        form <- Sync[F].delay(SignupForm.validate(formData))
-        // Only check for existing email or username if the first validation was successful.
-        // TODO: Find a way to remove the nesting of `Validated[Validated[...]]` to also remove the nested pattern match later on.
-        checkExisting <- form.traverse { signupForm =>
-          for {
-            duplicateEmail <- repo.findEmail(signupForm.email)
-            duplicateName  <- repo.findUsername(signupForm.name)
-            validatedEmail = duplicateEmail.fold(signupForm.email.validNec[FormErrors])(_ =>
-              Map(
-                SignupForm.fieldEmail -> List(FormFieldError("This email address is already in use!"))
-              ).invalidNec
-            )
-            validatedName = duplicateName.fold(signupForm.name.validNec[FormErrors])(_ =>
-              Map(SignupForm.fieldName -> List(FormFieldError("This username is already in use!"))).invalidNec
-            )
-            validatedEmailAndName = (validatedEmail, validatedName).mapN { case (_, _) =>
-              signupForm: SignupForm
-            }
-          } yield validatedEmailAndName
-        }
-        resp <- checkExisting match {
-          case Validated.Invalid(es) =>
-            BadRequest(
-              views.html
-                .signup()(signupUri, csrf, "Smederee - Sign up for an account".some)(
-                  formData,
-                  FormErrors.fromNec(es)
-                )
-            )
-          case Validated.Valid(innerValidation) =>
-            innerValidation match {
-              case Validated.Invalid(es) =>
-                BadRequest(
-                  views.html
-                    .signup()(signupUri, csrf, "Smederee - Sign up for an account".some)(
-                      formData,
-                      FormErrors.fromNec(es)
-                    )
-                )
-              case Validated.Valid(signupForm) =>
-                for {
-                  uid <- Sync[F].delay(UserId.randomUserId)
-                  account <- Sync[F].delay(
-                    Account(
-                      uid = uid,
-                      name = signupForm.name,
-                      email = signupForm.email,
-                      validatedEmail = false,
-                      language = None
-                    )
-                  )
-                  hash     <- Sync[F].delay(signupForm.password.encode)
-                  _        <- Sync[F].delay(log.info(s"Going to create account for ${account.name}."))
-                  _        <- repo.createAccount(account, hash)
-                  redirect <- SeeOther(Location(signupUri.addPath("welcome")))
-                } yield redirect
-            }
-        }
-      } yield resp
     }
-  }
 
-  private val parseSignUpFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case _ @POST -> Root / "signup" as _ =>
-      SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
-  }
-
-  // Render the signup form.
-  private val showSignUpForm = HttpRoutes.of[F] { case request @ GET -> Root / "signup" =>
-    for {
-      csrf <- Sync[F].delay(request.getCsrfToken)
-      resp <- Ok(views.html.signup()(signupUri, csrf, "Smederee - Sign up for an account".some)())
-    } yield resp
-  }
-
-  private val showSignUpFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case _ @GET -> Root / "signup" as _ =>
-      SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
-  }
-
-  private val showWelcomePage: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "signup" / "welcome" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- Ok(views.html.welcome(baseUri)(csrf, "Thank you and welcome!".some))
-    } yield resp
-  }
-
-  val protectedRoutes =
-    if (signupConfig.enabled)
-      showSignUpFormForLoggedInUsers <+> parseSignUpFormForLoggedInUsers
-    else
-      AuthedRoutes.empty[Account, F]
-
-  val routes =
-    if (signupConfig.enabled)
-      showSignUpForm <+> parseSignUpForm <+> showWelcomePage
-    else
-      HttpRoutes.empty[F]
+    private val parseSignUpFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case _ @POST -> Root / "signup" as _ =>
+            SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
+    }
+
+    // Render the signup form.
+    private val showSignUpForm = HttpRoutes.of[F] { case request @ GET -> Root / "signup" =>
+        for {
+            csrf <- Sync[F].delay(request.getCsrfToken)
+            resp <- Ok(views.html.signup()(signupUri, csrf, "Smederee - Sign up for an account".some)())
+        } yield resp
+    }
+
+    private val showSignUpFormForLoggedInUsers: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case _ @GET -> Root / "signup" as _ =>
+            SeeOther(Location(Uri(path = Uri.Path.Root))) // Redirect already logged in users.
+    }
+
+    private val showWelcomePage: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "signup" / "welcome" =>
+        for {
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- Ok(views.html.welcome(baseUri)(csrf, "Thank you and welcome!".some))
+        } yield resp
+    }
+
+    val protectedRoutes =
+        if (signupConfig.enabled)
+            showSignUpFormForLoggedInUsers <+> parseSignUpFormForLoggedInUsers
+        else
+            AuthedRoutes.empty[Account, F]
+
+    val routes =
+        if (signupConfig.enabled)
+            showSignUpForm <+> parseSignUpForm <+> showWelcomePage
+        else
+            HttpRoutes.empty[F]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/types.scala	2025-01-13 17:13:25.056470978 +0000
@@ -28,75 +28,75 @@
 opaque type ErrorMessageKey = String
 object ErrorMessageKey {
 
-  /** Create an instance of ErrorMessageKey from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ErrorMessageKey.
-    * @return
-    *   The appropriate instance of ErrorMessageKey.
-    */
-  def apply(source: String): ErrorMessageKey = source
-
-  /** Try to create an instance of ErrorMessageKey from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ErrorMessageKey.
-    * @return
-    *   An option to the successfully converted ErrorMessageKey.
-    */
-  def from(source: String): Option[ErrorMessageKey] =
-    if (Option(source).map(_.startsWith("error.")).getOrElse(false))
-      Option(source)
-    else
-      None
+    /** Create an instance of ErrorMessageKey from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ErrorMessageKey.
+      * @return
+      *   The appropriate instance of ErrorMessageKey.
+      */
+    def apply(source: String): ErrorMessageKey = source
+
+    /** Try to create an instance of ErrorMessageKey from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ErrorMessageKey.
+      * @return
+      *   An option to the successfully converted ErrorMessageKey.
+      */
+    def from(source: String): Option[ErrorMessageKey] =
+        if (Option(source).map(_.startsWith("error.")).getOrElse(false))
+            Option(source)
+        else
+            None
 
 }
 
 opaque type SessionId = String
 object SessionId {
-  val Format: Regex = "^([A-Za-z0-9+/\\-\\_]{4})+$".r
-  val Length: Int   = 24 // 24 Bytes => 192 Bits
+    val Format: Regex = "^([A-Za-z0-9+/\\-\\_]{4})+$".r
+    val Length: Int   = 24 // 24 Bytes => 192 Bits
 
-  given Eq[SessionId] = Eq.fromUniversalEquals
+    given Eq[SessionId] = Eq.fromUniversalEquals
 
-  private val base64Decoder   = Base64.getUrlDecoder
-  private val base64Encoder   = Base64.getUrlEncoder.withoutPadding
-  private val randomGenerator = new SecureRandom()
-
-  /** Create an instance of SessionId from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a SessionId.
-    * @return
-    *   The appropriate instance of SessionId.
-    */
-  def apply(source: String): SessionId = source
-
-  /** Try to create an instance of SessionId from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SessionId.
-    * @return
-    *   An option to the successfully converted SessionId.
-    */
-  def from(source: String): Option[SessionId] =
-    Option(source).filter(Format.matches).map(base64Decoder.decode) match {
-      case Some(bytes) =>
-        if (bytes.length === Length)
-          Option(source)
-        else
-          None
-      case _ => None
+    private val base64Decoder   = Base64.getUrlDecoder
+    private val base64Encoder   = Base64.getUrlEncoder.withoutPadding
+    private val randomGenerator = new SecureRandom()
+
+    /** Create an instance of SessionId from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a SessionId.
+      * @return
+      *   The appropriate instance of SessionId.
+      */
+    def apply(source: String): SessionId = source
+
+    /** Try to create an instance of SessionId from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SessionId.
+      * @return
+      *   An option to the successfully converted SessionId.
+      */
+    def from(source: String): Option[SessionId] =
+        Option(source).filter(Format.matches).map(base64Decoder.decode) match {
+            case Some(bytes) =>
+                if (bytes.length === Length)
+                    Option(source)
+                else
+                    None
+            case _ => None
+        }
+
+    /** Generate a new globally unique session id using a secure random generator.
+      *
+      * @return
+      *   A string containing a base 64 encoded url safe version of the generated random bytes.
+      */
+    def generate: SessionId = {
+        var buffer = new Array[Byte](Length) // scalafix:ok; Mutability is needed here for the random generator.
+        randomGenerator.nextBytes(buffer)
+        base64Encoder.encodeToString(buffer)
     }
-
-  /** Generate a new globally unique session id using a secure random generator.
-    *
-    * @return
-    *   A string containing a base 64 encoded url safe version of the generated random bytes.
-    */
-  def generate: SessionId = {
-    var buffer = new Array[Byte](Length) // scalafix:ok; Mutability is needed here for the random generator.
-    randomGenerator.nextBytes(buffer)
-    base64Encoder.encodeToString(buffer)
-  }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsMetadataRepository.scala	2025-01-13 17:13:25.056470978 +0000
@@ -28,128 +28,128 @@
   */
 abstract class VcsMetadataRepository[F[_]] {
 
-  /** Create an entry in the forks table to save the relationship between to repositories.
-    *
-    * @param source
-    *   The ID of the original repository from which was forked.
-    * @param target
-    *   The ID of the forked repository.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createFork(source: VcsRepositoryId, target: VcsRepositoryId): F[Int]
-
-  /** Create a database entry for the given vcs repository.
-    *
-    * @param repository
-    *   The vcs repository metadata that shall be written to the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createVcsRepository(repository: VcsRepository): F[Int]
-
-  /** Delete the repository from the database.
-    *
-    * @param repository
-    *   The vcs repository metadata that shall be deleted from the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteVcsRepository(repository: VcsRepository): F[Int]
-
-  /** Search for the vcs repository entry with the given owner and name.
-    *
-    * @param owner
-    *   Data about the owner of the repository containing information needed to query the database.
-    * @param name
-    *   The repository name which must be unique in regard to the owner.
-    * @return
-    *   An option to the successfully found vcs repository entry.
-    */
-  def findVcsRepository(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepository]]
-
-  /** Find all branches (created via the `fork` functionality) for the repository with the given id.
-    *
-    * @param originalRepositoryId
-    *   The id from the original repository from which the branches (forks) were created.
-    * @return
-    *   A stream of tuples holding the username of the branch owner and the branch (repository) names.
-    */
-  def findVcsRepositoryBranches(originalRepositoryId: VcsRepositoryId): Stream[F, (Username, VcsRepositoryName)]
-
-  /** Search for the internal database specific (auto generated) ID of the given owner / repository combination which
-    * serves as a primary key for the database table.
-    *
-    * @param owner
-    *   Data about the owner of the repository containing information needed to query the database.
-    * @param name
-    *   The repository name which must be unique in regard to the owner.
-    * @return
-    *   An option to the internal database ID.
-    */
-  def findVcsRepositoryId(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepositoryId]]
-
-  /** Search for a repository owner of whom we only know the name.
-    *
-    * @param name
-    *   The name of the repository owner which is the username of the actual owners account.
-    * @return
-    *   An option to successfully found repository owner.
-    */
-  def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]]
-
-  /** Search for a possible parent repository (i.e. a repository forked from) of the one described by the given name and
-    * owner.
-    *
-    * @param owner
-    *   Data about the owner of the repository containing information needed to query the database.
-    * @param name
-    *   The repository name which must be unique in regard to the owner.
-    * @return
-    *   An option to the successfully found parent vcs repository entry.
-    */
-  def findVcsRepositoryParentFork(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepository]]
-
-  /** Return a list of all repositories from all users.
-    *
-    * @param requester
-    *   An optional account (None if guest i.e. not logged in user) for whom the list shall be created. This will affect
-    *   which repositories will be returned in regard to access rights.
-    * @param ordering
-    *   The desired ordering of the list.
-    * @return
-    *   A stream of vcs repository entries in the requested order.
-    */
-  def listAllRepositories(requester: Option[Account])(
-      ordering: VcsMetadataRepositoriesOrdering
-  ): Stream[F, VcsRepository]
-
-  /** Return a list of all repositories of the given owner.
-    *
-    * @param requester
-    *   An optional account (None if guest i.e. not logged in user) for whom the list shall be created. This will affect
-    *   which repositories will be returned in regard to access rights.
-    * @param owner
-    *   Data about the owner of the repositories containing information needed to query the database.
-    * @return
-    *   A stream of vcs repository entries ordered by name which may be empty.
-    */
-  def listRepositories(requester: Option[Account])(owner: VcsRepositoryOwner): Stream[F, VcsRepository]
-
-  /** Update the database entry for the given vcs repository.
-    *
-    * @param repository
-    *   The vcs repository metadata that shall be updated within the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def updateVcsRepository(repository: VcsRepository): F[Int]
+    /** Create an entry in the forks table to save the relationship between to repositories.
+      *
+      * @param source
+      *   The ID of the original repository from which was forked.
+      * @param target
+      *   The ID of the forked repository.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createFork(source: VcsRepositoryId, target: VcsRepositoryId): F[Int]
+
+    /** Create a database entry for the given vcs repository.
+      *
+      * @param repository
+      *   The vcs repository metadata that shall be written to the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createVcsRepository(repository: VcsRepository): F[Int]
+
+    /** Delete the repository from the database.
+      *
+      * @param repository
+      *   The vcs repository metadata that shall be deleted from the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteVcsRepository(repository: VcsRepository): F[Int]
+
+    /** Search for the vcs repository entry with the given owner and name.
+      *
+      * @param owner
+      *   Data about the owner of the repository containing information needed to query the database.
+      * @param name
+      *   The repository name which must be unique in regard to the owner.
+      * @return
+      *   An option to the successfully found vcs repository entry.
+      */
+    def findVcsRepository(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepository]]
+
+    /** Find all branches (created via the `fork` functionality) for the repository with the given id.
+      *
+      * @param originalRepositoryId
+      *   The id from the original repository from which the branches (forks) were created.
+      * @return
+      *   A stream of tuples holding the username of the branch owner and the branch (repository) names.
+      */
+    def findVcsRepositoryBranches(originalRepositoryId: VcsRepositoryId): Stream[F, (Username, VcsRepositoryName)]
+
+    /** Search for the internal database specific (auto generated) ID of the given owner / repository combination which
+      * serves as a primary key for the database table.
+      *
+      * @param owner
+      *   Data about the owner of the repository containing information needed to query the database.
+      * @param name
+      *   The repository name which must be unique in regard to the owner.
+      * @return
+      *   An option to the internal database ID.
+      */
+    def findVcsRepositoryId(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepositoryId]]
+
+    /** Search for a repository owner of whom we only know the name.
+      *
+      * @param name
+      *   The name of the repository owner which is the username of the actual owners account.
+      * @return
+      *   An option to successfully found repository owner.
+      */
+    def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]]
+
+    /** Search for a possible parent repository (i.e. a repository forked from) of the one described by the given name
+      * and owner.
+      *
+      * @param owner
+      *   Data about the owner of the repository containing information needed to query the database.
+      * @param name
+      *   The repository name which must be unique in regard to the owner.
+      * @return
+      *   An option to the successfully found parent vcs repository entry.
+      */
+    def findVcsRepositoryParentFork(owner: VcsRepositoryOwner, name: VcsRepositoryName): F[Option[VcsRepository]]
+
+    /** Return a list of all repositories from all users.
+      *
+      * @param requester
+      *   An optional account (None if guest i.e. not logged in user) for whom the list shall be created. This will
+      *   affect which repositories will be returned in regard to access rights.
+      * @param ordering
+      *   The desired ordering of the list.
+      * @return
+      *   A stream of vcs repository entries in the requested order.
+      */
+    def listAllRepositories(requester: Option[Account])(
+        ordering: VcsMetadataRepositoriesOrdering
+    ): Stream[F, VcsRepository]
+
+    /** Return a list of all repositories of the given owner.
+      *
+      * @param requester
+      *   An optional account (None if guest i.e. not logged in user) for whom the list shall be created. This will
+      *   affect which repositories will be returned in regard to access rights.
+      * @param owner
+      *   Data about the owner of the repositories containing information needed to query the database.
+      * @return
+      *   A stream of vcs repository entries ordered by name which may be empty.
+      */
+    def listRepositories(requester: Option[Account])(owner: VcsRepositoryOwner): Stream[F, VcsRepository]
+
+    /** Update the database entry for the given vcs repository.
+      *
+      * @param repository
+      *   The vcs repository metadata that shall be updated within the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def updateVcsRepository(repository: VcsRepository): F[Int]
 
 }
 
 /** Helper types to provide sorting instructions to several functions.
   */
 enum VcsMetadataRepositoriesOrdering {
-  case NameAscending  extends VcsMetadataRepositoriesOrdering
-  case NameDescending extends VcsMetadataRepositoriesOrdering
+    case NameAscending  extends VcsMetadataRepositoriesOrdering
+    case NameDescending extends VcsMetadataRepositoriesOrdering
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala	2025-01-13 17:13:25.056470978 +0000
@@ -65,1572 +65,1661 @@
     vcsMetadataRepo: VcsMetadataRepository[F],
     ticketsProjectRepo: ProjectRepository[F]
 ) extends Http4sDsl[F] {
-  private val log                               = LoggerFactory.getLogger(getClass)
-  given org.typelevel.log4cats.LoggerFactory[F] = org.typelevel.log4cats.slf4j.Slf4jFactory.create[F]
+    private val log                               = LoggerFactory.getLogger(getClass)
+    given org.typelevel.log4cats.LoggerFactory[F] = org.typelevel.log4cats.slf4j.Slf4jFactory.create[F]
 
-  private val MaximumFileSize     = configuration.renderMaximumFileSize
-  private val createRepoPath      = uri"/repo/create"
-  private val darcsConfig         = configuration.darcs
-  private val linkConfig          = configuration.external
-  private val linkToTicketService = if (configuration.tickets.enabled) configuration.tickets.baseUri.some else None
-  private val sshConfig           = configuration.ssh
-
-  // The base URI for our site which that be passed into some templates which create links themselfes.
-  private val baseUri = linkConfig.createFullUri(Uri())
-
-  /** Load the repository metadata with the given owner and name from the database and return it and its primary key id
-    * if the repository exists and is readable by the given user account.
-    *
-    * @param currentUser
-    *   The user account that is requesting access to the repository or None for a guest user.
-    * @param repositoryOwnerName
-    *   The name of the account that owns the repository.
-    * @param repositoryName
-    *   The name of the repository. A repository name must start with a letter or number and must contain only
-    *   alphanumeric ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
-    * @return
-    *   An option to a tuple holding the [[VcsRepository]] and its primary key id.
-    */
-  private def loadRepo(
-      currentUser: Option[Account]
-  )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Option[(VcsRepository, VcsRepositoryId)]] =
-    for {
-      owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
-      loadedRepo <- owner match {
-        case None => Sync[F].pure(None)
-        case Some(owner) =>
-          (
-            vcsMetadataRepo.findVcsRepository(owner, repositoryName),
-            vcsMetadataRepo.findVcsRepositoryId(owner, repositoryName)
-          ).mapN {
-            case (Some(repo), Some(repoId)) => (repo, repoId).some
-            case _                          => None
-          }
-      }
-      // TODO: Replace with whatever we implement as proper permission model. ;-)
-      repoAndId = currentUser match {
-        case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
-        case Some(user) =>
-          loadedRepo.filter(tuple => tuple._1.isPrivate === false || tuple._1.owner === user.toVcsRepositoryOwner)
-      }
-    } yield repoAndId
-
-  /** Delete the given directory recursively. It is checked if the given directory is a direct sub directory of the
-    * owner's directory under `repositoriesDirectory` and only if this is the case is the directory removed.
-    *
-    * @param ownerName
-    *   The name of the repository owner which is used for the directory check.
-    * @param repoDirectory
-    *   The path on the filesystem to the directory that shall be deleted.
-    * @return
-    *   `true` if the directory was deleted.
-    */
-  protected def deleteRepositoryDirectory(
-      ownerName: Username
-  )(repoDirectory: java.nio.file.Path): F[Boolean] =
-    for {
-      _ <- Sync[F].delay(log.debug(s"Request to delete repository dir: $repoDirectory"))
-      reposDirPath <- Sync[F].delay(
-        configuration.darcs.repositoriesDirectory.toPath.resolve(ownerName.toString)
-      )
-      isSubDir <- Sync[F].delay(reposDirPath.equals(repoDirectory.getParent()))
-      deleted <-
-        Sync[F].delay {
-          if (isSubDir) {
-            Files.walkFileTree(
-              repoDirectory,
-              new FileVisitor[java.nio.file.Path] {
-                override def visitFileFailed(file: java.nio.file.Path, exc: IOException): FileVisitResult =
-                  FileVisitResult.CONTINUE
-
-                override def visitFile(
-                    file: java.nio.file.Path,
-                    attrs: java.nio.file.attribute.BasicFileAttributes
-                ): FileVisitResult = {
-                  Files.delete(file)
-                  FileVisitResult.CONTINUE
-                }
+    private val MaximumFileSize     = configuration.renderMaximumFileSize
+    private val createRepoPath      = uri"/repo/create"
+    private val darcsConfig         = configuration.darcs
+    private val linkConfig          = configuration.external
+    private val linkToTicketService = if (configuration.tickets.enabled) configuration.tickets.baseUri.some else None
+    private val sshConfig           = configuration.ssh
+
+    // The base URI for our site which that be passed into some templates which create links themselfes.
+    private val baseUri = linkConfig.createFullUri(Uri())
+
+    /** Load the repository metadata with the given owner and name from the database and return it and its primary key
+      * id if the repository exists and is readable by the given user account.
+      *
+      * @param currentUser
+      *   The user account that is requesting access to the repository or None for a guest user.
+      * @param repositoryOwnerName
+      *   The name of the account that owns the repository.
+      * @param repositoryName
+      *   The name of the repository. A repository name must start with a letter or number and must contain only
+      *   alphanumeric ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters
+      *   long.
+      * @return
+      *   An option to a tuple holding the [[VcsRepository]] and its primary key id.
+      */
+    private def loadRepo(
+        currentUser: Option[Account]
+    )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Option[(VcsRepository, VcsRepositoryId)]] =
+        for {
+            owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+            loadedRepo <- owner match {
+                case None => Sync[F].pure(None)
+                case Some(owner) =>
+                    (
+                        vcsMetadataRepo.findVcsRepository(owner, repositoryName),
+                        vcsMetadataRepo.findVcsRepositoryId(owner, repositoryName)
+                    ).mapN {
+                        case (Some(repo), Some(repoId)) => (repo, repoId).some
+                        case _                          => None
+                    }
+            }
+            // TODO: Replace with whatever we implement as proper permission model. ;-)
+            repoAndId = currentUser match {
+                case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
+                case Some(user) =>
+                    loadedRepo.filter(tuple =>
+                        tuple._1.isPrivate === false || tuple._1.owner === user.toVcsRepositoryOwner
+                    )
+            }
+        } yield repoAndId
 
-                override def preVisitDirectory(
-                    dir: java.nio.file.Path,
-                    attrs: java.nio.file.attribute.BasicFileAttributes
-                ): FileVisitResult = FileVisitResult.CONTINUE
-
-                override def postVisitDirectory(
-                    dir: java.nio.file.Path,
-                    exc: IOException
-                ): FileVisitResult = {
-                  Files.delete(dir)
-                  FileVisitResult.CONTINUE
+    /** Delete the given directory recursively. It is checked if the given directory is a direct sub directory of the
+      * owner's directory under `repositoriesDirectory` and only if this is the case is the directory removed.
+      *
+      * @param ownerName
+      *   The name of the repository owner which is used for the directory check.
+      * @param repoDirectory
+      *   The path on the filesystem to the directory that shall be deleted.
+      * @return
+      *   `true` if the directory was deleted.
+      */
+    protected def deleteRepositoryDirectory(
+        ownerName: Username
+    )(repoDirectory: java.nio.file.Path): F[Boolean] =
+        for {
+            _ <- Sync[F].delay(log.debug(s"Request to delete repository dir: $repoDirectory"))
+            reposDirPath <- Sync[F].delay(
+                configuration.darcs.repositoriesDirectory.toPath.resolve(ownerName.toString)
+            )
+            isSubDir <- Sync[F].delay(reposDirPath.equals(repoDirectory.getParent()))
+            deleted <-
+                Sync[F].delay {
+                    if (isSubDir) {
+                        Files.walkFileTree(
+                            repoDirectory,
+                            new FileVisitor[java.nio.file.Path] {
+                                override def visitFileFailed(
+                                    file: java.nio.file.Path,
+                                    exc: IOException
+                                ): FileVisitResult = FileVisitResult.CONTINUE
+
+                                override def visitFile(
+                                    file: java.nio.file.Path,
+                                    attrs: java.nio.file.attribute.BasicFileAttributes
+                                ): FileVisitResult = {
+                                    Files.delete(file)
+                                    FileVisitResult.CONTINUE
+                                }
+
+                                override def preVisitDirectory(
+                                    dir: java.nio.file.Path,
+                                    attrs: java.nio.file.attribute.BasicFileAttributes
+                                ): FileVisitResult = FileVisitResult.CONTINUE
+
+                                override def postVisitDirectory(
+                                    dir: java.nio.file.Path,
+                                    exc: IOException
+                                ): FileVisitResult = {
+                                    Files.delete(dir)
+                                    FileVisitResult.CONTINUE
+                                }
+                            }
+                        )
+                        Files.deleteIfExists(repoDirectory)
+                    } else {
+                        log.warn(
+                            s"Refused requested removal of directory $repoDirectory which is not a direct sub directory of the configured repositories directory!"
+                        )
+                        false
+                    }
                 }
-              }
+        } yield deleted
+
+    /** Logic for creating a distribution file and serving it as a download to the user.
+      *
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
+      * @param repositoryName
+      *   The actual name of the repository.
+      * @return
+      *   An option to the file name that can be streamed to the user.
+      */
+    private def doDownloadDistribution(
+        user: Option[Account]
+    )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Option[fs2.io.file.Path]] = {
+        // TODO: Clean this up and refactor into proper helpers.
+
+        /** Helper function to package a repository into a target directory because the darcs dist task has problems
+          * with unicode characters in filenames. The archive will be created with a temporary name and afterwards it
+          * will be moved to the given target directory and will be named `$repositoryName.tar.gz`.
+          *
+          * @param respositoryOwnerDirectory
+          *   The directory in which the repository resides and in which the command will be executed.
+          * @param repositoryName
+          *   The name of the repository (and the name of the directory to be packaged).
+          * @param targetDirectory
+          *   The target (destination) directory into which the archive will be created.
+          */
+        def packageRepository(
+            basePath: os.Path,
+            repositoryName: String,
+            targetDirectory: os.Path
+        ): F[(Int, Chain[String], Chain[String])] = {
+            val targetFile     = targetDirectory / s"${repositoryName}.tar.gz"
+            val tempTargetFile = Files.createTempFile(targetDirectory.toNIO, "download", s"${repositoryName}.tar.gz")
+            log.debug(s"Packaging repository $repositoryName into $targetFile.")
+            val tarOptions      = List("-czf", tempTargetFile.toString, "--exclude=_darcs", repositoryName.toString)
+            val externalCommand = os.proc("tar", tarOptions)
+            for {
+                _       <- Sync[F].delay(Files.createDirectories(targetDirectory.toNIO))
+                process <- Sync[F].delay(externalCommand.call(cwd = basePath, check = false))
+                _       <- Sync[F].delay(os.remove(targetFile))
+                _       <- Sync[F].delay(os.move(os.Path(tempTargetFile), targetFile))
+            } yield (process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
+        }
+
+        for {
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            repositoryOwnerDirectory <- Sync[F].delay(
+                os.Path(
+                    Paths.get(
+                        darcsConfig.repositoriesDirectory.toPath.toString,
+                        repositoryOwnerName.toString
+                    )
+                )
             )
-            Files.deleteIfExists(repoDirectory)
-          } else {
-            log.warn(
-              s"Refused requested removal of directory $repoDirectory which is not a direct sub directory of the configured repositories directory!"
+            targetDirectory <- Sync[F].delay(
+                os.Path(
+                    Paths.get(
+                        configuration.downloadDirectory.toPath.toString,
+                        repositoryOwnerName.toString
+                    )
+                )
             )
-            false
-          }
-        }
-    } yield deleted
+            createDist <- repo.traverse(repo =>
+                packageRepository(repositoryOwnerDirectory, repositoryName.toString, targetDirectory)
+            )
+            _ = createDist.map { case (exitCode, stdout, stderr) =>
+                if (exitCode =!= 0)
+                    log.error(
+                        s"An error occured while packaging $repositoryName: ${stderr.toList.mkString}, ${stdout.toList.mkString}"
+                    )
+            }
+            requestedFilePath <- repo.traverse(_ =>
+                Sync[F].delay(
+                    fs2.io.file.Path.fromNioPath((targetDirectory / s"${repositoryName.toString}.tar.gz").toNIO)
+                )
+            )
+        } yield requestedFilePath
+    }
+
+    /** Logic for rendering a list of all repositories visible to the given user account.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowAllRepositories(csrf: Option[CsrfToken])(user: Option[Account]): F[Response[F]] =
+        for {
+            language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repos <- vcsMetadataRepo
+                .listAllRepositories(user)(VcsMetadataRepositoriesOrdering.NameAscending)
+                .compile
+                .toList
+            actionBaseUri <- Sync[F].delay(linkConfig.createFullUri(uri"projects"))
+            resp <- Ok(
+                views.html.showAllRepositories(lang = language)(actionBaseUri, csrf, s"Smederee - Projects".some, user)(
+                    repos
+                )
+            )
+        } yield resp
 
-  /** Logic for creating a distribution file and serving it as a download to the user.
-    *
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @return
-    *   An option to the file name that can be streamed to the user.
-    */
-  private def doDownloadDistribution(
-      user: Option[Account]
-  )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Option[fs2.io.file.Path]] = {
-    // TODO: Clean this up and refactor into proper helpers.
-
-    /** Helper function to package a repository into a target directory because the darcs dist task has problems with
-      * unicode characters in filenames. The archive will be created with a temporary name and afterwards it will be
-      * moved to the given target directory and will be named `$repositoryName.tar.gz`.
+    /** Logic for rendering a list of repositories of the given owner for a specific user account. This function takes
+      * visibility into account.
       *
-      * @param respositoryOwnerDirectory
-      *   The directory in which the repository resides and in which the command will be executed.
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param repositoriesOwnerName
+      *   The name of a user whos repositories shall be listed.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @return
+      *   An HTTP response containing the rendered HTML or an error.
+      */
+    private def doShowRepositories(
+        csrf: Option[CsrfToken]
+    )(repositoriesOwnerName: Username)(user: Option[Account]): F[Response[F]] =
+        for {
+            owner    <- vcsMetadataRepo.findVcsRepositoryOwner(repositoriesOwnerName)
+            language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repos    <- owner.traverse(owner => vcsMetadataRepo.listRepositories(user)(owner).compile.toList)
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(Uri(path = Uri.Path.unsafeFromString(s"~$repositoriesOwnerName")))
+            )
+            resp <- owner match {
+                case None => // TODO: Better error message...
+                    NotFound(
+                        views.html.showRepositories(lang = language)(
+                            actionBaseUri,
+                            csrf,
+                            s"Smederee/~$repositoriesOwnerName".some,
+                            user
+                        )(repos.getOrElse(List.empty), repositoriesOwnerName)
+                    )
+                case Some(_) =>
+                    Ok(
+                        views.html.showRepositories(lang = language)(
+                            actionBaseUri,
+                            csrf,
+                            s"Smederee/~$repositoriesOwnerName".some,
+                            user
+                        )(repos.getOrElse(List.empty), repositoriesOwnerName)
+                    )
+            }
+        } yield resp
+
+    /** Logic for rendering an overview of the existing branches of a repository.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
       * @param repositoryName
-      *   The name of the repository (and the name of the directory to be packaged).
-      * @param targetDirectory
-      *   The target (destination) directory into which the archive will be created.
+      *   The actual name of the repository.
+      * @return
+      *   An HTTP response containing the rendered HTML.
       */
-    def packageRepository(
-        basePath: os.Path,
-        repositoryName: String,
-        targetDirectory: os.Path
-    ): F[(Int, Chain[String], Chain[String])] = {
-      val targetFile     = targetDirectory / s"${repositoryName}.tar.gz"
-      val tempTargetFile = Files.createTempFile(targetDirectory.toNIO, "download", s"${repositoryName}.tar.gz")
-      log.debug(s"Packaging repository $repositoryName into $targetFile.")
-      val tarOptions      = List("-czf", tempTargetFile.toString, "--exclude=_darcs", repositoryName.toString)
-      val externalCommand = os.proc("tar", tarOptions)
-      for {
-        _       <- Sync[F].delay(Files.createDirectories(targetDirectory.toNIO))
-        process <- Sync[F].delay(externalCommand.call(cwd = basePath, check = false))
-        _       <- Sync[F].delay(os.remove(targetFile))
-        _       <- Sync[F].delay(os.move(os.Path(tempTargetFile), targetFile))
-      } yield (process.exitCode, Chain(process.out.text()), Chain(process.err.text()))
-    }
-
-    for {
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      repositoryOwnerDirectory <- Sync[F].delay(
-        os.Path(
-          Paths.get(
-            darcsConfig.repositoriesDirectory.toPath.toString,
-            repositoryOwnerName.toString
-          )
-        )
-      )
-      targetDirectory <- Sync[F].delay(
-        os.Path(
-          Paths.get(
-            configuration.downloadDirectory.toPath.toString,
-            repositoryOwnerName.toString
-          )
-        )
-      )
-      createDist <- repo.traverse(repo =>
-        packageRepository(repositoryOwnerDirectory, repositoryName.toString, targetDirectory)
-      )
-      _ = createDist.map { case (exitCode, stdout, stderr) =>
-        if (exitCode =!= 0)
-          log.error(
-            s"An error occured while packaging $repositoryName: ${stderr.toList.mkString}, ${stdout.toList.mkString}"
-          )
-      }
-      requestedFilePath <- repo.traverse(_ =>
-        Sync[F].delay(
-          fs2.io.file.Path.fromNioPath((targetDirectory / s"${repositoryName.toString}.tar.gz").toNIO)
-        )
-      )
-    } yield requestedFilePath
-  }
-
-  /** Logic for rendering a list of all repositories visible to the given user account.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowAllRepositories(csrf: Option[CsrfToken])(user: Option[Account]): F[Response[F]] =
-    for {
-      language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repos <- vcsMetadataRepo
-        .listAllRepositories(user)(VcsMetadataRepositoriesOrdering.NameAscending)
-        .compile
-        .toList
-      actionBaseUri <- Sync[F].delay(linkConfig.createFullUri(uri"projects"))
-      resp <- Ok(
-        views.html.showAllRepositories(lang = language)(actionBaseUri, csrf, s"Smederee - Projects".some, user)(repos)
-      )
-    } yield resp
-
-  /** Logic for rendering a list of repositories of the given owner for a specific user account. This function takes
-    * visibility into account.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param repositoriesOwnerName
-    *   The name of a user whos repositories shall be listed.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @return
-    *   An HTTP response containing the rendered HTML or an error.
-    */
-  private def doShowRepositories(
-      csrf: Option[CsrfToken]
-  )(repositoriesOwnerName: Username)(user: Option[Account]): F[Response[F]] =
-    for {
-      owner    <- vcsMetadataRepo.findVcsRepositoryOwner(repositoriesOwnerName)
-      language <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repos    <- owner.traverse(owner => vcsMetadataRepo.listRepositories(user)(owner).compile.toList)
-      actionBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(Uri(path = Uri.Path.unsafeFromString(s"~$repositoriesOwnerName")))
-      )
-      resp <- owner match {
-        case None => // TODO: Better error message...
-          NotFound(
-            views.html.showRepositories(lang = language)(
-              actionBaseUri,
-              csrf,
-              s"Smederee/~$repositoriesOwnerName".some,
-              user
-            )(repos.getOrElse(List.empty), repositoriesOwnerName)
-          )
-        case Some(_) =>
-          Ok(
-            views.html.showRepositories(lang = language)(
-              actionBaseUri,
-              csrf,
-              s"Smederee/~$repositoriesOwnerName".some,
-              user
-            )(repos.getOrElse(List.empty), repositoriesOwnerName)
-          )
-      }
-    } yield resp
-
-  /** Logic for rendering an overview of the existing branches of a repository.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowRepositoryBranches(
-      csrf: Option[CsrfToken]
-  )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
-    for {
-      language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      actionBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-            )
-          )
-        )
-      )
-      branches <- repoAndId.map(_._2) match {
-        case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-        case _            => Sync[F].delay(List.empty)
-      }
-      parentFork <- repo match {
-        case None       => Sync[F].pure(None)
-        case Some(repo) => vcsMetadataRepo.findVcsRepositoryParentFork(repo.owner, repo.name)
-      }
-      resp <- repo match {
-        case None => NotFound("Repository not found!")
-        case Some(repo) =>
-          Ok(
-            views.html.showRepositoryBranches(baseUri, lang = language)(
-              actionBaseUri,
-              csrf,
-              linkToTicketService,
-              s"Smederee/~$repositoryOwnerName/$repositoryName".some,
-              user
-            )(repo, branches)
-          )
-      }
-    } yield resp
-
-  /** Logic for rendering the content of a repository directory or file visible to the given user account.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowRepositoryOverview(
-      csrf: Option[CsrfToken]
-  )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
-    for {
-      language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      actionBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-            )
-          )
-        )
-      )
-      sshUri <- sshConfig.enabled match {
-        case false => Sync[F].pure(None)
-        case true =>
-          Sync[F].delay(
-            s"SSH_PORT=${sshConfig.port.toString} darcs clone darcs@${sshConfig.host.toString}:${repositoryOwnerName.toString}/${repositoryName.toString}".some
-          )
-      }
-      directory <- Sync[F].delay(
-        os.Path(
-          Paths.get(
-            darcsConfig.repositoriesDirectory.toPath.toString,
-            repositoryOwnerName.toString
-          )
-        )
-      )
-      branches <- repoAndId.map(_._2) match {
-        case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-        case _            => Sync[F].delay(List.empty)
-      }
-      vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain(s"--max-count=2", "--xml-output"))
-      xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
-      patches <- Sync[F].delay(
-        (xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList.map { patch =>
-          // TODO: Move the hard coded number into a configuration value.
-          if (patch.comment.exists(_.length > 320))
-            patch.copy(comment = patch.comment.map(c => VcsPatchComment(c.toString.take(320) + "...")))
-          else
-            patch
-        }
-      )
-      readmeData <- repo.traverse(repo => doLoadReadme(repo))
-      readme <- readmeData match {
-        case Some((lines, Some(filename))) =>
-          if (filename.matches("(?iu).*\\.(md|markdown)$")) {
-            Sync[F]
-              .delay(MarkdownRenderer.renderRepositoryMarkdownFile(repo.map(_.name.toString))(lines.mkString("\n")))
-              .map(_.some)
-          } else {
-            Sync[F].delay(lines.mkString("\n").some)
-          }
-        case _ => Sync[F].delay(None)
-      }
-      readmeName = readmeData.flatMap(_._2)
-      parentFork <- repo match {
-        case None       => Sync[F].pure(None)
-        case Some(repo) => vcsMetadataRepo.findVcsRepositoryParentFork(repo.owner, repo.name)
-      }
-      resp <- repo match {
-        case None => NotFound("Repository not found!")
-        case Some(repo) =>
-          Ok(
-            views.html.showRepositoryOverview(baseUri, lang = language)(
-              actionBaseUri,
-              csrf,
-              linkToTicketService,
-              s"Smederee/~$repositoryOwnerName/$repositoryName".some,
-              user
-            )(
-              repo,
-              vcsRepositoryBranches = branches,
-              vcsRepositoryHistory = patches,
-              vcsRepositoryParentFork = parentFork,
-              vcsRepositoryReadme = readme,
-              vcsRepositoryReadmeFilename = readmeName,
-              vcsRepositorySshUri = sshUri
-            )
-          )
-      }
-    } yield resp
-
-  /** Logic for rendering the content of a repository directory or file visible to the given user account.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @param filePath
-    *   An URI path which describes the path the the requested part of the repository (empty or `/` for the root
-    *   directory of the repo).
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowRepositoryFiles(csrf: Option[CsrfToken])(
-      user: Option[Account]
-  )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(filePath: Uri.Path): F[Response[F]] =
-    for {
-      language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      requestedFilePath <- repo
-        .traverse(_ =>
-          Sync[F].delay(
-            os.Path(
-              Paths.get(
-                darcsConfig.repositoriesDirectory.toPath.toString,
-                repositoryOwnerName.toString,
-                repositoryName.toString,
-                filePath.segments.map(_.decoded()).mkString("/")
-              )
-            )
-          )
-        )
-      fileType <- requestedFilePath
-        .filter(os.isFile) // Only determine the mime type for regular files.
-        .fold(Sync[F].pure(none[MediaType]))(path =>
-          // TODO: Refactor this into a dedicated function and clean it up.
-          Sync[F].delay(
-            MediaType
-              .parse(
-                os.proc("file", "--mime-type", path.toString)
-                  .call()
-                  .out
-                  .text()
-                  .trim
-                  .reverse
-                  .takeWhile(char => !char.isWhitespace)
-                  .reverse
-              )
-              .toOption
-          )
-        )
-      _        <- Sync[F].delay(log.debug(s"Requested to view file type: $fileType"))
-      viewFile <- Sync[F].delay(fileType.exists(_.isText)) // Only display certain file types.
-      listing <-
-        requestedFilePath match {
-          case None                => Sync[F].delay(IndexedSeq.empty)
-          case Some(_) if viewFile => Sync[F].delay(IndexedSeq.empty)
-          case Some(path)          => doListFiles(path)
-        }
-      content <-
-        requestedFilePath match {
-          case None => Sync[F].delay(IndexedSeq.empty)
-          case Some(path) =>
-            if (viewFile)
-              for {
-                size <- Sync[F].delay(os.size(path))
-                stream <-
-                  if (size <= MaximumFileSize)
-                    Sync[F].delay(os.read.lines.stream(path))
-                  else
-                    Sync[F].delay(os.Generator("File is too big!"))
-                lines <- Sync[F].delay(stream.toVector)
-              } yield lines
-            else if (os.isFile(path))
-              Sync[F].delay(Vector("Sorry, but displaying such file types is currently not implemented!"))
-            else
-              Sync[F].delay(IndexedSeq.empty)
-        }
-      repositoryBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-            )
-          )
-        )
-      )
-      actionBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(Uri(path = repositoryBaseUri.path.addSegment("files") |+| filePath))
-      )
-      goBackUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path = Uri.Path(actionBaseUri.path.segments.reverse.drop(1).reverse))
-        )
-      )
-      fileContent <- content.isEmpty match {
-        case false =>
-          if (actionBaseUri.path.toString.toLowerCase(java.util.Locale.ROOT).endsWith(".md"))
-            Sync[F].delay(
-              List(MarkdownRenderer.renderRepositoryMarkdownFile(None)(content.mkString("\n")))
-            )
-          else
-            Sync[F].delay(content.toList)
-        case true =>
-          Sync[F].pure(List.empty)
-      }
-      branches <- repoAndId.map(_._2) match {
-        case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-        case _            => Sync[F].delay(List.empty)
-      }
-      title <- Sync[F].delay(
-        s"Smederee/~$repositoryOwnerName/$repositoryName/${filePath.segments.map(_.decoded()).mkString("/")}"
-      )
-      resp <-
-        repo match {
-          case None => NotFound("Repository not found!")
-          case Some(repo) =>
-            if (
-              filePath.segments.mkString
-                .startsWith("_darcs") || filePath.segments.mkString.startsWith("/_darcs")
+    private def doShowRepositoryBranches(
+        csrf: Option[CsrfToken]
+    )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
+        for {
+            language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
             )
-              NotFound("File not found!")
-            else
-              Ok(
-                views.html.showRepositoryFiles(baseUri, lang = language)(
-                  actionBaseUri,
-                  csrf,
-                  goBackUri.some,
-                  linkToTicketService,
-                  title.some,
-                  user
-                )(fileContent, listing, repositoryBaseUri, repo, branches)
-              )
-        }
-    } yield resp
+            branches <- repoAndId.map(_._2) match {
+                case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                case _            => Sync[F].delay(List.empty)
+            }
+            parentFork <- repo match {
+                case None       => Sync[F].pure(None)
+                case Some(repo) => vcsMetadataRepo.findVcsRepositoryParentFork(repo.owner, repo.name)
+            }
+            resp <- repo match {
+                case None => NotFound("Repository not found!")
+                case Some(repo) =>
+                    Ok(
+                        views.html.showRepositoryBranches(baseUri, lang = language)(
+                            actionBaseUri,
+                            csrf,
+                            linkToTicketService,
+                            s"Smederee/~$repositoryOwnerName/$repositoryName".some,
+                            user
+                        )(repo, branches)
+                    )
+            }
+        } yield resp
 
-  /** Logic for showing the history (changes) for the requested repository.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @param fromEntry
-    *   The optional number of the change from which the history shall be shown.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  def doShowRepositoryHistory(
-      csrf: Option[CsrfToken]
-  )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(
-      fromEntry: Option[Int]
-  ): F[Response[F]] =
-    for {
-      language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      directory <- Sync[F].delay(
-        os.Path(
-          Paths.get(
-            darcsConfig.repositoriesDirectory.toPath.toString,
-            repositoryOwnerName.toString
-          )
-        )
-      )
-      branches <- repoAndId.map(_._2) match {
-        case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-        case _            => Sync[F].delay(List.empty)
-      }
-      countChanges    <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count"))
-      numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt)
-      maxCount = 10
-      from     = fromEntry.getOrElse(1)
-      to =
-        if ((from + maxCount > numberOfChanges) && (numberOfChanges > 0))
-          numberOfChanges
-        else
-          from + maxCount
-      next = if (to < numberOfChanges) Option(to + 1) else None
-      vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(
-        Chain(s"--index=$from-$to", "--summary", "--xml-output")
-      )
-      xmlLog  <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
-      patches <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList)
-      actionBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-            )
-          )
-        )
-      )
-      goBackUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(
-                Uri.Path.Segment(s"~$repositoryOwnerName"),
-                Uri.Path.Segment(repositoryName.toString)
-              )
-            )
-          )
-        )
-      )
-      repositoryBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-            )
-          )
-        )
-      )
-      resp <- repo match {
-        case None => NotFound()
-        case Some(repo) =>
-          Ok(
-            views.html.showRepositoryHistory(baseUri, lang = language)(
-              actionBaseUri,
-              csrf,
-              goBackUri.some,
-              linkToTicketService,
-              s"Smederee - History of ~$repositoryOwnerName/$repositoryName".some,
-              user
-            )(patches, next, repositoryBaseUri, repo, branches)
-          )
-      }
-    } yield resp
-
-  /** Logic for generating the RSS feed for the requested repository.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @return
-    *   An HTTP response containing the rendered RSS XML.
-    */
-  def doShowRepositoryRssFeed(
-      csrf: Option[CsrfToken]
-  )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
-    for {
-      language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      response <- repo match {
-        case None => NotFound()
-        case Some(repo) =>
-          for {
+    /** Logic for rendering the content of a repository directory or file visible to the given user account.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
+      * @param repositoryName
+      *   The actual name of the repository.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowRepositoryOverview(
+        csrf: Option[CsrfToken]
+    )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
+        for {
+            language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
+            sshUri <- sshConfig.enabled match {
+                case false => Sync[F].pure(None)
+                case true =>
+                    Sync[F].delay(
+                        s"SSH_PORT=${sshConfig.port.toString} darcs clone darcs@${sshConfig.host.toString}:${repositoryOwnerName.toString}/${repositoryName.toString}".some
+                    )
+            }
             directory <- Sync[F].delay(
-              os.Path(
-                Paths.get(
-                  darcsConfig.repositoriesDirectory.toPath.toString,
-                  repositoryOwnerName.toString
+                os.Path(
+                    Paths.get(
+                        darcsConfig.repositoriesDirectory.toPath.toString,
+                        repositoryOwnerName.toString
+                    )
                 )
-              )
             )
+            branches <- repoAndId.map(_._2) match {
+                case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                case _            => Sync[F].delay(List.empty)
+            }
+            vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain(s"--max-count=2", "--xml-output"))
+            xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
+            patches <- Sync[F].delay(
+                (xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList.map { patch =>
+                    // TODO: Move the hard coded number into a configuration value.
+                    if (patch.comment.exists(_.length > 320))
+                        patch.copy(comment = patch.comment.map(c => VcsPatchComment(c.toString.take(320) + "...")))
+                    else
+                        patch
+                }
+            )
+            readmeData <- repo.traverse(repo => doLoadReadme(repo))
+            readme <- readmeData match {
+                case Some((lines, Some(filename))) =>
+                    if (filename.matches("(?iu).*\\.(md|markdown)$")) {
+                        Sync[F]
+                            .delay(
+                                MarkdownRenderer
+                                    .renderRepositoryMarkdownFile(repo.map(_.name.toString))(lines.mkString("\n"))
+                            )
+                            .map(_.some)
+                    } else {
+                        Sync[F].delay(lines.mkString("\n").some)
+                    }
+                case _ => Sync[F].delay(None)
+            }
+            readmeName = readmeData.flatMap(_._2)
+            parentFork <- repo match {
+                case None       => Sync[F].pure(None)
+                case Some(repo) => vcsMetadataRepo.findVcsRepositoryParentFork(repo.owner, repo.name)
+            }
+            resp <- repo match {
+                case None => NotFound("Repository not found!")
+                case Some(repo) =>
+                    Ok(
+                        views.html.showRepositoryOverview(baseUri, lang = language)(
+                            actionBaseUri,
+                            csrf,
+                            linkToTicketService,
+                            s"Smederee/~$repositoryOwnerName/$repositoryName".some,
+                            user
+                        )(
+                            repo,
+                            vcsRepositoryBranches = branches,
+                            vcsRepositoryHistory = patches,
+                            vcsRepositoryParentFork = parentFork,
+                            vcsRepositoryReadme = readme,
+                            vcsRepositoryReadmeFilename = readmeName,
+                            vcsRepositorySshUri = sshUri
+                        )
+                    )
+            }
+        } yield resp
+
+    /** Logic for rendering the content of a repository directory or file visible to the given user account.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
+      * @param repositoryName
+      *   The actual name of the repository.
+      * @param filePath
+      *   An URI path which describes the path the the requested part of the repository (empty or `/` for the root
+      *   directory of the repo).
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowRepositoryFiles(csrf: Option[CsrfToken])(
+        user: Option[Account]
+    )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(filePath: Uri.Path): F[Response[F]] =
+        for {
+            language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            requestedFilePath <- repo
+                .traverse(_ =>
+                    Sync[F].delay(
+                        os.Path(
+                            Paths.get(
+                                darcsConfig.repositoriesDirectory.toPath.toString,
+                                repositoryOwnerName.toString,
+                                repositoryName.toString,
+                                filePath.segments.map(_.decoded()).mkString("/")
+                            )
+                        )
+                    )
+                )
+            fileType <- requestedFilePath
+                .filter(os.isFile) // Only determine the mime type for regular files.
+                .fold(Sync[F].pure(none[MediaType]))(path =>
+                    // TODO: Refactor this into a dedicated function and clean it up.
+                    Sync[F].delay(
+                        MediaType
+                            .parse(
+                                os.proc("file", "--mime-type", path.toString)
+                                    .call()
+                                    .out
+                                    .text()
+                                    .trim
+                                    .reverse
+                                    .takeWhile(char => !char.isWhitespace)
+                                    .reverse
+                            )
+                            .toOption
+                    )
+                )
+            _        <- Sync[F].delay(log.debug(s"Requested to view file type: $fileType"))
+            viewFile <- Sync[F].delay(fileType.exists(_.isText)) // Only display certain file types.
+            listing <-
+                requestedFilePath match {
+                    case None                => Sync[F].delay(IndexedSeq.empty)
+                    case Some(_) if viewFile => Sync[F].delay(IndexedSeq.empty)
+                    case Some(path)          => doListFiles(path)
+                }
+            content <-
+                requestedFilePath match {
+                    case None => Sync[F].delay(IndexedSeq.empty)
+                    case Some(path) =>
+                        if (viewFile)
+                            for {
+                                size <- Sync[F].delay(os.size(path))
+                                stream <-
+                                    if (size <= MaximumFileSize)
+                                        Sync[F].delay(os.read.lines.stream(path))
+                                    else
+                                        Sync[F].delay(os.Generator("File is too big!"))
+                                lines <- Sync[F].delay(stream.toVector)
+                            } yield lines
+                        else if (os.isFile(path))
+                            Sync[F].delay(Vector("Sorry, but displaying such file types is currently not implemented!"))
+                        else
+                            Sync[F].delay(IndexedSeq.empty)
+                }
+            repositoryBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(Uri(path = repositoryBaseUri.path.addSegment("files") |+| filePath))
+            )
+            goBackUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path = Uri.Path(actionBaseUri.path.segments.reverse.drop(1).reverse))
+                )
+            )
+            fileContent <- content.isEmpty match {
+                case false =>
+                    if (actionBaseUri.path.toString.toLowerCase(java.util.Locale.ROOT).endsWith(".md"))
+                        Sync[F].delay(
+                            List(MarkdownRenderer.renderRepositoryMarkdownFile(None)(content.mkString("\n")))
+                        )
+                    else
+                        Sync[F].delay(content.toList)
+                case true =>
+                    Sync[F].pure(List.empty)
+            }
+            branches <- repoAndId.map(_._2) match {
+                case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                case _            => Sync[F].delay(List.empty)
+            }
+            title <- Sync[F].delay(
+                s"Smederee/~$repositoryOwnerName/$repositoryName/${filePath.segments.map(_.decoded()).mkString("/")}"
+            )
+            resp <-
+                repo match {
+                    case None => NotFound("Repository not found!")
+                    case Some(repo) =>
+                        if (
+                            filePath.segments.mkString
+                                .startsWith("_darcs") || filePath.segments.mkString.startsWith("/_darcs")
+                        )
+                            NotFound("File not found!")
+                        else
+                            Ok(
+                                views.html.showRepositoryFiles(baseUri, lang = language)(
+                                    actionBaseUri,
+                                    csrf,
+                                    goBackUri.some,
+                                    linkToTicketService,
+                                    title.some,
+                                    user
+                                )(fileContent, listing, repositoryBaseUri, repo, branches)
+                            )
+                }
+        } yield resp
+
+    /** Logic for showing the history (changes) for the requested repository.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
+      * @param repositoryName
+      *   The actual name of the repository.
+      * @param fromEntry
+      *   The optional number of the change from which the history shall be shown.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    def doShowRepositoryHistory(
+        csrf: Option[CsrfToken]
+    )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(
+        fromEntry: Option[Int]
+    ): F[Response[F]] =
+        for {
+            language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            directory <- Sync[F].delay(
+                os.Path(
+                    Paths.get(
+                        darcsConfig.repositoriesDirectory.toPath.toString,
+                        repositoryOwnerName.toString
+                    )
+                )
+            )
+            branches <- repoAndId.map(_._2) match {
+                case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                case _            => Sync[F].delay(List.empty)
+            }
             countChanges    <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count"))
             numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt)
-            maxCount = 15
-            from     = 1
+            maxCount = 10
+            from     = fromEntry.getOrElse(1)
             to =
-              if ((from + maxCount > numberOfChanges) && (numberOfChanges > 0))
-                numberOfChanges
-              else
-                from + maxCount
+                if ((from + maxCount > numberOfChanges) && (numberOfChanges > 0))
+                    numberOfChanges
+                else
+                    from + maxCount
             next = if (to < numberOfChanges) Option(to + 1) else None
             vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(
-              Chain(s"--index=$from-$to", "--summary", "--xml-output")
+                Chain(s"--index=$from-$to", "--summary", "--xml-output")
             )
             xmlLog  <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
             patches <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList)
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
+            goBackUri <- Sync[F].delay(
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
             repositoryBaseUri <- Sync[F].delay(
-              linkConfig.createFullUri(
-                Uri(path =
-                  Uri.Path(
-                    Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-                  )
-                )
-              )
-            )
-            items = patches.map(_.toRssItem(repositoryBaseUri.addSegment("patch")))
-            channel = RssChannel(
-              title = s"Smederee: ~$repositoryOwnerName/$repositoryName",
-              link = repositoryBaseUri.toString,
-              description =
-                repo.description.map(_.toString).getOrElse(s"RSS feed for $repositoryOwnerName/$repositoryName"),
-              copyright = None,
-              image = None,
-              items = items
-            )
-            resp <- Ok(views.xml.rss(channel))
-          } yield resp
-      }
-    } yield response
-
-  /** Get a diff for the requested patch and return the rendered response of it.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of repositories shall be rendered.
-    * @param repositoryOwnerName
-    *   The name of the user who owns the repository.
-    * @param repositoryName
-    *   The actual name of the repository.
-    * @param hash
-    *   The unique hash of the patch identifying it.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  def doShowRepositoryPatchDetails(csrf: Option[CsrfToken])(
-      user: Option[Account]
-  )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(hash: DarcsHash): F[Response[F]] =
-    for {
-      language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
-      repo = repoAndId.map(_._1)
-      directory <- Sync[F].delay(
-        os.Path(
-          Paths.get(
-            darcsConfig.repositoriesDirectory.toPath.toString,
-            repositoryOwnerName.toString
-          )
-        )
-      )
-      vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(
-        Chain(s"--hash=${hash.toString}", "--summary", "--xml-output")
-      )
-      xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
-      patch  <- Sync[F].delay((xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList.headOption)
-      darcsDiff        <- darcs.diff(directory.toNIO)(repositoryName.toString)(hash)(Chain("--no-pause-for-gui"))
-      patchCutMarker   <- Sync[F].delay(s"patch ${hash.toString}")
-      cleanedPatchDiff <- Sync[F].delay(darcsDiff.stdout.toList.mkString.split(patchCutMarker)(0))
-      patchDetails     <- Sync[F].delay(cleanedPatchDiff)
-      htmlPatchDetails <- Sync[F].delay(new UtilsAnsiHtml().convertAnsiToHtml(patchDetails))
-      cleanedHtmlPatchDetails <- Sync[F].delay(
-        htmlPatchDetails.replaceAll("""style="color: white;"""", """style="color: #121212;"""")
-      )
-      branches <- repoAndId.map(_._2) match {
-        case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-        case _            => Sync[F].delay(List.empty)
-      }
-      actionBaseUri <- Sync[F].delay(
-        linkConfig.createFullUri(
-          Uri(path =
-            Uri.Path(
-              Vector(Uri.Path.Segment(s"~$repositoryOwnerName"), Uri.Path.Segment(repositoryName.toString))
-            )
-          )
-        )
-      )
-      resp <- repo match {
-        case None => NotFound()
-        case Some(repo) =>
-          Ok(
-            views.html
-              .showRepositoryPatch(baseUri, lang = language)(
-                actionBaseUri,
-                csrf,
-                linkToTicketService,
-                patch.map(_.name.toString),
-                user
-              )(
-                patch,
-                cleanedHtmlPatchDetails,
-                repo,
-                branches
-              )
-          )
-      }
-    } yield resp
-
-  /** List walk the given directory at the first level and return all found files and directories and their stats sorted
-    * by directory first and by name second. If the given path is _not_ a directory then no traversal is done and an
-    * empty list is returned.
-    *
-    * @param directory
-    *   The path to the directory that shall be traversed.
-    * @return
-    *   A list of tuples containing a relative path and the related stats.
-    */
-  private def doListFiles(directory: os.Path): F[IndexedSeq[(os.RelPath, os.StatInfo)]] =
-    for {
-      isDirectory <- Sync[F].delay(os.isDir(directory))
-      listing <-
-        if (isDirectory)
-          Sync[F].delay(
-            os.walk
-              .attrs(directory, skip = (path, _) => path.baseName === "_darcs", maxDepth = 1)
-              .map((path, attrs) => (path.relativeTo(directory), attrs))
-              .sortWith { (left, right) =>
-                val (leftPath, leftAttrs)  = left
-                val (rightPath, rightAttr) = right
-                (leftAttrs.fileType, rightAttr.fileType) match {
-                  case (os.FileType.Dir, os.FileType.Dir) => leftPath.last < rightPath.last
-                  case (os.FileType.Dir, _)               => true
-                  case (_, os.FileType.Dir)               => false
-                  case (_, _)                             => leftPath.last < rightPath.last
-                }
-              }
-          )
-        else
-          Sync[F].delay(IndexedSeq.empty)
-    } yield listing
-
-  /** Load the content of the first file matching our "readme file" heuristic which does not exceed the maximum file
-    * size and return it.
-    *
-    * @param repo
-    *   A repository in which we should search.
-    * @return
-    *   A tuple containing a list of strings (lines) that may be empty and an option to the file name.
-    */
-  private def doLoadReadme(repo: VcsRepository): F[(Vector[String], Option[String])] =
-    for {
-      path <- Sync[F].delay(
-        Paths.get(
-          darcsConfig.repositoriesDirectory.toPath.toString,
-          repo.owner.name.toString,
-          repo.name.toString
-        )
-      )
-      _     <- Sync[F].delay(log.debug(s"Trying to find README file in $path."))
-      files <- Sync[F].delay(os.list(os.Path(path)))
-      readme = files.find(_.last.matches("(?iu)^readme(\\..+)?$"))
-      _    <- Sync[F].delay(log.debug(s"Found README at $readme."))
-      size <- readme.traverse(path => Sync[F].delay(os.size(path)))
-      stream <- readme.traverse { path =>
-        if (size.getOrElse(0L) <= MaximumFileSize)
-          Sync[F].delay(os.read.lines.stream(path))
-        else
-          Sync[F].delay(os.Generator(""))
-      }
-      lines <- Sync[F].delay(stream.map(_.toVector).getOrElse(Vector.empty))
-    } yield (lines, readme.map(_.last))
-
-  private val cloneRepository: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryClonePathParameter(
-          repositoryName
-        ) /: filePath =>
-      for {
-        csrf  <- Sync[F].delay(req.getCsrfToken)
-        owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
-        loadedRepo <- owner match {
-          case None        => Sync[F].pure(None)
-          case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
-        }
-        // Cloning via HTTP protocol is public only, so we don't need more sophisticated checks here.
-        repo = loadedRepo.filter(r => r.isPrivate === false)
-        requestedFilePath <- Sync[F].delay(
-          repo.map(_ =>
-            fs2.io.file.Path.fromNioPath(
-              Paths.get(
-                darcsConfig.repositoriesDirectory.toPath.toString,
-                repositoryOwnerName.toString,
-                repositoryName.toString,
-                filePath.segments.mkString("/")
-              )
-            )
-          )
-        )
-        _ <- Sync[F].delay(log.info(s"Repository $repo ($requestedFilePath) is cloned."))
-        resp <- (repo, requestedFilePath) match {
-          case (Some(_), Some(path)) => StaticFile.fromPath(path, req.some).getOrElseF(NotFound())
-          case _                     => NotFound()
-        }
-      } yield resp
-  }
-
-  private val downloadDistribution: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "download" as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        file <- doDownloadDistribution(user.some)(repositoryOwnerName, repositoryName)
-        resp <- file match {
-          case None => NotFound()
-          case Some(filePath) =>
-            StaticFile
-              .fromPath(filePath, ar.req.some)
-              .map(_.putHeaders(`Content-Disposition`("attachment", Map(ci"filename" -> s"${repositoryName}.tar.gz"))))
-              .getOrElseF(NotFound())
-        }
-      } yield resp
-  }
+                linkConfig.createFullUri(
+                    Uri(path =
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
+            resp <- repo match {
+                case None => NotFound()
+                case Some(repo) =>
+                    Ok(
+                        views.html.showRepositoryHistory(baseUri, lang = language)(
+                            actionBaseUri,
+                            csrf,
+                            goBackUri.some,
+                            linkToTicketService,
+                            s"Smederee - History of ~$repositoryOwnerName/$repositoryName".some,
+                            user
+                        )(patches, next, repositoryBaseUri, repo, branches)
+                    )
+            }
+        } yield resp
 
-  private val downloadDistributionForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "download" =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        file <- doDownloadDistribution(None)(repositoryOwnerName, repositoryName)
-        resp <- file match {
-          case None => NotFound()
-          case Some(filePath) =>
-            StaticFile
-              .fromPath(filePath, req.some)
-              .map(_.putHeaders(`Content-Disposition`("attachment", Map(ci"filename" -> s"${repositoryName}.tar.gz"))))
-              .getOrElseF(NotFound())
-        }
-      } yield resp
-  }
+    /** Logic for generating the RSS feed for the requested repository.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
+      * @param repositoryName
+      *   The actual name of the repository.
+      * @return
+      *   An HTTP response containing the rendered RSS XML.
+      */
+    def doShowRepositoryRssFeed(
+        csrf: Option[CsrfToken]
+    )(user: Option[Account])(repositoryOwnerName: Username, repositoryName: VcsRepositoryName): F[Response[F]] =
+        for {
+            language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            response <- repo match {
+                case None => NotFound()
+                case Some(repo) =>
+                    for {
+                        directory <- Sync[F].delay(
+                            os.Path(
+                                Paths.get(
+                                    darcsConfig.repositoriesDirectory.toPath.toString,
+                                    repositoryOwnerName.toString
+                                )
+                            )
+                        )
+                        countChanges    <- darcs.log(directory.toNIO)(repositoryName.toString)(Chain("--count"))
+                        numberOfChanges <- Sync[F].delay(countChanges.stdout.toList.mkString.trim.toInt)
+                        maxCount = 15
+                        from     = 1
+                        to =
+                            if ((from + maxCount > numberOfChanges) && (numberOfChanges > 0))
+                                numberOfChanges
+                            else
+                                from + maxCount
+                        next = if (to < numberOfChanges) Option(to + 1) else None
+                        vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(
+                            Chain(s"--index=$from-$to", "--summary", "--xml-output")
+                        )
+                        xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
+                        patches <- Sync[F].delay(
+                            (xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList
+                        )
+                        repositoryBaseUri <- Sync[F].delay(
+                            linkConfig.createFullUri(
+                                Uri(path =
+                                    Uri.Path(
+                                        Vector(
+                                            Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                            Uri.Path.Segment(repositoryName.toString)
+                                        )
+                                    )
+                                )
+                            )
+                        )
+                        items = patches.map(_.toRssItem(repositoryBaseUri.addSegment("patch")))
+                        channel = RssChannel(
+                            title = s"Smederee: ~$repositoryOwnerName/$repositoryName",
+                            link = repositoryBaseUri.toString,
+                            description = repo.description
+                                .map(_.toString)
+                                .getOrElse(s"RSS feed for $repositoryOwnerName/$repositoryName"),
+                            copyright = None,
+                            image = None,
+                            items = items
+                        )
+                        resp <- Ok(views.xml.rss(channel))
+                    } yield resp
+            }
+        } yield response
 
-  private val forkRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "fork" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+    /** Get a diff for the requested patch and return the rendered response of it.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of repositories shall be rendered.
+      * @param repositoryOwnerName
+      *   The name of the user who owns the repository.
+      * @param repositoryName
+      *   The actual name of the repository.
+      * @param hash
+      *   The unique hash of the patch identifying it.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    def doShowRepositoryPatchDetails(csrf: Option[CsrfToken])(
+        user: Option[Account]
+    )(repositoryOwnerName: Username, repositoryName: VcsRepositoryName)(hash: DarcsHash): F[Response[F]] =
         for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp <- user.validatedEmail match {
-            case false =>
-              Forbidden(
-                views.html.errors
-                  .unvalidatedAccount(lang = language)(csrf, "Smederee - Account not validated!".some, user)
-              )
-            case true =>
-              for {
-                repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
-                sourceRepository = repoAndId.map(_._1)
-                // Check if a repository with that name already exists for the user.
-                loadedTargetRepo <- vcsMetadataRepo.findVcsRepository(user.toVcsRepositoryOwner, repositoryName)
-                // If no repo exists we copy and adjust the source one, otherwise we return `None`.
-                targetRepository = loadedTargetRepo.fold(
-                  sourceRepository.map(_.copy(owner = user.toVcsRepositoryOwner))
-                )(_ => None)
-                targetUri <- Sync[F].delay(
-                  linkConfig.createFullUri(
+            language  <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            repoAndId <- loadRepo(user)(repositoryOwnerName, repositoryName)
+            repo = repoAndId.map(_._1)
+            directory <- Sync[F].delay(
+                os.Path(
+                    Paths.get(
+                        darcsConfig.repositoriesDirectory.toPath.toString,
+                        repositoryOwnerName.toString
+                    )
+                )
+            )
+            vcsLog <- darcs.log(directory.toNIO)(repositoryName.toString)(
+                Chain(s"--hash=${hash.toString}", "--summary", "--xml-output")
+            )
+            xmlLog <- Sync[F].delay(scala.xml.XML.loadString(vcsLog.stdout.toList.mkString))
+            patch <- Sync[F].delay(
+                (xmlLog \ "patch").flatMap(VcsRepositoryPatchMetadata.fromDarcsXmlLog).toList.headOption
+            )
+            darcsDiff        <- darcs.diff(directory.toNIO)(repositoryName.toString)(hash)(Chain("--no-pause-for-gui"))
+            patchCutMarker   <- Sync[F].delay(s"patch ${hash.toString}")
+            cleanedPatchDiff <- Sync[F].delay(darcsDiff.stdout.toList.mkString.split(patchCutMarker)(0))
+            patchDetails     <- Sync[F].delay(cleanedPatchDiff)
+            htmlPatchDetails <- Sync[F].delay(new UtilsAnsiHtml().convertAnsiToHtml(patchDetails))
+            cleanedHtmlPatchDetails <- Sync[F].delay(
+                htmlPatchDetails.replaceAll("""style="color: white;"""", """style="color: #121212;"""")
+            )
+            branches <- repoAndId.map(_._2) match {
+                case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                case _            => Sync[F].delay(List.empty)
+            }
+            actionBaseUri <- Sync[F].delay(
+                linkConfig.createFullUri(
                     Uri(path =
-                      Uri.Path(
-                        Vector(
-                          Uri.Path.Segment(s"~${user.name.toString}"),
-                          Uri.Path.Segment(repositoryName.toString)
-                        )
-                      )
-                    )
-                  )
-                )
-                sourceDirectory <- Sync[F].delay(
-                  Paths.get(
-                    darcsConfig.repositoriesDirectory.toString,
-                    repositoryOwnerName.toString,
-                    repositoryName.toString
-                  )
-                )
-                targetDirectory <- Sync[F].delay(
-                  Paths.get(
-                    darcsConfig.repositoriesDirectory.toString,
-                    user.name.toString,
-                    repositoryName.toString
-                  )
-                )
-                output <- targetRepository match {
-                  case None => Sync[F].pure(0)
-                  case Some(repoMetadata) =>
-                    for {
-                      _ <- Sync[F].delay(
-                        Files.createDirectories(
-                          Paths.get(darcsConfig.repositoriesDirectory.toString, repoMetadata.owner.name.toString)
-                        )
-                      )
-                      darcsClone <- darcs.clone(sourceDirectory, targetDirectory)(Chain("--complete"))
-                      createRepo <-
-                        if (darcsClone.exitValue === 0)
-                          for {
-                            create <- vcsMetadataRepo.createVcsRepository(repoMetadata)
-                            // Get IDs to create the fork entry in the database.
-                            source <- sourceRepository.traverse(sourceRepo =>
-                              vcsMetadataRepo.findVcsRepositoryId(sourceRepo.owner, sourceRepo.name)
-                            )
-                            target <- vcsMetadataRepo.findVcsRepositoryId(repoMetadata.owner, repoMetadata.name)
-                            fork <- (source, target) match {
-                              case (Some(Some(source)), Some(target)) => vcsMetadataRepo.createFork(source, target)
-                              case _                                  => Sync[F].pure(0)
-                            }
-                          } yield create + fork
-                        else
-                          Sync[F].pure(0) // Do not create DB entry if darcs init failed!
-                    } yield createRepo
-                }
-                resp <- SeeOther(Location(targetUri))
-              } yield resp
-          }
+                        Uri.Path(
+                            Vector(
+                                Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                Uri.Path.Segment(repositoryName.toString)
+                            )
+                        )
+                    )
+                )
+            )
+            resp <- repo match {
+                case None => NotFound()
+                case Some(repo) =>
+                    Ok(
+                        views.html
+                            .showRepositoryPatch(baseUri, lang = language)(
+                                actionBaseUri,
+                                csrf,
+                                linkToTicketService,
+                                patch.map(_.name.toString),
+                                user
+                            )(
+                                patch,
+                                cleanedHtmlPatchDetails,
+                                repo,
+                                branches
+                            )
+                    )
+            }
         } yield resp
-      }
-  }
 
-  private val createRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / "repo" / "create" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        val response = for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          _ <- Sync[F].raiseUnless(user.validatedEmail)(
-            new Error(
-              "An unvalidated account is not allowed to create a repository!"
-            ) // FIXME: Proper error handling!
-          )
-          formData <- Sync[F].delay {
-            urlForm.values.map { t =>
-              val (key, values) = t
-              (
-                key,
-                values.headOption.getOrElse("")
-              ) // Pick the first value (a field might get submitted multiple times)!
-            }
-          }
-          form <- Sync[F].delay(NewVcsRepositoryForm.validate(formData))
-          resp <- form match {
-            case Validated.Invalid(es) =>
-              BadRequest(
-                views.html
-                  .createRepository(lang = language)(
-                    createRepoPath,
-                    csrf,
-                    "Smederee - Create a new repository".some,
-                    user
-                  )(
-                    formData,
-                    FormErrors.fromNec(es)
-                  )
-              )
-            case Validated.Valid(newVcsRepositoryForm) =>
-              for {
-                directory <- Sync[F].delay(
-                  Paths.get(darcsConfig.repositoriesDirectory.toPath.toString, user.name.toString)
-                )
-                repoInDb <- vcsMetadataRepo.findVcsRepository(
-                  user.toVcsRepositoryOwner,
-                  newVcsRepositoryForm.name
+    /** List walk the given directory at the first level and return all found files and directories and their stats
+      * sorted by directory first and by name second. If the given path is _not_ a directory then no traversal is done
+      * and an empty list is returned.
+      *
+      * @param directory
+      *   The path to the directory that shall be traversed.
+      * @return
+      *   A list of tuples containing a relative path and the related stats.
+      */
+    private def doListFiles(directory: os.Path): F[IndexedSeq[(os.RelPath, os.StatInfo)]] =
+        for {
+            isDirectory <- Sync[F].delay(os.isDir(directory))
+            listing <-
+                if (isDirectory)
+                    Sync[F].delay(
+                        os.walk
+                            .attrs(directory, skip = (path, _) => path.baseName === "_darcs", maxDepth = 1)
+                            .map((path, attrs) => (path.relativeTo(directory), attrs))
+                            .sortWith { (left, right) =>
+                                val (leftPath, leftAttrs)  = left
+                                val (rightPath, rightAttr) = right
+                                (leftAttrs.fileType, rightAttr.fileType) match {
+                                    case (os.FileType.Dir, os.FileType.Dir) => leftPath.last < rightPath.last
+                                    case (os.FileType.Dir, _)               => true
+                                    case (_, os.FileType.Dir)               => false
+                                    case (_, _)                             => leftPath.last < rightPath.last
+                                }
+                            }
+                    )
+                else
+                    Sync[F].delay(IndexedSeq.empty)
+        } yield listing
+
+    /** Load the content of the first file matching our "readme file" heuristic which does not exceed the maximum file
+      * size and return it.
+      *
+      * @param repo
+      *   A repository in which we should search.
+      * @return
+      *   A tuple containing a list of strings (lines) that may be empty and an option to the file name.
+      */
+    private def doLoadReadme(repo: VcsRepository): F[(Vector[String], Option[String])] =
+        for {
+            path <- Sync[F].delay(
+                Paths.get(
+                    darcsConfig.repositoriesDirectory.toPath.toString,
+                    repo.owner.name.toString,
+                    repo.name.toString
+                )
+            )
+            _     <- Sync[F].delay(log.debug(s"Trying to find README file in $path."))
+            files <- Sync[F].delay(os.list(os.Path(path)))
+            readme = files.find(_.last.matches("(?iu)^readme(\\..+)?$"))
+            _    <- Sync[F].delay(log.debug(s"Found README at $readme."))
+            size <- readme.traverse(path => Sync[F].delay(os.size(path)))
+            stream <- readme.traverse { path =>
+                if (size.getOrElse(0L) <= MaximumFileSize)
+                    Sync[F].delay(os.read.lines.stream(path))
+                else
+                    Sync[F].delay(os.Generator(""))
+            }
+            lines <- Sync[F].delay(stream.map(_.toVector).getOrElse(Vector.empty))
+        } yield (lines, readme.map(_.last))
+
+    private val cloneRepository: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryClonePathParameter(
+                repositoryName
+            ) /: filePath =>
+            for {
+                csrf  <- Sync[F].delay(req.getCsrfToken)
+                owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+                loadedRepo <- owner match {
+                    case None        => Sync[F].pure(None)
+                    case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
+                }
+                // Cloning via HTTP protocol is public only, so we don't need more sophisticated checks here.
+                repo = loadedRepo.filter(r => r.isPrivate === false)
+                requestedFilePath <- Sync[F].delay(
+                    repo.map(_ =>
+                        fs2.io.file.Path.fromNioPath(
+                            Paths.get(
+                                darcsConfig.repositoriesDirectory.toPath.toString,
+                                repositoryOwnerName.toString,
+                                repositoryName.toString,
+                                filePath.segments.mkString("/")
+                            )
+                        )
+                    )
                 )
-                output <- repoInDb match {
-                  case None =>
-                    for {
-                      _ <- Sync[F].delay {
-                        if (repoInDb.isEmpty && !Files.exists(directory)) {
-                          log.debug(
-                            s"User repository directory does not exist, trying to create it: $directory"
-                          )
-                          val _ = Files.createDirectories(directory)
-                        }
-                      }
-                      newRepo = VcsRepository(
-                        newVcsRepositoryForm.name,
-                        user.toVcsRepositoryOwner,
-                        newVcsRepositoryForm.isPrivate,
-                        newVcsRepositoryForm.description,
-                        newVcsRepositoryForm.ticketsEnabled,
-                        VcsType.Darcs,
-                        newVcsRepositoryForm.website
-                      )
-                      output <- darcs.initialize(directory)(newRepo.name.toString)(Chain.empty)
-                      _ <-
-                        if (output.exitValue === 0)
-                          for {
-                            written <- vcsMetadataRepo.createVcsRepository(newRepo)
-                            _ <- Option(newRepo)
-                              .filter(_.ticketsEnabled)
-                              .traverse(repo => ticketsProjectRepo.createProject(repo.convert))
-                          } yield written
-                        else
-                          Sync[F].pure(0) // Do not create DB entry if darcs init failed!
-                    } yield output
-                  case Some(_) =>
-                    Sync[F].delay(DarcsCommandOutput(1, Chain.empty, Chain("The repository already exists!")))
+                _ <- Sync[F].delay(log.info(s"Repository $repo ($requestedFilePath) is cloned."))
+                resp <- (repo, requestedFilePath) match {
+                    case (Some(_), Some(path)) => StaticFile.fromPath(path, req.some).getOrElseF(NotFound())
+                    case _                     => NotFound()
+                }
+            } yield resp
+    }
+
+    private val downloadDistribution: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "download" as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                file <- doDownloadDistribution(user.some)(repositoryOwnerName, repositoryName)
+                resp <- file match {
+                    case None => NotFound()
+                    case Some(filePath) =>
+                        StaticFile
+                            .fromPath(filePath, ar.req.some)
+                            .map(
+                                _.putHeaders(
+                                    `Content-Disposition`(
+                                        "attachment",
+                                        Map(ci"filename" -> s"${repositoryName}.tar.gz")
+                                    )
+                                )
+                            )
+                            .getOrElseF(NotFound())
+                }
+            } yield resp
+    }
+
+    private val downloadDistributionForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "download" =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                file <- doDownloadDistribution(None)(repositoryOwnerName, repositoryName)
+                resp <- file match {
+                    case None => NotFound()
+                    case Some(filePath) =>
+                        StaticFile
+                            .fromPath(filePath, req.some)
+                            .map(
+                                _.putHeaders(
+                                    `Content-Disposition`(
+                                        "attachment",
+                                        Map(ci"filename" -> s"${repositoryName}.tar.gz")
+                                    )
+                                )
+                            )
+                            .getOrElseF(NotFound())
                 }
-                resp <- output.exitValue match {
-                  case 0 =>
-                    SeeOther(
-                      Location(
-                        Uri(path = Uri.Path.Root |+| Uri.Path(Vector(Uri.Path.Segment(s"~${user.name.toString}"))))
-                      )
+            } yield resp
+    }
+
+    private val forkRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "fork" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- user.validatedEmail match {
+                        case false =>
+                            Forbidden(
+                                views.html.errors
+                                    .unvalidatedAccount(lang = language)(
+                                        csrf,
+                                        "Smederee - Account not validated!".some,
+                                        user
+                                    )
+                            )
+                        case true =>
+                            for {
+                                repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
+                                sourceRepository = repoAndId.map(_._1)
+                                // Check if a repository with that name already exists for the user.
+                                loadedTargetRepo <- vcsMetadataRepo.findVcsRepository(
+                                    user.toVcsRepositoryOwner,
+                                    repositoryName
+                                )
+                                // If no repo exists we copy and adjust the source one, otherwise we return `None`.
+                                targetRepository = loadedTargetRepo.fold(
+                                    sourceRepository.map(_.copy(owner = user.toVcsRepositoryOwner))
+                                )(_ => None)
+                                targetUri <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~${user.name.toString}"),
+                                                    Uri.Path.Segment(repositoryName.toString)
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                sourceDirectory <- Sync[F].delay(
+                                    Paths.get(
+                                        darcsConfig.repositoriesDirectory.toString,
+                                        repositoryOwnerName.toString,
+                                        repositoryName.toString
+                                    )
+                                )
+                                targetDirectory <- Sync[F].delay(
+                                    Paths.get(
+                                        darcsConfig.repositoriesDirectory.toString,
+                                        user.name.toString,
+                                        repositoryName.toString
+                                    )
+                                )
+                                output <- targetRepository match {
+                                    case None => Sync[F].pure(0)
+                                    case Some(repoMetadata) =>
+                                        for {
+                                            _ <- Sync[F].delay(
+                                                Files.createDirectories(
+                                                    Paths.get(
+                                                        darcsConfig.repositoriesDirectory.toString,
+                                                        repoMetadata.owner.name.toString
+                                                    )
+                                                )
+                                            )
+                                            darcsClone <- darcs.clone(sourceDirectory, targetDirectory)(
+                                                Chain("--complete")
+                                            )
+                                            createRepo <-
+                                                if (darcsClone.exitValue === 0)
+                                                    for {
+                                                        create <- vcsMetadataRepo.createVcsRepository(repoMetadata)
+                                                        // Get IDs to create the fork entry in the database.
+                                                        source <- sourceRepository.traverse(sourceRepo =>
+                                                            vcsMetadataRepo.findVcsRepositoryId(
+                                                                sourceRepo.owner,
+                                                                sourceRepo.name
+                                                            )
+                                                        )
+                                                        target <- vcsMetadataRepo.findVcsRepositoryId(
+                                                            repoMetadata.owner,
+                                                            repoMetadata.name
+                                                        )
+                                                        fork <- (source, target) match {
+                                                            case (Some(Some(source)), Some(target)) =>
+                                                                vcsMetadataRepo.createFork(source, target)
+                                                            case _ => Sync[F].pure(0)
+                                                        }
+                                                    } yield create + fork
+                                                else
+                                                    Sync[F].pure(0) // Do not create DB entry if darcs init failed!
+                                        } yield createRepo
+                                }
+                                resp <- SeeOther(Location(targetUri))
+                            } yield resp
+                    }
+                } yield resp
+            }
+    }
+
+    private val createRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / "repo" / "create" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                val response = for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    _ <- Sync[F].raiseUnless(user.validatedEmail)(
+                        new Error(
+                            "An unvalidated account is not allowed to create a repository!"
+                        ) // FIXME: Proper error handling!
                     )
-                  case _ =>
+                    formData <- Sync[F].delay {
+                        urlForm.values.map { t =>
+                            val (key, values) = t
+                            (
+                                key,
+                                values.headOption.getOrElse("")
+                            ) // Pick the first value (a field might get submitted multiple times)!
+                        }
+                    }
+                    form <- Sync[F].delay(NewVcsRepositoryForm.validate(formData))
+                    resp <- form match {
+                        case Validated.Invalid(es) =>
+                            BadRequest(
+                                views.html
+                                    .createRepository(lang = language)(
+                                        createRepoPath,
+                                        csrf,
+                                        "Smederee - Create a new repository".some,
+                                        user
+                                    )(
+                                        formData,
+                                        FormErrors.fromNec(es)
+                                    )
+                            )
+                        case Validated.Valid(newVcsRepositoryForm) =>
+                            for {
+                                directory <- Sync[F].delay(
+                                    Paths.get(darcsConfig.repositoriesDirectory.toPath.toString, user.name.toString)
+                                )
+                                repoInDb <- vcsMetadataRepo.findVcsRepository(
+                                    user.toVcsRepositoryOwner,
+                                    newVcsRepositoryForm.name
+                                )
+                                output <- repoInDb match {
+                                    case None =>
+                                        for {
+                                            _ <- Sync[F].delay {
+                                                if (repoInDb.isEmpty && !Files.exists(directory)) {
+                                                    log.debug(
+                                                        s"User repository directory does not exist, trying to create it: $directory"
+                                                    )
+                                                    val _ = Files.createDirectories(directory)
+                                                }
+                                            }
+                                            newRepo = VcsRepository(
+                                                newVcsRepositoryForm.name,
+                                                user.toVcsRepositoryOwner,
+                                                newVcsRepositoryForm.isPrivate,
+                                                newVcsRepositoryForm.description,
+                                                newVcsRepositoryForm.ticketsEnabled,
+                                                VcsType.Darcs,
+                                                newVcsRepositoryForm.website
+                                            )
+                                            output <- darcs.initialize(directory)(newRepo.name.toString)(Chain.empty)
+                                            _ <-
+                                                if (output.exitValue === 0)
+                                                    for {
+                                                        written <- vcsMetadataRepo.createVcsRepository(newRepo)
+                                                        _ <- Option(newRepo)
+                                                            .filter(_.ticketsEnabled)
+                                                            .traverse(repo =>
+                                                                ticketsProjectRepo.createProject(repo.convert)
+                                                            )
+                                                    } yield written
+                                                else
+                                                    Sync[F].pure(0) // Do not create DB entry if darcs init failed!
+                                        } yield output
+                                    case Some(_) =>
+                                        Sync[F].delay(
+                                            DarcsCommandOutput(1, Chain.empty, Chain("The repository already exists!"))
+                                        )
+                                }
+                                resp <- output.exitValue match {
+                                    case 0 =>
+                                        SeeOther(
+                                            Location(
+                                                Uri(path =
+                                                    Uri.Path.Root |+| Uri.Path(
+                                                        Vector(Uri.Path.Segment(s"~${user.name.toString}"))
+                                                    )
+                                                )
+                                            )
+                                        )
+                                    case _ =>
+                                        for {
+                                            _ <- Sync[F].delay(
+                                                log.error(
+                                                    s"Error creating the repository ${newVcsRepositoryForm.name} in directory $directory: ${output.stderr.toList.mkString}"
+                                                )
+                                            )
+                                            resp <- InternalServerError(
+                                                s"Error creating the repository: ${output.stderr.toList.mkString}"
+                                            )
+                                        } yield resp
+                                }
+                            } yield resp
+                    }
+                } yield resp
+                response.recoverWith { error =>
+                    log.error("Internal Server Error", error)
                     for {
-                      _ <- Sync[F].delay(
-                        log.error(
-                          s"Error creating the repository ${newVcsRepositoryForm.name} in directory $directory: ${output.stderr.toList.mkString}"
-                        )
-                      )
-                      resp <- InternalServerError(
-                        s"Error creating the repository: ${output.stderr.toList.mkString}"
-                      )
+                        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        resp <- InternalServerError(
+                            views.html.errors.internalServerError(lang = language)(csrf, user.some)
+                        )
                     } yield resp
                 }
-              } yield resp
-          }
-        } yield resp
-        response.recoverWith { error =>
-          log.error("Internal Server Error", error)
-          for {
-            csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-            language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-          } yield resp
-        }
-      }
-  }
+            }
+    }
 
-  private val deleteRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "delete" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+    private val deleteRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "delete" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                    _ <- Sync[F].raiseUnless(user.validatedEmail)(
+                        new Error(
+                            "An unvalidated account is not allowed to edit a repository!"
+                        ) // FIXME: Proper error handling!
+                    )
+                    owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
+                    loadedRepo <- owner match {
+                        case None        => Sync[F].pure(None)
+                        case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
+                    }
+                    redirectUri <- Sync[F].delay(
+                        linkConfig.createFullUri(
+                            Uri(path = Uri.Path(Vector(Uri.Path.Segment(s"~$repositoryOwnerName"))))
+                        )
+                    )
+                    // You can only delete repositories that you own!
+                    repo = loadedRepo.filter(_.owner === user.toVcsRepositoryOwner)
+                    _ <- Sync[F].raiseUnless(repo.nonEmpty)(new Error("Repository not found!"))
+                    repoDir <- Sync[F].delay(
+                        repo.map(repo =>
+                            darcsConfig.repositoriesDirectory.toPath
+                                .resolve(user.name.toString)
+                                .resolve(repo.name.toString)
+                        )
+                    )
+                    _ <- repoDir.traverse(directory => deleteRepositoryDirectory(repositoryOwnerName)(directory))
+                    _ <- repo.traverse(repo =>
+                        vcsMetadataRepo.deleteVcsRepository(repo) *> ticketsProjectRepo.deleteProject(repo.convert)
+                    )
+                    resp <- SeeOther(Location(redirectUri))
+                } yield resp
+            }
+    }
+
+    private val editRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "edit" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    _ <- Sync[F].raiseUnless(user.validatedEmail)(
+                        new Error(
+                            "An unvalidated account is not allowed to edit a repository!"
+                        ) // FIXME: Proper error handling!
+                    )
+                    repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
+                    repo = repoAndId.map(_._1)
+                    branches <- repoAndId.map(_._2) match {
+                        case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                        case _            => Sync[F].delay(List.empty)
+                    }
+                    resp <- repo match {
+                        case None => NotFound()
+                        case Some(repo) =>
+                            for {
+                                editAction <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                                    Uri.Path.Segment(repositoryName.toString),
+                                                    Uri.Path.Segment("edit")
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                repoUri <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                                    Uri.Path.Segment(repositoryName.toString)
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                formData <- Sync[F].delay {
+                                    urlForm.values.map { t =>
+                                        val (key, values) = t
+                                        (
+                                            key,
+                                            values.headOption.getOrElse("")
+                                        ) // Pick the first value (a field might get submitted multiple times)!
+                                    }
+                                }
+                                form <- Sync[F].delay(EditVcsRepositoryForm.validate(formData))
+                                resp <- form match {
+                                    case Validated.Invalid(errors) =>
+                                        BadRequest(
+                                            views.html.editRepository(lang = language)(
+                                                editAction,
+                                                csrf,
+                                                repoUri,
+                                                Option(s"~$repositoryOwnerName/$repositoryName - edit"),
+                                                user,
+                                                repo,
+                                                branches
+                                            )(formData, FormErrors.fromNec(errors))
+                                        )
+                                    case Validated.Valid(updatedVcsRepositoryForm) =>
+                                        val updatedRepo = repo.copy(
+                                            isPrivate = updatedVcsRepositoryForm.isPrivate,
+                                            description = updatedVcsRepositoryForm.description,
+                                            ticketsEnabled = updatedVcsRepositoryForm.ticketsEnabled,
+                                            website = updatedVcsRepositoryForm.website
+                                        )
+                                        // If the repo switched from tickets disabled to enabled, we create a ticket tracker.
+                                        val createTicketTracker =
+                                            updatedRepo.ticketsEnabled && updatedRepo.ticketsEnabled =!= repo.ticketsEnabled
+                                        // If the repo switched from tickets enabled to disabled, we delete the ticket tracker.
+                                        val deleteTicketTracker =
+                                            !updatedRepo.ticketsEnabled && updatedRepo.ticketsEnabled =!= repo.ticketsEnabled
+                                        for {
+                                            _ <- vcsMetadataRepo.updateVcsRepository(updatedRepo)
+                                            _ <-
+                                                if (createTicketTracker)
+                                                    ticketsProjectRepo.createProject(updatedRepo.convert)
+                                                else
+                                                    Sync[F].pure(0)
+                                            _ <-
+                                                if (deleteTicketTracker)
+                                                    ticketsProjectRepo.deleteProject(updatedRepo.convert)
+                                                else
+                                                    Sync[F].pure(0)
+                                            _ <-
+                                                if (
+                                                    !createTicketTracker && !deleteTicketTracker && updatedRepo.ticketsEnabled
+                                                )
+                                                    ticketsProjectRepo.updateProject(updatedRepo.convert)
+                                                else
+                                                    Sync[F].pure(0)
+                                            resp <- SeeOther(Location(repoUri))
+                                        } yield resp
+                                }
+                            } yield resp
+                    }
+                } yield resp
+            }
+    }
+
+    private val showAllRepositories: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "projects" as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowAllRepositories(csrf)(user.some)
+            } yield resp
+    }
+
+    private val showAllRepositoriesForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "projects" =>
         for {
-          csrf <- Sync[F].delay(ar.req.getCsrfToken)
-          _ <- Sync[F].raiseUnless(user.validatedEmail)(
-            new Error(
-              "An unvalidated account is not allowed to edit a repository!"
-            ) // FIXME: Proper error handling!
-          )
-          owner <- vcsMetadataRepo.findVcsRepositoryOwner(repositoryOwnerName)
-          loadedRepo <- owner match {
-            case None        => Sync[F].pure(None)
-            case Some(owner) => vcsMetadataRepo.findVcsRepository(owner, repositoryName)
-          }
-          redirectUri <- Sync[F].delay(
-            linkConfig.createFullUri(Uri(path = Uri.Path(Vector(Uri.Path.Segment(s"~$repositoryOwnerName")))))
-          )
-          // You can only delete repositories that you own!
-          repo = loadedRepo.filter(_.owner === user.toVcsRepositoryOwner)
-          _ <- Sync[F].raiseUnless(repo.nonEmpty)(new Error("Repository not found!"))
-          repoDir <- Sync[F].delay(
-            repo.map(repo =>
-              darcsConfig.repositoriesDirectory.toPath.resolve(user.name.toString).resolve(repo.name.toString)
-            )
-          )
-          _ <- repoDir.traverse(directory => deleteRepositoryDirectory(repositoryOwnerName)(directory))
-          _ <- repo.traverse(repo =>
-            vcsMetadataRepo.deleteVcsRepository(repo) *> ticketsProjectRepo.deleteProject(repo.convert)
-          )
-          resp <- SeeOther(Location(redirectUri))
+            csrf <- Sync[F].delay(req.getCsrfToken)
+            resp <- doShowAllRepositories(csrf)(None)
         } yield resp
-      }
-  }
+    }
 
-  private val editRepository: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "edit" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          _ <- Sync[F].raiseUnless(user.validatedEmail)(
-            new Error(
-              "An unvalidated account is not allowed to edit a repository!"
-            ) // FIXME: Proper error handling!
-          )
-          repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
-          repo = repoAndId.map(_._1)
-          branches <- repoAndId.map(_._2) match {
-            case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-            case _            => Sync[F].delay(List.empty)
-          }
-          resp <- repo match {
-            case None => NotFound()
-            case Some(repo) =>
-              for {
-                editAction <- Sync[F].delay(
-                  linkConfig.createFullUri(
-                    Uri(path =
-                      Uri.Path(
-                        Vector(
-                          Uri.Path.Segment(s"~$repositoryOwnerName"),
-                          Uri.Path.Segment(repositoryName.toString),
-                          Uri.Path.Segment("edit")
+    private val showCreateRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / "repo" / "create" as user =>
+            for {
+                csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                resp <- user.validatedEmail match {
+                    case false =>
+                        Forbidden(
+                            views.html.errors
+                                .unvalidatedAccount(lang = language)(
+                                    csrf,
+                                    "Smederee - Account not validated!".some,
+                                    user
+                                )
+                        )
+                    case true =>
+                        Ok(
+                            views.html
+                                .createRepository(lang = language)(
+                                    createRepoPath,
+                                    csrf,
+                                    "Smederee - Create a new repository".some,
+                                    user
+                                )()
+                        )
+                }
+            } yield resp
+    }
+
+    private val showDeleteRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "delete" as user =>
+            for {
+                csrf      <- Sync[F].delay(ar.req.getCsrfToken)
+                repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
+                repo = repoAndId.map(_._1)
+                branches <- repoAndId.map(_._2) match {
+                    case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                    case _            => Sync[F].delay(List.empty)
+                }
+                deleteAction <- Sync[F].delay(
+                    linkConfig.createFullUri(
+                        Uri(path =
+                            Uri.Path(
+                                Vector(
+                                    Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                    Uri.Path.Segment(repositoryName.toString),
+                                    Uri.Path.Segment("delete")
+                                )
+                            )
                         )
-                      )
                     )
-                  )
                 )
                 repoUri <- Sync[F].delay(
-                  linkConfig.createFullUri(
-                    Uri(path =
-                      Uri.Path(
-                        Vector(
-                          Uri.Path.Segment(s"~$repositoryOwnerName"),
-                          Uri.Path.Segment(repositoryName.toString)
-                        )
-                      )
-                    )
-                  )
-                )
-                formData <- Sync[F].delay {
-                  urlForm.values.map { t =>
-                    val (key, values) = t
-                    (
-                      key,
-                      values.headOption.getOrElse("")
-                    ) // Pick the first value (a field might get submitted multiple times)!
-                  }
+                    linkConfig.createFullUri(
+                        Uri(path =
+                            Uri.Path(
+                                Vector(
+                                    Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                    Uri.Path.Segment(repositoryName.toString)
+                                )
+                            )
+                        )
+                    )
+                )
+                resp <- repo match {
+                    case None => NotFound()
+                    case Some(repo) =>
+                        Ok(
+                            views.html.deleteRepository()(
+                                deleteAction,
+                                repoUri,
+                                csrf,
+                                Option(s"~$repositoryOwnerName/$repositoryName - delete"),
+                                user,
+                                repo,
+                                branches
+                            )
+                        )
                 }
-                form <- Sync[F].delay(EditVcsRepositoryForm.validate(formData))
-                resp <- form match {
-                  case Validated.Invalid(errors) =>
-                    BadRequest(
-                      views.html.editRepository(lang = language)(
-                        editAction,
-                        csrf,
-                        repoUri,
-                        Option(s"~$repositoryOwnerName/$repositoryName - edit"),
-                        user,
-                        repo,
-                        branches
-                      )(formData, FormErrors.fromNec(errors))
-                    )
-                  case Validated.Valid(updatedVcsRepositoryForm) =>
-                    val updatedRepo = repo.copy(
-                      isPrivate = updatedVcsRepositoryForm.isPrivate,
-                      description = updatedVcsRepositoryForm.description,
-                      ticketsEnabled = updatedVcsRepositoryForm.ticketsEnabled,
-                      website = updatedVcsRepositoryForm.website
-                    )
-                    // If the repo switched from tickets disabled to enabled, we create a ticket tracker.
-                    val createTicketTracker =
-                      updatedRepo.ticketsEnabled && updatedRepo.ticketsEnabled =!= repo.ticketsEnabled
-                    // If the repo switched from tickets enabled to disabled, we delete the ticket tracker.
-                    val deleteTicketTracker =
-                      !updatedRepo.ticketsEnabled && updatedRepo.ticketsEnabled =!= repo.ticketsEnabled
-                    for {
-                      _ <- vcsMetadataRepo.updateVcsRepository(updatedRepo)
-                      _ <-
-                        if (createTicketTracker)
-                          ticketsProjectRepo.createProject(updatedRepo.convert)
-                        else
-                          Sync[F].pure(0)
-                      _ <-
-                        if (deleteTicketTracker)
-                          ticketsProjectRepo.deleteProject(updatedRepo.convert)
-                        else
-                          Sync[F].pure(0)
-                      _ <-
-                        if (!createTicketTracker && !deleteTicketTracker && updatedRepo.ticketsEnabled)
-                          ticketsProjectRepo.updateProject(updatedRepo.convert)
-                        else
-                          Sync[F].pure(0)
-                      resp <- SeeOther(Location(repoUri))
-                    } yield resp
+            } yield resp
+    }
+
+    private val showEditRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "edit" as user =>
+            for {
+                csrf      <- Sync[F].delay(ar.req.getCsrfToken)
+                repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
+                repo = repoAndId.map(_._1)
+                branches <- repoAndId.map(_._2) match {
+                    case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
+                    case _            => Sync[F].delay(List.empty)
                 }
-              } yield resp
-          }
-        } yield resp
-      }
-  }
+                editAction <- Sync[F].delay(
+                    linkConfig.createFullUri(
+                        Uri(path =
+                            Uri.Path(
+                                Vector(
+                                    Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                    Uri.Path.Segment(repositoryName.toString),
+                                    Uri.Path.Segment("edit")
+                                )
+                            )
+                        )
+                    )
+                )
+                repoUri <- Sync[F].delay(
+                    linkConfig.createFullUri(
+                        Uri(path =
+                            Uri.Path(
+                                Vector(
+                                    Uri.Path.Segment(s"~$repositoryOwnerName"),
+                                    Uri.Path.Segment(repositoryName.toString)
+                                )
+                            )
+                        )
+                    )
+                )
+                formData <- Sync[F].delay(repo.map(EditVcsRepositoryForm.fromVcsRepository).map(_.toMap))
+                resp <- (formData, repo) match {
+                    case (Some(formData), Some(repo)) =>
+                        Ok(
+                            views.html.editRepository()(
+                                editAction,
+                                csrf,
+                                repoUri,
+                                Option(s"~$repositoryOwnerName/$repositoryName - edit"),
+                                user,
+                                repo,
+                                branches
+                            )(formData)
+                        )
+                    case _ => NotFound()
+                }
+            } yield resp
+    }
 
-  private val showAllRepositories: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / "projects" as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowAllRepositories(csrf)(user.some)
-      } yield resp
-  }
-
-  private val showAllRepositoriesForGuests: HttpRoutes[F] = HttpRoutes.of { case req @ GET -> Root / "projects" =>
-    for {
-      csrf <- Sync[F].delay(req.getCsrfToken)
-      resp <- doShowAllRepositories(csrf)(None)
-    } yield resp
-  }
-
-  private val showCreateRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / "repo" / "create" as user =>
-      for {
-        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        resp <- user.validatedEmail match {
-          case false =>
-            Forbidden(
-              views.html.errors
-                .unvalidatedAccount(lang = language)(csrf, "Smederee - Account not validated!".some, user)
-            )
-          case true =>
-            Ok(
-              views.html
-                .createRepository(lang = language)(
-                  createRepoPath,
-                  csrf,
-                  "Smederee - Create a new repository".some,
-                  user
-                )()
-            )
-        }
-      } yield resp
-  }
+    private val showRepositories: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoriesOwnerName) as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowRepositories(csrf)(repositoriesOwnerName)(user.some)
+            } yield resp
+    }
 
-  private val showDeleteRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "delete" as user =>
-      for {
-        csrf      <- Sync[F].delay(ar.req.getCsrfToken)
-        repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
-        repo = repoAndId.map(_._1)
-        branches <- repoAndId.map(_._2) match {
-          case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-          case _            => Sync[F].delay(List.empty)
-        }
-        deleteAction <- Sync[F].delay(
-          linkConfig.createFullUri(
-            Uri(path =
-              Uri.Path(
-                Vector(
-                  Uri.Path.Segment(s"~$repositoryOwnerName"),
-                  Uri.Path.Segment(repositoryName.toString),
-                  Uri.Path.Segment("delete")
-                )
-              )
-            )
-          )
-        )
-        repoUri <- Sync[F].delay(
-          linkConfig.createFullUri(
-            Uri(path =
-              Uri.Path(
-                Vector(
-                  Uri.Path.Segment(s"~$repositoryOwnerName"),
-                  Uri.Path.Segment(repositoryName.toString)
-                )
-              )
-            )
-          )
-        )
-        resp <- repo match {
-          case None => NotFound()
-          case Some(repo) =>
-            Ok(
-              views.html.deleteRepository()(
-                deleteAction,
-                repoUri,
-                csrf,
-                Option(s"~$repositoryOwnerName/$repositoryName - delete"),
-                user,
-                repo,
-                branches
-              )
-            )
-        }
-      } yield resp
-  }
+    private val showRepositoriesForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoriesOwnerName) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositories(csrf)(repositoriesOwnerName)(None)
+            } yield resp
+    }
 
-  private val showEditRepositoryForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "edit" as user =>
-      for {
-        csrf      <- Sync[F].delay(ar.req.getCsrfToken)
-        repoAndId <- loadRepo(user.some)(repositoryOwnerName, repositoryName)
-        repo = repoAndId.map(_._1)
-        branches <- repoAndId.map(_._2) match {
-          case Some(repoId) => vcsMetadataRepo.findVcsRepositoryBranches(repoId).compile.toList
-          case _            => Sync[F].delay(List.empty)
-        }
-        editAction <- Sync[F].delay(
-          linkConfig.createFullUri(
-            Uri(path =
-              Uri.Path(
-                Vector(
-                  Uri.Path.Segment(s"~$repositoryOwnerName"),
-                  Uri.Path.Segment(repositoryName.toString),
-                  Uri.Path.Segment("edit")
-                )
-              )
-            )
-          )
-        )
-        repoUri <- Sync[F].delay(
-          linkConfig.createFullUri(
-            Uri(path =
-              Uri.Path(
-                Vector(
-                  Uri.Path.Segment(s"~$repositoryOwnerName"),
-                  Uri.Path.Segment(repositoryName.toString)
-                )
-              )
-            )
-          )
-        )
-        formData <- Sync[F].delay(repo.map(EditVcsRepositoryForm.fromVcsRepository).map(_.toMap))
-        resp <- (formData, repo) match {
-          case (Some(formData), Some(repo)) =>
-            Ok(
-              views.html.editRepository()(
-                editAction,
-                csrf,
-                repoUri,
-                Option(s"~$repositoryOwnerName/$repositoryName - edit"),
-                user,
-                repo,
-                branches
-              )(formData)
-            )
-          case _ => NotFound()
-        }
-      } yield resp
-  }
+    private val showRepositoryBranches: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "branches" as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowRepositoryBranches(csrf)(user.some)(repositoryOwnerName, repositoryName)
+            } yield resp
+    }
 
-  private val showRepositories: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoriesOwnerName) as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowRepositories(csrf)(repositoriesOwnerName)(user.some)
-      } yield resp
-  }
-
-  private val showRepositoriesForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoriesOwnerName) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositories(csrf)(repositoriesOwnerName)(None)
-      } yield resp
-  }
-
-  private val showRepositoryBranches: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "branches" as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowRepositoryBranches(csrf)(user.some)(repositoryOwnerName, repositoryName)
-      } yield resp
-  }
-
-  private val showRepositoryBranchesForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "branches" =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositoryBranches(csrf)(None)(repositoryOwnerName, repositoryName)
-      } yield resp
-  }
-
-  private val showRepositoryOverview: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowRepositoryOverview(csrf)(user.some)(repositoryOwnerName, repositoryName)
-      } yield resp
-  }
-
-  private val showRepositoryOverviewForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositoryOverview(csrf)(None)(repositoryOwnerName, repositoryName)
-      } yield resp
-  }
-
-  private val showRepositoryFiles: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter(
-          repositoryName
-        ) /: "files" /: filePath as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowRepositoryFiles(csrf)(user.some)(repositoryOwnerName, repositoryName)(filePath)
-      } yield resp
-  }
-
-  private val showRepositoryFilesForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter(
-          repositoryName
-        ) /: "files" /: filePath =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositoryFiles(csrf)(None)(repositoryOwnerName, repositoryName)(filePath)
-      } yield resp
-  }
-
-  private val showRepositoryHistory: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "history" :? HistoryFromQueryParameter(fromCount) as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowRepositoryHistory(csrf)(user.some)(repositoryOwnerName, repositoryName)(fromCount)
-      } yield resp
-  }
-
-  private val showRepositoryHistoryForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "history" :? HistoryFromQueryParameter(fromCount) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositoryHistory(csrf)(None)(repositoryOwnerName, repositoryName)(fromCount)
-      } yield resp
-  }
-
-  private val showRepositoryRssFeed: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "feed.rss" =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositoryRssFeed(csrf)(None)(repositoryOwnerName, repositoryName)
-      } yield resp
-  }
-
-  private val showRepositoryPatchDetails: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "patch" / DarcsHashPathParameter(hash) as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowRepositoryPatchDetails(csrf)(user.some)(repositoryOwnerName, repositoryName)(hash)
-      } yield resp
-  }
-
-  private val showRepositoryPatchDetailsForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
-          repositoryName
-        ) / "patch" / DarcsHashPathParameter(hash) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowRepositoryPatchDetails(csrf)(None)(repositoryOwnerName, repositoryName)(hash)
-      } yield resp
-  }
-
-  val protectedRoutes =
-    downloadDistribution <+> forkRepository <+> showAllRepositories <+> showRepositories <+>
-      createRepository <+> deleteRepository <+> editRepository <+>
-      showCreateRepositoryForm <+> showDeleteRepositoryForm <+> showEditRepositoryForm <+>
-      showRepositoryOverview <+> showRepositoryBranches <+> showRepositoryHistory <+>
-      showRepositoryPatchDetails <+> showRepositoryFiles
-
-  val routes =
-    cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+>
-      showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryBranchesForGuests <+>
-      showRepositoryHistoryForGuests <+> showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests <+>
-      showRepositoryRssFeed
+    private val showRepositoryBranchesForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "branches" =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositoryBranches(csrf)(None)(repositoryOwnerName, repositoryName)
+            } yield resp
+    }
+
+    private val showRepositoryOverview: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowRepositoryOverview(csrf)(user.some)(repositoryOwnerName, repositoryName)
+            } yield resp
+    }
+
+    private val showRepositoryOverviewForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositoryOverview(csrf)(None)(repositoryOwnerName, repositoryName)
+            } yield resp
+    }
+
+    private val showRepositoryFiles: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter(
+                repositoryName
+            ) /: "files" /: filePath as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowRepositoryFiles(csrf)(user.some)(repositoryOwnerName, repositoryName)(filePath)
+            } yield resp
+    }
+
+    private val showRepositoryFilesForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> UsernamePathParameter(repositoryOwnerName) /: VcsRepositoryNamePathParameter(
+                repositoryName
+            ) /: "files" /: filePath =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositoryFiles(csrf)(None)(repositoryOwnerName, repositoryName)(filePath)
+            } yield resp
+    }
+
+    private val showRepositoryHistory: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "history" :? HistoryFromQueryParameter(fromCount) as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowRepositoryHistory(csrf)(user.some)(repositoryOwnerName, repositoryName)(fromCount)
+            } yield resp
+    }
+
+    private val showRepositoryHistoryForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "history" :? HistoryFromQueryParameter(fromCount) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositoryHistory(csrf)(None)(repositoryOwnerName, repositoryName)(fromCount)
+            } yield resp
+    }
+
+    private val showRepositoryRssFeed: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "feed.rss" =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositoryRssFeed(csrf)(None)(repositoryOwnerName, repositoryName)
+            } yield resp
+    }
+
+    private val showRepositoryPatchDetails: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "patch" / DarcsHashPathParameter(hash) as user =>
+            for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowRepositoryPatchDetails(csrf)(user.some)(repositoryOwnerName, repositoryName)(hash)
+            } yield resp
+    }
+
+    private val showRepositoryPatchDetailsForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(repositoryOwnerName) / VcsRepositoryNamePathParameter(
+                repositoryName
+            ) / "patch" / DarcsHashPathParameter(hash) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowRepositoryPatchDetails(csrf)(None)(repositoryOwnerName, repositoryName)(hash)
+            } yield resp
+    }
+
+    val protectedRoutes =
+        downloadDistribution <+> forkRepository <+> showAllRepositories <+> showRepositories <+>
+            createRepository <+> deleteRepository <+> editRepository <+>
+            showCreateRepositoryForm <+> showDeleteRepositoryForm <+> showEditRepositoryForm <+>
+            showRepositoryOverview <+> showRepositoryBranches <+> showRepositoryHistory <+>
+            showRepositoryPatchDetails <+> showRepositoryFiles
+
+    val routes =
+        cloneRepository <+> downloadDistributionForGuests <+> showAllRepositoriesForGuests <+>
+            showRepositoriesForGuests <+> showRepositoryOverviewForGuests <+> showRepositoryBranchesForGuests <+>
+            showRepositoryHistoryForGuests <+> showRepositoryPatchDetailsForGuests <+> showRepositoryFilesForGuests <+>
+            showRepositoryRssFeed
 
 }
 
 /** A path parameter extractor to get the hash of a darcs patch for showing patch details.
   */
 object DarcsHashPathParameter {
-  def unapply(str: String): Option[DarcsHash] = Option(str).flatMap(DarcsHash.from)
+    def unapply(str: String): Option[DarcsHash] = Option(str).flatMap(DarcsHash.from)
 }
 
 /** Extractor for an optional query parameter we use in our history route.
@@ -1640,11 +1729,11 @@
 /** A path parameter extractor to get the vcs repository name for a clone operation.
   */
 object VcsRepositoryClonePathParameter {
-  def unapply(str: String): Option[VcsRepositoryName] =
-    Option(str).flatMap { string =>
-      if (string.endsWith(".darcs"))
-        VcsRepositoryName.from(string.reverse.drop(6).reverse)
-      else
-        None
-    }
+    def unapply(str: String): Option[VcsRepositoryName] =
+        Option(str).flatMap { string =>
+            if (string.endsWith(".darcs"))
+                VcsRepositoryName.from(string.reverse.drop(6).reverse)
+            else
+                None
+        }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala	2025-01-13 17:13:25.056470978 +0000
@@ -36,141 +36,141 @@
   */
 enum VcsType {
 
-  /** The darcs DVCS.
-    */
-  case Darcs
+    /** The darcs DVCS.
+      */
+    case Darcs
 }
 
 opaque type VcsRepositoryDescription = String
 object VcsRepositoryDescription {
-  val MaximumLength: Int = 254
+    val MaximumLength: Int = 254
 
-  /** Create an instance of VcsRepositoryDescription from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsRepositoryDescription.
-    * @return
-    *   The appropriate instance of VcsRepositoryDescription.
-    */
-  def apply(source: String): VcsRepositoryDescription = source
-
-  /** Try to create an instance of VcsRepositoryDescription from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsRepositoryDescription.
-    * @return
-    *   An option to the successfully converted VcsRepositoryDescription.
-    */
-  def from(source: String): Option[VcsRepositoryDescription] = Option(source).map(_.take(MaximumLength))
+    /** Create an instance of VcsRepositoryDescription from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsRepositoryDescription.
+      * @return
+      *   The appropriate instance of VcsRepositoryDescription.
+      */
+    def apply(source: String): VcsRepositoryDescription = source
+
+    /** Try to create an instance of VcsRepositoryDescription from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsRepositoryDescription.
+      * @return
+      *   An option to the successfully converted VcsRepositoryDescription.
+      */
+    def from(source: String): Option[VcsRepositoryDescription] = Option(source).map(_.take(MaximumLength))
 
 }
 
 opaque type VcsRepositoryId = Long
 object VcsRepositoryId {
-  given Eq[VcsRepositoryId] = Eq.fromUniversalEquals
+    given Eq[VcsRepositoryId] = Eq.fromUniversalEquals
+
+    val Format: Regex = "^-?\\d+$".r
 
-  val Format: Regex = "^-?\\d+$".r
+    /** Create an instance of VcsRepositoryId from the given Long type.
+      *
+      * @param source
+      *   An instance of type Long which will be returned as a VcsRepositoryId.
+      * @return
+      *   The appropriate instance of VcsRepositoryId.
+      */
+    def apply(source: Long): VcsRepositoryId = source
 
-  /** Create an instance of VcsRepositoryId from the given Long type.
-    *
-    * @param source
-    *   An instance of type Long which will be returned as a VcsRepositoryId.
-    * @return
-    *   The appropriate instance of VcsRepositoryId.
-    */
-  def apply(source: Long): VcsRepositoryId = source
-
-  /** Try to create an instance of VcsRepositoryId from the given Long.
-    *
-    * @param source
-    *   A Long that should fulfil the requirements to be converted into a VcsRepositoryId.
-    * @return
-    *   An option to the successfully converted VcsRepositoryId.
-    */
-  def from(source: Long): Option[VcsRepositoryId] = Option(source)
-
-  /** Try to create an instance of VcsRepositoryId from the given String.
-    *
-    * @param source
-    *   A string that should fulfil the requirements to be converted into a VcsRepositoryId.
-    * @return
-    *   An option to the successfully converted VcsRepositoryId.
-    */
-  def fromString(source: String): Option[VcsRepositoryId] =
-    Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
-
-  extension (id: VcsRepositoryId) {
-    def toLong: Long = id
-  }
+    /** Try to create an instance of VcsRepositoryId from the given Long.
+      *
+      * @param source
+      *   A Long that should fulfil the requirements to be converted into a VcsRepositoryId.
+      * @return
+      *   An option to the successfully converted VcsRepositoryId.
+      */
+    def from(source: Long): Option[VcsRepositoryId] = Option(source)
+
+    /** Try to create an instance of VcsRepositoryId from the given String.
+      *
+      * @param source
+      *   A string that should fulfil the requirements to be converted into a VcsRepositoryId.
+      * @return
+      *   An option to the successfully converted VcsRepositoryId.
+      */
+    def fromString(source: String): Option[VcsRepositoryId] =
+        Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
+
+    extension (id: VcsRepositoryId) {
+        def toLong: Long = id
+    }
 }
 
 opaque type VcsRepositoryName = String
 object VcsRepositoryName {
 
-  given Eq[VcsRepositoryName] = Eq.fromUniversalEquals
+    given Eq[VcsRepositoryName] = Eq.fromUniversalEquals
+
+    given Order[VcsRepositoryName] = Order.from((a, b) => a.toString.compareTo(b.toString))
 
-  given Order[VcsRepositoryName] = Order.from((a, b) => a.toString.compareTo(b.toString))
+    // TODO: Can we rewrite this in a Scala-3 way (i.e. without implicitly)?
+    given Ordering[VcsRepositoryName] = implicitly[Order[VcsRepositoryName]].toOrdering
 
-  // TODO: Can we rewrite this in a Scala-3 way (i.e. without implicitly)?
-  given Ordering[VcsRepositoryName] = implicitly[Order[VcsRepositoryName]].toOrdering
+    val Format: Regex = "^[a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63}$".r
 
-  val Format: Regex = "^[a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63}$".r
+    /** Create an instance of VcsRepositoryName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsRepositoryName.
+      * @return
+      *   The appropriate instance of VcsRepositoryName.
+      */
+    def apply(source: String): VcsRepositoryName = source
 
-  /** Create an instance of VcsRepositoryName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsRepositoryName.
-    * @return
-    *   The appropriate instance of VcsRepositoryName.
-    */
-  def apply(source: String): VcsRepositoryName = source
-
-  /** Try to create an instance of VcsRepositoryName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsRepositoryName.
-    * @return
-    *   An option to the successfully converted VcsRepositoryName.
-    */
-  def from(source: String): Option[VcsRepositoryName] = validate(source).toOption
-
-  /** Validate the given string and return either the validated repository name or a list of errors.
-    *
-    * @param s
-    *   An arbitrary string which should be a repository name.
-    * @return
-    *   Either a list of errors or the validated repository name.
-    */
-  def validate(s: String): ValidatedNec[String, VcsRepositoryName] =
-    Option(s).map(_.trim.nonEmpty) match {
-      case Some(true) =>
-        val input = s.trim
-        val miniumLength =
-          if (input.length > 1)
-            input.validNec
-          else
-            "Repository name too short (min. 2 characters)!".invalidNec
-        val maximumLength =
-          if (input.length < 65)
-            input.validNec
-          else
-            "Repository name too long (max. 64 characters)!".invalidNec
-        val validFormat =
-          if (Format.matches(input))
-            input.validNec
-          else
-            "Repository name must start with a letter or number and is only allowed to contain alphanumeric characters plus .".invalidNec
-        (miniumLength, maximumLength, validFormat).mapN { case (_, _, name) =>
-          name
+    /** Try to create an instance of VcsRepositoryName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsRepositoryName.
+      * @return
+      *   An option to the successfully converted VcsRepositoryName.
+      */
+    def from(source: String): Option[VcsRepositoryName] = validate(source).toOption
+
+    /** Validate the given string and return either the validated repository name or a list of errors.
+      *
+      * @param s
+      *   An arbitrary string which should be a repository name.
+      * @return
+      *   Either a list of errors or the validated repository name.
+      */
+    def validate(s: String): ValidatedNec[String, VcsRepositoryName] =
+        Option(s).map(_.trim.nonEmpty) match {
+            case Some(true) =>
+                val input = s.trim
+                val miniumLength =
+                    if (input.length > 1)
+                        input.validNec
+                    else
+                        "Repository name too short (min. 2 characters)!".invalidNec
+                val maximumLength =
+                    if (input.length < 65)
+                        input.validNec
+                    else
+                        "Repository name too long (max. 64 characters)!".invalidNec
+                val validFormat =
+                    if (Format.matches(input))
+                        input.validNec
+                    else
+                        "Repository name must start with a letter or number and is only allowed to contain alphanumeric characters plus .".invalidNec
+                (miniumLength, maximumLength, validFormat).mapN { case (_, _, name) =>
+                    name
+                }
+            case _ => "Repository name must not be empty!".invalidNec
         }
-      case _ => "Repository name must not be empty!".invalidNec
-    }
 }
 
 /** Extractor to retrieve a VcsRepositoryName from a path parameter.
   */
 object VcsRepositoryNamePathParameter {
-  def unapply(str: String): Option[VcsRepositoryName] = Option(str).flatMap(VcsRepositoryName.from)
+    def unapply(str: String): Option[VcsRepositoryName] = Option(str).flatMap(VcsRepositoryName.from)
 }
 
 /** Descriptive information about the owner of a vcs repository.
@@ -186,154 +186,154 @@
 
 object VcsRepositoryOwner {
 
-  given Eq[VcsRepositoryOwner] = Eq.fromUniversalEquals
+    given Eq[VcsRepositoryOwner] = Eq.fromUniversalEquals
 
 }
 
 opaque type VcsPatchAuthor = String
 object VcsPatchAuthor {
-  val RemoveEmail: Regex = """<?([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)>?""".r
+    val RemoveEmail: Regex = """<?([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)>?""".r
+
+    /** Create an instance of VcsPatchAuthor from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsPatchAuthor.
+      * @return
+      *   The appropriate instance of VcsPatchAuthor.
+      */
+    def apply(source: String): VcsPatchAuthor = source
 
-  /** Create an instance of VcsPatchAuthor from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsPatchAuthor.
-    * @return
-    *   The appropriate instance of VcsPatchAuthor.
-    */
-  def apply(source: String): VcsPatchAuthor = source
-
-  /** Try to create an instance of VcsPatchAuthor from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsPatchAuthor.
-    * @return
-    *   An option to the successfully converted VcsPatchAuthor.
-    */
-  def from(source: String): Option[VcsPatchAuthor] = Option(source).filter(_.nonEmpty)
+    /** Try to create an instance of VcsPatchAuthor from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsPatchAuthor.
+      * @return
+      *   An option to the successfully converted VcsPatchAuthor.
+      */
+    def from(source: String): Option[VcsPatchAuthor] = Option(source).filter(_.nonEmpty)
 
 }
 
 extension (author: VcsPatchAuthor) {
 
-  /** Remove everything from the author string which could be an email address.
-    *
-    * @return
-    *   A string with every occurence of a potential email address replaced by an empty string.
-    */
-  def withoutEmail: String = VcsPatchAuthor.RemoveEmail.replaceAllIn(author.toString, "")
+    /** Remove everything from the author string which could be an email address.
+      *
+      * @return
+      *   A string with every occurence of a potential email address replaced by an empty string.
+      */
+    def withoutEmail: String = VcsPatchAuthor.RemoveEmail.replaceAllIn(author.toString, "")
 
 }
 
 opaque type VcsPatchComment = String
 object VcsPatchComment {
-  val FixesHashMarker: Regex = "\\bFixes: (?<hash>[a-f0-9]{40})\\b".r
+    val FixesHashMarker: Regex = "\\bFixes: (?<hash>[a-f0-9]{40})\\b".r
+
+    /** Create an instance of VcsPatchComment from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsPatchComment.
+      * @return
+      *   The appropriate instance of VcsPatchComment.
+      */
+    def apply(source: String): VcsPatchComment = source
+
+    /** Try to create an instance of VcsPatchComment from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsPatchComment.
+      * @return
+      *   An option to the successfully converted VcsPatchComment.
+      */
+    def from(source: String): Option[VcsPatchComment] = Option(source).filter(_.nonEmpty)
 
-  /** Create an instance of VcsPatchComment from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsPatchComment.
-    * @return
-    *   The appropriate instance of VcsPatchComment.
-    */
-  def apply(source: String): VcsPatchComment = source
-
-  /** Try to create an instance of VcsPatchComment from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsPatchComment.
-    * @return
-    *   An option to the successfully converted VcsPatchComment.
-    */
-  def from(source: String): Option[VcsPatchComment] = Option(source).filter(_.nonEmpty)
-
-  extension (comment: VcsPatchComment) {
-    def length: Int = comment.length
-
-    /** Find trailing `Fixes: <HASH>` lines in a patch comment as described in the contributing guidelines and replace
-      * them with a link that is supposed to point where the patch details will be displayed.
-      *
-      * @param baseUri
-      *   The base URI to which the actual hash value will be appended.
-      * @return
-      *   All lines of the comment string with every line containing the patch comment with all occurrences of `Fixes:
-      *   <HASH>` replaced by `Fixes: <baseUri/HASH>`.
-      */
-    def withFixesHashUrl(baseUri: Uri): Array[String] = {
-      def replace(m: Regex.Match): String =
-        s"""Fixes: <a href="${baseUri.addSegment(m.group("hash")).toString}">${m.group("hash")}</a>"""
-      val lines = comment.toString.split("\\r?\\n")
-      lines.map(line => FixesHashMarker.replaceAllIn(line, replace))
+    extension (comment: VcsPatchComment) {
+        def length: Int = comment.length
+
+        /** Find trailing `Fixes: <HASH>` lines in a patch comment as described in the contributing guidelines and
+          * replace them with a link that is supposed to point where the patch details will be displayed.
+          *
+          * @param baseUri
+          *   The base URI to which the actual hash value will be appended.
+          * @return
+          *   All lines of the comment string with every line containing the patch comment with all occurrences of
+          *   `Fixes: <HASH>` replaced by `Fixes: <baseUri/HASH>`.
+          */
+        def withFixesHashUrl(baseUri: Uri): Array[String] = {
+            def replace(m: Regex.Match): String =
+                s"""Fixes: <a href="${baseUri.addSegment(m.group("hash")).toString}">${m.group("hash")}</a>"""
+            val lines = comment.toString.split("\\r?\\n")
+            lines.map(line => FixesHashMarker.replaceAllIn(line, replace))
+        }
     }
-  }
 }
 
 opaque type VcsPatchFilename = String
 object VcsPatchFilename {
 
-  /** Create an instance of VcsPatchFilename from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsPatchFilename.
-    * @return
-    *   The appropriate instance of VcsPatchFilename.
-    */
-  def apply(source: String): VcsPatchFilename = source
-
-  /** Try to create an instance of VcsPatchFilename from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsPatchFilename.
-    * @return
-    *   An option to the successfully converted VcsPatchFilename.
-    */
-  def from(source: String): Option[VcsPatchFilename] = Option(source).filter(_.nonEmpty)
+    /** Create an instance of VcsPatchFilename from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsPatchFilename.
+      * @return
+      *   The appropriate instance of VcsPatchFilename.
+      */
+    def apply(source: String): VcsPatchFilename = source
+
+    /** Try to create an instance of VcsPatchFilename from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsPatchFilename.
+      * @return
+      *   An option to the successfully converted VcsPatchFilename.
+      */
+    def from(source: String): Option[VcsPatchFilename] = Option(source).filter(_.nonEmpty)
 
 }
 
 opaque type VcsPatchHash = String
 object VcsPatchHash {
 
-  /** Create an instance of VcsPatchHash from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsPatchHash.
-    * @return
-    *   The appropriate instance of VcsPatchHash.
-    */
-  def apply(source: String): VcsPatchHash = source
-
-  /** Try to create an instance of VcsPatchHash from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsPatchHash.
-    * @return
-    *   An option to the successfully converted VcsPatchHash.
-    */
-  def from(source: String): Option[VcsPatchHash] = Option(source).filter(_.nonEmpty)
+    /** Create an instance of VcsPatchHash from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsPatchHash.
+      * @return
+      *   The appropriate instance of VcsPatchHash.
+      */
+    def apply(source: String): VcsPatchHash = source
+
+    /** Try to create an instance of VcsPatchHash from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsPatchHash.
+      * @return
+      *   An option to the successfully converted VcsPatchHash.
+      */
+    def from(source: String): Option[VcsPatchHash] = Option(source).filter(_.nonEmpty)
 
 }
 
 opaque type VcsPatchName = String
 object VcsPatchName {
 
-  /** Create an instance of VcsPatchName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a VcsPatchName.
-    * @return
-    *   The appropriate instance of VcsPatchName.
-    */
-  def apply(source: String): VcsPatchName = source
-
-  /** Try to create an instance of VcsPatchName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a VcsPatchName.
-    * @return
-    *   An option to the successfully converted VcsPatchName.
-    */
-  def from(source: String): Option[VcsPatchName] = Option(source).filter(_.nonEmpty)
+    /** Create an instance of VcsPatchName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a VcsPatchName.
+      * @return
+      *   The appropriate instance of VcsPatchName.
+      */
+    def apply(source: String): VcsPatchName = source
+
+    /** Try to create an instance of VcsPatchName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a VcsPatchName.
+      * @return
+      *   An option to the successfully converted VcsPatchName.
+      */
+    def from(source: String): Option[VcsPatchName] = Option(source).filter(_.nonEmpty)
 
 }
 
@@ -342,41 +342,41 @@
 opaque type VcsPatchTimestamp = OffsetDateTime
 object VcsPatchTimestamp {
 
-  /** Create an instance of VcsPatchTimestamp from the given OffsetDateTime type.
-    *
-    * @param source
-    *   An instance of type OffsetDateTime which will be returned as a VcsPatchTimestamp.
-    * @return
-    *   The appropriate instance of VcsPatchTimestamp.
-    */
-  def apply(source: OffsetDateTime): VcsPatchTimestamp = source.withOffsetSameLocal(ZoneOffset.UTC)
-
-  /** Try to create an instance of VcsPatchTimestamp from the given OffsetDateTime.
-    *
-    * @param source
-    *   A OffsetDateTime that should fulfil the requirements to be converted into a VcsPatchTimestamp.
-    * @return
-    *   An option to the successfully converted VcsPatchTimestamp.
-    */
-  def from(source: OffsetDateTime): Option[VcsPatchTimestamp] =
-    source.getOffset() match {
-      case ZoneOffset.UTC => Option(source)
-      case _              => None
-    }
+    /** Create an instance of VcsPatchTimestamp from the given OffsetDateTime type.
+      *
+      * @param source
+      *   An instance of type OffsetDateTime which will be returned as a VcsPatchTimestamp.
+      * @return
+      *   The appropriate instance of VcsPatchTimestamp.
+      */
+    def apply(source: OffsetDateTime): VcsPatchTimestamp = source.withOffsetSameLocal(ZoneOffset.UTC)
+
+    /** Try to create an instance of VcsPatchTimestamp from the given OffsetDateTime.
+      *
+      * @param source
+      *   A OffsetDateTime that should fulfil the requirements to be converted into a VcsPatchTimestamp.
+      * @return
+      *   An option to the successfully converted VcsPatchTimestamp.
+      */
+    def from(source: OffsetDateTime): Option[VcsPatchTimestamp] =
+        source.getOffset() match {
+            case ZoneOffset.UTC => Option(source)
+            case _              => None
+        }
 
 }
 
 extension (timestamp: VcsPatchTimestamp) {
 
-  /** Converts this date-time to an Instant. This returns an Instant representing the same point on the time-line as
-    * this date-time.
-    *
-    * @return
-    *   an Instant representing the same instant, not null
-    */
-  def toInstant: Instant = timestamp.toInstant
+    /** Converts this date-time to an Instant. This returns an Instant representing the same point on the time-line as
+      * this date-time.
+      *
+      * @return
+      *   an Instant representing the same instant, not null
+      */
+    def toInstant: Instant = timestamp.toInstant
 
-  def toOffsetDateTime: OffsetDateTime = timestamp
+    def toOffsetDateTime: OffsetDateTime = timestamp
 }
 
 /** Summary construct for a file modification within a patch.
@@ -406,55 +406,55 @@
 )
 
 object VcsPatchSummary {
-  private val log = LoggerFactory.getLogger(classOf[VcsPatchSummary])
+    private val log = LoggerFactory.getLogger(classOf[VcsPatchSummary])
 
-  /** Parse the given xml element which SHALL originate from the xml output of a darcs log command. Any exceptional
-    * errors which occur during conversion are logged.
-    *
-    * @param darcsLogEntry
-    *   A log entry from a darcs log using the xml output (i.e. the `<patch>` element).
-    * @return
-    *   An option to a successfully extracted patch metadata object.
-    */
-  def fromDarcsXmlLog(darcsLogEntry: scala.xml.Node): Option[VcsPatchSummary] =
-    Try {
-      val summary = (darcsLogEntry \ "summary").headOption
-      val added = summary
-        .map(summary => (summary \ "add_file").flatMap(file => VcsPatchFilename.from(file.text)).toList)
-        .getOrElse(List.empty)
-      val modified = summary
-        .map(summary =>
-          (summary \ "modify_file").toList.flatMap { file =>
-            val filename      = VcsPatchFilename.from(file.text)
-            val added_lines   = file \ "added_lines" \@ "num"
-            val removed_lines = file \ "removed_lines" \@ "num"
-            val added =
-              if (added_lines.nonEmpty && added_lines.forall(_.isDigit))
-                added_lines.toInt
-              else
-                0
-            val removed =
-              if (removed_lines.nonEmpty && removed_lines.forall(_.isDigit))
-                removed_lines.toInt
-              else
-                0
-            filename.map(filename => VcsPatchSummaryFileModification(added, filename, removed))
-          }
-        )
-        .getOrElse(List.empty)
-      val removed = summary
-        .map(summary => (summary \ "remove_file").flatMap(file => VcsPatchFilename.from(file.text)).toList)
-        .getOrElse(List.empty)
-      summary.map(_ => VcsPatchSummary(added, modified, removed))
-    } match {
-      case scala.util.Failure(exception) =>
-        log.error(
-          "Error occured while trying to extract patch summary from darcs log xml output!",
-          exception
-        )
-        None
-      case scala.util.Success(patchSummary) => patchSummary
-    }
+    /** Parse the given xml element which SHALL originate from the xml output of a darcs log command. Any exceptional
+      * errors which occur during conversion are logged.
+      *
+      * @param darcsLogEntry
+      *   A log entry from a darcs log using the xml output (i.e. the `<patch>` element).
+      * @return
+      *   An option to a successfully extracted patch metadata object.
+      */
+    def fromDarcsXmlLog(darcsLogEntry: scala.xml.Node): Option[VcsPatchSummary] =
+        Try {
+            val summary = (darcsLogEntry \ "summary").headOption
+            val added = summary
+                .map(summary => (summary \ "add_file").flatMap(file => VcsPatchFilename.from(file.text)).toList)
+                .getOrElse(List.empty)
+            val modified = summary
+                .map(summary =>
+                    (summary \ "modify_file").toList.flatMap { file =>
+                        val filename      = VcsPatchFilename.from(file.text)
+                        val added_lines   = file \ "added_lines" \@ "num"
+                        val removed_lines = file \ "removed_lines" \@ "num"
+                        val added =
+                            if (added_lines.nonEmpty && added_lines.forall(_.isDigit))
+                                added_lines.toInt
+                            else
+                                0
+                        val removed =
+                            if (removed_lines.nonEmpty && removed_lines.forall(_.isDigit))
+                                removed_lines.toInt
+                            else
+                                0
+                        filename.map(filename => VcsPatchSummaryFileModification(added, filename, removed))
+                    }
+                )
+                .getOrElse(List.empty)
+            val removed = summary
+                .map(summary => (summary \ "remove_file").flatMap(file => VcsPatchFilename.from(file.text)).toList)
+                .getOrElse(List.empty)
+            summary.map(_ => VcsPatchSummary(added, modified, removed))
+        } match {
+            case scala.util.Failure(exception) =>
+                log.error(
+                    "Error occured while trying to extract patch summary from darcs log xml output!",
+                    exception
+                )
+                None
+            case scala.util.Success(patchSummary) => patchSummary
+        }
 }
 
 /** Data describing a patch in a vcs repository.
@@ -482,68 +482,69 @@
 )
 
 object VcsRepositoryPatchMetadata {
-  private val log = LoggerFactory.getLogger(classOf[VcsRepositoryPatchMetadata])
-
-  private val replaceEmail = """<([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)>""".r
+    private val log = LoggerFactory.getLogger(classOf[VcsRepositoryPatchMetadata])
 
-  val DarcsCommentFilter: Regex          = "^Ignore-this: [0-9a-f]+".r
-  val DarcsDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
+    private val replaceEmail = """<([a-zA-Z0-9.!#$%&’'*+/=?^_`{|}~-]+)@([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*)>""".r
 
-  /** Parse the given xml element which SHALL originate from the xml output of a darcs log command. Any exceptional
-    * errors which occur during conversion are logged.
-    *
-    * @param darcsLogEntry
-    *   A log entry from a darcs log using the xml output (i.e. the `<patch>` element).
-    * @return
-    *   An option to a successfully extracted patch metadata object.
-    */
-  def fromDarcsXmlLog(darcsLogEntry: scala.xml.Node): Option[VcsRepositoryPatchMetadata] =
-    Try {
-      val author = VcsPatchAuthor.from(darcsLogEntry \@ "author")
-      val comment = (darcsLogEntry \ "comment").headOption
-        .map(comment => DarcsCommentFilter.replaceFirstIn(comment.text, "").trim)
-        .flatMap(VcsPatchComment.from)
-      val hash    = VcsPatchHash.from(darcsLogEntry \@ "hash")
-      val name    = (darcsLogEntry \ "name").headOption.map(_.text).flatMap(VcsPatchName.from)
-      val summary = VcsPatchSummary.fromDarcsXmlLog(darcsLogEntry)
-      val timestamp =
-        VcsPatchTimestamp.from(
-          LocalDateTime.parse(darcsLogEntry \@ "date", DarcsDateFormat).atOffset(ZoneOffset.UTC)
-        )
-      (author, hash, name, timestamp).mapN { case (author, hash, name, timestamp) =>
-        VcsRepositoryPatchMetadata(author, comment, hash, name, summary, timestamp)
-      }
-    } match {
-      case scala.util.Failure(exception) =>
-        log.error(
-          "Error occured while trying to extract patch information from darcs log xml output!",
-          exception
-        )
-        None
-      case scala.util.Success(patchMetadata) => patchMetadata
-    }
-
-  extension (patch: VcsRepositoryPatchMetadata) {
+    val DarcsCommentFilter: Regex          = "^Ignore-this: [0-9a-f]+".r
+    val DarcsDateFormat: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss")
 
-    /** Convert the patch metadata information into an RSS item that may be included in a RSS feed.
+    /** Parse the given xml element which SHALL originate from the xml output of a darcs log command. Any exceptional
+      * errors which occur during conversion are logged.
       *
-      * @param baseUri
-      *   The URI needed to access the patch on platform i.e. the address to which only the hash needs to be appended.
+      * @param darcsLogEntry
+      *   A log entry from a darcs log using the xml output (i.e. the `<patch>` element).
       * @return
-      *   An RSS item describing the patch.
+      *   An option to a successfully extracted patch metadata object.
       */
-    def toRssItem(baseUri: Uri): RssItem = {
-      val uri = baseUri.addSegment(patch.hash.toString).toString
-      RssItem(
-        title = patch.name.toString,
-        link = uri,
-        description = patch.comment.map(_.toString).getOrElse(patch.name.toString).replaceAll("\n", "<br />"),
-        author = replaceEmail.replaceAllIn(patch.author.toString, "").trim.some,
-        guid = uri.some,
-        pubDate = patch.timestamp.toOffsetDateTime.some
-      )
+    def fromDarcsXmlLog(darcsLogEntry: scala.xml.Node): Option[VcsRepositoryPatchMetadata] =
+        Try {
+            val author = VcsPatchAuthor.from(darcsLogEntry \@ "author")
+            val comment = (darcsLogEntry \ "comment").headOption
+                .map(comment => DarcsCommentFilter.replaceFirstIn(comment.text, "").trim)
+                .flatMap(VcsPatchComment.from)
+            val hash    = VcsPatchHash.from(darcsLogEntry \@ "hash")
+            val name    = (darcsLogEntry \ "name").headOption.map(_.text).flatMap(VcsPatchName.from)
+            val summary = VcsPatchSummary.fromDarcsXmlLog(darcsLogEntry)
+            val timestamp =
+                VcsPatchTimestamp.from(
+                    LocalDateTime.parse(darcsLogEntry \@ "date", DarcsDateFormat).atOffset(ZoneOffset.UTC)
+                )
+            (author, hash, name, timestamp).mapN { case (author, hash, name, timestamp) =>
+                VcsRepositoryPatchMetadata(author, comment, hash, name, summary, timestamp)
+            }
+        } match {
+            case scala.util.Failure(exception) =>
+                log.error(
+                    "Error occured while trying to extract patch information from darcs log xml output!",
+                    exception
+                )
+                None
+            case scala.util.Success(patchMetadata) => patchMetadata
+        }
+
+    extension (patch: VcsRepositoryPatchMetadata) {
+
+        /** Convert the patch metadata information into an RSS item that may be included in a RSS feed.
+          *
+          * @param baseUri
+          *   The URI needed to access the patch on platform i.e. the address to which only the hash needs to be
+          *   appended.
+          * @return
+          *   An RSS item describing the patch.
+          */
+        def toRssItem(baseUri: Uri): RssItem = {
+            val uri = baseUri.addSegment(patch.hash.toString).toString
+            RssItem(
+                title = patch.name.toString,
+                link = uri,
+                description = patch.comment.map(_.toString).getOrElse(patch.name.toString).replaceAll("\n", "<br />"),
+                author = replaceEmail.replaceAllIn(patch.author.toString, "").trim.some,
+                guid = uri.some,
+                pubDate = patch.timestamp.toOffsetDateTime.some
+            )
+        }
     }
-  }
 }
 
 /** Data about a VCS respository.
@@ -576,5 +577,5 @@
 )
 
 object VcsRepository {
-  given Eq[VcsRepository] = Eq.fromUniversalEquals
+    given Eq[VcsRepository] = Eq.fromUniversalEquals
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSftpFileSystemAccessor.scala	2025-01-13 17:13:25.056470978 +0000
@@ -45,59 +45,62 @@
     darcsConfiguration: DarcsConfiguration,
     repository: SshAuthenticationRepository[IO]
 ) extends SftpFileSystemAccessor {
-  private val log = LoggerFactory.getLogger(classOf[DarcsSftpFileSystemAccessor])
+    private val log = LoggerFactory.getLogger(classOf[DarcsSftpFileSystemAccessor])
 
-  /** Check if the given repository of the given owner is readable by the user with the given id.
-    *
-    * @param ownerName
-    *   The unique name (user name) of the owner of the repository.
-    * @param repoName
-    *   The name of the repository which is unique within the context of the owner.
-    * @param userId
-    *   The unique id of the account that is requesting access.
-    * @return
-    *   Either `true` if the repository is readable by the user or `false` otherwise.
-    */
-  protected def repositoryIsReadableBy(ownerName: Username, repoName: VcsRepositoryName, userId: UserId): Boolean =
-    Dispatcher
-      .sequential[IO]
-      .use { dispatcher =>
-        val checkPermissions =
-          for {
-            _        <- IO.delay(log.debug(s"Checking if vcs repository $ownerName/$repoName is readable by $userId."))
-            vcsOwner <- repository.findVcsRepositoryOwner(ownerName)
-            _        <- IO.delay(log.debug(s"VCS repository owner name maps to $vcsOwner."))
-            userIsOwner = vcsOwner.exists(_.uid === userId)
-          } yield userIsOwner
-        checkPermissions.recoverWith { error =>
-          log.error("Internal Server Error", error)
-          false.pure[IO]
-        }
-      }
-      .unsafeRunSync()
+    /** Check if the given repository of the given owner is readable by the user with the given id.
+      *
+      * @param ownerName
+      *   The unique name (user name) of the owner of the repository.
+      * @param repoName
+      *   The name of the repository which is unique within the context of the owner.
+      * @param userId
+      *   The unique id of the account that is requesting access.
+      * @return
+      *   Either `true` if the repository is readable by the user or `false` otherwise.
+      */
+    protected def repositoryIsReadableBy(ownerName: Username, repoName: VcsRepositoryName, userId: UserId): Boolean =
+        Dispatcher
+            .sequential[IO]
+            .use { dispatcher =>
+                val checkPermissions =
+                    for {
+                        _ <- IO.delay(
+                            log.debug(s"Checking if vcs repository $ownerName/$repoName is readable by $userId.")
+                        )
+                        vcsOwner <- repository.findVcsRepositoryOwner(ownerName)
+                        _        <- IO.delay(log.debug(s"VCS repository owner name maps to $vcsOwner."))
+                        userIsOwner = vcsOwner.exists(_.uid === userId)
+                    } yield userIsOwner
+                checkPermissions.recoverWith { error =>
+                    log.error("Internal Server Error", error)
+                    false.pure[IO]
+                }
+            }
+            .unsafeRunSync()
 
-  @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
-  override def resolveLocalFilePath(subsystem: SftpSubsystemProxy, rootDir: Path, remotePath: String): Path = {
-    // FIXME: This works but is pretty clumsy/noisy. Find a way to kill the connection on the first illegal access.
-    val sshKeyOwnerId = subsystem.getServerSession().getAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute)
-    val accessIsPermitted = remotePath match {
-      case DarcsSftpFileSystemAccessor.ExtractRepositoryOwnerAndName(owner, repository, path) =>
-        log.debug("SFTP permission check for {} on {}/{} ({})", sshKeyOwnerId, owner, repository, path)
-        (SshUsername.from(owner), VcsRepositoryName.from(repository)).mapN { case (owner, repository) =>
-          repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains("..")
+    @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
+    override def resolveLocalFilePath(subsystem: SftpSubsystemProxy, rootDir: Path, remotePath: String): Path = {
+        // FIXME: This works but is pretty clumsy/noisy. Find a way to kill the connection on the first illegal access.
+        val sshKeyOwnerId = subsystem.getServerSession().getAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute)
+        val accessIsPermitted = remotePath match {
+            case DarcsSftpFileSystemAccessor.ExtractRepositoryOwnerAndName(owner, repository, path) =>
+                log.debug("SFTP permission check for {} on {}/{} ({})", sshKeyOwnerId, owner, repository, path)
+                (SshUsername.from(owner), VcsRepositoryName.from(repository)).mapN { case (owner, repository) =>
+                    repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains("..")
+                }
+            case noMatch =>
+                log.error("SFTP permission check regex did not match for: {} ({})", remotePath, noMatch)
+                false.some
+        }
+        accessIsPermitted.filter(_ === true) match {
+            case Some(true) => super.resolveLocalFilePath(subsystem, rootDir, remotePath)
+            case _ =>
+                throw new InvalidPathException(remotePath, "You are only allowed to access your own repositories!")
         }
-      case noMatch =>
-        log.error("SFTP permission check regex did not match for: {} ({})", remotePath, noMatch)
-        false.some
-    }
-    accessIsPermitted.filter(_ === true) match {
-      case Some(true) => super.resolveLocalFilePath(subsystem, rootDir, remotePath)
-      case _ => throw new InvalidPathException(remotePath, "You are only allowed to access your own repositories!")
     }
-  }
 
 }
 
 object DarcsSftpFileSystemAccessor {
-  val ExtractRepositoryOwnerAndName: Regex = "^([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})(.*)".r
+    val ExtractRepositoryOwnerAndName: Regex = "^([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})(.*)".r
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DarcsSshCommand.scala	2025-01-13 17:13:25.056470978 +0000
@@ -48,61 +48,61 @@
   */
 @SuppressWarnings(Array("scalafix:DisableSyntax.null", "scalafix:DisableSyntax.var"))
 abstract class DarcsSshCommand extends Command with ServerSessionAware {
-  private val log = LoggerFactory.getLogger(classOf[DarcsSshCommand])
+    private val log = LoggerFactory.getLogger(classOf[DarcsSshCommand])
 
-  @volatile protected var session: ServerSession = null
-  @volatile protected var stdin: InputStream     = null
-  @volatile protected var stdout: OutputStream   = null
-  @volatile protected var stderr: OutputStream   = null
-  @volatile protected var callback: ExitCallback = null
-
-  /** This method SHALL implement the custom logic for the ssh command.
-    *
-    * The `start` method MUST execute this within a [[java.lang.Thread]]!
-    *
-    * {{{
-    * val thread = new Thread {
-    *   override def run(): Unit = {
-    *     val result = commandLogic(cmd)
-    *     callback.onExit(result.exitCode)
-    *   }
-    * }
-    * thread.start()
-    * }}}
-    *
-    * @param cmd
-    *   A command that will be executed.
-    * @return
-    *   The result of the given command.
-    * @todo
-    *   De-couple this from the os-lib sometime in the future.
-    */
-  protected def commandLogic(cmd: os.proc): os.CommandResult
-
-  override def destroy(channel: ChannelSession): Unit = {
-    log.debug("destroy: {}", channel)
-    ()
-  }
-  override def setErrorStream(errorStream: OutputStream): Unit = {
-    log.debug("setErrorStream")
-    this.stderr = errorStream
-  }
-  override def setExitCallback(callback: ExitCallback): Unit = {
-    log.debug("setExitCallback: {}", callback)
-    this.callback = callback
-  }
-  override def setInputStream(inputStream: InputStream): Unit = {
-    log.debug("setInputStream")
-    this.stdin = inputStream
-  }
-  override def setOutputStream(outputStream: OutputStream): Unit = {
-    log.debug("setOutputStream")
-    this.stdout = outputStream
-  }
-  override def setSession(session: ServerSession): Unit = {
-    log.debug("setSession: {}", session)
-    this.session = session
-  }
+    @volatile protected var session: ServerSession = null
+    @volatile protected var stdin: InputStream     = null
+    @volatile protected var stdout: OutputStream   = null
+    @volatile protected var stderr: OutputStream   = null
+    @volatile protected var callback: ExitCallback = null
+
+    /** This method SHALL implement the custom logic for the ssh command.
+      *
+      * The `start` method MUST execute this within a [[java.lang.Thread]]!
+      *
+      * {{{
+      * val thread = new Thread {
+      *   override def run(): Unit = {
+      *     val result = commandLogic(cmd)
+      *     callback.onExit(result.exitCode)
+      *   }
+      * }
+      * thread.start()
+      * }}}
+      *
+      * @param cmd
+      *   A command that will be executed.
+      * @return
+      *   The result of the given command.
+      * @todo
+      *   De-couple this from the os-lib sometime in the future.
+      */
+    protected def commandLogic(cmd: os.proc): os.CommandResult
+
+    override def destroy(channel: ChannelSession): Unit = {
+        log.debug("destroy: {}", channel)
+        ()
+    }
+    override def setErrorStream(errorStream: OutputStream): Unit = {
+        log.debug("setErrorStream")
+        this.stderr = errorStream
+    }
+    override def setExitCallback(callback: ExitCallback): Unit = {
+        log.debug("setExitCallback: {}", callback)
+        this.callback = callback
+    }
+    override def setInputStream(inputStream: InputStream): Unit = {
+        log.debug("setInputStream")
+        this.stdin = inputStream
+    }
+    override def setOutputStream(outputStream: OutputStream): Unit = {
+        log.debug("setOutputStream")
+        this.stdout = outputStream
+    }
+    override def setSession(session: ServerSession): Unit = {
+        log.debug("setSession: {}", session)
+        this.session = session
+    }
 
 }
 
@@ -124,42 +124,42 @@
     repository: VcsRepositoryName,
     useDebugFlag: Boolean
 ) extends DarcsSshCommand {
-  private val log = LoggerFactory.getLogger(classOf[DarcsApply])
+    private val log = LoggerFactory.getLogger(classOf[DarcsApply])
 
-  override protected def commandLogic(cmd: os.proc): os.CommandResult = {
-    val workingDirectory = darcsConfiguration.repositoriesDirectory.toPath
-    log.debug(s"DarcsApply: calling command: $cmd in directory $workingDirectory.")
-    cmd.call(
-      cwd = os.Path(workingDirectory),
-      stdin = this.stdin,
-      stdout = os.ProcessOutput { (bytes, _) =>
-        log.trace("DarcsApply: STDOUT: {}", new String(bytes, "UTF-8"))
-        this.stdout.write(bytes)
-      },
-      stderr = os.ProcessOutput { (bytes, _) =>
-        log.trace("DarcsApply: STDERR: {}", new String(bytes, "UTF-8"))
-        this.stderr.write(bytes)
-      }
-    )
-  }
-
-  override def start(channel: ChannelSession, env: Environment): Unit = {
-    log.debug(s"DarcsApply for $owner/$repository")
-    val repoDir = Paths.get(owner.toString, repository.toString)
-    val options =
-      if (useDebugFlag)
-        List("apply", "--all", "--debug", "--repodir", repoDir.toString)
-      else
-        List("apply", "--all", "--repodir", repoDir.toString)
-    val cmd = os.proc(darcsConfiguration.executable.toString, options)
-    val thread = new Thread {
-      override def run(): Unit = {
-        val result = commandLogic(cmd)
-        callback.onExit(result.exitCode)
-      }
+    override protected def commandLogic(cmd: os.proc): os.CommandResult = {
+        val workingDirectory = darcsConfiguration.repositoriesDirectory.toPath
+        log.debug(s"DarcsApply: calling command: $cmd in directory $workingDirectory.")
+        cmd.call(
+            cwd = os.Path(workingDirectory),
+            stdin = this.stdin,
+            stdout = os.ProcessOutput { (bytes, _) =>
+                log.trace("DarcsApply: STDOUT: {}", new String(bytes, "UTF-8"))
+                this.stdout.write(bytes)
+            },
+            stderr = os.ProcessOutput { (bytes, _) =>
+                log.trace("DarcsApply: STDERR: {}", new String(bytes, "UTF-8"))
+                this.stderr.write(bytes)
+            }
+        )
+    }
+
+    override def start(channel: ChannelSession, env: Environment): Unit = {
+        log.debug(s"DarcsApply for $owner/$repository")
+        val repoDir = Paths.get(owner.toString, repository.toString)
+        val options =
+            if (useDebugFlag)
+                List("apply", "--all", "--debug", "--repodir", repoDir.toString)
+            else
+                List("apply", "--all", "--repodir", repoDir.toString)
+        val cmd = os.proc(darcsConfiguration.executable.toString, options)
+        val thread = new Thread {
+            override def run(): Unit = {
+                val result = commandLogic(cmd)
+                callback.onExit(result.exitCode)
+            }
+        }
+        thread.start()
     }
-    thread.start()
-  }
 }
 
 /** Implementation of a ssh command to perform a darcs transfer-mode command.
@@ -177,40 +177,40 @@
     owner: Username,
     repository: VcsRepositoryName
 ) extends DarcsSshCommand {
-  private val log = LoggerFactory.getLogger(classOf[DarcsTransferMode])
+    private val log = LoggerFactory.getLogger(classOf[DarcsTransferMode])
 
-  override protected def commandLogic(cmd: os.proc): os.CommandResult = {
-    val workingDirectory = darcsConfiguration.repositoriesDirectory.toPath
-    log.debug(s"DarcsTransferMode: calling command: $cmd in directory $workingDirectory.")
-    cmd.call(
-      cwd = os.Path(workingDirectory),
-      stdin = this.stdin,
-      stdout = os.ProcessOutput { (bytes, _) =>
-        log.trace("DarcsTransferMode: STDOUT: {}", new String(bytes, "UTF-8"))
-        this.stdout.write(bytes)
-      },
-      stderr = os.ProcessOutput { (bytes, _) =>
-        log.trace("DarcsTransferMode: STDERR: {}", new String(bytes, "UTF-8"))
-        this.stderr.write(bytes)
-      }
-    )
-  }
-
-  override def start(channel: ChannelSession, env: Environment): Unit = {
-    log.debug(s"DarcsTransferMode for $owner/$repository")
-    val repoDir = Paths.get(owner.toString, repository.toString)
-    val cmd = os.proc(
-      darcsConfiguration.executable.toString,
-      List("transfer-mode", "--repodir", repoDir.toString)
-    )
-    val thread = new Thread {
-      override def run(): Unit = {
-        val result = commandLogic(cmd)
-        callback.onExit(result.exitCode)
-      }
+    override protected def commandLogic(cmd: os.proc): os.CommandResult = {
+        val workingDirectory = darcsConfiguration.repositoriesDirectory.toPath
+        log.debug(s"DarcsTransferMode: calling command: $cmd in directory $workingDirectory.")
+        cmd.call(
+            cwd = os.Path(workingDirectory),
+            stdin = this.stdin,
+            stdout = os.ProcessOutput { (bytes, _) =>
+                log.trace("DarcsTransferMode: STDOUT: {}", new String(bytes, "UTF-8"))
+                this.stdout.write(bytes)
+            },
+            stderr = os.ProcessOutput { (bytes, _) =>
+                log.trace("DarcsTransferMode: STDERR: {}", new String(bytes, "UTF-8"))
+                this.stderr.write(bytes)
+            }
+        )
+    }
+
+    override def start(channel: ChannelSession, env: Environment): Unit = {
+        log.debug(s"DarcsTransferMode for $owner/$repository")
+        val repoDir = Paths.get(owner.toString, repository.toString)
+        val cmd = os.proc(
+            darcsConfiguration.executable.toString,
+            List("transfer-mode", "--repodir", repoDir.toString)
+        )
+        val thread = new Thread {
+            override def run(): Unit = {
+                val result = commandLogic(cmd)
+                callback.onExit(result.exitCode)
+            }
+        }
+        thread.start()
     }
-    thread.start()
-  }
 }
 
 /** The command factory is appended to the apache ssh server and responsible for parsing requested commands and
@@ -224,141 +224,143 @@
   */
 final class DarcsSshCommandFactory(darcsConfiguration: DarcsConfiguration, repository: SshAuthenticationRepository[IO])
     extends CommandFactory {
-  private val log = LoggerFactory.getLogger(classOf[DarcsSshCommandFactory])
+    private val log = LoggerFactory.getLogger(classOf[DarcsSshCommandFactory])
 
-  /** Check if the given repository of the given owner is readable by the user with the given id.
-    *
-    * @param ownerName
-    *   The unique name (user name) of the owner of the repository.
-    * @param repoName
-    *   The name of the repository which is unique within the context of the owner.
-    * @param userId
-    *   The unique id of the account that is requesting access.
-    * @return
-    *   Either `true` if the repository is readable by the user or `false` otherwise.
-    */
-  protected def repositoryIsReadableBy(ownerName: Username, repoName: VcsRepositoryName, userId: UserId): Boolean =
-    Dispatcher
-      .sequential[IO]
-      .use { dispatcher =>
-        val checkPermissions =
-          for {
-            _        <- IO.delay(log.debug(s"Checking if vcs repository $ownerName/$repoName is readable by $userId."))
-            vcsOwner <- repository.findVcsRepositoryOwner(ownerName)
-            _        <- IO.delay(log.debug(s"VCS repository owner name maps to $vcsOwner."))
-            userIsOwner = vcsOwner.exists(_.uid === userId)
-          } yield userIsOwner
-        checkPermissions.recoverWith { error =>
-          log.error("Internal Server Error", error)
-          false.pure[IO]
+    /** Check if the given repository of the given owner is readable by the user with the given id.
+      *
+      * @param ownerName
+      *   The unique name (user name) of the owner of the repository.
+      * @param repoName
+      *   The name of the repository which is unique within the context of the owner.
+      * @param userId
+      *   The unique id of the account that is requesting access.
+      * @return
+      *   Either `true` if the repository is readable by the user or `false` otherwise.
+      */
+    protected def repositoryIsReadableBy(ownerName: Username, repoName: VcsRepositoryName, userId: UserId): Boolean =
+        Dispatcher
+            .sequential[IO]
+            .use { dispatcher =>
+                val checkPermissions =
+                    for {
+                        _ <- IO.delay(
+                            log.debug(s"Checking if vcs repository $ownerName/$repoName is readable by $userId.")
+                        )
+                        vcsOwner <- repository.findVcsRepositoryOwner(ownerName)
+                        _        <- IO.delay(log.debug(s"VCS repository owner name maps to $vcsOwner."))
+                        userIsOwner = vcsOwner.exists(_.uid === userId)
+                    } yield userIsOwner
+                checkPermissions.recoverWith { error =>
+                    log.error("Internal Server Error", error)
+                    false.pure[IO]
+                }
+            }
+            .unsafeRunSync()
+
+    @SuppressWarnings(Array("scalafix:DisableSyntax.null"))
+    override def createCommand(channel: ChannelSession, command: String): Command = {
+        log.debug(s"Requested SSH command: $command")
+        val sshKeyOwnerId = channel.getSession().getAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute)
+
+        command match {
+            case DarcsSshCommandFactory.FilterDarcsApplyCommand(
+                    "apply",
+                    _,
+                    _,
+                    _,
+                    debugFlag,
+                    "--repodir",
+                    owner,
+                    repository
+                ) =>
+                (
+                    SshUsername.from(owner),
+                    VcsRepositoryName.from(repository)
+                )
+                    .mapN { case (owner, repository) =>
+                        if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId))
+                            new DarcsApply(
+                                darcsConfiguration,
+                                owner.toUsername,
+                                repository,
+                                debugFlag === "--debug"
+                            )
+                        else
+                            new UnknownCommand("You are only allowed to access your own repositories!")
+                    }
+                    .getOrElse(new UnknownCommand(command))
+            case DarcsSshCommandFactory.FilterDarcsTransferModeCommand(
+                    "transfer-mode",
+                    "--repodir",
+                    owner,
+                    repository
+                ) =>
+                (
+                    SshUsername.from(owner),
+                    VcsRepositoryName.from(repository)
+                )
+                    .mapN { case (owner, repository) =>
+                        if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId))
+                            // new DarcsTransferMode(darcsConfiguration, owner.toUsername, repository)
+                            new UnknownCommand(command) // FIXME: Make transfer-mode work (stalls currently).
+                        else
+                            new UnknownCommand("You are only allowed to access your own repositories!")
+                    }
+                    .getOrElse(new UnknownCommand(command))
+            case DarcsSshCommandFactory.FilterScpCommand("-f", _, "--", owner, repository, path) =>
+                (
+                    SshUsername.from(owner),
+                    VcsRepositoryName.from(repository)
+                )
+                    .mapN { case (owner, repository) =>
+                        if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains(".."))
+                            new ScpCommand(
+                                channel,
+                                command,
+                                null,
+                                ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
+                                ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
+                                null,
+                                null
+                            )
+                        else
+                            new UnknownCommand("You are only allowed to access your own repositories!")
+                    }
+                    .getOrElse(new UnknownCommand(command))
+            case DarcsSshCommandFactory.FilterScpCommand("-f", null, null, owner, repository, path) =>
+                (
+                    SshUsername.from(owner),
+                    VcsRepositoryName.from(repository)
+                )
+                    .mapN { case (owner, repository) =>
+                        if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains(".."))
+                            new ScpCommand(
+                                channel,
+                                command,
+                                null,
+                                ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
+                                ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
+                                null,
+                                null
+                            )
+                        else
+                            new UnknownCommand("You are only allowed to access your own repositories!")
+                    }
+                    .getOrElse(new UnknownCommand(command))
+            case _ => new UnknownCommand(command)
         }
-      }
-      .unsafeRunSync()
-
-  @SuppressWarnings(Array("scalafix:DisableSyntax.null"))
-  override def createCommand(channel: ChannelSession, command: String): Command = {
-    log.debug(s"Requested SSH command: $command")
-    val sshKeyOwnerId = channel.getSession().getAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute)
-
-    command match {
-      case DarcsSshCommandFactory.FilterDarcsApplyCommand(
-            "apply",
-            _,
-            _,
-            _,
-            debugFlag,
-            "--repodir",
-            owner,
-            repository
-          ) =>
-        (
-          SshUsername.from(owner),
-          VcsRepositoryName.from(repository)
-        )
-          .mapN { case (owner, repository) =>
-            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId))
-              new DarcsApply(
-                darcsConfiguration,
-                owner.toUsername,
-                repository,
-                debugFlag === "--debug"
-              )
-            else
-              new UnknownCommand("You are only allowed to access your own repositories!")
-          }
-          .getOrElse(new UnknownCommand(command))
-      case DarcsSshCommandFactory.FilterDarcsTransferModeCommand(
-            "transfer-mode",
-            "--repodir",
-            owner,
-            repository
-          ) =>
-        (
-          SshUsername.from(owner),
-          VcsRepositoryName.from(repository)
-        )
-          .mapN { case (owner, repository) =>
-            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId))
-              // new DarcsTransferMode(darcsConfiguration, owner.toUsername, repository)
-              new UnknownCommand(command) // FIXME: Make transfer-mode work (stalls currently).
-            else
-              new UnknownCommand("You are only allowed to access your own repositories!")
-          }
-          .getOrElse(new UnknownCommand(command))
-      case DarcsSshCommandFactory.FilterScpCommand("-f", _, "--", owner, repository, path) =>
-        (
-          SshUsername.from(owner),
-          VcsRepositoryName.from(repository)
-        )
-          .mapN { case (owner, repository) =>
-            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains(".."))
-              new ScpCommand(
-                channel,
-                command,
-                null,
-                ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
-                ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
-                null,
-                null
-              )
-            else
-              new UnknownCommand("You are only allowed to access your own repositories!")
-          }
-          .getOrElse(new UnknownCommand(command))
-      case DarcsSshCommandFactory.FilterScpCommand("-f", null, null, owner, repository, path) =>
-        (
-          SshUsername.from(owner),
-          VcsRepositoryName.from(repository)
-        )
-          .mapN { case (owner, repository) =>
-            if (repositoryIsReadableBy(owner.toUsername, repository, sshKeyOwnerId) && !path.contains(".."))
-              new ScpCommand(
-                channel,
-                command,
-                null,
-                ScpHelper.DEFAULT_SEND_BUFFER_SIZE,
-                ScpHelper.DEFAULT_RECEIVE_BUFFER_SIZE,
-                null,
-                null
-              )
-            else
-              new UnknownCommand("You are only allowed to access your own repositories!")
-          }
-          .getOrElse(new UnknownCommand(command))
-      case _ => new UnknownCommand(command)
     }
-  }
 }
 
 object DarcsSshCommandFactory {
-  // A regular expression which should match on our allowed darcs commands and their arguments.
-  val FilterDarcsApplyCommand: Regex =
-    "^darcs (apply) ((--all)\\s)?((--debug)\\s)?\\s*(--repodir) '([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})'$".r
-  val FilterDarcsTransferModeCommand: Regex =
-    "^darcs (transfer-mode) (--repodir) ([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})/?$".r
-  // After the initial transfer-mode call darcs will fallback to using scp.
-  // val FilterScpCommand: Regex = "^scp (-f) ((--)\\s)?([^\u0000]+)$".r
-  val FilterScpCommand: Regex =
-    "^scp (-f) ((--)\\s)?([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})/([^\u0000]+)$".r
-  // TODO: Add support for sftp because that could be enforced from the client side.
+    // A regular expression which should match on our allowed darcs commands and their arguments.
+    val FilterDarcsApplyCommand: Regex =
+        "^darcs (apply) ((--all)\\s)?((--debug)\\s)?\\s*(--repodir) '([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})'$".r
+    val FilterDarcsTransferModeCommand: Regex =
+        "^darcs (transfer-mode) (--repodir) ([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})/?$".r
+    // After the initial transfer-mode call darcs will fallback to using scp.
+    // val FilterScpCommand: Regex = "^scp (-f) ((--)\\s)?([^\u0000]+)$".r
+    val FilterScpCommand: Regex =
+        "^scp (-f) ((--)\\s)?([a-z][a-z0-9]{1,31})/([a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63})/([^\u0000]+)$".r
+    // TODO: Add support for sftp because that could be enforced from the client side.
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/DoobieSshAuthenticationRepository.scala	2025-01-13 17:13:25.056470978 +0000
@@ -29,28 +29,28 @@
 import doobie.postgres.implicits.*
 
 final class DoobieSshAuthenticationRepository[F[_]: Sync](tx: Transactor[F]) extends SshAuthenticationRepository[F] {
-  given Meta[EmailAddress]      = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[EncodedKeyBytes]   = Meta[String].timap(EncodedKeyBytes.unsafeFrom)(_.toString)
-  given Meta[KeyComment]        = Meta[String].timap(KeyComment.apply)(_.toString)
-  given Meta[KeyFingerprint]    = Meta[String].timap(KeyFingerprint.apply)(_.toString)
-  given Meta[SshKeyType]        = Meta[String].timap(id => SshKeyType.Mappings(id))(_.identifier)
-  given Meta[UserId]            = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]          = Meta[String].timap(Username.apply)(_.toString)
-  given Meta[VcsRepositoryName] = Meta[String].timap(VcsRepositoryName.apply)(_.toString)
+    given Meta[EmailAddress]      = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[EncodedKeyBytes]   = Meta[String].timap(EncodedKeyBytes.unsafeFrom)(_.toString)
+    given Meta[KeyComment]        = Meta[String].timap(KeyComment.apply)(_.toString)
+    given Meta[KeyFingerprint]    = Meta[String].timap(KeyFingerprint.apply)(_.toString)
+    given Meta[SshKeyType]        = Meta[String].timap(id => SshKeyType.Mappings(id))(_.identifier)
+    given Meta[UserId]            = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]          = Meta[String].timap(Username.apply)(_.toString)
+    given Meta[VcsRepositoryName] = Meta[String].timap(VcsRepositoryName.apply)(_.toString)
 
-  override def findSshKey(fingerprint: KeyFingerprint): F[Option[PublicSshKey]] =
-    sql"""SELECT id, uid, key_type, key, fingerprint, comment, created_at, last_used_at FROM "hub"."ssh_keys" WHERE fingerprint = $fingerprint"""
-      .query[PublicSshKey]
-      .option
-      .transact(tx)
+    override def findSshKey(fingerprint: KeyFingerprint): F[Option[PublicSshKey]] =
+        sql"""SELECT id, uid, key_type, key, fingerprint, comment, created_at, last_used_at FROM "hub"."ssh_keys" WHERE fingerprint = $fingerprint"""
+            .query[PublicSshKey]
+            .option
+            .transact(tx)
 
-  override def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]] =
-    sql"""SELECT uid, name, email FROM "hub"."accounts" WHERE name = $name LIMIT 1"""
-      .query[VcsRepositoryOwner]
-      .option
-      .transact(tx)
+    override def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]] =
+        sql"""SELECT uid, name, email FROM "hub"."accounts" WHERE name = $name LIMIT 1"""
+            .query[VcsRepositoryOwner]
+            .option
+            .transact(tx)
 
-  override def updateLastUsed(keyId: UUID): F[Int] =
-    sql"""UPDATE "hub"."ssh_keys" SET last_used_at = NOW() WHERE id = $keyId""".update.run.transact(tx)
+    override def updateLastUsed(keyId: UUID): F[Int] =
+        sql"""UPDATE "hub"."ssh_keys" SET last_used_at = NOW() WHERE id = $keyId""".update.run.transact(tx)
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/NoLogin.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/NoLogin.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/NoLogin.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/NoLogin.scala	2025-01-13 17:13:25.056470978 +0000
@@ -41,15 +41,15 @@
   *   The port number on which the SSH server is listening.
   */
 final class NoLogin(genericUser: SshUsername, host: Host, port: Port) extends ShellFactory {
-  final val DefaultPort = port"22"
-  // Generate a possible needed hint about setting the SSH_PORT environment variable.
-  val sshPort =
-    if (port === DefaultPort)
-      ""
-    else
-      s"SSH_PORT=${port.toString} "
-  val banner =
-    s"""
+    final val DefaultPort = port"22"
+    // Generate a possible needed hint about setting the SSH_PORT environment variable.
+    val sshPort =
+        if (port === DefaultPort)
+            ""
+        else
+            s"SSH_PORT=${port.toString} "
+    val banner =
+        s"""
        |  █████████                               █████                                     
        | ███░░░░░███                             ░░███                                      
        |░███    ░░░  █████████████    ██████   ███████   ██████  ████████   ██████   ██████ 
@@ -66,28 +66,28 @@
        |${sshPort}darcs clone ${genericUser.toString}@${host.toString}:username/repository
      """.stripMargin.replaceAll("\n", "\r\n") + "\r\n"
 
-  @SuppressWarnings(Array("scalafix:DisableSyntax.null", "scalafix:DisableSyntax.var"))
-  override def createShell(channel: ChannelSession): Command =
-    new Command() {
-      private var stdin: InputStream     = null
-      private var stdout: OutputStream   = null
-      private var stderr: OutputStream   = null
-      private var callback: ExitCallback = null
+    @SuppressWarnings(Array("scalafix:DisableSyntax.null", "scalafix:DisableSyntax.var"))
+    override def createShell(channel: ChannelSession): Command =
+        new Command() {
+            private var stdin: InputStream     = null
+            private var stdout: OutputStream   = null
+            private var stderr: OutputStream   = null
+            private var callback: ExitCallback = null
 
-      override def destroy(channel: ChannelSession): Unit            = ()
-      override def setErrorStream(errorStream: OutputStream): Unit   = this.stderr = errorStream
-      override def setExitCallback(callback: ExitCallback): Unit     = this.callback = callback
-      override def setInputStream(inputStream: InputStream): Unit    = this.stdin = inputStream
-      override def setOutputStream(outputStream: OutputStream): Unit = this.stdout = outputStream
+            override def destroy(channel: ChannelSession): Unit            = ()
+            override def setErrorStream(errorStream: OutputStream): Unit   = this.stderr = errorStream
+            override def setExitCallback(callback: ExitCallback): Unit     = this.callback = callback
+            override def setInputStream(inputStream: InputStream): Unit    = this.stdin = inputStream
+            override def setOutputStream(outputStream: OutputStream): Unit = this.stdout = outputStream
 
-      override def start(channel: ChannelSession, env: Environment): Unit = {
-        // Just print out our banner message and close the connection.
-        stderr.write(banner.getBytes(StandardCharsets.UTF_8))
-        stderr.flush()
-        stdin.close()
-        stdout.close()
-        stderr.close()
-        callback.onExit(127)
-      }
-    }
+            override def start(channel: ChannelSession, env: Environment): Unit = {
+                // Just print out our banner message and close the connection.
+                stderr.write(banner.getBytes(StandardCharsets.UTF_8))
+                stderr.flush()
+                stdin.close()
+                stdout.close()
+                stderr.close()
+                callback.onExit(127)
+            }
+        }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/PublicSshKey.scala	2025-01-13 17:13:25.056470978 +0000
@@ -33,121 +33,121 @@
 
 opaque type EncodedKeyBytes = String
 object EncodedKeyBytes {
-  val Format: Regex = "^[a-zA-z0-9+/]+=*$".r
+    val Format: Regex = "^[a-zA-z0-9+/]+=*$".r
 
-  /** Create an instance of EncodedKeyBytes from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a EncodedKeyBytes.
-    * @return
-    *   The appropriate instance of EncodedKeyBytes.
-    */
-  def apply(source: String): EncodedKeyBytes = source
-
-  /** Try to create an instance of EncodedKeyBytes from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a EncodedKeyBytes.
-    * @return
-    *   An option to the successfully converted EncodedKeyBytes.
-    */
-  def from(source: String): Option[EncodedKeyBytes] = Option(source).filter(string => Format.matches(string))
-
-  /** An unsafe method to create an instance of EncodedKeyBytes from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a EncodedKeyBytes.
-    * @return
-    *   The converted EncodedKeyBytes instance.
-    */
-  @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
-  @throws[IllegalArgumentException]("if the given string is not syntactically valid")
-  def unsafeFrom(source: String): EncodedKeyBytes =
-    from(source) match {
-      case None           => throw new IllegalArgumentException(s"Illegal format for EncodedKeyBytes: $source")
-      case Some(keyBytes) => keyBytes
-    }
+    /** Create an instance of EncodedKeyBytes from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a EncodedKeyBytes.
+      * @return
+      *   The appropriate instance of EncodedKeyBytes.
+      */
+    def apply(source: String): EncodedKeyBytes = source
 
-  extension (keyBytes: EncodedKeyBytes) {
+    /** Try to create an instance of EncodedKeyBytes from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a EncodedKeyBytes.
+      * @return
+      *   An option to the successfully converted EncodedKeyBytes.
+      */
+    def from(source: String): Option[EncodedKeyBytes] = Option(source).filter(string => Format.matches(string))
 
-    /** Convert the key bytes into an array of bytes by decoding the base64 string.
+    /** An unsafe method to create an instance of EncodedKeyBytes from the given String.
       *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a EncodedKeyBytes.
       * @return
-      *   The decoded bytes from the actual key bytes that were base64 encoded.
+      *   The converted EncodedKeyBytes instance.
       */
-    def toByteArray: Array[Byte] = Base64.getDecoder().decode(keyBytes)
-  }
+    @SuppressWarnings(Array("scalafix:DisableSyntax.throw"))
+    @throws[IllegalArgumentException]("if the given string is not syntactically valid")
+    def unsafeFrom(source: String): EncodedKeyBytes =
+        from(source) match {
+            case None           => throw new IllegalArgumentException(s"Illegal format for EncodedKeyBytes: $source")
+            case Some(keyBytes) => keyBytes
+        }
+
+    extension (keyBytes: EncodedKeyBytes) {
+
+        /** Convert the key bytes into an array of bytes by decoding the base64 string.
+          *
+          * @return
+          *   The decoded bytes from the actual key bytes that were base64 encoded.
+          */
+        def toByteArray: Array[Byte] = Base64.getDecoder().decode(keyBytes)
+    }
 
 }
 
 opaque type KeyComment = String
 object KeyComment {
-  val MaxLength: Int = 256
+    val MaxLength: Int = 256
 
-  /** Create an instance of KeyComment from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a KeyComment.
-    * @return
-    *   The appropriate instance of KeyComment.
-    */
-  def apply(source: String): KeyComment = source
-
-  /** Try to create an instance of KeyComment from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a KeyComment.
-    * @return
-    *   An option to the successfully converted KeyComment.
-    */
-  def from(source: String): Option[KeyComment] = Option(source).map(_.take(MaxLength)).filter(_.nonEmpty)
+    /** Create an instance of KeyComment from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a KeyComment.
+      * @return
+      *   The appropriate instance of KeyComment.
+      */
+    def apply(source: String): KeyComment = source
+
+    /** Try to create an instance of KeyComment from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a KeyComment.
+      * @return
+      *   An option to the successfully converted KeyComment.
+      */
+    def from(source: String): Option[KeyComment] = Option(source).map(_.take(MaxLength)).filter(_.nonEmpty)
 
 }
 
 opaque type KeyFingerprint = String
 object KeyFingerprint {
 
-  /** Create an instance of KeyFingerprint from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a KeyFingerprint.
-    * @return
-    *   The appropriate instance of KeyFingerprint.
-    */
-  def apply(source: String): KeyFingerprint = source
-
-  /** Try to create an instance of KeyFingerprint from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a KeyFingerprint.
-    * @return
-    *   An option to the successfully converted KeyFingerprint.
-    */
-  def from(source: String): Option[KeyFingerprint] = Option(source).filter(_.nonEmpty)
+    /** Create an instance of KeyFingerprint from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a KeyFingerprint.
+      * @return
+      *   The appropriate instance of KeyFingerprint.
+      */
+    def apply(source: String): KeyFingerprint = source
+
+    /** Try to create an instance of KeyFingerprint from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a KeyFingerprint.
+      * @return
+      *   An option to the successfully converted KeyFingerprint.
+      */
+    def from(source: String): Option[KeyFingerprint] = Option(source).filter(_.nonEmpty)
 
 }
 
 opaque type SshPublicKeyString = String
 object SshPublicKeyString {
-  val Format: Regex = "^([\\w-]+)\\s(.+)(\\s(.+))?$".r
+    val Format: Regex = "^([\\w-]+)\\s(.+)(\\s(.+))?$".r
+
+    /** Create an instance of SshPublicKeyString from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a SshPublicKeyString.
+      * @return
+      *   The appropriate instance of SshPublicKeyString.
+      */
+    def apply(source: String): SshPublicKeyString = source
 
-  /** Create an instance of SshPublicKeyString from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a SshPublicKeyString.
-    * @return
-    *   The appropriate instance of SshPublicKeyString.
-    */
-  def apply(source: String): SshPublicKeyString = source
-
-  /** Try to create an instance of SshPublicKeyString from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SshPublicKeyString.
-    * @return
-    *   An option to the successfully converted SshPublicKeyString.
-    */
-  def from(source: String): Option[SshPublicKeyString] = Option(source).filter(string => Format.matches(string))
+    /** Try to create an instance of SshPublicKeyString from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SshPublicKeyString.
+      * @return
+      *   An option to the successfully converted SshPublicKeyString.
+      */
+    def from(source: String): Option[SshPublicKeyString] = Option(source).filter(string => Format.matches(string))
 }
 
 /** Possible ssh key types.
@@ -156,49 +156,49 @@
   *   A unique identifier (the code written into openssh key files).
   */
 enum SshKeyType(val identifier: String) {
-  case SshDsa extends SshKeyType("ssh-dss")
+    case SshDsa extends SshKeyType("ssh-dss")
 
-  case SshEcDsa extends SshKeyType("ecdsa-sha2-nistp256")
+    case SshEcDsa extends SshKeyType("ecdsa-sha2-nistp256")
 
-  // case SshEcDsaSk
+    // case SshEcDsaSk
 
-  /** Recommended key type because smaller payload and more secure but some servers still don't support them.
-    */
-  case SshEd25519 extends SshKeyType("ssh-ed25519")
+    /** Recommended key type because smaller payload and more secure but some servers still don't support them.
+      */
+    case SshEd25519 extends SshKeyType("ssh-ed25519")
 
-  // case SshEd25519Sk
+    // case SshEd25519Sk
 
-  /** RSA keys are (still) the most common ones and the most compatible.
-    */
-  case SshRsa extends SshKeyType("ssh-rsa")
+    /** RSA keys are (still) the most common ones and the most compatible.
+      */
+    case SshRsa extends SshKeyType("ssh-rsa")
 }
 
 object SshKeyType {
-  val Mappings: Map[String, SshKeyType] = Map(
-    SshDsa.identifier     -> SshDsa,
-    SshEcDsa.identifier   -> SshEcDsa,
-    SshEd25519.identifier -> SshEd25519,
-    SshRsa.identifier     -> SshRsa
-  )
-
-  given Eq[SshKeyType] = Eq.fromUniversalEquals
-
-  /** Try to extract a supported [[SshKeyType]] from the given string that should contain a valid ssh public key.
-    * According to RFC 4253 the key format is as follows:
-    * {{{
-    * [type-name] [base-64-encoded-public-key] [comment]
-    * }}}
-    * While the comment is optional the other parts are required.
-    *
-    * @param sshKeyString
-    *   A string containing a correctly formatted public ssh key.
-    * @return
-    *   An option to the extracted key type.
-    */
-  def from(sshKeyString: String): Option[SshKeyType] = {
-    val typeString = sshKeyString.takeWhile(char => !char.isWhitespace)
-    Mappings.get(typeString)
-  }
+    val Mappings: Map[String, SshKeyType] = Map(
+        SshDsa.identifier     -> SshDsa,
+        SshEcDsa.identifier   -> SshEcDsa,
+        SshEd25519.identifier -> SshEd25519,
+        SshRsa.identifier     -> SshRsa
+    )
+
+    given Eq[SshKeyType] = Eq.fromUniversalEquals
+
+    /** Try to extract a supported [[SshKeyType]] from the given string that should contain a valid ssh public key.
+      * According to RFC 4253 the key format is as follows:
+      * {{{
+      * [type-name] [base-64-encoded-public-key] [comment]
+      * }}}
+      * While the comment is optional the other parts are required.
+      *
+      * @param sshKeyString
+      *   A string containing a correctly formatted public ssh key.
+      * @return
+      *   An option to the extracted key type.
+      */
+    def from(sshKeyString: String): Option[SshKeyType] = {
+        val typeString = sshKeyString.takeWhile(char => !char.isWhitespace)
+        Mappings.get(typeString)
+    }
 }
 
 /** Actual ssh key and related metadata describing the public ssh key of a user.
@@ -233,65 +233,65 @@
 )
 
 object PublicSshKey {
-  private val digest = MessageDigest.getInstance("SHA256")
+    private val digest = MessageDigest.getInstance("SHA256")
 
-  /** Create a [[PublicSshKey]] from the given parameters and [[SshPublicKeyString]].
-    *
-    * @param id
-    *   The globally unique id of the ssh key.
-    * @param ownerId
-    *   The unique id of the user account associated with this key.
-    * @param createdAt
-    *   The timestamp of when the ssh key (this [[PublicSshKey]]) was created.
-    * @param sshKey
-    *   A string containing a valid OpenSSH public key.
-    * @return
-    *   An option to the sucessfully created [[PublicSshKey]].
-    */
-  def from(id: UUID)(ownerId: UserId)(createdAt: OffsetDateTime)(sshKey: SshPublicKeyString): Option[PublicSshKey] = {
-    val keyType = SshKeyType.from(sshKey.toString)
-    val base64Key = EncodedKeyBytes.from(
-      sshKey.toString.dropWhile(char => !char.isWhitespace).trim.takeWhile(char => !char.isWhitespace)
-    )
-    val comment = sshKey.toString.split("\\s").drop(2).toList match {
-      case Nil          => None
-      case commentParts => KeyComment.from(commentParts.mkString(" "))
-    }
-    val fingerprint = base64Key.flatMap { base64Key =>
-      val rawFingerprint = Try {
-        val publicKey   = OpenSSHPublicKeyUtil.parsePublicKey(base64Key.toByteArray)
-        val digestedKey = digest.digest(OpenSSHPublicKeyUtil.encodePublicKey(publicKey))
-        Base64.getEncoder().withoutPadding().encodeToString(digestedKey)
-      }.toOption
-      rawFingerprint.flatMap(KeyFingerprint.from)
-    }
-    (keyType, base64Key, fingerprint).mapN { case (keyType, base64Key, fingerprint) =>
-      PublicSshKey(id, ownerId, keyType, base64Key, fingerprint, comment, createdAt, None)
-    }
-  }
-
-  extension (sshKey: PublicSshKey) {
-
-    /** Convert this key into an instance of a [[SshPublicKeyStringhPublicKeyString]].
+    /** Create a [[PublicSshKey]] from the given parameters and [[SshPublicKeyString]].
       *
+      * @param id
+      *   The globally unique id of the ssh key.
+      * @param ownerId
+      *   The unique id of the user account associated with this key.
+      * @param createdAt
+      *   The timestamp of when the ssh key (this [[PublicSshKey]]) was created.
+      * @param sshKey
+      *   A string containing a valid OpenSSH public key.
       * @return
-      *   A well formatted ssh public key string.
+      *   An option to the sucessfully created [[PublicSshKey]].
       */
-    def toSshPublicKeyString: SshPublicKeyString =
-      SshPublicKeyString(
-        s"""${sshKey.keyType.identifier} ${sshKey.keyBytes.toString}${sshKey.comment
-            .map(c => s" ${c.toString}")
-            .getOrElse("")}"""
-      )
+    def from(id: UUID)(ownerId: UserId)(createdAt: OffsetDateTime)(sshKey: SshPublicKeyString): Option[PublicSshKey] = {
+        val keyType = SshKeyType.from(sshKey.toString)
+        val base64Key = EncodedKeyBytes.from(
+            sshKey.toString.dropWhile(char => !char.isWhitespace).trim.takeWhile(char => !char.isWhitespace)
+        )
+        val comment = sshKey.toString.split("\\s").drop(2).toList match {
+            case Nil          => None
+            case commentParts => KeyComment.from(commentParts.mkString(" "))
+        }
+        val fingerprint = base64Key.flatMap { base64Key =>
+            val rawFingerprint = Try {
+                val publicKey   = OpenSSHPublicKeyUtil.parsePublicKey(base64Key.toByteArray)
+                val digestedKey = digest.digest(OpenSSHPublicKeyUtil.encodePublicKey(publicKey))
+                Base64.getEncoder().withoutPadding().encodeToString(digestedKey)
+            }.toOption
+            rawFingerprint.flatMap(KeyFingerprint.from)
+        }
+        (keyType, base64Key, fingerprint).mapN { case (keyType, base64Key, fingerprint) =>
+            PublicSshKey(id, ownerId, keyType, base64Key, fingerprint, comment, createdAt, None)
+        }
+    }
 
-    /** Convert this key into an instance of a AuthorizedKeyEntry useable by the Apache Mina SSHD library.
-      *
-      * @return
-      *   Either the converted AuthorizedKeyEntry or an error message.
-      */
-    def toAuthorizedKeyEntry: Either[String, AuthorizedKeyEntry] =
-      Try {
-        AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshKey.toSshPublicKeyString.toString)
-      }.toEither.leftMap(_.getMessage())
-  }
+    extension (sshKey: PublicSshKey) {
+
+        /** Convert this key into an instance of a [[SshPublicKeyStringhPublicKeyString]].
+          *
+          * @return
+          *   A well formatted ssh public key string.
+          */
+        def toSshPublicKeyString: SshPublicKeyString =
+            SshPublicKeyString(
+                s"""${sshKey.keyType.identifier} ${sshKey.keyBytes.toString}${sshKey.comment
+                        .map(c => s" ${c.toString}")
+                        .getOrElse("")}"""
+            )
+
+        /** Convert this key into an instance of a AuthorizedKeyEntry useable by the Apache Mina SSHD library.
+          *
+          * @return
+          *   Either the converted AuthorizedKeyEntry or an error message.
+          */
+        def toAuthorizedKeyEntry: Either[String, AuthorizedKeyEntry] =
+            Try {
+                AuthorizedKeyEntry.parseAuthorizedKeyEntry(sshKey.toSshPublicKeyString.toString)
+            }.toEither.leftMap(_.getMessage())
+    }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticationRepository.scala	2025-01-13 17:13:25.056470978 +0000
@@ -37,31 +37,31 @@
   */
 abstract class SshAuthenticationRepository[F[_]] {
 
-  /** Search for the ssh key with the given fingerprint for the specified owner who must not be locked.
-    *
-    * @param fingerprint
-    *   The unique fingerprint of the ssh key.
-    * @return
-    *   An option to the ssh key if it exists.
-    */
-  def findSshKey(fingerprint: KeyFingerprint): F[Option[PublicSshKey]]
+    /** Search for the ssh key with the given fingerprint for the specified owner who must not be locked.
+      *
+      * @param fingerprint
+      *   The unique fingerprint of the ssh key.
+      * @return
+      *   An option to the ssh key if it exists.
+      */
+    def findSshKey(fingerprint: KeyFingerprint): F[Option[PublicSshKey]]
 
-  /** Search for a repository owner of whom we only know the name.
-    *
-    * @param name
-    *   The name of the repository owner which is the username of the actual owners account.
-    * @return
-    *   An option to successfully found repository owner.
-    */
-  def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]]
+    /** Search for a repository owner of whom we only know the name.
+      *
+      * @param name
+      *   The name of the repository owner which is the username of the actual owners account.
+      * @return
+      *   An option to successfully found repository owner.
+      */
+    def findVcsRepositoryOwner(name: Username): F[Option[VcsRepositoryOwner]]
 
-  /** Update the last used column for the key in the database.
-    *
-    * @param keyId
-    *   The unique id of the public ssh key.
-    * @return
-    *   The number of affected database rows.
-    */
-  def updateLastUsed(keyId: UUID): F[Int]
+    /** Update the last used column for the key in the database.
+      *
+      * @param keyId
+      *   The unique id of the public ssh key.
+      * @return
+      *   The number of affected database rows.
+      */
+    def updateLastUsed(keyId: UUID): F[Int]
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshAuthenticator.scala	2025-01-13 17:13:25.056470978 +0000
@@ -53,74 +53,95 @@
     genericUser: SshUsername,
     repository: SshAuthenticationRepository[IO]
 ) extends PublickeyAuthenticator {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  override def authenticate(providedUsername: String, providedPublicKey: PublicKey, session: ServerSession): Boolean =
-    Dispatcher
-      .sequential[IO]
-      .use { dispatcher =>
-        val check = for {
-          _ <- IO.delay(log.debug(s"Authentication request for $providedUsername from ${session.getRemoteAddress()}."))
-          convertedName   <- IO.delay(SshUsername.from(providedUsername))
-          usernameIsValid <- IO.delay(convertedName.exists(_ === genericUser))
-          keyIsValid <-
-            if (usernameIsValid)
-              for {
-                fingerprint <- IO.delay(
-                  KeyFingerprint.from(KeyUtils.getFingerPrint(providedPublicKey).drop(7))
-                ) // drop the "SHA256:"
-                _ <- IO.delay(
-                  fingerprint.map(fingerprint =>
-                    log.debug(s"User provided ${providedPublicKey.getAlgorithm()} key with fingerprint: $fingerprint.")
-                  )
-                )
-                providedKeyBytes <- IO.delay(providedPublicKey.getEncoded())
-                possibleUserKey <- fingerprint match {
-                  case None              => IO.pure(None)
-                  case Some(fingerprint) => repository.findSshKey(fingerprint)
-                }
-                _ <- possibleUserKey match {
-                  case None =>
-                    IO.delay(log.warn(s"No ssh key found in the database for given fingerprint: $fingerprint!"))
-                  case Some(userKey) =>
-                    IO.delay(log.debug(s"Found matching ${userKey.keyType} key (owner: ${userKey.ownerId})."))
-                }
-                keyIsValid <- possibleUserKey match {
-                  case Some(userKey) =>
-                    // We create a `AuthorizedKeyEntry` from our stored key and delegate the key validation to mina sshd.
-                    // Also we store the owner id of the key in the server session for later usage.
-                    IO.delay {
-                      userKey.toAuthorizedKeyEntry match {
-                        case Left(error) =>
-                          log.error(s"Could not convert user key ${userKey.fingerprint} to AuthorizedKeyEntry: $error")
-                          false
-                        case Right(authKey) =>
-                          session.setAttribute(SshServerConfiguration.SshKeyOwnerIdAttribute, userKey.ownerId)
-                          val authenticator = PublickeyAuthenticator.fromAuthorizedEntries(
-                            userKey.fingerprint.toString,
-                            session,
-                            List(authKey).asJava,
-                            PublicKeyEntryResolver.IGNORING
-                          )
-                          authenticator.authenticate(providedUsername, providedPublicKey, session)
-                      }
-                    }
-                  case _ => IO.pure(false)
-                }
-                _ <- IO.delay {
-                  if (keyIsValid)
-                    log.debug("Keys are matching!")
-                  else
-                    log.info("Stored key and provided one are not matching!")
-                }
-              } yield keyIsValid
-            else
-              for {
-                _ <- IO.delay(log.debug(s"Provided username $providedUsername was not accepted!"))
-              } yield false
-        } yield usernameIsValid && keyIsValid
-        check
-      }
-      .unsafeRunSync()
+    override def authenticate(providedUsername: String, providedPublicKey: PublicKey, session: ServerSession): Boolean =
+        Dispatcher
+            .sequential[IO]
+            .use { dispatcher =>
+                val check = for {
+                    _ <- IO.delay(
+                        log.debug(s"Authentication request for $providedUsername from ${session.getRemoteAddress()}.")
+                    )
+                    convertedName   <- IO.delay(SshUsername.from(providedUsername))
+                    usernameIsValid <- IO.delay(convertedName.exists(_ === genericUser))
+                    keyIsValid <-
+                        if (usernameIsValid)
+                            for {
+                                fingerprint <- IO.delay(
+                                    KeyFingerprint.from(KeyUtils.getFingerPrint(providedPublicKey).drop(7))
+                                ) // drop the "SHA256:"
+                                _ <- IO.delay(
+                                    fingerprint.map(fingerprint =>
+                                        log.debug(
+                                            s"User provided ${providedPublicKey.getAlgorithm()} key with fingerprint: $fingerprint."
+                                        )
+                                    )
+                                )
+                                providedKeyBytes <- IO.delay(providedPublicKey.getEncoded())
+                                possibleUserKey <- fingerprint match {
+                                    case None              => IO.pure(None)
+                                    case Some(fingerprint) => repository.findSshKey(fingerprint)
+                                }
+                                _ <- possibleUserKey match {
+                                    case None =>
+                                        IO.delay(
+                                            log.warn(
+                                                s"No ssh key found in the database for given fingerprint: $fingerprint!"
+                                            )
+                                        )
+                                    case Some(userKey) =>
+                                        IO.delay(
+                                            log.debug(
+                                                s"Found matching ${userKey.keyType} key (owner: ${userKey.ownerId})."
+                                            )
+                                        )
+                                }
+                                keyIsValid <- possibleUserKey match {
+                                    case Some(userKey) =>
+                                        // We create a `AuthorizedKeyEntry` from our stored key and delegate the key validation to mina sshd.
+                                        // Also we store the owner id of the key in the server session for later usage.
+                                        IO.delay {
+                                            userKey.toAuthorizedKeyEntry match {
+                                                case Left(error) =>
+                                                    log.error(
+                                                        s"Could not convert user key ${userKey.fingerprint} to AuthorizedKeyEntry: $error"
+                                                    )
+                                                    false
+                                                case Right(authKey) =>
+                                                    session.setAttribute(
+                                                        SshServerConfiguration.SshKeyOwnerIdAttribute,
+                                                        userKey.ownerId
+                                                    )
+                                                    val authenticator = PublickeyAuthenticator.fromAuthorizedEntries(
+                                                        userKey.fingerprint.toString,
+                                                        session,
+                                                        List(authKey).asJava,
+                                                        PublicKeyEntryResolver.IGNORING
+                                                    )
+                                                    authenticator.authenticate(
+                                                        providedUsername,
+                                                        providedPublicKey,
+                                                        session
+                                                    )
+                                            }
+                                        }
+                                    case _ => IO.pure(false)
+                                }
+                                _ <- IO.delay {
+                                    if (keyIsValid)
+                                        log.debug("Keys are matching!")
+                                    else
+                                        log.info("Stored key and provided one are not matching!")
+                                }
+                            } yield keyIsValid
+                        else
+                            for {
+                                _ <- IO.delay(log.debug(s"Provided username $providedUsername was not accepted!"))
+                            } yield false
+                } yield usernameIsValid && keyIsValid
+                check
+            }
+            .unsafeRunSync()
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/ssh/SshServer.scala	2025-01-13 17:13:25.056470978 +0000
@@ -38,38 +38,38 @@
 
 opaque type SshUsername = String
 object SshUsername {
-  given Eq[SshUsername] = Eq.fromUniversalEquals
+    given Eq[SshUsername] = Eq.fromUniversalEquals
 
-  val Format: Regex = "^[a-z][a-z0-9]{2,31}$".r
+    val Format: Regex = "^[a-z][a-z0-9]{2,31}$".r
 
-  /** Create an instance of SshUsername from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a SshUsername.
-    * @return
-    *   The appropriate instance of SshUsername.
-    */
-  def apply(source: String): SshUsername = source
-
-  /** Try to create an instance of SshUsername from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SshUsername.
-    * @return
-    *   An option to the successfully converted SshUsername.
-    */
-  def from(source: String): Option[SshUsername] = Option(source).filter(string => Format.matches(string))
-
-  extension (sshUsername: SshUsername) {
+    /** Create an instance of SshUsername from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a SshUsername.
+      * @return
+      *   The appropriate instance of SshUsername.
+      */
+    def apply(source: String): SshUsername = source
 
-    /** Convert to a [[Username]] instance. The format of both is identical so we just return the wrapped ssh username
-      * string.
+    /** Try to create an instance of SshUsername from the given String.
       *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SshUsername.
       * @return
-      *   A proper username derived from the given ssh username.
+      *   An option to the successfully converted SshUsername.
       */
-    def toUsername: Username = Username(sshUsername)
-  }
+    def from(source: String): Option[SshUsername] = Option(source).filter(string => Format.matches(string))
+
+    extension (sshUsername: SshUsername) {
+
+        /** Convert to a [[Username]] instance. The format of both is identical so we just return the wrapped ssh
+          * username string.
+          *
+          * @return
+          *   A proper username derived from the given ssh username.
+          */
+        def toUsername: Username = Username(sshUsername)
+    }
 
 }
 
@@ -96,19 +96,19 @@
 )
 
 object SshServerConfiguration {
-  // A key marker for storing the owner id of an ssh-key in the ssh server session.
-  final val SshKeyOwnerIdAttribute: AttributeKey[UserId] = new AttributeKey[UserId]()
+    // A key marker for storing the owner id of an ssh-key in the ssh server session.
+    final val SshKeyOwnerIdAttribute: AttributeKey[UserId] = new AttributeKey[UserId]()
 
-  given Eq[SshServerConfiguration] = Eq.fromUniversalEquals
+    given Eq[SshServerConfiguration] = Eq.fromUniversalEquals
 
-  given ConfigReader[Host]        = ConfigReader.fromStringOpt[Host](Host.fromString)
-  given ConfigReader[Port]        = ConfigReader.fromStringOpt[Port](Port.fromString)
-  given ConfigReader[SshUsername] = ConfigReader.fromStringOpt[SshUsername](SshUsername.from)
-
-  given ConfigReader[SshServerConfiguration] =
-    ConfigReader.forProduct5("enabled", "generic-user", "host", "port", "server-key-file")(
-      SshServerConfiguration.apply
-    )
+    given ConfigReader[Host]        = ConfigReader.fromStringOpt[Host](Host.fromString)
+    given ConfigReader[Port]        = ConfigReader.fromStringOpt[Port](Port.fromString)
+    given ConfigReader[SshUsername] = ConfigReader.fromStringOpt[SshUsername](SshUsername.from)
+
+    given ConfigReader[SshServerConfiguration] =
+        ConfigReader.forProduct5("enabled", "generic-user", "host", "port", "server-key-file")(
+            SshServerConfiguration.apply
+        )
 }
 
 /** A ssh server using the [Apache MINA SSHD](https://mina.apache.org/sshd-project/) library and the IO monad from cats
@@ -128,50 +128,50 @@
     sshConfiguration: SshServerConfiguration
 ) {
 
-  /** Create an instance of a ssh server with defaults configure it according to the given configuration.
-    *
-    * @return
-    *   An instance of a Apache MINA SSHD server which is not yet started.
-    */
-  private def createServer(): SshServer = {
-    val transactor = Transactor.fromDriverManager[IO](
-      driver = databaseConfiguration.driver,
-      url = databaseConfiguration.url,
-      user = databaseConfiguration.user,
-      password = databaseConfiguration.pass,
-      logHandler = None
-    )
-    val repository = new DoobieSshAuthenticationRepository[IO](transactor)
-    val server     = SshServer.setUpDefaultServer()
-    // We only set the port but not the hostname for the server because we want it to listen on all addresses.
-    server.setPort(sshConfiguration.port.toString.toInt)
-    val keyProvider = new SimpleGeneratorHostKeyProvider(sshConfiguration.serverKeyFile)
-    keyProvider.setAlgorithm("EC")
-    keyProvider.setOverwriteAllowed(false)
-    server.setKeyPairProvider(keyProvider)
-    server.setPublickeyAuthenticator(new SshAuthenticator(sshConfiguration.genericUser, repository))
-    server.setShellFactory(
-      new NoLogin(sshConfiguration.genericUser, sshConfiguration.host, sshConfiguration.port)
-    )
-    server.setFileSystemFactory(
-      new VirtualFileSystemFactory(darcsConfiguration.repositoriesDirectory.toPath)
-    )
-    // Add our custom sftp subsystem to provide more performant access for darcs operations.
-    val sftpFileSystemAccessor = new DarcsSftpFileSystemAccessor(darcsConfiguration, repository)
-    val sftpSubsystem = new SftpSubsystemFactory.Builder().withFileSystemAccessor(sftpFileSystemAccessor).build()
-    server.setSubsystemFactories(Collections.singletonList(sftpSubsystem))
-    // Add our custom command factory which must provide darcs and scp functionality.
-    val darcsCommand = new DarcsSshCommandFactory(darcsConfiguration, repository)
-    server.setCommandFactory(darcsCommand)
-    server
-  }
-
-  def run(): Resource[IO, SshServer] =
-    Resource.make {
-      for {
-        server <- IO.delay(createServer())
-        _      <- IO.delay(server.start())
-      } yield server
-    }(server => IO.delay(server.stop()))
+    /** Create an instance of a ssh server with defaults configure it according to the given configuration.
+      *
+      * @return
+      *   An instance of a Apache MINA SSHD server which is not yet started.
+      */
+    private def createServer(): SshServer = {
+        val transactor = Transactor.fromDriverManager[IO](
+            driver = databaseConfiguration.driver,
+            url = databaseConfiguration.url,
+            user = databaseConfiguration.user,
+            password = databaseConfiguration.pass,
+            logHandler = None
+        )
+        val repository = new DoobieSshAuthenticationRepository[IO](transactor)
+        val server     = SshServer.setUpDefaultServer()
+        // We only set the port but not the hostname for the server because we want it to listen on all addresses.
+        server.setPort(sshConfiguration.port.toString.toInt)
+        val keyProvider = new SimpleGeneratorHostKeyProvider(sshConfiguration.serverKeyFile)
+        keyProvider.setAlgorithm("EC")
+        keyProvider.setOverwriteAllowed(false)
+        server.setKeyPairProvider(keyProvider)
+        server.setPublickeyAuthenticator(new SshAuthenticator(sshConfiguration.genericUser, repository))
+        server.setShellFactory(
+            new NoLogin(sshConfiguration.genericUser, sshConfiguration.host, sshConfiguration.port)
+        )
+        server.setFileSystemFactory(
+            new VirtualFileSystemFactory(darcsConfiguration.repositoriesDirectory.toPath)
+        )
+        // Add our custom sftp subsystem to provide more performant access for darcs operations.
+        val sftpFileSystemAccessor = new DarcsSftpFileSystemAccessor(darcsConfiguration, repository)
+        val sftpSubsystem = new SftpSubsystemFactory.Builder().withFileSystemAccessor(sftpFileSystemAccessor).build()
+        server.setSubsystemFactories(Collections.singletonList(sftpSubsystem))
+        // Add our custom command factory which must provide darcs and scp functionality.
+        val darcsCommand = new DarcsSshCommandFactory(darcsConfiguration, repository)
+        server.setCommandFactory(darcsCommand)
+        server
+    }
+
+    def run(): Resource[IO, SshServer] =
+        Resource.make {
+            for {
+                server <- IO.delay(createServer())
+                _      <- IO.delay(server.start())
+            } yield server
+        }(server => IO.delay(server.stop()))
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelForm.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelForm.scala	2025-01-13 17:13:25.056470978 +0000
@@ -42,78 +42,84 @@
 )
 
 object LabelForm extends FormValidator[LabelForm] {
-  val fieldColour: FormField      = FormField("colour")
-  val fieldDescription: FormField = FormField("description")
-  val fieldId: FormField          = FormField("id")
-  val fieldName: FormField        = FormField("name")
-
-  /** Create a form for editing a label from the given label data.
-    *
-    * @param label
-    *   The label which provides the data for the edit form.
-    * @return
-    *   A label form filled with the data from the given label.
-    */
-  def fromLabel(label: Label): LabelForm =
-    LabelForm(id = label.id, name = label.name, description = label.description, colour = label.colour)
-
-  override def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, LabelForm] = {
-    val id = data
-      .get(fieldId)
-      .fold(Option.empty[LabelId].validNec)(
-        _.headOption
-          .flatMap(LabelId.fromString)
-          .fold(FormFieldError("Invalid label id!").invalidNec)(id => Option(id).validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldId -> es.toList)))
-    val name = data
-      .get(fieldName)
-      .fold(FormFieldError("No label name given!").invalidNec)(
-        _.headOption
-          .map(_.trim) // We strip leading and trailing whitespace!
-          .flatMap(LabelName.from)
-          .fold(FormFieldError("Invalid label name!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
-    val description = data
-      .get(fieldDescription)
-      .fold(Option.empty[LabelDescription].validNec)(
-        _.headOption
-          .map(_.trim)
-          .filter(_.nonEmpty)
-          .fold(none[LabelDescription].validNec)(s =>
-            LabelDescription
-              .from(s)
-              .fold(FormFieldError("Invalid label description!").invalidNec)(descr => Option(descr).validNec)
-          )
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
-    val colour = data
-      .get(fieldColour)
-      .fold(FormFieldError("No label colour given!").invalidNec)(
-        _.headOption.flatMap(ColourCode.from).fold(FormFieldError("Invalid label colour!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldColour -> es.toList)))
-    (id, name, description, colour).mapN { case (id, name, description, colour) =>
-      LabelForm(id, name, description, colour)
-    }
-  }
-
-  extension (form: LabelForm) {
+    val fieldColour: FormField      = FormField("colour")
+    val fieldDescription: FormField = FormField("description")
+    val fieldId: FormField          = FormField("id")
+    val fieldName: FormField        = FormField("name")
 
-    /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
-      * twirl templating library.
+    /** Create a form for editing a label from the given label data.
       *
+      * @param label
+      *   The label which provides the data for the edit form.
       * @return
-      *   A stringified map containing the data of the form.
+      *   A label form filled with the data from the given label.
       */
-    def toMap: Map[String, Chain[String]] =
-      Map(
-        LabelForm.fieldId.toString          -> form.id.map(_.toString).fold(Chain.empty)(id => Chain(id)),
-        LabelForm.fieldName.toString        -> Chain(form.name.toString),
-        LabelForm.fieldDescription.toString -> form.description.map(_.toString).fold(Chain.empty)(d => Chain(d)),
-        LabelForm.fieldColour.toString      -> Chain(form.colour.toString)
-      )
-  }
+    def fromLabel(label: Label): LabelForm =
+        LabelForm(id = label.id, name = label.name, description = label.description, colour = label.colour)
+
+    override def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, LabelForm] = {
+        val id = data
+            .get(fieldId)
+            .fold(Option.empty[LabelId].validNec)(
+                _.headOption
+                    .flatMap(LabelId.fromString)
+                    .fold(FormFieldError("Invalid label id!").invalidNec)(id => Option(id).validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldId -> es.toList)))
+        val name = data
+            .get(fieldName)
+            .fold(FormFieldError("No label name given!").invalidNec)(
+                _.headOption
+                    .map(_.trim) // We strip leading and trailing whitespace!
+                    .flatMap(LabelName.from)
+                    .fold(FormFieldError("Invalid label name!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldName -> es.toList)))
+        val description = data
+            .get(fieldDescription)
+            .fold(Option.empty[LabelDescription].validNec)(
+                _.headOption
+                    .map(_.trim)
+                    .filter(_.nonEmpty)
+                    .fold(none[LabelDescription].validNec)(s =>
+                        LabelDescription
+                            .from(s)
+                            .fold(FormFieldError("Invalid label description!").invalidNec)(descr =>
+                                Option(descr).validNec
+                            )
+                    )
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
+        val colour = data
+            .get(fieldColour)
+            .fold(FormFieldError("No label colour given!").invalidNec)(
+                _.headOption
+                    .flatMap(ColourCode.from)
+                    .fold(FormFieldError("Invalid label colour!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldColour -> es.toList)))
+        (id, name, description, colour).mapN { case (id, name, description, colour) =>
+            LabelForm(id, name, description, colour)
+        }
+    }
+
+    extension (form: LabelForm) {
+
+        /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
+          * twirl templating library.
+          *
+          * @return
+          *   A stringified map containing the data of the form.
+          */
+        def toMap: Map[String, Chain[String]] =
+            Map(
+                LabelForm.fieldId.toString   -> form.id.map(_.toString).fold(Chain.empty)(id => Chain(id)),
+                LabelForm.fieldName.toString -> Chain(form.name.toString),
+                LabelForm.fieldDescription.toString -> form.description
+                    .map(_.toString)
+                    .fold(Chain.empty)(d => Chain(d)),
+                LabelForm.fieldColour.toString -> Chain(form.colour.toString)
+            )
+    }
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala	2025-01-13 17:13:25.056470978 +0000
@@ -52,287 +52,310 @@
     labelRepo: LabelRepository[F],
     projectRepo: ProjectRepository[F]
 ) extends Http4sDsl[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given CsrfProtectionConfiguration = configuration.csrfProtection
+    given CsrfProtectionConfiguration = configuration.csrfProtection
 
-  private val linkToHubService = configuration.hub.baseUri
-  private val linkConfig       = configuration.externalUrl
+    private val linkToHubService = configuration.hub.baseUri
+    private val linkConfig       = configuration.externalUrl
 
-  /** Logic for rendering a list of all labels for a project and optionally management functionality.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of labels shall be rendered.
-    * @param projectOwnerName
-    *   The username of the account who owns the project.
-    * @param projectName
-    *   The name of the project.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowLabels(
-      csrf: Option[CsrfToken]
-  )(user: Option[Account])(projectOwnerName: Username)(projectName: ProjectName): F[Response[F]] =
-    for {
-      _            <- Sync[F].delay(log.debug(s"doShowLabels: $csrf, $user, $projectOwnerName, $projectName"))
-      language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      projectAndId <- loadProject(user)(projectOwnerName, projectName)
-      resp <- projectAndId match {
-        case Some((project, projectId)) =>
-          for {
-            labels <- labelRepo.allLabels(projectId).compile.toList
-            projectBaseUri <- Sync[F].delay(
-              linkConfig.createFullUri(
-                Uri(path =
-                  Uri.Path(
-                    Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                  )
-                )
-              )
-            )
-            resp <- Ok(
-              views.html.editLabels(lang = language)(
-                projectBaseUri.addSegment("labels"),
-                csrf,
-                linkToHubService,
-                labels,
-                projectBaseUri,
-                "Manage your project labels.".some,
-                user,
-                project
-              )()
-            )
-          } yield resp
-        case _ => NotFound("Ticket project not found!")
-      }
-    } yield resp
-
-  /** Load the project metadata with the given owner and name from the database and return it and its primary key id if
-    * the project exists and is readable by the given user account.
-    *
-    * @param currentUser
-    *   The user account that is requesting access to the project or None for a guest user.
-    * @param projectOwnerName
-    *   The name of the account that owns the project.
-    * @param projectName
-    *   The name of the project. A project name must start with a letter or number and must contain only alphanumeric
-    *   ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
-    * @return
-    *   An option to a tuple holding the [[Project]] and its primary key id.
-    */
-  private def loadProject(
-      currentUser: Option[Account]
-  )(projectOwnerName: ProjectOwnerName, projectName: ProjectName): F[Option[(Project, ProjectId)]] =
-    for {
-      owner <- projectRepo.findProjectOwner(projectOwnerName)
-      loadedRepo <- owner match {
-        case None => Sync[F].pure(None)
-        case Some(owner) =>
-          (
-            projectRepo.findProject(owner, projectName),
-            projectRepo.findProjectId(owner, projectName)
-          ).mapN {
-            case (Some(project), Some(projectId)) => (project, projectId).some
-            case _                                => None
-          }
-      }
-      // TODO: Replace with whatever we implement as proper permission model. ;-)
-      projectAndId = currentUser match {
-        case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
-        case Some(user) =>
-          loadedRepo.filter(tuple =>
-            tuple._1.isPrivate === false || tuple._1.owner.uid === ProjectOwnerId.fromUserId(user.uid)
-          )
-      }
-    } yield projectAndId
-
-  private val addLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "labels" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        val response =
-          for {
-            csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-            language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+    /** Logic for rendering a list of all labels for a project and optionally management functionality.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of labels shall be rendered.
+      * @param projectOwnerName
+      *   The username of the account who owns the project.
+      * @param projectName
+      *   The name of the project.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowLabels(
+        csrf: Option[CsrfToken]
+    )(user: Option[Account])(projectOwnerName: Username)(projectName: ProjectName): F[Response[F]] =
+        for {
+            _            <- Sync[F].delay(log.debug(s"doShowLabels: $csrf, $user, $projectOwnerName, $projectName"))
+            language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            projectAndId <- loadProject(user)(projectOwnerName, projectName)
             resp <- projectAndId match {
-              case Some(project, projectId) =>
-                for {
-                  _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                    new Error("Only maintainers may add labels!")
-                  )
-                  formData <- Sync[F].delay(urlForm.values)
-                  form     <- Sync[F].delay(LabelForm.validate(formData))
-                  labels   <- projectAndId.traverse(tuple => labelRepo.allLabels(tuple._2).compile.toList)
-                  projectBaseUri <- Sync[F].delay(
-                    linkConfig.createFullUri(
-                      Uri(path =
-                        Uri.Path(
-                          Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                        )
-                      )
-                    )
-                  )
-                  resp <- form match {
-                    case Validated.Invalid(errors) =>
-                      BadRequest(
-                        views.html.editLabels(lang = language)(
-                          projectBaseUri.addSegment("labels"),
-                          csrf,
-                          linkToHubService,
-                          labels.getOrElse(List.empty),
-                          projectBaseUri,
-                          "Manage your project labels.".some,
-                          user.some,
-                          project
-                        )(formData.withDefaultValue(Chain.empty), FormErrors.fromNec(errors))
-                      )
-                    case Validated.Valid(labelData) =>
-                      val label = Label(None, labelData.name, labelData.description, labelData.colour)
-                      for {
-                        checkDuplicate <- labelRepo.findLabel(projectId)(labelData.name)
-                        resp <- checkDuplicate match {
-                          case None =>
-                            labelRepo.createLabel(projectId)(label) *> SeeOther(
-                              Location(projectBaseUri.addSegment("labels"))
+                case Some((project, projectId)) =>
+                    for {
+                        labels <- labelRepo.allLabels(projectId).compile.toList
+                        projectBaseUri <- Sync[F].delay(
+                            linkConfig.createFullUri(
+                                Uri(path =
+                                    Uri.Path(
+                                        Vector(
+                                            Uri.Path.Segment(s"~$projectOwnerName"),
+                                            Uri.Path.Segment(projectName.toString)
+                                        )
+                                    )
+                                )
                             )
-                          case Some(_) =>
-                            BadRequest(
-                              views.html.editLabels(lang = language)(
+                        )
+                        resp <- Ok(
+                            views.html.editLabels(lang = language)(
                                 projectBaseUri.addSegment("labels"),
                                 csrf,
                                 linkToHubService,
-                                labels.getOrElse(List.empty),
+                                labels,
                                 projectBaseUri,
                                 "Manage your project labels.".some,
-                                user.some,
+                                user,
                                 project
-                              )(
-                                formData.withDefaultValue(Chain.empty),
-                                Map(
-                                  LabelForm.fieldName -> List(FormFieldError("A label with that name already exists!"))
-                                )
-                              )
-                            )
-                        }
-                      } yield resp
-                  }
-                } yield resp
-              case _ => NotFound()
+                            )()
+                        )
+                    } yield resp
+                case _ => NotFound("Ticket project not found!")
             }
-          } yield resp
-        response.recoverWith { error =>
-          log.error("Internal Server Error", error)
-          for {
-            csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-            language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-          } yield resp
-        }
-      }
-  }
-
-  private val deleteLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "labels" / LabelNamePathParameter(labelName) / "delete" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+        } yield resp
+
+    /** Load the project metadata with the given owner and name from the database and return it and its primary key id
+      * if the project exists and is readable by the given user account.
+      *
+      * @param currentUser
+      *   The user account that is requesting access to the project or None for a guest user.
+      * @param projectOwnerName
+      *   The name of the account that owns the project.
+      * @param projectName
+      *   The name of the project. A project name must start with a letter or number and must contain only alphanumeric
+      *   ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
+      * @return
+      *   An option to a tuple holding the [[Project]] and its primary key id.
+      */
+    private def loadProject(
+        currentUser: Option[Account]
+    )(projectOwnerName: ProjectOwnerName, projectName: ProjectName): F[Option[(Project, ProjectId)]] =
         for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          resp <- projectAndId match {
-            case Some(project, projectId) =>
-              for {
-                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                  new Error("Only maintainers may add labels!")
-                )
-                label <- labelRepo.findLabel(projectId)(labelName)
-                resp <- label match {
-                  case Some(label) =>
+            owner <- projectRepo.findProjectOwner(projectOwnerName)
+            loadedRepo <- owner match {
+                case None => Sync[F].pure(None)
+                case Some(owner) =>
+                    (
+                        projectRepo.findProject(owner, projectName),
+                        projectRepo.findProjectId(owner, projectName)
+                    ).mapN {
+                        case (Some(project), Some(projectId)) => (project, projectId).some
+                        case _                                => None
+                    }
+            }
+            // TODO: Replace with whatever we implement as proper permission model. ;-)
+            projectAndId = currentUser match {
+                case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
+                case Some(user) =>
+                    loadedRepo.filter(tuple =>
+                        tuple._1.isPrivate === false || tuple._1.owner.uid === ProjectOwnerId.fromUserId(user.uid)
+                    )
+            }
+        } yield projectAndId
+
+    private val addLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "labels" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                val response =
                     for {
-                      formData <- Sync[F].delay(urlForm.values)
-                      projectBaseUri <- Sync[F].delay(
-                        linkConfig.createFullUri(
-                          Uri(path =
-                            Uri.Path(
-                              Vector(
-                                Uri.Path.Segment(s"~$projectOwnerName"),
-                                Uri.Path.Segment(projectName.toString)
-                              )
-                            )
-                          )
+                        csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                        language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                        resp <- projectAndId match {
+                            case Some(project, projectId) =>
+                                for {
+                                    _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                        new Error("Only maintainers may add labels!")
+                                    )
+                                    formData <- Sync[F].delay(urlForm.values)
+                                    form     <- Sync[F].delay(LabelForm.validate(formData))
+                                    labels <- projectAndId.traverse(tuple =>
+                                        labelRepo.allLabels(tuple._2).compile.toList
+                                    )
+                                    projectBaseUri <- Sync[F].delay(
+                                        linkConfig.createFullUri(
+                                            Uri(path =
+                                                Uri.Path(
+                                                    Vector(
+                                                        Uri.Path.Segment(s"~$projectOwnerName"),
+                                                        Uri.Path.Segment(projectName.toString)
+                                                    )
+                                                )
+                                            )
+                                        )
+                                    )
+                                    resp <- form match {
+                                        case Validated.Invalid(errors) =>
+                                            BadRequest(
+                                                views.html.editLabels(lang = language)(
+                                                    projectBaseUri.addSegment("labels"),
+                                                    csrf,
+                                                    linkToHubService,
+                                                    labels.getOrElse(List.empty),
+                                                    projectBaseUri,
+                                                    "Manage your project labels.".some,
+                                                    user.some,
+                                                    project
+                                                )(formData.withDefaultValue(Chain.empty), FormErrors.fromNec(errors))
+                                            )
+                                        case Validated.Valid(labelData) =>
+                                            val label =
+                                                Label(None, labelData.name, labelData.description, labelData.colour)
+                                            for {
+                                                checkDuplicate <- labelRepo.findLabel(projectId)(labelData.name)
+                                                resp <- checkDuplicate match {
+                                                    case None =>
+                                                        labelRepo.createLabel(projectId)(label) *> SeeOther(
+                                                            Location(projectBaseUri.addSegment("labels"))
+                                                        )
+                                                    case Some(_) =>
+                                                        BadRequest(
+                                                            views.html.editLabels(lang = language)(
+                                                                projectBaseUri.addSegment("labels"),
+                                                                csrf,
+                                                                linkToHubService,
+                                                                labels.getOrElse(List.empty),
+                                                                projectBaseUri,
+                                                                "Manage your project labels.".some,
+                                                                user.some,
+                                                                project
+                                                            )(
+                                                                formData.withDefaultValue(Chain.empty),
+                                                                Map(
+                                                                    LabelForm.fieldName -> List(
+                                                                        FormFieldError(
+                                                                            "A label with that name already exists!"
+                                                                        )
+                                                                    )
+                                                                )
+                                                            )
+                                                        )
+                                                }
+                                            } yield resp
+                                    }
+                                } yield resp
+                            case _ => NotFound()
+                        }
+                    } yield resp
+                response.recoverWith { error =>
+                    log.error("Internal Server Error", error)
+                    for {
+                        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        resp <- InternalServerError(
+                            views.html.errors.internalServerError(lang = language)(csrf, user.some)
                         )
-                      )
-                      userIsSure <- Sync[F].delay(
-                        formData.get("i-am-sure").map(_.headOption.exists(_ === "yes")).getOrElse(false)
-                      )
-                      labelIdMatches <- Sync[F].delay(
-                        formData
-                          .get(LabelForm.fieldId)
-                          .map(
-                            _.headOption
-                              .flatMap(LabelId.fromString)
-                              .exists(id => label.id.exists(_ === id))
-                          )
-                          .getOrElse(false)
-                      )
-                      labelNameMatches <- Sync[F].delay(
-                        formData
-                          .get(LabelForm.fieldName)
-                          .map(_.headOption.flatMap(LabelName.from).exists(_ === labelName))
-                          .getOrElse(false)
-                      )
-                      resp <- (labelIdMatches && labelNameMatches && userIsSure) match {
-                        case false => BadRequest("Invalid form data!")
-                        case true =>
-                          labelRepo.deleteLabel(label) *> SeeOther(
-                            Location(projectBaseUri.addSegment("labels"))
-                          )
-                      }
                     } yield resp
-                  case _ => NotFound("Label not found!")
                 }
-              } yield resp
-            case _ => NotFound("Repository not found!")
-          }
-        } yield resp
-      }
-  }
-
-  private val editLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "labels" / LabelNamePathParameter(labelName) as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        val response =
-          for {
-            csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-            language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-            label <- projectAndId match {
-              case Some((_, projectId)) => labelRepo.findLabel(projectId)(labelName)
-              case _                    => Sync[F].delay(None)
             }
-            resp <- (projectAndId, label) match {
-              case (Some(project, projectId), Some(label)) =>
+    }
+
+    private val deleteLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "labels" / LabelNamePathParameter(labelName) / "delete" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
                 for {
-                  _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                    new Error("Only maintainers may add labels!")
-                  )
-                  projectBaseUri <- Sync[F].delay(
-                    linkConfig.createFullUri(
-                      Uri(path =
-                        Uri.Path(
-                          Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                        )
-                      )
-                    )
-                  )
-                  actionUri <- Sync[F].delay(projectBaseUri.addSegment("labels").addSegment(label.name.toString))
-                  formData  <- Sync[F].delay(urlForm.values)
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    resp <- projectAndId match {
+                        case Some(project, projectId) =>
+                            for {
+                                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                    new Error("Only maintainers may add labels!")
+                                )
+                                label <- labelRepo.findLabel(projectId)(labelName)
+                                resp <- label match {
+                                    case Some(label) =>
+                                        for {
+                                            formData <- Sync[F].delay(urlForm.values)
+                                            projectBaseUri <- Sync[F].delay(
+                                                linkConfig.createFullUri(
+                                                    Uri(path =
+                                                        Uri.Path(
+                                                            Vector(
+                                                                Uri.Path.Segment(s"~$projectOwnerName"),
+                                                                Uri.Path.Segment(projectName.toString)
+                                                            )
+                                                        )
+                                                    )
+                                                )
+                                            )
+                                            userIsSure <- Sync[F].delay(
+                                                formData
+                                                    .get("i-am-sure")
+                                                    .map(_.headOption.exists(_ === "yes"))
+                                                    .getOrElse(false)
+                                            )
+                                            labelIdMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(LabelForm.fieldId)
+                                                    .map(
+                                                        _.headOption
+                                                            .flatMap(LabelId.fromString)
+                                                            .exists(id => label.id.exists(_ === id))
+                                                    )
+                                                    .getOrElse(false)
+                                            )
+                                            labelNameMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(LabelForm.fieldName)
+                                                    .map(_.headOption.flatMap(LabelName.from).exists(_ === labelName))
+                                                    .getOrElse(false)
+                                            )
+                                            resp <- (labelIdMatches && labelNameMatches && userIsSure) match {
+                                                case false => BadRequest("Invalid form data!")
+                                                case true =>
+                                                    labelRepo.deleteLabel(label) *> SeeOther(
+                                                        Location(projectBaseUri.addSegment("labels"))
+                                                    )
+                                            }
+                                        } yield resp
+                                    case _ => NotFound("Label not found!")
+                                }
+                            } yield resp
+                        case _ => NotFound("Repository not found!")
+                    }
+                } yield resp
+            }
+    }
+
+    private val editLabel: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "labels" / LabelNamePathParameter(labelName) as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                val response =
+                    for {
+                        csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                        language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                        label <- projectAndId match {
+                            case Some((_, projectId)) => labelRepo.findLabel(projectId)(labelName)
+                            case _                    => Sync[F].delay(None)
+                        }
+                        resp <- (projectAndId, label) match {
+                            case (Some(project, projectId), Some(label)) =>
+                                for {
+                                    _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                        new Error("Only maintainers may add labels!")
+                                    )
+                                    projectBaseUri <- Sync[F].delay(
+                                        linkConfig.createFullUri(
+                                            Uri(path =
+                                                Uri.Path(
+                                                    Vector(
+                                                        Uri.Path.Segment(s"~$projectOwnerName"),
+                                                        Uri.Path.Segment(projectName.toString)
+                                                    )
+                                                )
+                                            )
+                                        )
+                                    )
+                                    actionUri <- Sync[F].delay(
+                                        projectBaseUri.addSegment("labels").addSegment(label.name.toString)
+                                    )
+                                    formData <- Sync[F].delay(urlForm.values)
 //                labelIdMatches <- Sync[F].delay(
 //                  formData
 //                    .get(LabelForm.fieldId)
@@ -345,151 +368,162 @@
 //                    case true => label.id.validNec
 //                  }
 //                )
-                  form <- Sync[F].delay(LabelForm.validate(formData))
-                  resp <- form match {
-                    case Validated.Invalid(errors) =>
-                      BadRequest(
-                        views.html.editLabel(lang = language)(
-                          actionUri,
-                          csrf,
-                          linkToHubService,
-                          label,
-                          projectBaseUri,
-                          s"Edit label ${label.name}".some,
-                          user,
-                          project
-                        )(
-                          formData.toMap.withDefaultValue(Chain.empty),
-                          FormErrors.fromNec(errors)
-                        )
-                      )
-                    case Validated.Valid(labelData) =>
-                      val updatedLabel =
-                        label.copy(
-                          name = labelData.name,
-                          description = labelData.description,
-                          colour = labelData.colour
+                                    form <- Sync[F].delay(LabelForm.validate(formData))
+                                    resp <- form match {
+                                        case Validated.Invalid(errors) =>
+                                            BadRequest(
+                                                views.html.editLabel(lang = language)(
+                                                    actionUri,
+                                                    csrf,
+                                                    linkToHubService,
+                                                    label,
+                                                    projectBaseUri,
+                                                    s"Edit label ${label.name}".some,
+                                                    user,
+                                                    project
+                                                )(
+                                                    formData.toMap.withDefaultValue(Chain.empty),
+                                                    FormErrors.fromNec(errors)
+                                                )
+                                            )
+                                        case Validated.Valid(labelData) =>
+                                            val updatedLabel =
+                                                label.copy(
+                                                    name = labelData.name,
+                                                    description = labelData.description,
+                                                    colour = labelData.colour
+                                                )
+                                            for {
+                                                checkDuplicate <- labelRepo.findLabel(projectId)(updatedLabel.name)
+                                                resp <- checkDuplicate.filterNot(_.id === updatedLabel.id) match {
+                                                    case None =>
+                                                        labelRepo.updateLabel(updatedLabel) *> SeeOther(
+                                                            Location(projectBaseUri.addSegment("labels"))
+                                                        )
+                                                    case Some(_) =>
+                                                        BadRequest(
+                                                            views.html.editLabel(lang = language)(
+                                                                actionUri,
+                                                                csrf,
+                                                                linkToHubService,
+                                                                label,
+                                                                projectBaseUri,
+                                                                s"Edit label ${label.name}".some,
+                                                                user,
+                                                                project
+                                                            )(
+                                                                formData.toMap.withDefaultValue(Chain.empty),
+                                                                Map(
+                                                                    LabelForm.fieldName -> List(
+                                                                        FormFieldError(
+                                                                            "A label with that name already exists!"
+                                                                        )
+                                                                    )
+                                                                )
+                                                            )
+                                                        )
+                                                }
+                                            } yield resp
+                                    }
+                                } yield resp
+                            case _ => NotFound()
+                        }
+                    } yield resp
+                response.recoverWith { error =>
+                    log.error("Internal Server Error", error)
+                    for {
+                        csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                        language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        resp <- InternalServerError(
+                            views.html.errors.internalServerError(lang = language)(csrf, user.some)
                         )
-                      for {
-                        checkDuplicate <- labelRepo.findLabel(projectId)(updatedLabel.name)
-                        resp <- checkDuplicate.filterNot(_.id === updatedLabel.id) match {
-                          case None =>
-                            labelRepo.updateLabel(updatedLabel) *> SeeOther(
-                              Location(projectBaseUri.addSegment("labels"))
-                            )
-                          case Some(_) =>
-                            BadRequest(
-                              views.html.editLabel(lang = language)(
-                                actionUri,
-                                csrf,
-                                linkToHubService,
-                                label,
-                                projectBaseUri,
-                                s"Edit label ${label.name}".some,
-                                user,
-                                project
-                              )(
-                                formData.toMap.withDefaultValue(Chain.empty),
-                                Map(
-                                  LabelForm.fieldName -> List(FormFieldError("A label with that name already exists!"))
+                    } yield resp
+                }
+            }
+    }
+
+    private val showEditLabelForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "labels" / LabelNamePathParameter(labelName) / "edit" as user =>
+            for {
+                csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                label <- projectAndId match {
+                    case Some((_, projectId)) => labelRepo.findLabel(projectId)(labelName)
+                    case _                    => Sync[F].delay(None)
+                }
+                resp <- (projectAndId, label) match {
+                    case (Some(project, _), Some(label)) =>
+                        for {
+                            projectBaseUri <- Sync[F].delay(
+                                linkConfig.createFullUri(
+                                    Uri(path =
+                                        Uri.Path(
+                                            Vector(
+                                                Uri.Path.Segment(s"~$projectOwnerName"),
+                                                Uri.Path.Segment(projectName.toString)
+                                            )
+                                        )
+                                    )
                                 )
-                              )
                             )
-                        }
-                      } yield resp
-                  }
+                            actionUri <- Sync[F].delay(
+                                projectBaseUri.addSegment("labels").addSegment(label.name.toString)
+                            )
+                            formData <- Sync[F].delay(LabelForm.fromLabel(label))
+                            resp <- Ok(
+                                views.html
+                                    .editLabel()(
+                                        actionUri,
+                                        csrf,
+                                        linkToHubService,
+                                        label,
+                                        projectBaseUri,
+                                        s"Edit label ${label.name}".some,
+                                        user,
+                                        project
+                                    )(
+                                        formData.toMap
+                                    )
+                            )
+                        } yield resp
+                    case _ => NotFound()
+                }
+            } yield resp
+    }
+
+    private val showEditLabelsPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "labels" as user =>
+            val response =
+                for {
+                    csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                    resp <- doShowLabels(csrf)(user.some)(projectOwnerName)(projectName)
+                } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
                 } yield resp
-              case _ => NotFound()
             }
-          } yield resp
-        response.recoverWith { error =>
-          log.error("Internal Server Error", error)
-          for {
-            csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-            language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-          } yield resp
-        }
-      }
-  }
-
-  private val showEditLabelForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "labels" / LabelNamePathParameter(labelName) / "edit" as user =>
-      for {
-        csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-        projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-        label <- projectAndId match {
-          case Some((_, projectId)) => labelRepo.findLabel(projectId)(labelName)
-          case _                    => Sync[F].delay(None)
-        }
-        resp <- (projectAndId, label) match {
-          case (Some(project, _), Some(label)) =>
+    }
+
+    private val showLabelsForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "labels" =>
             for {
-              projectBaseUri <- Sync[F].delay(
-                linkConfig.createFullUri(
-                  Uri(path =
-                    Uri.Path(
-                      Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                    )
-                  )
-                )
-              )
-              actionUri <- Sync[F].delay(projectBaseUri.addSegment("labels").addSegment(label.name.toString))
-              formData  <- Sync[F].delay(LabelForm.fromLabel(label))
-              resp <- Ok(
-                views.html
-                  .editLabel()(
-                    actionUri,
-                    csrf,
-                    linkToHubService,
-                    label,
-                    projectBaseUri,
-                    s"Edit label ${label.name}".some,
-                    user,
-                    project
-                  )(
-                    formData.toMap
-                  )
-              )
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowLabels(csrf)(None)(projectOwnerName)(projectName)
             } yield resp
-          case _ => NotFound()
-        }
-      } yield resp
-  }
-
-  private val showEditLabelsPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "labels" as user =>
-      val response =
-        for {
-          csrf <- Sync[F].delay(ar.req.getCsrfToken)
-          resp <- doShowLabels(csrf)(user.some)(projectOwnerName)(projectName)
-        } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-        } yield resp
-      }
-  }
-
-  private val showLabelsForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "labels" =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowLabels(csrf)(None)(projectOwnerName)(projectName)
-      } yield resp
-  }
+    }
 
-  val protectedRoutes = addLabel <+> deleteLabel <+> editLabel <+> showEditLabelForm <+> showEditLabelsPage
+    val protectedRoutes = addLabel <+> deleteLabel <+> editLabel <+> showEditLabelForm <+> showEditLabelsPage
 
-  val routes = showLabelsForGuests
+    val routes = showLabelsForGuests
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneForm.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneForm.scala	2025-01-13 17:13:25.056470978 +0000
@@ -45,86 +45,94 @@
 )
 
 object MilestoneForm extends FormValidator[MilestoneForm] {
-  val fieldDescription: FormField = FormField("description")
-  val fieldDueDate: FormField     = FormField("due_date")
-  val fieldId: FormField          = FormField("id")
-  val fieldTitle: FormField       = FormField("title")
-
-  /** Create a form for editing a milestone from the given milestone data.
-    *
-    * @param milestone
-    *   The milestone which provides the data for the edit form.
-    * @return
-    *   A milestone form filled with the data from the given milestone.
-    */
-  def fromMilestone(milestone: Milestone): MilestoneForm =
-    MilestoneForm(
-      id = milestone.id,
-      title = milestone.title,
-      description = milestone.description,
-      dueDate = milestone.dueDate
-    )
-
-  override def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, MilestoneForm] = {
-    val id = data
-      .get(fieldId)
-      .fold(Option.empty[MilestoneId].validNec)(
-        _.headOption
-          .flatMap(MilestoneId.fromString)
-          .fold(FormFieldError("Invalid milestone id!").invalidNec)(id => Option(id).validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldId -> es.toList)))
-    val title = data
-      .get(fieldTitle)
-      .fold(FormFieldError("No milestone title given!").invalidNec)(
-        _.headOption
-          .map(_.trim) // We strip leading and trailing whitespace!
-          .flatMap(MilestoneTitle.from)
-          .fold(FormFieldError("Invalid milestone title!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldTitle -> es.toList)))
-    val description = data
-      .get(fieldDescription)
-      .fold(Option.empty[MilestoneDescription].validNec)(
-        _.headOption
-          .map(_.trim)
-          .filter(_.nonEmpty)
-          .fold(none[MilestoneDescription].validNec)(s =>
-            MilestoneDescription
-              .from(s)
-              .fold(FormFieldError("Invalid milestone description!").invalidNec)(descr => Option(descr).validNec)
-          )
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
-    val dueDate = data
-      .get(fieldDueDate)
-      .fold(Option.empty[LocalDate].validNec)(
-        _.headOption
-          .map(_.trim)
-          .filter(_.nonEmpty)
-          .fold(none[LocalDate].validNec)(s => Validated.catchNonFatal(LocalDate.parse(s)).map(date => date.some))
-      )
-      .leftMap(_ => NonEmptyChain.of(Map(fieldDueDate -> List(FormFieldError("Invalid milestone due date!")))))
-    (id, title, description, dueDate).mapN { case (id, title, description, dueDate) =>
-      MilestoneForm(id, title, description, dueDate)
-    }
-  }
-
-  extension (form: MilestoneForm) {
+    val fieldDescription: FormField = FormField("description")
+    val fieldDueDate: FormField     = FormField("due_date")
+    val fieldId: FormField          = FormField("id")
+    val fieldTitle: FormField       = FormField("title")
 
-    /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
-      * twirl templating library.
+    /** Create a form for editing a milestone from the given milestone data.
       *
+      * @param milestone
+      *   The milestone which provides the data for the edit form.
       * @return
-      *   A stringified map containing the data of the form.
+      *   A milestone form filled with the data from the given milestone.
       */
-    def toMap: Map[String, Chain[String]] =
-      Map(
-        MilestoneForm.fieldId.toString          -> form.id.map(_.toString).fold(Chain.empty)(id => Chain(id)),
-        MilestoneForm.fieldTitle.toString       -> Chain(form.title.toString),
-        MilestoneForm.fieldDescription.toString -> form.description.map(_.toString).fold(Chain.empty)(d => Chain(d)),
-        MilestoneForm.fieldDueDate.toString     -> form.dueDate.map(_.toString).fold(Chain.empty)(date => Chain(date))
-      )
-  }
+    def fromMilestone(milestone: Milestone): MilestoneForm =
+        MilestoneForm(
+            id = milestone.id,
+            title = milestone.title,
+            description = milestone.description,
+            dueDate = milestone.dueDate
+        )
+
+    override def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, MilestoneForm] = {
+        val id = data
+            .get(fieldId)
+            .fold(Option.empty[MilestoneId].validNec)(
+                _.headOption
+                    .flatMap(MilestoneId.fromString)
+                    .fold(FormFieldError("Invalid milestone id!").invalidNec)(id => Option(id).validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldId -> es.toList)))
+        val title = data
+            .get(fieldTitle)
+            .fold(FormFieldError("No milestone title given!").invalidNec)(
+                _.headOption
+                    .map(_.trim) // We strip leading and trailing whitespace!
+                    .flatMap(MilestoneTitle.from)
+                    .fold(FormFieldError("Invalid milestone title!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldTitle -> es.toList)))
+        val description = data
+            .get(fieldDescription)
+            .fold(Option.empty[MilestoneDescription].validNec)(
+                _.headOption
+                    .map(_.trim)
+                    .filter(_.nonEmpty)
+                    .fold(none[MilestoneDescription].validNec)(s =>
+                        MilestoneDescription
+                            .from(s)
+                            .fold(FormFieldError("Invalid milestone description!").invalidNec)(descr =>
+                                Option(descr).validNec
+                            )
+                    )
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldDescription -> es.toList)))
+        val dueDate = data
+            .get(fieldDueDate)
+            .fold(Option.empty[LocalDate].validNec)(
+                _.headOption
+                    .map(_.trim)
+                    .filter(_.nonEmpty)
+                    .fold(none[LocalDate].validNec)(s =>
+                        Validated.catchNonFatal(LocalDate.parse(s)).map(date => date.some)
+                    )
+            )
+            .leftMap(_ => NonEmptyChain.of(Map(fieldDueDate -> List(FormFieldError("Invalid milestone due date!")))))
+        (id, title, description, dueDate).mapN { case (id, title, description, dueDate) =>
+            MilestoneForm(id, title, description, dueDate)
+        }
+    }
+
+    extension (form: MilestoneForm) {
+
+        /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
+          * twirl templating library.
+          *
+          * @return
+          *   A stringified map containing the data of the form.
+          */
+        def toMap: Map[String, Chain[String]] =
+            Map(
+                MilestoneForm.fieldId.toString    -> form.id.map(_.toString).fold(Chain.empty)(id => Chain(id)),
+                MilestoneForm.fieldTitle.toString -> Chain(form.title.toString),
+                MilestoneForm.fieldDescription.toString -> form.description
+                    .map(_.toString)
+                    .fold(Chain.empty)(d => Chain(d)),
+                MilestoneForm.fieldDueDate.toString -> form.dueDate
+                    .map(_.toString)
+                    .fold(Chain.empty)(date => Chain(date))
+            )
+    }
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala	2025-01-13 17:13:25.056470978 +0000
@@ -52,645 +52,701 @@
     milestoneRepo: MilestoneRepository[F],
     projectRepo: ProjectRepository[F]
 ) extends Http4sDsl[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given CsrfProtectionConfiguration = configuration.csrfProtection
+    given CsrfProtectionConfiguration = configuration.csrfProtection
 
-  private val linkToHubService = configuration.hub.baseUri
-  private val linkConfig       = configuration.externalUrl
+    private val linkToHubService = configuration.hub.baseUri
+    private val linkConfig       = configuration.externalUrl
 
-  /** Logic for rendering a detail page for a single milestone.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of tickets shall be rendered.
-    * @param projectOwnerName
-    *   The username of the account who owns the project.
-    * @param projectName
-    *   The name of the project.
-    * @param milestoneTitle
-    *   The title of the milestone that shall be rendered.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowMilestone(csrf: Option[CsrfToken])(user: Option[Account])(projectOwnerName: Username)(
-      projectName: ProjectName
-  )(milestoneTitle: MilestoneTitle): F[Response[F]] =
-    for {
-      language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      projectAndId <- loadProject(user)(projectOwnerName, projectName)
-      milestone <- projectAndId match {
-        case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
-        case _                    => Sync[F].delay(None)
-      }
-      resp <- (projectAndId, milestone) match {
-        case (Some(project, _), Some(milestone)) =>
-          for {
-            tickets <- milestone.id.traverse(milestoneId => milestoneRepo.allTickets(None)(milestoneId).compile.toList)
-            projectBaseUri <- Sync[F].delay(
-              linkConfig.createFullUri(
-                Uri(path =
-                  Uri.Path(
-                    Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                  )
-                )
-              )
-            )
-            actionUri <- Sync[F].delay(projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString))
-            renderedDescription <- Sync[F].delay(milestone.description.map(_.toString).map(MarkdownRenderer.render))
-            resp <- Ok(
-              views.html.showMilestone(lang = language)(
-                actionUri,
-                csrf,
-                linkToHubService,
-                milestone,
-                renderedDescription,
-                projectBaseUri,
-                tickets.getOrElse(Nil),
-                s"Milestone ${milestone.title}".some,
-                user,
-                project
-              )
-            )
-          } yield resp
-        case _ => NotFound()
-      }
-    } yield resp
-
-  /** Logic for rendering a list of all milestones for a project and optionally management functionality.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of milestones shall be rendered.
-    * @param projectOwnerName
-    *   The username of the account who owns the project.
-    * @param projectName
-    *   The name of the project.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowMilestones(
-      csrf: Option[CsrfToken]
-  )(user: Option[Account])(projectOwnerName: Username)(projectName: ProjectName): F[Response[F]] =
-    for {
-      language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      projectAndId <- loadProject(user)(projectOwnerName, projectName)
-      resp <- projectAndId match {
-        case Some((project, projectId)) =>
-          for {
-            milestones <- milestoneRepo.allMilestones(projectId).compile.toList
-            projectBaseUri <- Sync[F].delay(
-              linkConfig.createFullUri(
-                Uri(path =
-                  Uri.Path(
-                    Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                  )
-                )
-              )
-            )
-            resp <- Ok(
-              views.html.editMilestones(lang = language)(
-                projectBaseUri.addSegment("milestones"),
-                csrf,
-                linkToHubService,
-                milestones,
-                projectBaseUri,
-                "Manage your project milestones.".some,
-                user,
-                project
-              )()
-            )
-          } yield resp
-        case _ => NotFound()
-      }
-    } yield resp
-
-  /** Load the project metadata with the given owner and name from the database and return it and its primary key id if
-    * the project exists and is readable by the given user account.
-    *
-    * @param currentUser
-    *   The user account that is requesting access to the project or None for a guest user.
-    * @param projectOwnerName
-    *   The name of the account that owns the project.
-    * @param projectName
-    *   The name of the project. A project name must start with a letter or number and must contain only alphanumeric
-    *   ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
-    * @return
-    *   An option to a tuple holding the [[Project]] and its primary key id.
-    */
-  private def loadProject(
-      currentUser: Option[Account]
-  )(projectOwnerName: Username, projectName: ProjectName): F[Option[(Project, ProjectId)]] =
-    for {
-      owner <- projectRepo.findProjectOwner(projectOwnerName)
-      loadedRepo <- owner match {
-        case None => Sync[F].pure(None)
-        case Some(owner) =>
-          (
-            projectRepo.findProject(owner, projectName),
-            projectRepo.findProjectId(owner, projectName)
-          ).mapN {
-            case (Some(project), Some(projectId)) => (project, projectId).some
-            case _                                => None
-          }
-      }
-      // TODO: Replace with whatever we implement as proper permission model. ;-)
-      repoAndId = currentUser match {
-        case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
-        case Some(user) =>
-          loadedRepo.filter(tuple =>
-            tuple._1.isPrivate === false || tuple._1.owner.uid === ProjectOwnerId.fromUserId(user.uid)
-          )
-      }
-    } yield repoAndId
-
-  private val addMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+    /** Logic for rendering a detail page for a single milestone.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of tickets shall be rendered.
+      * @param projectOwnerName
+      *   The username of the account who owns the project.
+      * @param projectName
+      *   The name of the project.
+      * @param milestoneTitle
+      *   The title of the milestone that shall be rendered.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowMilestone(csrf: Option[CsrfToken])(user: Option[Account])(projectOwnerName: Username)(
+        projectName: ProjectName
+    )(milestoneTitle: MilestoneTitle): F[Response[F]] =
         for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          resp <- projectAndId match {
-            case Some(project, projectId) =>
-              for {
-                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                  new Error("Only maintainers may add milestones!")
-                )
-                formData   <- Sync[F].delay(urlForm.values)
-                form       <- Sync[F].delay(MilestoneForm.validate(formData))
-                milestones <- projectAndId.traverse(tuple => milestoneRepo.allMilestones(tuple._2).compile.toList)
-                projectBaseUri <- Sync[F].delay(
-                  linkConfig.createFullUri(
-                    Uri(path =
-                      Uri.Path(
-                        Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                      )
-                    )
-                  )
-                )
-                resp <- form match {
-                  case Validated.Invalid(errors) =>
-                    BadRequest(
-                      views.html.editMilestones(lang = language)(
-                        projectBaseUri.addSegment("milestones"),
-                        csrf,
-                        linkToHubService,
-                        milestones.getOrElse(List.empty),
-                        projectBaseUri,
-                        "Manage your project milestones.".some,
-                        user.some,
-                        project
-                      )(formData, FormErrors.fromNec(errors))
-                    )
-                  case Validated.Valid(milestoneData) =>
-                    val milestone =
-                      Milestone(
-                        None,
-                        milestoneData.title,
-                        milestoneData.description,
-                        milestoneData.dueDate,
-                        closed = false
-                      )
+            language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            projectAndId <- loadProject(user)(projectOwnerName, projectName)
+            milestone <- projectAndId match {
+                case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
+                case _                    => Sync[F].delay(None)
+            }
+            resp <- (projectAndId, milestone) match {
+                case (Some(project, _), Some(milestone)) =>
                     for {
-                      checkDuplicate <- milestoneRepo.findMilestone(projectId)(milestoneData.title)
-                      resp <- checkDuplicate match {
-                        case None =>
-                          milestoneRepo.createMilestone(projectId)(milestone) *> SeeOther(
-                            Location(projectBaseUri.addSegment("milestones"))
-                          )
-                        case Some(_) =>
-                          BadRequest(
-                            views.html.editMilestones(lang = language)(
-                              projectBaseUri.addSegment("milestones"),
-                              csrf,
-                              linkToHubService,
-                              milestones.getOrElse(List.empty),
-                              projectBaseUri,
-                              "Manage your project milestones.".some,
-                              user.some,
-                              project
-                            )(
-                              formData,
-                              Map(
-                                MilestoneForm.fieldTitle -> List(
-                                  FormFieldError("A milestone with that name already exists!")
+                        tickets <- milestone.id.traverse(milestoneId =>
+                            milestoneRepo.allTickets(None)(milestoneId).compile.toList
+                        )
+                        projectBaseUri <- Sync[F].delay(
+                            linkConfig.createFullUri(
+                                Uri(path =
+                                    Uri.Path(
+                                        Vector(
+                                            Uri.Path.Segment(s"~$projectOwnerName"),
+                                            Uri.Path.Segment(projectName.toString)
+                                        )
+                                    )
                                 )
-                              )
                             )
-                          )
-                      }
-                    } yield resp
-                }
-              } yield resp
-            case _ => NotFound()
-          }
-        } yield resp
-      }
-  }
-
-  private val closeMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "close" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          resp <- projectAndId match {
-            case Some(project, projectId) =>
-              for {
-                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                  new Error("Only maintainers may close milestones!")
-                )
-                milestone <- milestoneRepo.findMilestone(projectId)(milestoneTitle)
-                resp <- milestone match {
-                  case Some(milestone) =>
-                    for {
-                      formData <- Sync[F].delay {
-                        urlForm.values.map { t =>
-                          val (key, values) = t
-                          (
-                            key,
-                            values.headOption.getOrElse("")
-                          ) // Pick the first value (a field might get submitted multiple times)!
-                        }
-                      }
-                      projectBaseUri <- Sync[F].delay(
-                        linkConfig.createFullUri(
-                          Uri(path =
-                            Uri.Path(
-                              Vector(
-                                Uri.Path.Segment(s"~$projectOwnerName"),
-                                Uri.Path.Segment(projectName.toString)
-                              )
+                        )
+                        actionUri <- Sync[F].delay(
+                            projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)
+                        )
+                        renderedDescription <- Sync[F].delay(
+                            milestone.description.map(_.toString).map(MarkdownRenderer.render)
+                        )
+                        resp <- Ok(
+                            views.html.showMilestone(lang = language)(
+                                actionUri,
+                                csrf,
+                                linkToHubService,
+                                milestone,
+                                renderedDescription,
+                                projectBaseUri,
+                                tickets.getOrElse(Nil),
+                                s"Milestone ${milestone.title}".some,
+                                user,
+                                project
                             )
-                          )
                         )
-                      )
-                      milestoneIdMatches <- Sync[F].delay(
-                        formData
-                          .get(MilestoneForm.fieldId)
-                          .flatMap(MilestoneId.fromString)
-                          .exists(id => milestone.id.exists(_ === id))
-                      )
-                      milestoneTitleMatches <- Sync[F].delay(
-                        formData.get(MilestoneForm.fieldTitle).flatMap(MilestoneTitle.from).exists(_ === milestoneTitle)
-                      )
-                      resp <- (milestoneIdMatches && milestoneTitleMatches) match {
-                        case false => BadRequest("Invalid form data!")
-                        case true =>
-                          milestone.id.traverse(milestoneRepo.closeMilestone) *> SeeOther(
-                            Location(projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString))
-                          )
-                      }
                     } yield resp
-                  case _ => NotFound("Milestone not found!")
-                }
-              } yield resp
-            case _ => NotFound("Repository not found!")
-          }
+                case _ => NotFound()
+            }
         } yield resp
-      }
-  }
 
-  private val deleteMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "delete" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+    /** Logic for rendering a list of all milestones for a project and optionally management functionality.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of milestones shall be rendered.
+      * @param projectOwnerName
+      *   The username of the account who owns the project.
+      * @param projectName
+      *   The name of the project.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowMilestones(
+        csrf: Option[CsrfToken]
+    )(user: Option[Account])(projectOwnerName: Username)(projectName: ProjectName): F[Response[F]] =
         for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          resp <- projectAndId match {
-            case Some(project, projectId) =>
-              for {
-                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                  new Error("Only maintainers may add milestones!")
-                )
-                milestone <- milestoneRepo.findMilestone(projectId)(milestoneTitle)
-                resp <- milestone match {
-                  case Some(milestone) =>
+            language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            projectAndId <- loadProject(user)(projectOwnerName, projectName)
+            resp <- projectAndId match {
+                case Some((project, projectId)) =>
                     for {
-                      formData <- Sync[F].delay {
-                        urlForm.values.map { t =>
-                          val (key, values) = t
-                          (
-                            key,
-                            values.headOption.getOrElse("")
-                          ) // Pick the first value (a field might get submitted multiple times)!
-                        }
-                      }
-                      projectBaseUri <- Sync[F].delay(
-                        linkConfig.createFullUri(
-                          Uri(path =
-                            Uri.Path(
-                              Vector(
-                                Uri.Path.Segment(s"~$projectOwnerName"),
-                                Uri.Path.Segment(projectName.toString)
-                              )
+                        milestones <- milestoneRepo.allMilestones(projectId).compile.toList
+                        projectBaseUri <- Sync[F].delay(
+                            linkConfig.createFullUri(
+                                Uri(path =
+                                    Uri.Path(
+                                        Vector(
+                                            Uri.Path.Segment(s"~$projectOwnerName"),
+                                            Uri.Path.Segment(projectName.toString)
+                                        )
+                                    )
+                                )
                             )
-                          )
                         )
-                      )
-                      userIsSure <- Sync[F].delay(formData.get("i-am-sure").exists(_ === "yes"))
-                      milestoneIdMatches <- Sync[F].delay(
-                        formData
-                          .get(MilestoneForm.fieldId)
-                          .flatMap(MilestoneId.fromString)
-                          .exists(id => milestone.id.exists(_ === id))
-                      )
-                      milestoneTitleMatches <- Sync[F].delay(
-                        formData.get(MilestoneForm.fieldTitle).flatMap(MilestoneTitle.from).exists(_ === milestoneTitle)
-                      )
-                      resp <- (milestoneIdMatches && milestoneTitleMatches && userIsSure) match {
-                        case false => BadRequest("Invalid form data!")
-                        case true =>
-                          milestoneRepo.deleteMilestone(milestone) *> SeeOther(
-                            Location(projectBaseUri.addSegment("milestones"))
-                          )
-                      }
+                        resp <- Ok(
+                            views.html.editMilestones(lang = language)(
+                                projectBaseUri.addSegment("milestones"),
+                                csrf,
+                                linkToHubService,
+                                milestones,
+                                projectBaseUri,
+                                "Manage your project milestones.".some,
+                                user,
+                                project
+                            )()
+                        )
                     } yield resp
-                  case _ => NotFound("Milestone not found!")
-                }
-              } yield resp
-            case _ => NotFound("Repository not found!")
-          }
+                case _ => NotFound()
+            }
         } yield resp
-      }
-  }
 
-  private val editMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
+    /** Load the project metadata with the given owner and name from the database and return it and its primary key id
+      * if the project exists and is readable by the given user account.
+      *
+      * @param currentUser
+      *   The user account that is requesting access to the project or None for a guest user.
+      * @param projectOwnerName
+      *   The name of the account that owns the project.
+      * @param projectName
+      *   The name of the project. A project name must start with a letter or number and must contain only alphanumeric
+      *   ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
+      * @return
+      *   An option to a tuple holding the [[Project]] and its primary key id.
+      */
+    private def loadProject(
+        currentUser: Option[Account]
+    )(projectOwnerName: Username, projectName: ProjectName): F[Option[(Project, ProjectId)]] =
         for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          milestone <- projectAndId match {
-            case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
-            case _                    => Sync[F].delay(None)
-          }
-          resp <- (projectAndId, milestone) match {
-            case (Some(project, projectId), Some(milestone)) =>
-              for {
-                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                  new Error("Only maintainers may add milestones!")
-                )
-                projectBaseUri <- Sync[F].delay(
-                  linkConfig.createFullUri(
-                    Uri(path =
-                      Uri.Path(
-                        Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                      )
+            owner <- projectRepo.findProjectOwner(projectOwnerName)
+            loadedRepo <- owner match {
+                case None => Sync[F].pure(None)
+                case Some(owner) =>
+                    (
+                        projectRepo.findProject(owner, projectName),
+                        projectRepo.findProjectId(owner, projectName)
+                    ).mapN {
+                        case (Some(project), Some(projectId)) => (project, projectId).some
+                        case _                                => None
+                    }
+            }
+            // TODO: Replace with whatever we implement as proper permission model. ;-)
+            repoAndId = currentUser match {
+                case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
+                case Some(user) =>
+                    loadedRepo.filter(tuple =>
+                        tuple._1.isPrivate === false || tuple._1.owner.uid === ProjectOwnerId.fromUserId(user.uid)
                     )
-                  )
-                )
-                actionUri <- Sync[F].delay(
-                  projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)
-                )
-                formData <- Sync[F].delay(urlForm.values)
-                milestoneIdMatches <- Sync[F].delay(
-                  formData
-                    .get(MilestoneForm.fieldId)
-                    .flatMap(_.headOption.flatMap(MilestoneId.fromString))
-                    .find(id => milestone.id.exists(_ === id))
-                    .fold(
-                      NonEmptyChain
-                        .of(Map(MilestoneForm.fieldGlobal -> List(FormFieldError("Milestone ID does not match!"))))
-                        .invalidNec
-                    )(_ => milestone.id.validNec)
-                )
-                form <- Sync[F].delay(MilestoneForm.validate(formData))
-                resp <- form match {
-                  case Validated.Invalid(errors) =>
-                    BadRequest(
-                      views.html.editMilestone(lang = language)(
-                        actionUri,
-                        csrf,
-                        linkToHubService,
-                        milestone,
-                        projectBaseUri,
-                        s"Edit milestone ${milestone.title}".some,
-                        user,
-                        project
-                      )(
-                        formData,
-                        FormErrors.fromNec(errors)
-                      )
-                    )
-                  case Validated.Valid(milestoneData) =>
-                    val updatedMilestone =
-                      milestone.copy(
-                        title = milestoneData.title,
-                        description = milestoneData.description,
-                        dueDate = milestoneData.dueDate
-                      )
-                    for {
-                      checkDuplicate <- milestoneRepo.findMilestone(projectId)(updatedMilestone.title)
-                      resp <- checkDuplicate.filterNot(_.id === updatedMilestone.id) match {
-                        case None =>
-                          milestoneRepo.updateMilestone(updatedMilestone) *> SeeOther(
-                            Location(projectBaseUri.addSegment("milestones"))
-                          )
-                        case Some(_) =>
-                          BadRequest(
-                            views.html.editMilestone(lang = language)(
-                              actionUri,
-                              csrf,
-                              linkToHubService,
-                              milestone,
-                              projectBaseUri,
-                              s"Edit milestone ${milestone.title}".some,
-                              user,
-                              project
-                            )(
-                              formData,
-                              Map(
-                                MilestoneForm.fieldTitle -> List(
-                                  FormFieldError("A milestone with that name already exists!")
+            }
+        } yield repoAndId
+
+    private val addMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    resp <- projectAndId match {
+                        case Some(project, projectId) =>
+                            for {
+                                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                    new Error("Only maintainers may add milestones!")
                                 )
-                              )
-                            )
-                          )
-                      }
-                    } yield resp
+                                formData <- Sync[F].delay(urlForm.values)
+                                form     <- Sync[F].delay(MilestoneForm.validate(formData))
+                                milestones <- projectAndId.traverse(tuple =>
+                                    milestoneRepo.allMilestones(tuple._2).compile.toList
+                                )
+                                projectBaseUri <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~$projectOwnerName"),
+                                                    Uri.Path.Segment(projectName.toString)
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                resp <- form match {
+                                    case Validated.Invalid(errors) =>
+                                        BadRequest(
+                                            views.html.editMilestones(lang = language)(
+                                                projectBaseUri.addSegment("milestones"),
+                                                csrf,
+                                                linkToHubService,
+                                                milestones.getOrElse(List.empty),
+                                                projectBaseUri,
+                                                "Manage your project milestones.".some,
+                                                user.some,
+                                                project
+                                            )(formData, FormErrors.fromNec(errors))
+                                        )
+                                    case Validated.Valid(milestoneData) =>
+                                        val milestone =
+                                            Milestone(
+                                                None,
+                                                milestoneData.title,
+                                                milestoneData.description,
+                                                milestoneData.dueDate,
+                                                closed = false
+                                            )
+                                        for {
+                                            checkDuplicate <- milestoneRepo.findMilestone(projectId)(
+                                                milestoneData.title
+                                            )
+                                            resp <- checkDuplicate match {
+                                                case None =>
+                                                    milestoneRepo.createMilestone(projectId)(milestone) *> SeeOther(
+                                                        Location(projectBaseUri.addSegment("milestones"))
+                                                    )
+                                                case Some(_) =>
+                                                    BadRequest(
+                                                        views.html.editMilestones(lang = language)(
+                                                            projectBaseUri.addSegment("milestones"),
+                                                            csrf,
+                                                            linkToHubService,
+                                                            milestones.getOrElse(List.empty),
+                                                            projectBaseUri,
+                                                            "Manage your project milestones.".some,
+                                                            user.some,
+                                                            project
+                                                        )(
+                                                            formData,
+                                                            Map(
+                                                                MilestoneForm.fieldTitle -> List(
+                                                                    FormFieldError(
+                                                                        "A milestone with that name already exists!"
+                                                                    )
+                                                                )
+                                                            )
+                                                        )
+                                                    )
+                                            }
+                                        } yield resp
+                                }
+                            } yield resp
+                        case _ => NotFound()
+                    }
+                } yield resp
+            }
+    }
+
+    private val closeMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "close" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    resp <- projectAndId match {
+                        case Some(project, projectId) =>
+                            for {
+                                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                    new Error("Only maintainers may close milestones!")
+                                )
+                                milestone <- milestoneRepo.findMilestone(projectId)(milestoneTitle)
+                                resp <- milestone match {
+                                    case Some(milestone) =>
+                                        for {
+                                            formData <- Sync[F].delay {
+                                                urlForm.values.map { t =>
+                                                    val (key, values) = t
+                                                    (
+                                                        key,
+                                                        values.headOption.getOrElse("")
+                                                    ) // Pick the first value (a field might get submitted multiple times)!
+                                                }
+                                            }
+                                            projectBaseUri <- Sync[F].delay(
+                                                linkConfig.createFullUri(
+                                                    Uri(path =
+                                                        Uri.Path(
+                                                            Vector(
+                                                                Uri.Path.Segment(s"~$projectOwnerName"),
+                                                                Uri.Path.Segment(projectName.toString)
+                                                            )
+                                                        )
+                                                    )
+                                                )
+                                            )
+                                            milestoneIdMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(MilestoneForm.fieldId)
+                                                    .flatMap(MilestoneId.fromString)
+                                                    .exists(id => milestone.id.exists(_ === id))
+                                            )
+                                            milestoneTitleMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(MilestoneForm.fieldTitle)
+                                                    .flatMap(MilestoneTitle.from)
+                                                    .exists(_ === milestoneTitle)
+                                            )
+                                            resp <- (milestoneIdMatches && milestoneTitleMatches) match {
+                                                case false => BadRequest("Invalid form data!")
+                                                case true =>
+                                                    milestone.id.traverse(milestoneRepo.closeMilestone) *> SeeOther(
+                                                        Location(
+                                                            projectBaseUri
+                                                                .addSegment("milestones")
+                                                                .addSegment(milestone.title.toString)
+                                                        )
+                                                    )
+                                            }
+                                        } yield resp
+                                    case _ => NotFound("Milestone not found!")
+                                }
+                            } yield resp
+                        case _ => NotFound("Repository not found!")
+                    }
+                } yield resp
+            }
+    }
+
+    private val deleteMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "delete" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    resp <- projectAndId match {
+                        case Some(project, projectId) =>
+                            for {
+                                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                    new Error("Only maintainers may add milestones!")
+                                )
+                                milestone <- milestoneRepo.findMilestone(projectId)(milestoneTitle)
+                                resp <- milestone match {
+                                    case Some(milestone) =>
+                                        for {
+                                            formData <- Sync[F].delay {
+                                                urlForm.values.map { t =>
+                                                    val (key, values) = t
+                                                    (
+                                                        key,
+                                                        values.headOption.getOrElse("")
+                                                    ) // Pick the first value (a field might get submitted multiple times)!
+                                                }
+                                            }
+                                            projectBaseUri <- Sync[F].delay(
+                                                linkConfig.createFullUri(
+                                                    Uri(path =
+                                                        Uri.Path(
+                                                            Vector(
+                                                                Uri.Path.Segment(s"~$projectOwnerName"),
+                                                                Uri.Path.Segment(projectName.toString)
+                                                            )
+                                                        )
+                                                    )
+                                                )
+                                            )
+                                            userIsSure <- Sync[F].delay(formData.get("i-am-sure").exists(_ === "yes"))
+                                            milestoneIdMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(MilestoneForm.fieldId)
+                                                    .flatMap(MilestoneId.fromString)
+                                                    .exists(id => milestone.id.exists(_ === id))
+                                            )
+                                            milestoneTitleMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(MilestoneForm.fieldTitle)
+                                                    .flatMap(MilestoneTitle.from)
+                                                    .exists(_ === milestoneTitle)
+                                            )
+                                            resp <- (milestoneIdMatches && milestoneTitleMatches && userIsSure) match {
+                                                case false => BadRequest("Invalid form data!")
+                                                case true =>
+                                                    milestoneRepo.deleteMilestone(milestone) *> SeeOther(
+                                                        Location(projectBaseUri.addSegment("milestones"))
+                                                    )
+                                            }
+                                        } yield resp
+                                    case _ => NotFound("Milestone not found!")
+                                }
+                            } yield resp
+                        case _ => NotFound("Repository not found!")
+                    }
+                } yield resp
+            }
+    }
+
+    private val editMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    milestone <- projectAndId match {
+                        case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
+                        case _                    => Sync[F].delay(None)
+                    }
+                    resp <- (projectAndId, milestone) match {
+                        case (Some(project, projectId), Some(milestone)) =>
+                            for {
+                                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                    new Error("Only maintainers may add milestones!")
+                                )
+                                projectBaseUri <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~$projectOwnerName"),
+                                                    Uri.Path.Segment(projectName.toString)
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                actionUri <- Sync[F].delay(
+                                    projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)
+                                )
+                                formData <- Sync[F].delay(urlForm.values)
+                                milestoneIdMatches <- Sync[F].delay(
+                                    formData
+                                        .get(MilestoneForm.fieldId)
+                                        .flatMap(_.headOption.flatMap(MilestoneId.fromString))
+                                        .find(id => milestone.id.exists(_ === id))
+                                        .fold(
+                                            NonEmptyChain
+                                                .of(
+                                                    Map(
+                                                        MilestoneForm.fieldGlobal -> List(
+                                                            FormFieldError("Milestone ID does not match!")
+                                                        )
+                                                    )
+                                                )
+                                                .invalidNec
+                                        )(_ => milestone.id.validNec)
+                                )
+                                form <- Sync[F].delay(MilestoneForm.validate(formData))
+                                resp <- form match {
+                                    case Validated.Invalid(errors) =>
+                                        BadRequest(
+                                            views.html.editMilestone(lang = language)(
+                                                actionUri,
+                                                csrf,
+                                                linkToHubService,
+                                                milestone,
+                                                projectBaseUri,
+                                                s"Edit milestone ${milestone.title}".some,
+                                                user,
+                                                project
+                                            )(
+                                                formData,
+                                                FormErrors.fromNec(errors)
+                                            )
+                                        )
+                                    case Validated.Valid(milestoneData) =>
+                                        val updatedMilestone =
+                                            milestone.copy(
+                                                title = milestoneData.title,
+                                                description = milestoneData.description,
+                                                dueDate = milestoneData.dueDate
+                                            )
+                                        for {
+                                            checkDuplicate <- milestoneRepo.findMilestone(projectId)(
+                                                updatedMilestone.title
+                                            )
+                                            resp <- checkDuplicate.filterNot(_.id === updatedMilestone.id) match {
+                                                case None =>
+                                                    milestoneRepo.updateMilestone(updatedMilestone) *> SeeOther(
+                                                        Location(projectBaseUri.addSegment("milestones"))
+                                                    )
+                                                case Some(_) =>
+                                                    BadRequest(
+                                                        views.html.editMilestone(lang = language)(
+                                                            actionUri,
+                                                            csrf,
+                                                            linkToHubService,
+                                                            milestone,
+                                                            projectBaseUri,
+                                                            s"Edit milestone ${milestone.title}".some,
+                                                            user,
+                                                            project
+                                                        )(
+                                                            formData,
+                                                            Map(
+                                                                MilestoneForm.fieldTitle -> List(
+                                                                    FormFieldError(
+                                                                        "A milestone with that name already exists!"
+                                                                    )
+                                                                )
+                                                            )
+                                                        )
+                                                    )
+                                            }
+                                        } yield resp
+                                }
+                            } yield resp
+                        case _ => NotFound()
+                    }
+                } yield resp
+            }
+    }
+
+    private val openMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "open" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    resp <- projectAndId match {
+                        case Some(project, projectId) =>
+                            for {
+                                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                    new Error("Only maintainers may open milestones!")
+                                )
+                                milestone <- milestoneRepo.findMilestone(projectId)(milestoneTitle)
+                                resp <- milestone match {
+                                    case Some(milestone) =>
+                                        for {
+                                            formData <- Sync[F].delay {
+                                                urlForm.values.map { t =>
+                                                    val (key, values) = t
+                                                    (
+                                                        key,
+                                                        values.headOption.getOrElse("")
+                                                    ) // Pick the first value (a field might get submitted multiple times)!
+                                                }
+                                            }
+                                            projectBaseUri <- Sync[F].delay(
+                                                linkConfig.createFullUri(
+                                                    Uri(path =
+                                                        Uri.Path(
+                                                            Vector(
+                                                                Uri.Path.Segment(s"~$projectOwnerName"),
+                                                                Uri.Path.Segment(projectName.toString)
+                                                            )
+                                                        )
+                                                    )
+                                                )
+                                            )
+                                            milestoneIdMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(MilestoneForm.fieldId)
+                                                    .flatMap(MilestoneId.fromString)
+                                                    .exists(id => milestone.id.exists(_ === id))
+                                            )
+                                            milestoneTitleMatches <- Sync[F].delay(
+                                                formData
+                                                    .get(MilestoneForm.fieldTitle)
+                                                    .flatMap(MilestoneTitle.from)
+                                                    .exists(_ === milestoneTitle)
+                                            )
+                                            resp <- (milestoneIdMatches && milestoneTitleMatches) match {
+                                                case false => BadRequest("Invalid form data!")
+                                                case true =>
+                                                    milestone.id.traverse(milestoneRepo.openMilestone) *> SeeOther(
+                                                        Location(
+                                                            projectBaseUri
+                                                                .addSegment("milestones")
+                                                                .addSegment(milestone.title.toString)
+                                                        )
+                                                    )
+                                            }
+                                        } yield resp
+                                    case _ => NotFound("Milestone not found!")
+                                }
+                            } yield resp
+                        case _ => NotFound("Repository not found!")
+                    }
+                } yield resp
+            }
+    }
+
+    private val showEditMilestoneForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "edit" as user =>
+            for {
+                csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                milestone <- projectAndId match {
+                    case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
+                    case _                    => Sync[F].delay(None)
                 }
-              } yield resp
-            case _ => NotFound()
-          }
-        } yield resp
-      }
-  }
-
-  private val openMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "open" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          resp <- projectAndId match {
-            case Some(project, projectId) =>
-              for {
-                _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                  new Error("Only maintainers may open milestones!")
-                )
-                milestone <- milestoneRepo.findMilestone(projectId)(milestoneTitle)
-                resp <- milestone match {
-                  case Some(milestone) =>
-                    for {
-                      formData <- Sync[F].delay {
-                        urlForm.values.map { t =>
-                          val (key, values) = t
-                          (
-                            key,
-                            values.headOption.getOrElse("")
-                          ) // Pick the first value (a field might get submitted multiple times)!
-                        }
-                      }
-                      projectBaseUri <- Sync[F].delay(
-                        linkConfig.createFullUri(
-                          Uri(path =
-                            Uri.Path(
-                              Vector(
-                                Uri.Path.Segment(s"~$projectOwnerName"),
-                                Uri.Path.Segment(projectName.toString)
-                              )
+                resp <- (projectAndId, milestone) match {
+                    case (Some(project, _), Some(milestone)) =>
+                        for {
+                            projectBaseUri <- Sync[F].delay(
+                                linkConfig.createFullUri(
+                                    Uri(path =
+                                        Uri.Path(
+                                            Vector(
+                                                Uri.Path.Segment(s"~$projectOwnerName"),
+                                                Uri.Path.Segment(projectName.toString)
+                                            )
+                                        )
+                                    )
+                                )
                             )
-                          )
-                        )
-                      )
-                      milestoneIdMatches <- Sync[F].delay(
-                        formData
-                          .get(MilestoneForm.fieldId)
-                          .flatMap(MilestoneId.fromString)
-                          .exists(id => milestone.id.exists(_ === id))
-                      )
-                      milestoneTitleMatches <- Sync[F].delay(
-                        formData.get(MilestoneForm.fieldTitle).flatMap(MilestoneTitle.from).exists(_ === milestoneTitle)
-                      )
-                      resp <- (milestoneIdMatches && milestoneTitleMatches) match {
-                        case false => BadRequest("Invalid form data!")
-                        case true =>
-                          milestone.id.traverse(milestoneRepo.openMilestone) *> SeeOther(
-                            Location(projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString))
-                          )
-                      }
-                    } yield resp
-                  case _ => NotFound("Milestone not found!")
+                            actionUri <- Sync[F].delay(
+                                projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString)
+                            )
+                            formData <- Sync[F].delay(MilestoneForm.fromMilestone(milestone))
+                            resp <- Ok(
+                                views.html.editMilestone(lang = language)(
+                                    actionUri,
+                                    csrf,
+                                    linkToHubService,
+                                    milestone,
+                                    projectBaseUri,
+                                    s"Edit milestone ${milestone.title}".some,
+                                    user,
+                                    project
+                                )(
+                                    formData.toMap
+                                )
+                            )
+                        } yield resp
+                    case _ => NotFound()
                 }
-              } yield resp
-            case _ => NotFound("Repository not found!")
-          }
-        } yield resp
-      }
-  }
+            } yield resp
+    }
 
-  private val showEditMilestoneForm: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) / "edit" as user =>
-      for {
-        csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-        language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-        projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-        milestone <- projectAndId match {
-          case Some((_, projectId)) => milestoneRepo.findMilestone(projectId)(milestoneTitle)
-          case _                    => Sync[F].delay(None)
-        }
-        resp <- (projectAndId, milestone) match {
-          case (Some(project, _), Some(milestone)) =>
+    private val showEditMilestonesPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" as user =>
             for {
-              projectBaseUri <- Sync[F].delay(
-                linkConfig.createFullUri(
-                  Uri(path =
-                    Uri.Path(
-                      Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                    )
-                  )
-                )
-              )
-              actionUri <- Sync[F].delay(projectBaseUri.addSegment("milestones").addSegment(milestone.title.toString))
-              formData  <- Sync[F].delay(MilestoneForm.fromMilestone(milestone))
-              resp <- Ok(
-                views.html.editMilestone(lang = language)(
-                  actionUri,
-                  csrf,
-                  linkToHubService,
-                  milestone,
-                  projectBaseUri,
-                  s"Edit milestone ${milestone.title}".some,
-                  user,
-                  project
-                )(
-                  formData.toMap
-                )
-              )
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowMilestones(csrf)(user.some)(projectOwnerName)(projectName)
             } yield resp
-          case _ => NotFound()
-        }
-      } yield resp
-  }
-
-  private val showEditMilestonesPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" as user =>
-      for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowMilestones(csrf)(user.some)(projectOwnerName)(projectName)
-      } yield resp
-  }
-
-  private val showMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) as user =>
-      val response = for {
-        csrf <- Sync[F].delay(ar.req.getCsrfToken)
-        resp <- doShowMilestone(csrf)(user.some)(projectOwnerName)(projectName)(milestoneTitle)
-      } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-        } yield resp
-      }
-  }
+    }
 
-  private val showMilestoneForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowMilestone(csrf)(None)(projectOwnerName)(projectName)(milestoneTitle)
-      } yield resp
-  }
-
-  private val showMilestonesForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "milestones" =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowMilestones(csrf)(None)(projectOwnerName)(projectName)
-      } yield resp
-  }
+    private val showMilestone: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) as user =>
+            val response = for {
+                csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                resp <- doShowMilestone(csrf)(user.some)(projectOwnerName)(projectName)(milestoneTitle)
+            } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+    }
+
+    private val showMilestoneForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" / MilestoneTitlePathParameter(milestoneTitle) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowMilestone(csrf)(None)(projectOwnerName)(projectName)(milestoneTitle)
+            } yield resp
+    }
+
+    private val showMilestonesForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "milestones" =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowMilestones(csrf)(None)(projectOwnerName)(projectName)
+            } yield resp
+    }
 
-  val protectedRoutes =
-    addMilestone <+> closeMilestone <+> deleteMilestone <+> editMilestone <+> openMilestone <+> showEditMilestoneForm <+> showEditMilestonesPage <+> showMilestone
+    val protectedRoutes =
+        addMilestone <+> closeMilestone <+> deleteMilestone <+> editMilestone <+> openMilestone <+> showEditMilestoneForm <+> showEditMilestonesPage <+> showMilestone
 
-  val routes = showMilestoneForGuests <+> showMilestonesForGuests
+    val routes = showMilestoneForGuests <+> showMilestonesForGuests
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketForm.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketForm.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketForm.scala	2025-01-13 17:13:25.032470944 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketForm.scala	2025-01-13 17:13:25.056470978 +0000
@@ -57,124 +57,126 @@
 )
 
 object TicketForm extends FormValidator[TicketForm] {
-  val fieldContent: FormField    = FormField("content")
-  val fieldLabels: FormField     = FormField("labels")
-  val fieldMilestones: FormField = FormField("milestones")
-  val fieldNumber: FormField     = FormField("number")
-  val fieldResolution: FormField = FormField("resolution")
-  val fieldStatus: FormField     = FormField("status")
-  val fieldSubmitter: FormField  = FormField("submitter")
-  val fieldTitle: FormField      = FormField("title")
+    val fieldContent: FormField    = FormField("content")
+    val fieldLabels: FormField     = FormField("labels")
+    val fieldMilestones: FormField = FormField("milestones")
+    val fieldNumber: FormField     = FormField("number")
+    val fieldResolution: FormField = FormField("resolution")
+    val fieldStatus: FormField     = FormField("status")
+    val fieldSubmitter: FormField  = FormField("submitter")
+    val fieldTitle: FormField      = FormField("title")
 
-  /** Create a form for editing a ticket from the given ticket data.
-    *
-    * @param labels
-    *   A list of labels which are associated with the ticket.
-    * @param milestones
-    *   A list of milestones which are associated with the ticket.
-    * @param ticket
-    *   The ticket which provides the data for the edit form.
-    * @return
-    *   A ticket form filled with the data from the given ticket.
-    */
-  def fromTicket(labels: List[LabelName])(milestones: List[MilestoneTitle])(ticket: Ticket): TicketForm =
-    TicketForm(
-      number = ticket.number.some,
-      title = ticket.title,
-      content = ticket.content,
-      status = ticket.status,
-      resolution = ticket.resolution,
-      submitter = ticket.submitter.map(_.id),
-      labels = labels,
-      milestones = milestones
-    )
-
-  override def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, TicketForm] = {
-    val number = data
-      .get(fieldNumber)
-      .fold(Option.empty[TicketNumber].validNec)(
-        _.headOption
-          .flatMap(TicketNumber.fromString)
-          .fold(FormFieldError("Invalid ticket number!").invalidNec)(number => Option(number).validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldNumber -> es.toList)))
-    val title = data
-      .get(fieldTitle)
-      .fold(FormFieldError("No ticket title given!").invalidNec)(
-        _.headOption
-          .map(_.trim)
-          .flatMap(TicketTitle.from)
-          .fold(FormFieldError("Invalid ticket title!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldTitle -> es.toList)))
-    val content = data
-      .get(fieldContent)
-      .fold(none[TicketContent].validNec)(
-        _.headOption
-          .filter(_.nonEmpty)
-          .fold(none[TicketContent].validNec)(s =>
-            TicketContent.from(s).fold(FormFieldError("Invalid ticket content!").invalidNec)(_.some.validNec)
-          )
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldContent -> es.toList)))
-    val status = data
-      .get(fieldStatus)
-      .fold(FormFieldError("No ticket status given!").invalidNec)(
-        _.headOption
-          .flatMap(s => Try(TicketStatus.valueOf(s)).toOption)
-          .fold(FormFieldError("Invalid ticket status!").invalidNec)(_.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldStatus -> es.toList)))
-    val resolution = data
-      .get(fieldResolution)
-      .fold(none[TicketResolution].validNec)(
-        _.headOption
-          .flatMap(s => Try(TicketResolution.valueOf(s)).toOption)
-          .fold(FormFieldError("Invalid ticket resolution!").invalidNec)(_.some.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldResolution -> es.toList)))
-    val submitterId = data
-      .get(fieldSubmitter)
-      .fold(none[SubmitterId].validNec)(
-        _.headOption
-          .flatMap(s => SubmitterId.fromString(s).toOption)
-          .fold(FormFieldError("Invalid submitter id!").invalidNec)(_.some.validNec)
-      )
-      .leftMap(es => NonEmptyChain.of(Map(fieldSubmitter -> es.toList)))
-    val labels =
-      data
-        .get(fieldLabels)
-        .fold(List.empty[LabelName].validNec[FormErrors])(_.toList.flatMap(LabelName.from).validNec[FormErrors])
-    val milestones =
-      data
-        .get(fieldMilestones)
-        .fold(List.empty[MilestoneTitle].validNec[FormErrors])(
-          _.toList.flatMap(MilestoneTitle.from).validNec[FormErrors]
+    /** Create a form for editing a ticket from the given ticket data.
+      *
+      * @param labels
+      *   A list of labels which are associated with the ticket.
+      * @param milestones
+      *   A list of milestones which are associated with the ticket.
+      * @param ticket
+      *   The ticket which provides the data for the edit form.
+      * @return
+      *   A ticket form filled with the data from the given ticket.
+      */
+    def fromTicket(labels: List[LabelName])(milestones: List[MilestoneTitle])(ticket: Ticket): TicketForm =
+        TicketForm(
+            number = ticket.number.some,
+            title = ticket.title,
+            content = ticket.content,
+            status = ticket.status,
+            resolution = ticket.resolution,
+            submitter = ticket.submitter.map(_.id),
+            labels = labels,
+            milestones = milestones
         )
-    (number, title, content, status, resolution, submitterId, labels, milestones).mapN {
-      case (number, title, content, status, resolution, submitterId, labels, milestones) =>
-        TicketForm(number, title, content, status, resolution, submitterId, labels, milestones)
+
+    override def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, TicketForm] = {
+        val number = data
+            .get(fieldNumber)
+            .fold(Option.empty[TicketNumber].validNec)(
+                _.headOption
+                    .flatMap(TicketNumber.fromString)
+                    .fold(FormFieldError("Invalid ticket number!").invalidNec)(number => Option(number).validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldNumber -> es.toList)))
+        val title = data
+            .get(fieldTitle)
+            .fold(FormFieldError("No ticket title given!").invalidNec)(
+                _.headOption
+                    .map(_.trim)
+                    .flatMap(TicketTitle.from)
+                    .fold(FormFieldError("Invalid ticket title!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldTitle -> es.toList)))
+        val content = data
+            .get(fieldContent)
+            .fold(none[TicketContent].validNec)(
+                _.headOption
+                    .filter(_.nonEmpty)
+                    .fold(none[TicketContent].validNec)(s =>
+                        TicketContent
+                            .from(s)
+                            .fold(FormFieldError("Invalid ticket content!").invalidNec)(_.some.validNec)
+                    )
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldContent -> es.toList)))
+        val status = data
+            .get(fieldStatus)
+            .fold(FormFieldError("No ticket status given!").invalidNec)(
+                _.headOption
+                    .flatMap(s => Try(TicketStatus.valueOf(s)).toOption)
+                    .fold(FormFieldError("Invalid ticket status!").invalidNec)(_.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldStatus -> es.toList)))
+        val resolution = data
+            .get(fieldResolution)
+            .fold(none[TicketResolution].validNec)(
+                _.headOption
+                    .flatMap(s => Try(TicketResolution.valueOf(s)).toOption)
+                    .fold(FormFieldError("Invalid ticket resolution!").invalidNec)(_.some.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldResolution -> es.toList)))
+        val submitterId = data
+            .get(fieldSubmitter)
+            .fold(none[SubmitterId].validNec)(
+                _.headOption
+                    .flatMap(s => SubmitterId.fromString(s).toOption)
+                    .fold(FormFieldError("Invalid submitter id!").invalidNec)(_.some.validNec)
+            )
+            .leftMap(es => NonEmptyChain.of(Map(fieldSubmitter -> es.toList)))
+        val labels =
+            data
+                .get(fieldLabels)
+                .fold(List.empty[LabelName].validNec[FormErrors])(_.toList.flatMap(LabelName.from).validNec[FormErrors])
+        val milestones =
+            data
+                .get(fieldMilestones)
+                .fold(List.empty[MilestoneTitle].validNec[FormErrors])(
+                    _.toList.flatMap(MilestoneTitle.from).validNec[FormErrors]
+                )
+        (number, title, content, status, resolution, submitterId, labels, milestones).mapN {
+            case (number, title, content, status, resolution, submitterId, labels, milestones) =>
+                TicketForm(number, title, content, status, resolution, submitterId, labels, milestones)
+        }
     }
-  }
 
-  extension (form: TicketForm) {
+    extension (form: TicketForm) {
 
-    /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
-      * twirl templating library.
-      *
-      * @return
-      *   A stringified map containing the data of the form.
-      */
-    def toMap: Map[String, Chain[String]] =
-      Map(
-        TicketForm.fieldNumber.toString     -> form.number.map(_.toString).fold(Chain.empty)(nr => Chain(nr)),
-        TicketForm.fieldTitle.toString      -> Chain(form.title.toString),
-        TicketForm.fieldContent.toString    -> form.content.map(_.toString).fold(Chain.empty)(c => Chain(c)),
-        TicketForm.fieldStatus.toString     -> Chain(form.status.toString),
-        TicketForm.fieldResolution.toString -> form.resolution.map(_.toString).fold(Chain.empty)(r => Chain(r)),
-        TicketForm.fieldSubmitter.toString  -> form.submitter.map(_.toString).fold(Chain.empty)(s => Chain(s)),
-        TicketForm.fieldLabels.toString     -> Chain.fromSeq(form.labels.map(_.toString)),
-        TicketForm.fieldMilestones.toString -> Chain.fromSeq(form.milestones.map(_.toString))
-      )
-  }
+        /** Convert the form class into a stringified map which is used as underlying data type for form handling in the
+          * twirl templating library.
+          *
+          * @return
+          *   A stringified map containing the data of the form.
+          */
+        def toMap: Map[String, Chain[String]] =
+            Map(
+                TicketForm.fieldNumber.toString     -> form.number.map(_.toString).fold(Chain.empty)(nr => Chain(nr)),
+                TicketForm.fieldTitle.toString      -> Chain(form.title.toString),
+                TicketForm.fieldContent.toString    -> form.content.map(_.toString).fold(Chain.empty)(c => Chain(c)),
+                TicketForm.fieldStatus.toString     -> Chain(form.status.toString),
+                TicketForm.fieldResolution.toString -> form.resolution.map(_.toString).fold(Chain.empty)(r => Chain(r)),
+                TicketForm.fieldSubmitter.toString  -> form.submitter.map(_.toString).fold(Chain.empty)(s => Chain(s)),
+                TicketForm.fieldLabels.toString     -> Chain.fromSeq(form.labels.map(_.toString)),
+                TicketForm.fieldMilestones.toString -> Chain.fromSeq(form.milestones.map(_.toString))
+            )
+    }
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala	2025-01-13 17:13:25.056470978 +0000
@@ -61,505 +61,558 @@
     projectRepo: ProjectRepository[F],
     ticketRepo: TicketRepository[F]
 ) extends Http4sDsl[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given CsrfProtectionConfiguration = configuration.csrfProtection
+    given CsrfProtectionConfiguration = configuration.csrfProtection
 
-  private val linkToHubService = configuration.hub.baseUri
-  private val linkConfig       = configuration.externalUrl
+    private val linkToHubService = configuration.hub.baseUri
+    private val linkConfig       = configuration.externalUrl
 
-  /** Load the project metadata with the given owner and name from the database and return it and its primary key id if
-    * the project exists and is readable by the given user account.
-    *
-    * @param currentUser
-    *   The user account that is requesting access to the project or None for a guest user.
-    * @param projectOwnerName
-    *   The name of the account that owns the project.
-    * @param projectName
-    *   The name of the project. A project name must start with a letter or number and must contain only alphanumeric
-    *   ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
-    * @return
-    *   An option to a tuple holding the [[Project]] and its primary key id.
-    */
-  private def loadProject(
-      currentUser: Option[Account]
-  )(projectOwnerName: ProjectOwnerName, projectName: ProjectName): F[Option[(Project, ProjectId)]] =
-    for {
-      owner <- projectRepo.findProjectOwner(projectOwnerName)
-      loadedRepo <- owner match {
-        case None => Sync[F].pure(None)
-        case Some(owner) =>
-          (
-            projectRepo.findProject(owner, projectName),
-            projectRepo.findProjectId(owner, projectName)
-          ).mapN {
-            case (Some(project), Some(projectId)) => (project, projectId).some
-            case _                                => None
-          }
-      }
-      // TODO: Replace with whatever we implement as proper permission model. ;-)
-      projectAndId = currentUser match {
-        case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
-        case Some(user) =>
-          loadedRepo.filter(tuple =>
-            tuple._1.isPrivate === false || tuple._1.owner.uid === ProjectOwnerId.fromUserId(user.uid)
-          )
-      }
-    } yield projectAndId
-
-  /** Logic for rendering a detail page for a single ticket.
-    *
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of tickets shall be rendered.
-    * @param projectOwnerName
-    *   The username of the account who owns the project.
-    * @param projectName
-    *   The name of the project.
-    * @param ticketNumber
-    *   The number of the ticket that shall be rendered.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowTicket(csrf: Option[CsrfToken])(user: Option[Account])(projectOwnerName: Username)(
-      projectName: ProjectName
-  )(ticketNumber: TicketNumber): F[Response[F]] =
-    for {
-      _ <- Sync[F].delay(log.debug(s"doShowTicket: $csrf, $user, $projectOwnerName, $projectName, $ticketNumber"))
-      language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      projectAndId <- loadProject(user)(projectOwnerName, projectName)
-      ticket       <- projectAndId.traverse(tuple => ticketRepo.findTicket(tuple._2)(ticketNumber))
-      resp <- (projectAndId, ticket.getOrElse(None)) match {
-        case (Some((project, projectId)), Some(ticket)) =>
-          for {
-            labels     <- ticketRepo.loadLabels(projectId)(ticket.number).compile.toList
-            milestones <- ticketRepo.loadMilestones(projectId)(ticket.number).compile.toList
-            projectBaseUri <- Sync[F].delay(
-              linkConfig.createFullUri(
-                Uri(path =
-                  Uri.Path(
-                    Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                  )
-                )
-              )
-            )
-            renderedTicketContent <- Sync[F].delay(ticket.content.map(_.toString).map(MarkdownRenderer.render))
-            resp <- Ok(
-              views.html.showTicket(lang = language)(
-                projectBaseUri.addSegment("tickets"),
-                csrf,
-                linkToHubService,
-                labels,
-                milestones,
-                ticket,
-                renderedTicketContent,
-                projectBaseUri,
-                ticket.title.toString.some,
-                user,
-                project
-              )
-            )
-          } yield resp
-        case _ => NotFound()
-      }
-    } yield resp
-
-  /** Logic for rendering a list of all tickets for a project and optionally management functionality.
-    *
-    * @param filter
-    *   An optional ticket filter containing possible values which will be used to filter the list of tickets.
-    * @param csrf
-    *   An optional CSRF-Token that shall be used.
-    * @param user
-    *   An optional user account for whom the list of tickets shall be rendered.
-    * @param projectOwnerName
-    *   The username of the account who owns the project.
-    * @param projectName
-    *   The name of the project.
-    * @return
-    *   An HTTP response containing the rendered HTML.
-    */
-  private def doShowTickets(filter: Option[TicketFilter])(csrf: Option[CsrfToken])(user: Option[Account])(
-      projectOwnerName: Username
-  )(projectName: ProjectName): F[Response[F]] =
-    for {
-      _            <- Sync[F].delay(log.debug(s"doShowTickets: $csrf, $user, $projectOwnerName, $projectName, $filter"))
-      language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
-      projectAndId <- loadProject(user)(projectOwnerName, projectName)
-      resp <- projectAndId match {
-        case Some((project, projectId)) =>
-          for {
-            tickets <- ticketRepo.allTickets(filter)(projectId).compile.toList
-            projectBaseUri <- Sync[F].delay(
-              linkConfig.createFullUri(
-                Uri(path =
-                  Uri.Path(
-                    Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                  )
-                )
-              )
-            )
-            resp <- Ok(
-              views.html.showTickets(lang = language)(
-                projectBaseUri.addSegment("tickets"),
-                csrf,
-                linkToHubService,
-                tickets,
-                filter,
-                projectBaseUri,
-                "Manage your project tickets.".some,
-                user,
-                project
-              )
-            )
-          } yield resp
-        case _ => NotFound("Ticket project not found!")
-      }
-    } yield resp
-
-  private val addTicket: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        val response =
-          for {
-            csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-            language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-            resp <- projectAndId match {
-              case Some((project, projectId)) =>
-                for {
-                  _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                    new Error("Only maintainers may add tickets!")
-                  )
-                  labels     <- labelRepo.allLabels(projectId).compile.toList
-                  milestones <- milestoneRepo.allMilestones(projectId).compile.toList
-                  formData   <- Sync[F].delay(urlForm.values)
-                  form       <- Sync[F].delay(TicketForm.validate(formData))
-                  projectBaseUri <- Sync[F].delay(
-                    linkConfig.createFullUri(
-                      Uri(path =
-                        Uri.Path(
-                          Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                        )
-                      )
+    /** Load the project metadata with the given owner and name from the database and return it and its primary key id
+      * if the project exists and is readable by the given user account.
+      *
+      * @param currentUser
+      *   The user account that is requesting access to the project or None for a guest user.
+      * @param projectOwnerName
+      *   The name of the account that owns the project.
+      * @param projectName
+      *   The name of the project. A project name must start with a letter or number and must contain only alphanumeric
+      *   ASCII characters as well as minus or underscore signs. It must be between 2 and 64 characters long.
+      * @return
+      *   An option to a tuple holding the [[Project]] and its primary key id.
+      */
+    private def loadProject(
+        currentUser: Option[Account]
+    )(projectOwnerName: ProjectOwnerName, projectName: ProjectName): F[Option[(Project, ProjectId)]] =
+        for {
+            owner <- projectRepo.findProjectOwner(projectOwnerName)
+            loadedRepo <- owner match {
+                case None => Sync[F].pure(None)
+                case Some(owner) =>
+                    (
+                        projectRepo.findProject(owner, projectName),
+                        projectRepo.findProjectId(owner, projectName)
+                    ).mapN {
+                        case (Some(project), Some(projectId)) => (project, projectId).some
+                        case _                                => None
+                    }
+            }
+            // TODO: Replace with whatever we implement as proper permission model. ;-)
+            projectAndId = currentUser match {
+                case None => loadedRepo.filter(tuple => tuple._1.isPrivate === false)
+                case Some(user) =>
+                    loadedRepo.filter(tuple =>
+                        tuple._1.isPrivate === false || tuple._1.owner.uid === ProjectOwnerId.fromUserId(user.uid)
                     )
-                  )
-                  resp <- form match {
-                    case Validated.Invalid(errors) =>
-                      BadRequest(
-                        views.html.createTicket(lang = language)(
-                          projectBaseUri.addSegment("tickets"),
-                          csrf,
-                          linkToHubService,
-                          labels,
-                          milestones,
-                          projectBaseUri,
-                          "Create a new ticket.".some,
-                          user.some,
-                          project
-                        )(formData.withDefaultValue(Chain.empty), FormErrors.fromNec(errors))
-                      )
-                    case Validated.Valid(ticketData) =>
-                      for {
-                        timestamp    <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
-                        ticketLabels <- ticketData.labels.traverse(name => labelRepo.findLabel(projectId)(name))
-                        ticketMilestones <- ticketData.milestones.traverse(title =>
-                          milestoneRepo.findMilestone(projectId)(title)
-                        )
-                        number <- projectRepo.incrementNextTicketNumber(projectId)
-                        ticket <- Sync[F].delay(
-                          Ticket(
-                            number = number,
-                            title = ticketData.title,
-                            content = ticketData.content,
-                            status = ticketData.status,
-                            resolution = ticketData.resolution,
-                            submitter = Submitter(SubmitterId(user.uid.toUUID), SubmitterName(user.name.toString)).some,
-                            createdAt = timestamp,
-                            updatedAt = timestamp
-                          )
-                        )
-                        _    <- ticketRepo.createTicket(projectId)(ticket)
-                        _    <- ticketLabels.flatten.traverse(ticketRepo.addLabel(projectId)(number))
-                        _    <- ticketMilestones.flatten.traverse(ticketRepo.addMilestone(projectId)(number))
-                        resp <- SeeOther(Location(projectBaseUri.addSegment("tickets")))
-                      } yield resp
-                  }
-                } yield resp
-              case _ => NotFound()
             }
-          } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
+        } yield projectAndId
+
+    /** Logic for rendering a detail page for a single ticket.
+      *
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of tickets shall be rendered.
+      * @param projectOwnerName
+      *   The username of the account who owns the project.
+      * @param projectName
+      *   The name of the project.
+      * @param ticketNumber
+      *   The number of the ticket that shall be rendered.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowTicket(csrf: Option[CsrfToken])(user: Option[Account])(projectOwnerName: Username)(
+        projectName: ProjectName
+    )(ticketNumber: TicketNumber): F[Response[F]] =
         for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-        } yield resp
-      }
-      }
-  }
-
-  private val editTicket: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" / TicketNumberPathParameter(ticketNumber) as user =>
-      ar.req.decodeStrict[F, UrlForm] { urlForm =>
-        val response =
-          for {
-            csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-            language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-            projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+            _ <- Sync[F].delay(log.debug(s"doShowTicket: $csrf, $user, $projectOwnerName, $projectName, $ticketNumber"))
+            language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            projectAndId <- loadProject(user)(projectOwnerName, projectName)
             ticket       <- projectAndId.traverse(tuple => ticketRepo.findTicket(tuple._2)(ticketNumber))
             resp <- (projectAndId, ticket.getOrElse(None)) match {
-              case (Some((project, projectId)), Some(ticket)) =>
-                for {
-                  _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
-                    new Error("Only maintainers may edit tickets!")
-                  )
-                  labels     <- labelRepo.allLabels(projectId).compile.toList
-                  milestones <- milestoneRepo.allMilestones(projectId).compile.toList
-                  formData   <- Sync[F].delay(urlForm.values)
-                  form       <- Sync[F].delay(TicketForm.validate(formData))
-                  projectBaseUri <- Sync[F].delay(
-                    linkConfig.createFullUri(
-                      Uri(path =
-                        Uri.Path(
-                          Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
+                case (Some((project, projectId)), Some(ticket)) =>
+                    for {
+                        labels     <- ticketRepo.loadLabels(projectId)(ticket.number).compile.toList
+                        milestones <- ticketRepo.loadMilestones(projectId)(ticket.number).compile.toList
+                        projectBaseUri <- Sync[F].delay(
+                            linkConfig.createFullUri(
+                                Uri(path =
+                                    Uri.Path(
+                                        Vector(
+                                            Uri.Path.Segment(s"~$projectOwnerName"),
+                                            Uri.Path.Segment(projectName.toString)
+                                        )
+                                    )
+                                )
+                            )
                         )
-                      )
-                    )
-                  )
-                  resp <- form match {
-                    case Validated.Invalid(errors) =>
-                      BadRequest(
-                        views.html.createTicket(lang = language)(
-                          projectBaseUri.addSegment("tickets"),
-                          csrf,
-                          linkToHubService,
-                          labels,
-                          milestones,
-                          projectBaseUri,
-                          "Create a new ticket.".some,
-                          user.some,
-                          project
-                        )(formData.withDefaultValue(Chain.empty), FormErrors.fromNec(errors))
-                      )
-                    case Validated.Valid(ticketData) =>
-                      for {
-                        timestamp     <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
-                        oldLabels     <- ticketRepo.loadLabels(projectId)(ticket.number).compile.toList
-                        oldMilestones <- ticketRepo.loadMilestones(projectId)(ticket.number).compile.toList
-                        ticketLabels  <- ticketData.labels.traverse(name => labelRepo.findLabel(projectId)(name))
-                        ticketMilestones <- ticketData.milestones.traverse(title =>
-                          milestoneRepo.findMilestone(projectId)(title)
+                        renderedTicketContent <- Sync[F].delay(
+                            ticket.content.map(_.toString).map(MarkdownRenderer.render)
                         )
-                        labelsToCreate     = ticketLabels.flatten.diff(oldLabels)
-                        labelsToRemove     = oldLabels.diff(ticketLabels.flatten)
-                        milestonesToCreate = ticketMilestones.flatten.diff(oldMilestones)
-                        milestonesToRemove = oldMilestones.diff(ticketMilestones.flatten)
-                        updatedTicket =
-                          ticket.copy(
-                            title = ticketData.title,
-                            content = ticketData.content,
-                            status = ticketData.status,
-                            resolution = ticketData.resolution,
-                            createdAt = ticket.createdAt,
-                            updatedAt = timestamp
-                          )
-                        _    <- ticketRepo.updateTicket(projectId)(updatedTicket)
-                        _    <- labelsToRemove.traverse(ticketRepo.removeLabel(projectId)(updatedTicket))
-                        _    <- milestonesToRemove.traverse(ticketRepo.removeMilestone(projectId)(updatedTicket))
-                        _    <- labelsToCreate.traverse(ticketRepo.addLabel(projectId)(ticket.number))
-                        _    <- milestonesToCreate.traverse(ticketRepo.addMilestone(projectId)(ticket.number))
-                        resp <- SeeOther(Location(projectBaseUri.addSegment("tickets")))
-                      } yield resp
-                  }
-                } yield resp
-              case _ => NotFound()
+                        resp <- Ok(
+                            views.html.showTicket(lang = language)(
+                                projectBaseUri.addSegment("tickets"),
+                                csrf,
+                                linkToHubService,
+                                labels,
+                                milestones,
+                                ticket,
+                                renderedTicketContent,
+                                projectBaseUri,
+                                ticket.title.toString.some,
+                                user,
+                                project
+                            )
+                        )
+                    } yield resp
+                case _ => NotFound()
             }
-          } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-        } yield resp
-      }
-      }
-  }
-
-  private val showCreateTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "newTicket" as user =>
-      val response =
-        for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          resp <- projectAndId match {
-            case Some((project, projectId)) =>
-              for {
-                labels     <- labelRepo.allLabels(projectId).compile.toList
-                milestones <- milestoneRepo.allMilestones(projectId).compile.toList
-                projectBaseUri <- Sync[F].delay(
-                  linkConfig.createFullUri(
-                    Uri(path =
-                      Uri.Path(
-                        Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                      )
-                    )
-                  )
-                )
-                resp <- Ok(
-                  views.html.createTicket(lang = language)(
-                    projectBaseUri.addSegment("tickets"),
-                    csrf,
-                    linkToHubService,
-                    labels,
-                    milestones,
-                    projectBaseUri,
-                    "Create a new ticket.".some,
-                    user.some,
-                    project
-                  )()
-                )
-              } yield resp
-            case _ => NotFound()
-          }
-        } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
         } yield resp
-      }
-  }
 
-  private val showEditTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" / TicketNumberPathParameter(ticketNumber) / "edit" as user =>
-      val response =
+    /** Logic for rendering a list of all tickets for a project and optionally management functionality.
+      *
+      * @param filter
+      *   An optional ticket filter containing possible values which will be used to filter the list of tickets.
+      * @param csrf
+      *   An optional CSRF-Token that shall be used.
+      * @param user
+      *   An optional user account for whom the list of tickets shall be rendered.
+      * @param projectOwnerName
+      *   The username of the account who owns the project.
+      * @param projectName
+      *   The name of the project.
+      * @return
+      *   An HTTP response containing the rendered HTML.
+      */
+    private def doShowTickets(filter: Option[TicketFilter])(csrf: Option[CsrfToken])(user: Option[Account])(
+        projectOwnerName: Username
+    )(projectName: ProjectName): F[Response[F]] =
         for {
-          csrf         <- Sync[F].delay(ar.req.getCsrfToken)
-          language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
-          ticket       <- projectAndId.traverse(tuple => ticketRepo.findTicket(tuple._2)(ticketNumber))
-          resp <- (projectAndId, ticket.getOrElse(None)) match {
-            case (Some((project, projectId)), Some(ticket)) =>
-              for {
-                ticketLabels     <- ticketRepo.loadLabels(projectId)(ticket.number).map(_.name).compile.toList
-                ticketMilestones <- ticketRepo.loadMilestones(projectId)(ticket.number).map(_.title).compile.toList
-                form             <- Sync[F].delay(TicketForm.fromTicket(ticketLabels)(ticketMilestones)(ticket))
-                formData         <- Sync[F].delay(form.toMap)
-                labels           <- labelRepo.allLabels(projectId).compile.toList
-                milestones       <- milestoneRepo.allMilestones(projectId).compile.toList
-                projectBaseUri <- Sync[F].delay(
-                  linkConfig.createFullUri(
-                    Uri(path =
-                      Uri.Path(
-                        Vector(Uri.Path.Segment(s"~$projectOwnerName"), Uri.Path.Segment(projectName.toString))
-                      )
-                    )
-                  )
-                )
-                resp <- Ok(
-                  views.html.editTicket(lang = language)(
-                    projectBaseUri.addSegment("tickets"),
-                    csrf,
-                    linkToHubService,
-                    labels,
-                    milestones,
-                    projectBaseUri,
-                    ticket.number,
-                    s"Edit ticket ${ticket.number}".some,
-                    user.some,
-                    project
-                  )(formData.withDefaultValue(Chain.empty), FormErrors.empty)
-                )
-              } yield resp
-            case _ => NotFound()
-          }
-        } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+            _ <- Sync[F].delay(log.debug(s"doShowTickets: $csrf, $user, $projectOwnerName, $projectName, $filter"))
+            language     <- Sync[F].delay(user.flatMap(_.language).getOrElse(LanguageCode("en")))
+            projectAndId <- loadProject(user)(projectOwnerName, projectName)
+            resp <- projectAndId match {
+                case Some((project, projectId)) =>
+                    for {
+                        tickets <- ticketRepo.allTickets(filter)(projectId).compile.toList
+                        projectBaseUri <- Sync[F].delay(
+                            linkConfig.createFullUri(
+                                Uri(path =
+                                    Uri.Path(
+                                        Vector(
+                                            Uri.Path.Segment(s"~$projectOwnerName"),
+                                            Uri.Path.Segment(projectName.toString)
+                                        )
+                                    )
+                                )
+                            )
+                        )
+                        resp <- Ok(
+                            views.html.showTickets(lang = language)(
+                                projectBaseUri.addSegment("tickets"),
+                                csrf,
+                                linkToHubService,
+                                tickets,
+                                filter,
+                                projectBaseUri,
+                                "Manage your project tickets.".some,
+                                user,
+                                project
+                            )
+                        )
+                    } yield resp
+                case _ => NotFound("Ticket project not found!")
+            }
         } yield resp
-      }
-  }
 
-  private val showTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" / TicketNumberPathParameter(ticketNumber) as user =>
-      val response =
-        for {
-          csrf <- Sync[F].delay(ar.req.getCsrfToken)
-          resp <- doShowTicket(csrf)(user.some)(projectOwnerName)(projectName)(ticketNumber)
-        } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-        } yield resp
-      }
-  }
+    private val addTicket: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                val response =
+                    for {
+                        csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                        language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                        resp <- projectAndId match {
+                            case Some((project, projectId)) =>
+                                for {
+                                    _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                        new Error("Only maintainers may add tickets!")
+                                    )
+                                    labels     <- labelRepo.allLabels(projectId).compile.toList
+                                    milestones <- milestoneRepo.allMilestones(projectId).compile.toList
+                                    formData   <- Sync[F].delay(urlForm.values)
+                                    form       <- Sync[F].delay(TicketForm.validate(formData))
+                                    projectBaseUri <- Sync[F].delay(
+                                        linkConfig.createFullUri(
+                                            Uri(path =
+                                                Uri.Path(
+                                                    Vector(
+                                                        Uri.Path.Segment(s"~$projectOwnerName"),
+                                                        Uri.Path.Segment(projectName.toString)
+                                                    )
+                                                )
+                                            )
+                                        )
+                                    )
+                                    resp <- form match {
+                                        case Validated.Invalid(errors) =>
+                                            BadRequest(
+                                                views.html.createTicket(lang = language)(
+                                                    projectBaseUri.addSegment("tickets"),
+                                                    csrf,
+                                                    linkToHubService,
+                                                    labels,
+                                                    milestones,
+                                                    projectBaseUri,
+                                                    "Create a new ticket.".some,
+                                                    user.some,
+                                                    project
+                                                )(formData.withDefaultValue(Chain.empty), FormErrors.fromNec(errors))
+                                            )
+                                        case Validated.Valid(ticketData) =>
+                                            for {
+                                                timestamp <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
+                                                ticketLabels <- ticketData.labels.traverse(name =>
+                                                    labelRepo.findLabel(projectId)(name)
+                                                )
+                                                ticketMilestones <- ticketData.milestones.traverse(title =>
+                                                    milestoneRepo.findMilestone(projectId)(title)
+                                                )
+                                                number <- projectRepo.incrementNextTicketNumber(projectId)
+                                                ticket <- Sync[F].delay(
+                                                    Ticket(
+                                                        number = number,
+                                                        title = ticketData.title,
+                                                        content = ticketData.content,
+                                                        status = ticketData.status,
+                                                        resolution = ticketData.resolution,
+                                                        submitter = Submitter(
+                                                            SubmitterId(user.uid.toUUID),
+                                                            SubmitterName(user.name.toString)
+                                                        ).some,
+                                                        createdAt = timestamp,
+                                                        updatedAt = timestamp
+                                                    )
+                                                )
+                                                _ <- ticketRepo.createTicket(projectId)(ticket)
+                                                _ <- ticketLabels.flatten.traverse(
+                                                    ticketRepo.addLabel(projectId)(number)
+                                                )
+                                                _ <- ticketMilestones.flatten.traverse(
+                                                    ticketRepo.addMilestone(projectId)(number)
+                                                )
+                                                resp <- SeeOther(Location(projectBaseUri.addSegment("tickets")))
+                                            } yield resp
+                                    }
+                                } yield resp
+                            case _ => NotFound()
+                        }
+                    } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+            }
+    }
 
-  private val showTicketPageForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" / TicketNumberPathParameter(ticketNumber) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowTicket(csrf)(None)(projectOwnerName)(projectName)(ticketNumber)
-      } yield resp
-  }
-
-  private val showTicketsPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
-    case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" :? TicketFilter.OptionalUrlParameter(maybeFilter) as user =>
-      val response =
-        for {
-          csrf <- Sync[F].delay(ar.req.getCsrfToken)
-          resp <- doShowTickets(maybeFilter)(csrf)(user.some)(projectOwnerName)(projectName)
-        } yield resp
-      response.recoverWith { error =>
-        log.error("Internal Server Error", error)
-        for {
-          csrf     <- Sync[F].delay(ar.req.getCsrfToken)
-          language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
-          resp     <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
-        } yield resp
-      }
-  }
+    private val editTicket: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ POST -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" / TicketNumberPathParameter(ticketNumber) as user =>
+            ar.req.decodeStrict[F, UrlForm] { urlForm =>
+                val response =
+                    for {
+                        csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                        language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                        projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                        ticket       <- projectAndId.traverse(tuple => ticketRepo.findTicket(tuple._2)(ticketNumber))
+                        resp <- (projectAndId, ticket.getOrElse(None)) match {
+                            case (Some((project, projectId)), Some(ticket)) =>
+                                for {
+                                    _ <- Sync[F].raiseUnless(project.owner.uid === ProjectOwnerId.fromUserId(user.uid))(
+                                        new Error("Only maintainers may edit tickets!")
+                                    )
+                                    labels     <- labelRepo.allLabels(projectId).compile.toList
+                                    milestones <- milestoneRepo.allMilestones(projectId).compile.toList
+                                    formData   <- Sync[F].delay(urlForm.values)
+                                    form       <- Sync[F].delay(TicketForm.validate(formData))
+                                    projectBaseUri <- Sync[F].delay(
+                                        linkConfig.createFullUri(
+                                            Uri(path =
+                                                Uri.Path(
+                                                    Vector(
+                                                        Uri.Path.Segment(s"~$projectOwnerName"),
+                                                        Uri.Path.Segment(projectName.toString)
+                                                    )
+                                                )
+                                            )
+                                        )
+                                    )
+                                    resp <- form match {
+                                        case Validated.Invalid(errors) =>
+                                            BadRequest(
+                                                views.html.createTicket(lang = language)(
+                                                    projectBaseUri.addSegment("tickets"),
+                                                    csrf,
+                                                    linkToHubService,
+                                                    labels,
+                                                    milestones,
+                                                    projectBaseUri,
+                                                    "Create a new ticket.".some,
+                                                    user.some,
+                                                    project
+                                                )(formData.withDefaultValue(Chain.empty), FormErrors.fromNec(errors))
+                                            )
+                                        case Validated.Valid(ticketData) =>
+                                            for {
+                                                timestamp <- Sync[F].delay(OffsetDateTime.now(ZoneOffset.UTC))
+                                                oldLabels <- ticketRepo
+                                                    .loadLabels(projectId)(ticket.number)
+                                                    .compile
+                                                    .toList
+                                                oldMilestones <- ticketRepo
+                                                    .loadMilestones(projectId)(ticket.number)
+                                                    .compile
+                                                    .toList
+                                                ticketLabels <- ticketData.labels.traverse(name =>
+                                                    labelRepo.findLabel(projectId)(name)
+                                                )
+                                                ticketMilestones <- ticketData.milestones.traverse(title =>
+                                                    milestoneRepo.findMilestone(projectId)(title)
+                                                )
+                                                labelsToCreate     = ticketLabels.flatten.diff(oldLabels)
+                                                labelsToRemove     = oldLabels.diff(ticketLabels.flatten)
+                                                milestonesToCreate = ticketMilestones.flatten.diff(oldMilestones)
+                                                milestonesToRemove = oldMilestones.diff(ticketMilestones.flatten)
+                                                updatedTicket =
+                                                    ticket.copy(
+                                                        title = ticketData.title,
+                                                        content = ticketData.content,
+                                                        status = ticketData.status,
+                                                        resolution = ticketData.resolution,
+                                                        createdAt = ticket.createdAt,
+                                                        updatedAt = timestamp
+                                                    )
+                                                _ <- ticketRepo.updateTicket(projectId)(updatedTicket)
+                                                _ <- labelsToRemove.traverse(
+                                                    ticketRepo.removeLabel(projectId)(updatedTicket)
+                                                )
+                                                _ <- milestonesToRemove.traverse(
+                                                    ticketRepo.removeMilestone(projectId)(updatedTicket)
+                                                )
+                                                _ <- labelsToCreate.traverse(
+                                                    ticketRepo.addLabel(projectId)(ticket.number)
+                                                )
+                                                _ <- milestonesToCreate.traverse(
+                                                    ticketRepo.addMilestone(projectId)(ticket.number)
+                                                )
+                                                resp <- SeeOther(Location(projectBaseUri.addSegment("tickets")))
+                                            } yield resp
+                                    }
+                                } yield resp
+                            case _ => NotFound()
+                        }
+                    } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+            }
+    }
+
+    private val showCreateTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "newTicket" as user =>
+            val response =
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    resp <- projectAndId match {
+                        case Some((project, projectId)) =>
+                            for {
+                                labels     <- labelRepo.allLabels(projectId).compile.toList
+                                milestones <- milestoneRepo.allMilestones(projectId).compile.toList
+                                projectBaseUri <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~$projectOwnerName"),
+                                                    Uri.Path.Segment(projectName.toString)
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                resp <- Ok(
+                                    views.html.createTicket(lang = language)(
+                                        projectBaseUri.addSegment("tickets"),
+                                        csrf,
+                                        linkToHubService,
+                                        labels,
+                                        milestones,
+                                        projectBaseUri,
+                                        "Create a new ticket.".some,
+                                        user.some,
+                                        project
+                                    )()
+                                )
+                            } yield resp
+                        case _ => NotFound()
+                    }
+                } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+    }
+
+    private val showEditTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" / TicketNumberPathParameter(ticketNumber) / "edit" as user =>
+            val response =
+                for {
+                    csrf         <- Sync[F].delay(ar.req.getCsrfToken)
+                    language     <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    projectAndId <- loadProject(user.some)(projectOwnerName, projectName)
+                    ticket       <- projectAndId.traverse(tuple => ticketRepo.findTicket(tuple._2)(ticketNumber))
+                    resp <- (projectAndId, ticket.getOrElse(None)) match {
+                        case (Some((project, projectId)), Some(ticket)) =>
+                            for {
+                                ticketLabels <- ticketRepo
+                                    .loadLabels(projectId)(ticket.number)
+                                    .map(_.name)
+                                    .compile
+                                    .toList
+                                ticketMilestones <- ticketRepo
+                                    .loadMilestones(projectId)(ticket.number)
+                                    .map(_.title)
+                                    .compile
+                                    .toList
+                                form     <- Sync[F].delay(TicketForm.fromTicket(ticketLabels)(ticketMilestones)(ticket))
+                                formData <- Sync[F].delay(form.toMap)
+                                labels   <- labelRepo.allLabels(projectId).compile.toList
+                                milestones <- milestoneRepo.allMilestones(projectId).compile.toList
+                                projectBaseUri <- Sync[F].delay(
+                                    linkConfig.createFullUri(
+                                        Uri(path =
+                                            Uri.Path(
+                                                Vector(
+                                                    Uri.Path.Segment(s"~$projectOwnerName"),
+                                                    Uri.Path.Segment(projectName.toString)
+                                                )
+                                            )
+                                        )
+                                    )
+                                )
+                                resp <- Ok(
+                                    views.html.editTicket(lang = language)(
+                                        projectBaseUri.addSegment("tickets"),
+                                        csrf,
+                                        linkToHubService,
+                                        labels,
+                                        milestones,
+                                        projectBaseUri,
+                                        ticket.number,
+                                        s"Edit ticket ${ticket.number}".some,
+                                        user.some,
+                                        project
+                                    )(formData.withDefaultValue(Chain.empty), FormErrors.empty)
+                                )
+                            } yield resp
+                        case _ => NotFound()
+                    }
+                } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+    }
+
+    private val showTicketPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" / TicketNumberPathParameter(ticketNumber) as user =>
+            val response =
+                for {
+                    csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                    resp <- doShowTicket(csrf)(user.some)(projectOwnerName)(projectName)(ticketNumber)
+                } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+    }
+
+    private val showTicketPageForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" / TicketNumberPathParameter(ticketNumber) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowTicket(csrf)(None)(projectOwnerName)(projectName)(ticketNumber)
+            } yield resp
+    }
+
+    private val showTicketsPage: AuthedRoutes[Account, F] = AuthedRoutes.of {
+        case ar @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" :? TicketFilter.OptionalUrlParameter(maybeFilter) as user =>
+            val response =
+                for {
+                    csrf <- Sync[F].delay(ar.req.getCsrfToken)
+                    resp <- doShowTickets(maybeFilter)(csrf)(user.some)(projectOwnerName)(projectName)
+                } yield resp
+            response.recoverWith { error =>
+                log.error("Internal Server Error", error)
+                for {
+                    csrf     <- Sync[F].delay(ar.req.getCsrfToken)
+                    language <- Sync[F].delay(user.language.getOrElse(LanguageCode("en")))
+                    resp <- InternalServerError(views.html.errors.internalServerError(lang = language)(csrf, user.some))
+                } yield resp
+            }
+    }
 
-  private val showTicketsForGuests: HttpRoutes[F] = HttpRoutes.of {
-    case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
-          projectName
-        ) / "tickets" :? TicketFilter.OptionalUrlParameter(maybeFilter) =>
-      for {
-        csrf <- Sync[F].delay(req.getCsrfToken)
-        resp <- doShowTickets(maybeFilter)(csrf)(None)(projectOwnerName)(projectName)
-      } yield resp
-  }
+    private val showTicketsForGuests: HttpRoutes[F] = HttpRoutes.of {
+        case req @ GET -> Root / UsernamePathParameter(projectOwnerName) / ProjectNamePathParameter(
+                projectName
+            ) / "tickets" :? TicketFilter.OptionalUrlParameter(maybeFilter) =>
+            for {
+                csrf <- Sync[F].delay(req.getCsrfToken)
+                resp <- doShowTickets(maybeFilter)(csrf)(None)(projectOwnerName)(projectName)
+            } yield resp
+    }
 
-  val protectedRoutes =
-    addTicket <+> editTicket <+> showCreateTicketPage <+> showEditTicketPage <+> showTicketPage <+> showTicketsPage
+    val protectedRoutes =
+        addTicket <+> editTicket <+> showCreateTicketPage <+> showEditTicketPage <+> showTicketPage <+> showTicketsPage
 
-  val routes = showTicketPageForGuests <+> showTicketsForGuests
+    val routes = showTicketPageForGuests <+> showTicketsForGuests
 
 }
diff -rN -u old-smederee/modules/hub/src/main/scala/de/smederee/tickets/UsernamePathParameter.scala new-smederee/modules/hub/src/main/scala/de/smederee/tickets/UsernamePathParameter.scala
--- old-smederee/modules/hub/src/main/scala/de/smederee/tickets/UsernamePathParameter.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/UsernamePathParameter.scala	2025-01-13 17:13:25.056470978 +0000
@@ -22,11 +22,11 @@
 /** Extractor to retrieve an Username from a path parameter.
   */
 object UsernamePathParameter {
-  def unapply(str: String): Option[Username] =
-    Option(str).flatMap { string =>
-      if (string.startsWith("~"))
-        Username.from(string.drop(1))
-      else
-        None
-    }
+    def unapply(str: String): Option[Username] =
+        Option(str).flatMap { string =>
+            if (string.startsWith("~"))
+                Username.from(string.drop(1))
+            else
+                None
+        }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationMiddlewareTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationMiddlewareTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationMiddlewareTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationMiddlewareTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -29,107 +29,107 @@
 import org.http4s.*
 
 final class AuthenticationMiddlewareTest extends BaseSpec with AuthenticationMiddleware {
-  test("extractSessionId must return the session id".tag(NeedsDatabase)) {
-    (genSignAndValidate.sample, genSessionId.sample) match {
-      case (Some(signAndValidate), Some(sessionId)) =>
-        val clock = java.time.Clock.systemUTC
-        val token = signAndValidate.signToken(sessionId.toString)(clock.millis.toString)
-        val request = Request[IO](method = Method.GET)
-          .addCookie(RequestCookie(Constants.authenticationCookieName.toString, token.toString))
-        val test = for {
-          id <- extractSessionId[IO](request, signAndValidate)
-        } yield id
-        test.map { result =>
-          assert(result === sessionId)
+    test("extractSessionId must return the session id".tag(NeedsDatabase)) {
+        (genSignAndValidate.sample, genSessionId.sample) match {
+            case (Some(signAndValidate), Some(sessionId)) =>
+                val clock = java.time.Clock.systemUTC
+                val token = signAndValidate.signToken(sessionId.toString)(clock.millis.toString)
+                val request = Request[IO](method = Method.GET)
+                    .addCookie(RequestCookie(Constants.authenticationCookieName.toString, token.toString))
+                val test = for {
+                    id <- extractSessionId[IO](request, signAndValidate)
+                } yield id
+                test.map { result =>
+                    assert(result === sessionId)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test(
-    "resolveUser must return the account if session id and account exist and the session has not reached absolute timeout"
-      .tag(NeedsDatabase)
-  ) {
-    (genFiniteDuration.sample, genValidSession.sample, genValidAccount.sample) match {
-      case (Some(duration), Some(s), Some(account)) =>
-        val createdAt = OffsetDateTime.now(ZoneOffset.UTC)
-        val session   = s.copy(uid = account.uid, createdAt = createdAt)
-        val timeouts  = AuthenticationTimeouts(duration, duration, duration)
-        val dbConfig  = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _    <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _    <- createUserSession(session)
-          user <- resolveUser[IO](repo)(timeouts).run(session.id)
-        } yield user
-        test.map { maybeUser =>
-          assert(clue(maybeUser) === clue(Option(account)))
+    test(
+        "resolveUser must return the account if session id and account exist and the session has not reached absolute timeout"
+            .tag(NeedsDatabase)
+    ) {
+        (genFiniteDuration.sample, genValidSession.sample, genValidAccount.sample) match {
+            case (Some(duration), Some(s), Some(account)) =>
+                val createdAt = OffsetDateTime.now(ZoneOffset.UTC)
+                val session   = s.copy(uid = account.uid, createdAt = createdAt)
+                val timeouts  = AuthenticationTimeouts(duration, duration, duration)
+                val dbConfig  = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _    <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _    <- createUserSession(session)
+                    user <- resolveUser[IO](repo)(timeouts).run(session.id)
+                } yield user
+                test.map { maybeUser =>
+                    assert(clue(maybeUser) === clue(Option(account)))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test(
-    "resolveUser must return None if session id and account exist but the session has reached absolute timeout".tag(
-      NeedsDatabase
-    )
-  ) {
-    (genFiniteDuration.sample, genValidSession.sample, genValidAccount.sample) match {
-      case (Some(duration), Some(s), Some(account)) =>
-        val createdAt = OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(duration.toSeconds + 5L)
-        val session   = s.copy(uid = account.uid, createdAt = createdAt)
-        val timeouts  = AuthenticationTimeouts(duration, duration, duration)
-        val dbConfig  = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
+    test(
+        "resolveUser must return None if session id and account exist but the session has reached absolute timeout".tag(
+            NeedsDatabase
         )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _    <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _    <- createUserSession(session)
-          user <- resolveUser[IO](repo)(timeouts).run(session.id)
-        } yield user
-        test.map { maybeUser =>
-          assert(clue(maybeUser) === clue(Option(account)))
+    ) {
+        (genFiniteDuration.sample, genValidSession.sample, genValidAccount.sample) match {
+            case (Some(duration), Some(s), Some(account)) =>
+                val createdAt = OffsetDateTime.now(ZoneOffset.UTC).minusSeconds(duration.toSeconds + 5L)
+                val session   = s.copy(uid = account.uid, createdAt = createdAt)
+                val timeouts  = AuthenticationTimeouts(duration, duration, duration)
+                val dbConfig  = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _    <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _    <- createUserSession(session)
+                    user <- resolveUser[IO](repo)(timeouts).run(session.id)
+                } yield user
+                test.map { maybeUser =>
+                    assert(clue(maybeUser) === clue(Option(account)))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("resolveUser must return None if no session exists".tag(NeedsDatabase)) {
-    (genFiniteDuration.sample, genValidSession.sample, genValidAccount.sample) match {
-      case (Some(duration), Some(s), Some(account)) =>
-        val session  = s.copy(uid = account.uid)
-        val timeouts = AuthenticationTimeouts(duration, duration, duration)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _    <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          user <- resolveUser[IO](repo)(timeouts).run(session.id)
-        } yield user
-        test.map { maybeUser =>
-          assert(clue(maybeUser) === None)
+    test("resolveUser must return None if no session exists".tag(NeedsDatabase)) {
+        (genFiniteDuration.sample, genValidSession.sample, genValidAccount.sample) match {
+            case (Some(duration), Some(s), Some(account)) =>
+                val session  = s.copy(uid = account.uid)
+                val timeouts = AuthenticationTimeouts(duration, duration, duration)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _    <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    user <- resolveUser[IO](repo)(timeouts).run(session.id)
+                } yield user
+                test.map { maybeUser =>
+                    assert(clue(maybeUser) === None)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/AuthenticationRoutesTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -36,304 +36,324 @@
 import munit.*
 
 class AuthenticationRoutesTest extends CatsEffectSuite {
-  val loginPath         = uri"/login"
-  private val resetPath = uri"/forgot-password"
+    val loginPath         = uri"/login"
+    private val resetPath = uri"/forgot-password"
 
-  protected final val configuration: SmedereeHubConfig =
-    ConfigSource
-      .fromConfig(ConfigFactory.load(getClass.getClassLoader))
-      .at(SmedereeHubConfig.location)
-      .loadOrThrow[SmedereeHubConfig]
-
-  test("GET /login must return the login form for guest users") {
-    val expectedHtml = views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)()
-
-    val clock                = java.time.Clock.systemUTC
-    val authenticationConfig = configuration.service.authentication
-    val externalConfig       = configuration.service.external
-    val repo                 = new TestAuthenticationRepository[IO](List.empty, List.empty)
-    val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
-
-    def service: HttpRoutes[IO] =
-      Router(
-        "/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, externalConfig, repo, signAndValidate).routes
-      )
-
-    def request = Request[IO](method = Method.GET, uri = loginPath)
-
-    def response: IO[Response[IO]] = service.orNotFound.run(request)
-
-    val test = for {
-      result <- response
-      body   <- result.as[String]
-    } yield (result, body)
-
-    test.map { output =>
-      val (result, body) = output
-      assertEquals(result.status, Status.Ok)
-      assertEquals(body, expectedHtml.toString)
-    }
-  }
-
-  test("POST /login must return 415 - Unsupported Media Type if the request is malformed") {
-    val clock                = java.time.Clock.systemUTC
-    val authenticationConfig = configuration.service.authentication
-    val externalConfig       = configuration.service.external
-    val repo                 = new TestAuthenticationRepository[IO](List.empty, List.empty)
-    val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
-
-    def service: HttpRoutes[IO] =
-      Router(
-        "/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, externalConfig, repo, signAndValidate).routes
-      )
-
-    val payload = "This is not the request body you are looking for.".getBytes(StandardCharsets.UTF_8)
-    def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
-
-    def response: IO[Response[IO]] = service.orNotFound.run(request)
-
-    val test = for {
-      result <- response
-      body   <- result.as[String]
-    } yield (result, body)
-
-    test.map { output =>
-      val (result, body) = output
-      assertEquals(result.status, Status.UnsupportedMediaType)
-      assertEquals(
-        body,
-        "Media type supplied in Content-Type header is not supported. Expected one of the following media ranges: application/*"
-      )
-    }
-  }
+    protected final val configuration: SmedereeHubConfig =
+        ConfigSource
+            .fromConfig(ConfigFactory.load(getClass.getClassLoader))
+            .at(SmedereeHubConfig.location)
+            .loadOrThrow[SmedereeHubConfig]
 
-  test("POST /login must return 400 - Bad Request if the form data is invalid") {
-    val expectedErrors = LoginForm.validate(Map.empty).leftMap(FormErrors.fromNec).swap.toOption.get
-    val expectedHtml =
-      views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(formErrors =
-        expectedErrors
-      )
-
-    val clock                = java.time.Clock.systemUTC
-    val authenticationConfig = configuration.service.authentication
-    val externalConfig       = configuration.service.external
-    val repo                 = new TestAuthenticationRepository[IO](List.empty, List.empty)
-    val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
-
-    def service: HttpRoutes[IO] =
-      Router(
-        "/" -> new AuthenticationRoutes[IO](clock, authenticationConfig, externalConfig, repo, signAndValidate).routes
-      )
-
-    val payload = UrlForm.empty
-    def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
-
-    def response: IO[Response[IO]] = service.orNotFound.run(request)
-
-    val test = for {
-      result <- response
-      body   <- result.as[String]
-    } yield (result, body)
-
-    test.map { output =>
-      val (result, body) = output
-      assertEquals(result.status, Status.BadRequest)
-      assertEquals(body, expectedHtml.toString)
-    }
-  }
+    test("GET /login must return the login form for guest users") {
+        val expectedHtml =
+            views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)()
 
-  test("POST /login must return 400 - Bad Request AND increase failed attempts if the credentials are invalid") {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
         val clock                = java.time.Clock.systemUTC
         val authenticationConfig = configuration.service.authentication
         val externalConfig       = configuration.service.external
-        val repo                 = new TestAuthenticationRepository[IO](List(account), List.empty)
+        val repo                 = new TestAuthenticationRepository[IO](List.empty, List.empty)
         val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
 
         def service: HttpRoutes[IO] =
-          Router(
-            "/" -> new AuthenticationRoutes[IO](
-              clock,
-              authenticationConfig,
-              externalConfig,
-              repo,
-              signAndValidate
-            ).routes
-          )
+            Router(
+                "/" -> new AuthenticationRoutes[IO](
+                    clock,
+                    authenticationConfig,
+                    externalConfig,
+                    repo,
+                    signAndValidate
+                ).routes
+            )
 
-        val payload = UrlForm.empty
-          .updateFormField(LoginForm.fieldName.toString, account.name.toString)
-          .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter")
-        def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
-
-        val expectedHtml =
-          views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(
-            formData = Map(LoginForm.fieldName.toString -> account.name.toString),
-            Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
-          )
+        def request = Request[IO](method = Method.GET, uri = loginPath)
 
         def response: IO[Response[IO]] = service.orNotFound.run(request)
 
         val test = for {
-          result <- response
-          body   <- result.as[String]
-          failed <- repo.getFailed
-        } yield (result, body, failed)
+            result <- response
+            body   <- result.as[String]
+        } yield (result, body)
 
         test.map { output =>
-          val (result, body, failed) = output
-          assertEquals(result.status, Status.BadRequest)
-          assertEquals(failed.find(_._1 === account.uid), Option((account.uid, 1)))
-          assertEquals(body, expectedHtml.toString)
+            val (result, body) = output
+            assertEquals(result.status, Status.Ok)
+            assertEquals(body, expectedHtml.toString)
         }
     }
-  }
 
-  test(
-    "POST /login must return 400 - Bad Request AND lock the account if the credentials are invalid and failed attempts are too high"
-  ) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
+    test("POST /login must return 415 - Unsupported Media Type if the request is malformed") {
         val clock                = java.time.Clock.systemUTC
         val authenticationConfig = configuration.service.authentication
         val externalConfig       = configuration.service.external
-        val repo                 = new TestAuthenticationRepository[IO](List(account), List.empty)
+        val repo                 = new TestAuthenticationRepository[IO](List.empty, List.empty)
         val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
 
-        for (_ <- 0 to authenticationConfig.lockAfter.toInt) yield repo.incrementFailedAttempts(account.uid)
-
         def service: HttpRoutes[IO] =
-          Router(
-            "/" -> new AuthenticationRoutes[IO](
-              clock,
-              authenticationConfig,
-              externalConfig,
-              repo,
-              signAndValidate
-            ).routes
-          )
+            Router(
+                "/" -> new AuthenticationRoutes[IO](
+                    clock,
+                    authenticationConfig,
+                    externalConfig,
+                    repo,
+                    signAndValidate
+                ).routes
+            )
 
-        val payload = UrlForm.empty
-          .updateFormField(LoginForm.fieldName.toString, account.name.toString)
-          .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter")
+        val payload = "This is not the request body you are looking for.".getBytes(StandardCharsets.UTF_8)
         def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
 
-        val expectedHtml =
-          views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(
-            formData = Map(LoginForm.fieldName.toString -> account.name.toString),
-            Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
-          )
-
         def response: IO[Response[IO]] = service.orNotFound.run(request)
 
         val test = for {
-          result <- response
-          body   <- result.as[String]
-          locked <- repo.getLocked
-        } yield (result, body, locked)
+            result <- response
+            body   <- result.as[String]
+        } yield (result, body)
 
         test.map { output =>
-          val (result, body, locked) = output
-          assertEquals(result.status, Status.BadRequest)
-          assert(locked.exists(_ === account.uid), s"Account was not locked! ($locked)")
-          assertEquals(body, expectedHtml.toString)
+            val (result, body) = output
+            assertEquals(result.status, Status.UnsupportedMediaType)
+            assertEquals(
+                body,
+                "Media type supplied in Content-Type header is not supported. Expected one of the following media ranges: application/*"
+            )
         }
     }
-  }
 
-  test("POST /login must return 400 - Bad Request if the credentials are valid but the account is locked") {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
+    test("POST /login must return 400 - Bad Request if the form data is invalid") {
+        val expectedErrors = LoginForm.validate(Map.empty).leftMap(FormErrors.fromNec).swap.toOption.get
+        val expectedHtml =
+            views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(formErrors =
+                expectedErrors
+            )
+
         val clock                = java.time.Clock.systemUTC
         val authenticationConfig = configuration.service.authentication
         val externalConfig       = configuration.service.external
-        // A locked account is not supposed to be returned by findAccount, so we leave the account list empty.
-        val repo            = new TestAuthenticationRepository[IO](List.empty, List.empty)
-        val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret)
+        val repo                 = new TestAuthenticationRepository[IO](List.empty, List.empty)
+        val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
 
         def service: HttpRoutes[IO] =
-          Router(
-            "/" -> new AuthenticationRoutes[IO](
-              clock,
-              authenticationConfig,
-              externalConfig,
-              repo,
-              signAndValidate
-            ).routes
-          )
+            Router(
+                "/" -> new AuthenticationRoutes[IO](
+                    clock,
+                    authenticationConfig,
+                    externalConfig,
+                    repo,
+                    signAndValidate
+                ).routes
+            )
 
         val payload = UrlForm.empty
-          .updateFormField(LoginForm.fieldName.toString, account.name.toString)
-          .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter")
         def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
 
-        val expectedHtml =
-          views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(
-            formData = Map(LoginForm.fieldName.toString -> account.name.toString),
-            Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
-          )
-
         def response: IO[Response[IO]] = service.orNotFound.run(request)
 
         val test = for {
-          result <- response
-          body   <- result.as[String]
+            result <- response
+            body   <- result.as[String]
         } yield (result, body)
 
         test.map { output =>
-          val (result, body) = output
-          assertEquals(result.status, Status.BadRequest)
-          assertEquals(body, expectedHtml.toString)
+            val (result, body) = output
+            assertEquals(result.status, Status.BadRequest)
+            assertEquals(body, expectedHtml.toString)
         }
     }
-  }
 
-  test("POST /login must return 303 - See Other AND set the authentication cookie if the credentials are valid") {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val clock                = java.time.Clock.systemUTC
-        val authenticationConfig = configuration.service.authentication
-        val externalConfig       = configuration.service.external
-        val repo                 = new TestAuthenticationRepository[IO](List(account), List.empty)
-        val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
-
-        def service: HttpRoutes[IO] =
-          Router(
-            "/" -> new AuthenticationRoutes[IO](
-              clock,
-              authenticationConfig,
-              externalConfig,
-              repo,
-              signAndValidate
-            ).routes
-          )
-
-        val payload = UrlForm.empty
-          .updateFormField(LoginForm.fieldName.toString, account.name.toString)
-          .updateFormField(LoginForm.fieldPassword.toString, repo.DefaultPassword)
-        def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
+    test("POST /login must return 400 - Bad Request AND increase failed attempts if the credentials are invalid") {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val clock                = java.time.Clock.systemUTC
+                val authenticationConfig = configuration.service.authentication
+                val externalConfig       = configuration.service.external
+                val repo                 = new TestAuthenticationRepository[IO](List(account), List.empty)
+                val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
+
+                def service: HttpRoutes[IO] =
+                    Router(
+                        "/" -> new AuthenticationRoutes[IO](
+                            clock,
+                            authenticationConfig,
+                            externalConfig,
+                            repo,
+                            signAndValidate
+                        ).routes
+                    )
+
+                val payload = UrlForm.empty
+                    .updateFormField(LoginForm.fieldName.toString, account.name.toString)
+                    .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter")
+                def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
+
+                val expectedHtml =
+                    views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(
+                        formData = Map(LoginForm.fieldName.toString -> account.name.toString),
+                        Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
+                    )
+
+                def response: IO[Response[IO]] = service.orNotFound.run(request)
+
+                val test = for {
+                    result <- response
+                    body   <- result.as[String]
+                    failed <- repo.getFailed
+                } yield (result, body, failed)
+
+                test.map { output =>
+                    val (result, body, failed) = output
+                    assertEquals(result.status, Status.BadRequest)
+                    assertEquals(failed.find(_._1 === account.uid), Option((account.uid, 1)))
+                    assertEquals(body, expectedHtml.toString)
+                }
+        }
+    }
 
-        def response: IO[Response[IO]] = service.orNotFound.run(request)
+    test(
+        "POST /login must return 400 - Bad Request AND lock the account if the credentials are invalid and failed attempts are too high"
+    ) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val clock                = java.time.Clock.systemUTC
+                val authenticationConfig = configuration.service.authentication
+                val externalConfig       = configuration.service.external
+                val repo                 = new TestAuthenticationRepository[IO](List(account), List.empty)
+                val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
+
+                for (_ <- 0 to authenticationConfig.lockAfter.toInt) yield repo.incrementFailedAttempts(account.uid)
+
+                def service: HttpRoutes[IO] =
+                    Router(
+                        "/" -> new AuthenticationRoutes[IO](
+                            clock,
+                            authenticationConfig,
+                            externalConfig,
+                            repo,
+                            signAndValidate
+                        ).routes
+                    )
+
+                val payload = UrlForm.empty
+                    .updateFormField(LoginForm.fieldName.toString, account.name.toString)
+                    .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter")
+                def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
+
+                val expectedHtml =
+                    views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(
+                        formData = Map(LoginForm.fieldName.toString -> account.name.toString),
+                        Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
+                    )
+
+                def response: IO[Response[IO]] = service.orNotFound.run(request)
+
+                val test = for {
+                    result <- response
+                    body   <- result.as[String]
+                    locked <- repo.getLocked
+                } yield (result, body, locked)
+
+                test.map { output =>
+                    val (result, body, locked) = output
+                    assertEquals(result.status, Status.BadRequest)
+                    assert(locked.exists(_ === account.uid), s"Account was not locked! ($locked)")
+                    assertEquals(body, expectedHtml.toString)
+                }
+        }
+    }
 
-        val test = for {
-          result <- response
-          body   <- result.as[String]
-        } yield (result, body)
+    test("POST /login must return 400 - Bad Request if the credentials are valid but the account is locked") {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val clock                = java.time.Clock.systemUTC
+                val authenticationConfig = configuration.service.authentication
+                val externalConfig       = configuration.service.external
+                // A locked account is not supposed to be returned by findAccount, so we leave the account list empty.
+                val repo            = new TestAuthenticationRepository[IO](List.empty, List.empty)
+                val signAndValidate = SignAndValidate(configuration.service.authentication.cookieSecret)
+
+                def service: HttpRoutes[IO] =
+                    Router(
+                        "/" -> new AuthenticationRoutes[IO](
+                            clock,
+                            authenticationConfig,
+                            externalConfig,
+                            repo,
+                            signAndValidate
+                        ).routes
+                    )
+
+                val payload = UrlForm.empty
+                    .updateFormField(LoginForm.fieldName.toString, account.name.toString)
+                    .updateFormField(LoginForm.fieldPassword.toString, "doesn't matter")
+                def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
+
+                val expectedHtml =
+                    views.html.login()(loginPath, None, resetPath, title = "Smederee - Login to your account".some)(
+                        formData = Map(LoginForm.fieldName.toString -> account.name.toString),
+                        Map(LoginForm.fieldGlobal -> List(FormFieldError("Invalid credentials!")))
+                    )
+
+                def response: IO[Response[IO]] = service.orNotFound.run(request)
+
+                val test = for {
+                    result <- response
+                    body   <- result.as[String]
+                } yield (result, body)
+
+                test.map { output =>
+                    val (result, body) = output
+                    assertEquals(result.status, Status.BadRequest)
+                    assertEquals(body, expectedHtml.toString)
+                }
+        }
+    }
 
-        test.map { output =>
-          val (result, body) = output
-          assertEquals(result.status, Status.SeeOther)
-          assert(body.isEmpty, "Response body must be empty!")
-          result.cookies.find(_.name === Constants.authenticationCookieName.toString).map(_.content) match {
-            case None          => fail("Authentication cookie not set!")
-            case Some(content) => assert(SignedToken.from(content).flatMap(signAndValidate.validate).nonEmpty)
-          }
+    test("POST /login must return 303 - See Other AND set the authentication cookie if the credentials are valid") {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val clock                = java.time.Clock.systemUTC
+                val authenticationConfig = configuration.service.authentication
+                val externalConfig       = configuration.service.external
+                val repo                 = new TestAuthenticationRepository[IO](List(account), List.empty)
+                val signAndValidate      = SignAndValidate(configuration.service.authentication.cookieSecret)
+
+                def service: HttpRoutes[IO] =
+                    Router(
+                        "/" -> new AuthenticationRoutes[IO](
+                            clock,
+                            authenticationConfig,
+                            externalConfig,
+                            repo,
+                            signAndValidate
+                        ).routes
+                    )
+
+                val payload = UrlForm.empty
+                    .updateFormField(LoginForm.fieldName.toString, account.name.toString)
+                    .updateFormField(LoginForm.fieldPassword.toString, repo.DefaultPassword)
+                def request = Request[IO](method = Method.POST, uri = loginPath).withEntity(payload)
+
+                def response: IO[Response[IO]] = service.orNotFound.run(request)
+
+                val test = for {
+                    result <- response
+                    body   <- result.as[String]
+                } yield (result, body)
+
+                test.map { output =>
+                    val (result, body) = output
+                    assertEquals(result.status, Status.SeeOther)
+                    assert(body.isEmpty, "Response body must be empty!")
+                    result.cookies.find(_.name === Constants.authenticationCookieName.toString).map(_.content) match {
+                        case None => fail("Authentication cookie not set!")
+                        case Some(content) =>
+                            assert(SignedToken.from(content).flatMap(signAndValidate.validate).nonEmpty)
+                    }
+                }
         }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/BaseSpec.scala	2025-01-13 17:13:25.060470984 +0000
@@ -46,405 +46,406 @@
   */
 abstract class BaseSpec extends CatsEffectSuite {
 
-  protected final val configuration: SmedereeHubConfig =
-    ConfigSource
-      .fromConfig(ConfigFactory.load(getClass.getClassLoader))
-      .at(SmedereeHubConfig.location)
-      .loadOrThrow[SmedereeHubConfig]
-
-  protected final val flyway: Flyway =
-    DatabaseMigrator
-      .configureFlyway(configuration.database.url, configuration.database.user, configuration.database.pass)
-      .cleanDisabled(false)
-      .load()
-
-  /** Connect to the DBMS using the generic "template1" database which should always be present.
-    *
-    * @param dbConfig
-    *   The database configuration.
-    * @return
-    *   The connection to the database ("template1").
-    */
-  private def connect(dbConfig: DatabaseConfig): IO[java.sql.Connection] =
-    for {
-      _        <- IO(Class.forName(dbConfig.driver))
-      database <- IO(dbConfig.url.split("/").reverse.take(1).mkString)
-      connection <- IO(
-        java.sql.DriverManager
-          .getConnection(dbConfig.url.replace(database, "template1"), dbConfig.user, dbConfig.pass)
-      )
-    } yield connection
-
-  @nowarn("msg=discarded non-Unit value.*")
-  override def beforeAll(): Unit = {
-    // Extract the database name from the URL.
-    val database = configuration.database.url.split("/").reverse.take(1).mkString
-    val db       = Resource.make(connect(configuration.database))(con => IO(con.close()))
-    // Create the test database if it does not already exist.
-    db.use { connection =>
-      for {
-        statement <- IO(connection.createStatement())
-        exists <- IO(
-          statement.executeQuery(
-            s"""SELECT datname FROM pg_catalog.pg_database WHERE datname = '$database'"""
-          )
-        )
-        _ <- IO {
-          if (!exists.next())
-            statement.execute(s"""CREATE DATABASE "$database"""")
-        }
-        _ <- IO(exists.close)
-        _ <- IO(statement.close)
-      } yield ()
-    }.unsafeRunSync()
-  }
-
-  override def beforeEach(context: BeforeEach): Unit = {
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-    val _ = flyway.migrate()
-  }
-
-  override def afterEach(context: AfterEach): Unit = {
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
-
-  /** Find and return a free port on the local machine by starting a server socket and closing it. The port number used
-    * by the socket is marked to allow reuse, considered free and returned.
-    *
-    * @return
-    *   An optional port number if a free one can be found.
-    */
-  protected def findFreePort(): Option[Port] = {
-    val socket = new ServerSocket(0)
-    val port   = socket.getLocalPort
-    socket.setReuseAddress(true) // Allow instant rebinding of the socket.
-    socket.close()               // Free the socket for further use by closing it.
-    Port.fromInt(port)
-  }
-
-  /** Provide a resource with a database connection to allow db operations and proper resource release later.
-    *
-    * @param cfg
-    *   The application configuration.
-    * @return
-    *   A cats resource encapsulation a database connection as defined within the given configuration.
-    */
-  protected def connectToDb(cfg: SmedereeHubConfig): Resource[IO, java.sql.Connection] =
-    Resource.make(
-      IO.delay(java.sql.DriverManager.getConnection(cfg.database.url, cfg.database.user, cfg.database.pass))
-    )(c => IO.delay(c.close()))
-
-  /** Create the given account in the database.
-    *
-    * @param account
-    *   The account to be created.
-    * @param hash
-    *   A password hash to be stored.
-    * @param unlockToken
-    *   An optional unlock token to be stored.
-    * @param attempts
-    *   Optional number of failed login attempts.
-    * @param validationToken
-    *   An optional validation token to be stored.
-    * @return
-    *   The number of affected database rows.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.defaultArgs"))
-  protected def createAccount(
-      account: Account,
-      hash: PasswordHash,
-      unlockToken: Option[UnlockToken] = None,
-      attempts: Option[Int] = None,
-      validationToken: Option[ValidationToken] = None
-  ): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay {
-          (unlockToken, validationToken) match {
-            case (None, None) =>
-              con.prepareStatement(
-                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, created_at, updated_at, validated_email) VALUES(?, ?, ?, ?, ?, NOW(), NOW(), ?)"""
-              )
-            case (Some(_), None) =>
-              con.prepareStatement(
-                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, unlock_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
-              )
-            case (None, Some(_)) =>
-              con.prepareStatement(
-                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
-              )
-            case (Some(_), Some(_)) =>
-              con.prepareStatement(
-                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, unlock_token, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
-              )
-          }
-        }
-        _ <- IO.delay(statement.setObject(1, account.uid))
-        _ <- IO.delay(statement.setString(2, account.name.toString))
-        _ <- IO.delay(statement.setString(3, account.email.toString))
-        _ <- IO.delay(statement.setString(4, hash.toString))
-        _ <- IO.delay(statement.setInt(5, attempts.getOrElse(1)))
-        _ <- IO.delay(statement.setBoolean(6, account.validatedEmail))
-        _ <- (unlockToken, validationToken) match {
-          case (None, None)     => IO.unit
-          case (Some(ut), None) => IO.delay(statement.setString(7, ut.toString))
-          case (None, Some(vt)) => IO.delay(statement.setString(7, vt.toString))
-          case (Some(ut), Some(vt)) =>
-            IO.delay {
-              statement.setString(7, ut.toString)
-              statement.setString(8, vt.toString)
-            }
-        }
-        _ <- IO.delay {
-          unlockToken.foreach { token =>
-            statement.setString(7, token.toString)
-          }
-        }
-        r <- IO.delay(statement.executeUpdate())
-        _ <- IO.delay(statement.close())
-      } yield r
+    protected final val configuration: SmedereeHubConfig =
+        ConfigSource
+            .fromConfig(ConfigFactory.load(getClass.getClassLoader))
+            .at(SmedereeHubConfig.location)
+            .loadOrThrow[SmedereeHubConfig]
+
+    protected final val flyway: Flyway =
+        DatabaseMigrator
+            .configureFlyway(configuration.database.url, configuration.database.user, configuration.database.pass)
+            .cleanDisabled(false)
+            .load()
+
+    /** Connect to the DBMS using the generic "template1" database which should always be present.
+      *
+      * @param dbConfig
+      *   The database configuration.
+      * @return
+      *   The connection to the database ("template1").
+      */
+    private def connect(dbConfig: DatabaseConfig): IO[java.sql.Connection] =
+        for {
+            _        <- IO(Class.forName(dbConfig.driver))
+            database <- IO(dbConfig.url.split("/").reverse.take(1).mkString)
+            connection <- IO(
+                java.sql.DriverManager
+                    .getConnection(dbConfig.url.replace(database, "template1"), dbConfig.user, dbConfig.pass)
+            )
+        } yield connection
+
+    @nowarn("msg=discarded non-Unit value.*")
+    override def beforeAll(): Unit = {
+        // Extract the database name from the URL.
+        val database = configuration.database.url.split("/").reverse.take(1).mkString
+        val db       = Resource.make(connect(configuration.database))(con => IO(con.close()))
+        // Create the test database if it does not already exist.
+        db.use { connection =>
+            for {
+                statement <- IO(connection.createStatement())
+                exists <- IO(
+                    statement.executeQuery(
+                        s"""SELECT datname FROM pg_catalog.pg_database WHERE datname = '$database'"""
+                    )
+                )
+                _ <- IO {
+                    if (!exists.next())
+                        statement.execute(s"""CREATE DATABASE "$database"""")
+                }
+                _ <- IO(exists.close)
+                _ <- IO(statement.close)
+            } yield ()
+        }.unsafeRunSync()
     }
 
-  /** Create the given user session in the database.
-    *
-    * @param session
-    *   The session that shall be created. The corresponding user account must exist!
-    * @return
-    *   The number of affected database rows.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def createUserSession(session: Session): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """INSERT INTO "hub"."sessions" (id, uid, created_at, updated_at) VALUES(?, ?, ?, ?)"""
-          )
-        )
-        _ <- IO.delay(statement.setString(1, session.id.toString))
-        _ <- IO.delay(statement.setObject(2, session.uid))
-        _ <- IO.delay(
-          statement.setTimestamp(3, java.sql.Timestamp.from(session.createdAt.toInstant))
-        )
-        _ <- IO.delay(
-          statement.setTimestamp(4, java.sql.Timestamp.from(session.updatedAt.toInstant))
-        )
-        r <- IO.delay(statement.executeUpdate())
-        _ <- IO.delay(statement.close())
-      } yield r
+    override def beforeEach(context: BeforeEach): Unit = {
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+        val _ = flyway.migrate()
     }
 
-  /** Load the account with the given uid from the database.
-    *
-    * @param uid
-    *   The unique identifier for the account.
-    * @return
-    *   An option to the account if it exists.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.null"))
-  protected def loadAccount(uid: UserId): IO[Option[Account]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT uid, name, email, password, created_at, updated_at, validated_email, language FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, uid))
-        result <- IO.delay(statement.executeQuery)
-        account <- IO.delay {
-          if (result.next()) {
-            val language =
-              if (result.getString("language") =!= null)
-                LanguageCode.from(result.getString("language"))
-              else
-                None
-            Option(
-              Account(
-                uid = uid,
-                name = Username(result.getString("name")),
-                email = EmailAddress(result.getString("email")),
-                validatedEmail = result.getBoolean("validated_email"),
-                language = language
-              )
-            )
-          } else {
-            None
-          }
-        }
-        _ <- IO(statement.close())
-      } yield account
+    override def afterEach(context: AfterEach): Unit = {
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
     }
 
-  /** Load the password hash for the account with the given unique user id.
-    *
-    * @param uid
-    *   The unique identifier for the account.
-    * @return
-    *   An option to the password hash if the user account exists.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.null"))
-  protected def loadPasswordHash(uid: UserId): IO[Option[PasswordHash]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement("""SELECT password FROM "hub"."accounts" WHERE uid = ? LIMIT 1""")
-        )
-        _      <- IO.delay(statement.setObject(1, uid))
-        result <- IO.delay(statement.executeQuery)
-        password <- IO.delay {
-          if (result.next()) {
-            Option(result.getString("password")).flatMap(PasswordHash.from)
-          } else {
-            None
-          }
-        }
-        _ <- IO.delay(statement.close())
-      } yield password
+    /** Find and return a free port on the local machine by starting a server socket and closing it. The port number
+      * used by the socket is marked to allow reuse, considered free and returned.
+      *
+      * @return
+      *   An optional port number if a free one can be found.
+      */
+    protected def findFreePort(): Option[Port] = {
+        val socket = new ServerSocket(0)
+        val port   = socket.getLocalPort
+        socket.setReuseAddress(true) // Allow instant rebinding of the socket.
+        socket.close()               // Free the socket for further use by closing it.
+        Port.fromInt(port)
     }
 
-  /** Load the password reset related columns for the account with the given unique user id.
-    *
-    * @param uid
-    *   The unique identifier for the account.
-    * @return
-    *   An option of the columns if it exists.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.null"))
-  protected def loadResetColumns(uid: UserId): IO[Option[(Option[OffsetDateTime], Option[ResetToken])]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT reset_expiry, reset_token FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, uid))
-        result <- IO.delay(statement.executeQuery)
-        columns <- IO.delay {
-          if (result.next()) {
-            val expiry =
-              if (result.getString("reset_expiry") =!= null)
-                Option(result.getObject("reset_expiry", classOf[OffsetDateTime]))
-              else
-                None
-            val token =
-              if (result.getString("reset_token") =!= null)
-                ResetToken.from(result.getString("reset_token"))
-              else
-                None
-            Option(expiry, token)
-          } else {
-            None
-          }
+    /** Provide a resource with a database connection to allow db operations and proper resource release later.
+      *
+      * @param cfg
+      *   The application configuration.
+      * @return
+      *   A cats resource encapsulation a database connection as defined within the given configuration.
+      */
+    protected def connectToDb(cfg: SmedereeHubConfig): Resource[IO, java.sql.Connection] =
+        Resource.make(
+            IO.delay(java.sql.DriverManager.getConnection(cfg.database.url, cfg.database.user, cfg.database.pass))
+        )(c => IO.delay(c.close()))
+
+    /** Create the given account in the database.
+      *
+      * @param account
+      *   The account to be created.
+      * @param hash
+      *   A password hash to be stored.
+      * @param unlockToken
+      *   An optional unlock token to be stored.
+      * @param attempts
+      *   Optional number of failed login attempts.
+      * @param validationToken
+      *   An optional validation token to be stored.
+      * @return
+      *   The number of affected database rows.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.defaultArgs"))
+    protected def createAccount(
+        account: Account,
+        hash: PasswordHash,
+        unlockToken: Option[UnlockToken] = None,
+        attempts: Option[Int] = None,
+        validationToken: Option[ValidationToken] = None
+    ): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay {
+                    (unlockToken, validationToken) match {
+                        case (None, None) =>
+                            con.prepareStatement(
+                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, created_at, updated_at, validated_email) VALUES(?, ?, ?, ?, ?, NOW(), NOW(), ?)"""
+                            )
+                        case (Some(_), None) =>
+                            con.prepareStatement(
+                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, unlock_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
+                            )
+                        case (None, Some(_)) =>
+                            con.prepareStatement(
+                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
+                            )
+                        case (Some(_), Some(_)) =>
+                            con.prepareStatement(
+                                """INSERT INTO "hub"."accounts" (uid, name, email, password, failed_attempts, validated_email, locked_at, unlock_token, validation_token, created_at, updated_at) VALUES(?, ?, ?, ?, ?, ?, NOW(), ?, NOW(), NOW())"""
+                            )
+                    }
+                }
+                _ <- IO.delay(statement.setObject(1, account.uid))
+                _ <- IO.delay(statement.setString(2, account.name.toString))
+                _ <- IO.delay(statement.setString(3, account.email.toString))
+                _ <- IO.delay(statement.setString(4, hash.toString))
+                _ <- IO.delay(statement.setInt(5, attempts.getOrElse(1)))
+                _ <- IO.delay(statement.setBoolean(6, account.validatedEmail))
+                _ <- (unlockToken, validationToken) match {
+                    case (None, None)     => IO.unit
+                    case (Some(ut), None) => IO.delay(statement.setString(7, ut.toString))
+                    case (None, Some(vt)) => IO.delay(statement.setString(7, vt.toString))
+                    case (Some(ut), Some(vt)) =>
+                        IO.delay {
+                            statement.setString(7, ut.toString)
+                            statement.setString(8, vt.toString)
+                        }
+                }
+                _ <- IO.delay {
+                    unlockToken.foreach { token =>
+                        statement.setString(7, token.toString)
+                    }
+                }
+                r <- IO.delay(statement.executeUpdate())
+                _ <- IO.delay(statement.close())
+            } yield r
         }
-        _ <- IO.delay(statement.close())
-      } yield columns
-    }
 
-  /** Load the validation related columns for the account with the given unique user id.
-    *
-    * @param uid
-    *   The unique identifier for the account.
-    * @return
-    *   An option of the columns if it exists.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def loadValidationColumns(uid: UserId): IO[Option[(Boolean, Option[ValidationToken])]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT validated_email, validation_token FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, uid))
-        result <- IO.delay(statement.executeQuery)
-        columns <- IO.delay {
-          if (result.next()) {
-            Option(
-              (
-                result.getBoolean("validated_email"),
-                ValidationToken
-                  .from(result.getString("validation_token"))
-              )
-            )
-          } else {
-            None
-          }
+    /** Create the given user session in the database.
+      *
+      * @param session
+      *   The session that shall be created. The corresponding user account must exist!
+      * @return
+      *   The number of affected database rows.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def createUserSession(session: Session): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """INSERT INTO "hub"."sessions" (id, uid, created_at, updated_at) VALUES(?, ?, ?, ?)"""
+                    )
+                )
+                _ <- IO.delay(statement.setString(1, session.id.toString))
+                _ <- IO.delay(statement.setObject(2, session.uid))
+                _ <- IO.delay(
+                    statement.setTimestamp(3, java.sql.Timestamp.from(session.createdAt.toInstant))
+                )
+                _ <- IO.delay(
+                    statement.setTimestamp(4, java.sql.Timestamp.from(session.updatedAt.toInstant))
+                )
+                r <- IO.delay(statement.executeUpdate())
+                _ <- IO.delay(statement.close())
+            } yield r
         }
-        _ <- IO.delay(statement.close())
-      } yield columns
-    }
 
-  /** Find the repository ID for the given owner and repository name.
-    *
-    * @param owner
-    *   The unique ID of the user account that owns the repository.
-    * @param name
-    *   The repository name which must be unique in regard to the owner.
-    * @return
-    *   An option to the internal database ID.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def loadVcsRepositoryId(owner: UserId, name: VcsRepositoryName): IO[Option[VcsRepositoryId]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT id FROM "hub"."repositories" WHERE owner = ? AND name = ? LIMIT 1"""
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, owner))
-        _      <- IO.delay(statement.setString(2, name.toString))
-        result <- IO.delay(statement.executeQuery)
-        account <- IO.delay {
-          if (result.next()) {
-            VcsRepositoryId.from(result.getLong("id"))
-          } else {
-            None
-          }
+    /** Load the account with the given uid from the database.
+      *
+      * @param uid
+      *   The unique identifier for the account.
+      * @return
+      *   An option to the account if it exists.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.null"))
+    protected def loadAccount(uid: UserId): IO[Option[Account]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT uid, name, email, password, created_at, updated_at, validated_email, language FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, uid))
+                result <- IO.delay(statement.executeQuery)
+                account <- IO.delay {
+                    if (result.next()) {
+                        val language =
+                            if (result.getString("language") =!= null)
+                                LanguageCode.from(result.getString("language"))
+                            else
+                                None
+                        Option(
+                            Account(
+                                uid = uid,
+                                name = Username(result.getString("name")),
+                                email = EmailAddress(result.getString("email")),
+                                validatedEmail = result.getBoolean("validated_email"),
+                                language = language
+                            )
+                        )
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO(statement.close())
+            } yield account
         }
-        _ <- IO(statement.close())
-      } yield account
-    }
 
-  /** Delete the given directory recursively.
-    *
-    * @param path
-    *   The path on the filesystem to the directory that shall be deleted.
-    * @return
-    *   `true` if the directory was deleted.
-    */
-  protected def deleteDirectory(path: Path): IO[Boolean] =
-    IO.delay {
-      if (path.toString.trim =!= "/") {
-        Files.walkFileTree(
-          path,
-          new FileVisitor[Path] {
-            override def visitFileFailed(file: Path, exc: IOException): FileVisitResult = FileVisitResult.CONTINUE
-
-            override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
-              Files.delete(file)
-              FileVisitResult.CONTINUE
-            }
-
-            override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult =
-              FileVisitResult.CONTINUE
-
-            override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
-              Files.delete(dir)
-              FileVisitResult.CONTINUE
-            }
-          }
-        )
-        Files.deleteIfExists(path)
-      } else false
-    }
+    /** Load the password hash for the account with the given unique user id.
+      *
+      * @param uid
+      *   The unique identifier for the account.
+      * @return
+      *   An option to the password hash if the user account exists.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.null"))
+    protected def loadPasswordHash(uid: UserId): IO[Option[PasswordHash]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement("""SELECT password FROM "hub"."accounts" WHERE uid = ? LIMIT 1""")
+                )
+                _      <- IO.delay(statement.setObject(1, uid))
+                result <- IO.delay(statement.executeQuery)
+                password <- IO.delay {
+                    if (result.next()) {
+                        Option(result.getString("password")).flatMap(PasswordHash.from)
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO.delay(statement.close())
+            } yield password
+        }
+
+    /** Load the password reset related columns for the account with the given unique user id.
+      *
+      * @param uid
+      *   The unique identifier for the account.
+      * @return
+      *   An option of the columns if it exists.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.null"))
+    protected def loadResetColumns(uid: UserId): IO[Option[(Option[OffsetDateTime], Option[ResetToken])]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT reset_expiry, reset_token FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, uid))
+                result <- IO.delay(statement.executeQuery)
+                columns <- IO.delay {
+                    if (result.next()) {
+                        val expiry =
+                            if (result.getString("reset_expiry") =!= null)
+                                Option(result.getObject("reset_expiry", classOf[OffsetDateTime]))
+                            else
+                                None
+                        val token =
+                            if (result.getString("reset_token") =!= null)
+                                ResetToken.from(result.getString("reset_token"))
+                            else
+                                None
+                        Option(expiry, token)
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO.delay(statement.close())
+            } yield columns
+        }
+
+    /** Load the validation related columns for the account with the given unique user id.
+      *
+      * @param uid
+      *   The unique identifier for the account.
+      * @return
+      *   An option of the columns if it exists.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def loadValidationColumns(uid: UserId): IO[Option[(Boolean, Option[ValidationToken])]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT validated_email, validation_token FROM "hub"."accounts" WHERE uid = ? LIMIT 1"""
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, uid))
+                result <- IO.delay(statement.executeQuery)
+                columns <- IO.delay {
+                    if (result.next()) {
+                        Option(
+                            (
+                                result.getBoolean("validated_email"),
+                                ValidationToken
+                                    .from(result.getString("validation_token"))
+                            )
+                        )
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO.delay(statement.close())
+            } yield columns
+        }
+
+    /** Find the repository ID for the given owner and repository name.
+      *
+      * @param owner
+      *   The unique ID of the user account that owns the repository.
+      * @param name
+      *   The repository name which must be unique in regard to the owner.
+      * @return
+      *   An option to the internal database ID.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def loadVcsRepositoryId(owner: UserId, name: VcsRepositoryName): IO[Option[VcsRepositoryId]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT id FROM "hub"."repositories" WHERE owner = ? AND name = ? LIMIT 1"""
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, owner))
+                _      <- IO.delay(statement.setString(2, name.toString))
+                result <- IO.delay(statement.executeQuery)
+                account <- IO.delay {
+                    if (result.next()) {
+                        VcsRepositoryId.from(result.getLong("id"))
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO(statement.close())
+            } yield account
+        }
+
+    /** Delete the given directory recursively.
+      *
+      * @param path
+      *   The path on the filesystem to the directory that shall be deleted.
+      * @return
+      *   `true` if the directory was deleted.
+      */
+    protected def deleteDirectory(path: Path): IO[Boolean] =
+        IO.delay {
+            if (path.toString.trim =!= "/") {
+                Files.walkFileTree(
+                    path,
+                    new FileVisitor[Path] {
+                        override def visitFileFailed(file: Path, exc: IOException): FileVisitResult =
+                            FileVisitResult.CONTINUE
+
+                        override def visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult = {
+                            Files.delete(file)
+                            FileVisitResult.CONTINUE
+                        }
+
+                        override def preVisitDirectory(dir: Path, attrs: BasicFileAttributes): FileVisitResult =
+                            FileVisitResult.CONTINUE
+
+                        override def postVisitDirectory(dir: Path, exc: IOException): FileVisitResult = {
+                            Files.delete(dir)
+                            FileVisitResult.CONTINUE
+                        }
+                    }
+                )
+                Files.deleteIfExists(path)
+            } else false
+        }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/config/ServiceConfigTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -25,64 +25,64 @@
 import munit.*
 
 final class ServiceConfigTest extends FunSuite {
-  val rawDefaultConfig = new Fixture[Config]("defaultConfig") {
-    def apply() = ConfigFactory.load(getClass.getClassLoader)
-  }
-
-  override def munitFixtures = List(rawDefaultConfig)
-
-  test("must load from the default configuration successfully") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeHubConfig.location.toString}.service")
-      .load[ServiceConfig] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(_)     => assert(true)
-    }
-  }
-
-  test("default configuration must have authentication enabled") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeHubConfig.location.toString}.service")
-      .load[ServiceConfig] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(cfg)   => assert(cfg.authentication.enabled)
-    }
-  }
-
-  test("default configuration must have billing disabled") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeHubConfig.location.toString}.service")
-      .load[ServiceConfig] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(cfg)   => assert(cfg.billing.enabled === false)
-    }
-  }
-
-  test("default configuration must have sign up enabled") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeHubConfig.location.toString}.service")
-      .load[ServiceConfig] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(cfg)   => assert(cfg.signup.enabled)
-    }
-  }
-
-  test("default values for external linking must be setup for local development") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeHubConfig.location.toString}.service")
-      .load[ServiceConfig] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(cfg) =>
-        val externalCfg = cfg.external
-        assert(externalCfg.host === cfg.host)
-        assert(externalCfg.port === Option(cfg.port))
-        assert(externalCfg.path.isEmpty)
-        assert(externalCfg.scheme === Uri.Scheme.http)
+    val rawDefaultConfig = new Fixture[Config]("defaultConfig") {
+        def apply() = ConfigFactory.load(getClass.getClassLoader)
+    }
+
+    override def munitFixtures = List(rawDefaultConfig)
+
+    test("must load from the default configuration successfully") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeHubConfig.location.toString}.service")
+            .load[ServiceConfig] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(_)     => assert(true)
+            }
+    }
+
+    test("default configuration must have authentication enabled") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeHubConfig.location.toString}.service")
+            .load[ServiceConfig] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(cfg)   => assert(cfg.authentication.enabled)
+            }
+    }
+
+    test("default configuration must have billing disabled") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeHubConfig.location.toString}.service")
+            .load[ServiceConfig] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(cfg)   => assert(cfg.billing.enabled === false)
+            }
+    }
+
+    test("default configuration must have sign up enabled") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeHubConfig.location.toString}.service")
+            .load[ServiceConfig] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(cfg)   => assert(cfg.signup.enabled)
+            }
+    }
+
+    test("default values for external linking must be setup for local development") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeHubConfig.location.toString}.service")
+            .load[ServiceConfig] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(cfg) =>
+                    val externalCfg = cfg.external
+                    assert(externalCfg.host === cfg.host)
+                    assert(externalCfg.port === Option(cfg.port))
+                    assert(externalCfg.path.isEmpty)
+                    assert(externalCfg.scheme === Uri.Scheme.http)
+            }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DatabaseMigratorTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DatabaseMigratorTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DatabaseMigratorTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DatabaseMigratorTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -22,36 +22,36 @@
 import de.smederee.TestTags.*
 
 final class DatabaseMigratorTest extends BaseSpec {
-  override def beforeEach(context: BeforeEach): Unit = {
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
+    override def beforeEach(context: BeforeEach): Unit = {
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+    }
 
-  override def afterEach(context: AfterEach): Unit = {
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
+    override def afterEach(context: AfterEach): Unit = {
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+    }
 
-  test("DatabaseMigrator must update available outdated database".tag(NeedsDatabase)) {
-    val dbConfig = configuration.database
-    val migrator = new DatabaseMigrator[IO]
-    val test     = migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
-    test.map(result => assert(result.migrationsExecuted > 0))
-  }
+    test("DatabaseMigrator must update available outdated database".tag(NeedsDatabase)) {
+        val dbConfig = configuration.database
+        val migrator = new DatabaseMigrator[IO]
+        val test     = migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
+        test.map(result => assert(result.migrationsExecuted > 0))
+    }
 
-  test("DatabaseMigrator must not update an up to date database".tag(NeedsDatabase)) {
-    val dbConfig = configuration.database
-    val migrator = new DatabaseMigrator[IO]
-    val test = for {
-      _ <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
-      r <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
-    } yield r
-    test.map(result => assert(result.migrationsExecuted === 0))
-  }
+    test("DatabaseMigrator must not update an up to date database".tag(NeedsDatabase)) {
+        val dbConfig = configuration.database
+        val migrator = new DatabaseMigrator[IO]
+        val test = for {
+            _ <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
+            r <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
+        } yield r
+        test.map(result => assert(result.migrationsExecuted === 0))
+    }
 
-  test("DatabaseMigrator must throw an exception if the database is not available".tag(NeedsDatabase)) {
-    val migrator = new DatabaseMigrator[IO]
-    val test     = migrator.migrate("jdbc:nodriver://", "", "")
-    test.attempt.map(r => assert(r.isLeft))
-  }
+    test("DatabaseMigrator must throw an exception if the database is not available".tag(NeedsDatabase)) {
+        val migrator = new DatabaseMigrator[IO]
+        val test     = migrator.migrate("jdbc:nodriver://", "", "")
+        test.attempt.map(r => assert(r.isLeft))
+    }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAccountManagementRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAccountManagementRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAccountManagementRepositoryTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAccountManagementRepositoryTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -30,268 +30,268 @@
 import doobie.*
 
 final class DoobieAccountManagementRepositoryTest extends BaseSpec {
-  val sshKeyWithComment = ResourceSuiteLocalFixture(
-    "ssh-key-with-comment",
-    Resource.make(IO {
-      val input = scala.io.Source
-        .fromInputStream(
-          getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
-          "UTF-8"
-        )
-        .getLines()
-        .mkString
-      val keyString = SshPublicKeyString(input)
-      PublicSshKey.from(UUID.randomUUID())(UserId.randomUserId)(OffsetDateTime.now(ZoneOffset.UTC))(keyString)
-    })(_ => IO.unit)
-  )
-
-  val sshKeyWithoutComment = ResourceSuiteLocalFixture(
-    "ssh-key-without-comment",
-    Resource.make(IO {
-      val input = scala.io.Source
-        .fromInputStream(
-          getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
-          "UTF-8"
-        )
-        .getLines()
-        .mkString
-      val keyString = SshPublicKeyString(input)
-      PublicSshKey.from(UUID.randomUUID())(UserId.randomUserId)(OffsetDateTime.now(ZoneOffset.UTC))(keyString)
-    })(_ => IO.unit)
-  )
-
-  override def munitFixtures = List(sshKeyWithComment, sshKeyWithoutComment)
-
-  test("addSshKey must save the key to the database".tag(NeedsDatabase)) {
-    (genValidAccount.sample, sshKeyWithComment()) match {
-      case (Some(account), Some(sshKey)) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAccountManagementRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _    <- createAccount(account, hash, None, Option(attempts))
-          w    <- repo.addSshKey(sshKey.copy(ownerId = account.uid))
-          keys <- repo.listSshKeys(account.uid).compile.toList
-        } yield (w, keys)
-        test.map { result =>
-          val (written, keys) = result
-          assert(written === 1, "No database rows written!")
-          assert(keys.exists(_.id === sshKey.id), "Key must be in the key list of the user!")
+    val sshKeyWithComment = ResourceSuiteLocalFixture(
+        "ssh-key-with-comment",
+        Resource.make(IO {
+            val input = scala.io.Source
+                .fromInputStream(
+                    getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
+                    "UTF-8"
+                )
+                .getLines()
+                .mkString
+            val keyString = SshPublicKeyString(input)
+            PublicSshKey.from(UUID.randomUUID())(UserId.randomUserId)(OffsetDateTime.now(ZoneOffset.UTC))(keyString)
+        })(_ => IO.unit)
+    )
+
+    val sshKeyWithoutComment = ResourceSuiteLocalFixture(
+        "ssh-key-without-comment",
+        Resource.make(IO {
+            val input = scala.io.Source
+                .fromInputStream(
+                    getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
+                    "UTF-8"
+                )
+                .getLines()
+                .mkString
+            val keyString = SshPublicKeyString(input)
+            PublicSshKey.from(UUID.randomUUID())(UserId.randomUserId)(OffsetDateTime.now(ZoneOffset.UTC))(keyString)
+        })(_ => IO.unit)
+    )
+
+    override def munitFixtures = List(sshKeyWithComment, sshKeyWithoutComment)
+
+    test("addSshKey must save the key to the database".tag(NeedsDatabase)) {
+        (genValidAccount.sample, sshKeyWithComment()) match {
+            case (Some(account), Some(sshKey)) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAccountManagementRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _    <- createAccount(account, hash, None, Option(attempts))
+                    w    <- repo.addSshKey(sshKey.copy(ownerId = account.uid))
+                    keys <- repo.listSshKeys(account.uid).compile.toList
+                } yield (w, keys)
+                test.map { result =>
+                    val (written, keys) = result
+                    assert(written === 1, "No database rows written!")
+                    assert(keys.exists(_.id === sshKey.id), "Key must be in the key list of the user!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("addSshKey must fail if a key with the same fingerprint already exists".tag(NeedsDatabase)) {
-    (genValidAccount.sample, sshKeyWithoutComment()) match {
-      case (Some(account), Some(sshKey)) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAccountManagementRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _ <- createAccount(account, hash, None, Option(attempts))
-          _ <- repo.addSshKey(sshKey.copy(ownerId = account.uid))
-          _ <- repo.addSshKey(sshKey.copy(ownerId = account.uid))
-        } yield ()
-        test.attempt.map { result =>
-          assert(result.isLeft, "Writing a key with a duplicate fingerprint must fail!")
+    test("addSshKey must fail if a key with the same fingerprint already exists".tag(NeedsDatabase)) {
+        (genValidAccount.sample, sshKeyWithoutComment()) match {
+            case (Some(account), Some(sshKey)) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAccountManagementRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _ <- createAccount(account, hash, None, Option(attempts))
+                    _ <- repo.addSshKey(sshKey.copy(ownerId = account.uid))
+                    _ <- repo.addSshKey(sshKey.copy(ownerId = account.uid))
+                } yield ()
+                test.attempt.map { result =>
+                    assert(result.isLeft, "Writing a key with a duplicate fingerprint must fail!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("deleteAccount must remove the account from the database".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAccountManagementRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _ <- createAccount(account, hash, None, Option(attempts))
-          _ <- repo.deleteAccount(account.uid)
-          o <- loadAccount(account.uid)
-        } yield o
-        test.map(result => assert(result === None, "Account not deleted from database!"))
+    test("deleteAccount must remove the account from the database".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAccountManagementRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _ <- createAccount(account, hash, None, Option(attempts))
+                    _ <- repo.deleteAccount(account.uid)
+                    o <- loadAccount(account.uid)
+                } yield o
+                test.map(result => assert(result === None, "Account not deleted from database!"))
+        }
     }
-  }
 
-  test("findByValidationToken must return the matching account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(ac) =>
-        val account  = ac.copy(validatedEmail = true)
-        val token    = ValidationToken.generate
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAccountManagementRepository[IO](tx)
-        val hash = PasswordHash("Yet another weak password!")
-        val test = for {
-          _ <- createAccount(account, hash, validationToken = token.some)
-          o <- repo.findByValidationToken(token)
-        } yield o
-        test.map(result => assert(result === Some(account)))
+    test("findByValidationToken must return the matching account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(ac) =>
+                val account  = ac.copy(validatedEmail = true)
+                val token    = ValidationToken.generate
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAccountManagementRepository[IO](tx)
+                val hash = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _ <- createAccount(account, hash, validationToken = token.some)
+                    o <- repo.findByValidationToken(token)
+                } yield o
+                test.map(result => assert(result === Some(account)))
+        }
     }
-  }
 
-  test("findPasswordHash must return correct hash".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAccountManagementRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _ <- createAccount(account, hash, None, Option(attempts))
-          o <- repo.findPasswordHash(account.uid)
-        } yield o
-        test.map(result => assert(result.exists(_ === hash), "Unexpected result from database!"))
+    test("findPasswordHash must return correct hash".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAccountManagementRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _ <- createAccount(account, hash, None, Option(attempts))
+                    o <- repo.findPasswordHash(account.uid)
+                } yield o
+                test.map(result => assert(result.exists(_ === hash), "Unexpected result from database!"))
+        }
     }
-  }
 
-  test("listSshKeys must return all keys for the user".tag(NeedsDatabase)) {
-    (genValidAccount.sample, sshKeyWithComment(), sshKeyWithoutComment()) match {
-      case (Some(account), Some(sshKeyA), Some(sshKeyB)) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAccountManagementRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _    <- createAccount(account, hash, None, Option(attempts))
-          _    <- repo.addSshKey(sshKeyA.copy(ownerId = account.uid))
-          _    <- repo.addSshKey(sshKeyB.copy(ownerId = account.uid))
-          keys <- repo.listSshKeys(account.uid).compile.toList
-        } yield keys
-        test.map { keys =>
-          assertEquals(keys.length, 2, "Expected 2 keys in the key list!")
-          assert(keys.exists(_.id === sshKeyA.id), "Key A must be in the key list of the user!")
-          assert(keys.exists(_.id === sshKeyB.id), "Key B must be in the key list of the user!")
+    test("listSshKeys must return all keys for the user".tag(NeedsDatabase)) {
+        (genValidAccount.sample, sshKeyWithComment(), sshKeyWithoutComment()) match {
+            case (Some(account), Some(sshKeyA), Some(sshKeyB)) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAccountManagementRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _    <- createAccount(account, hash, None, Option(attempts))
+                    _    <- repo.addSshKey(sshKeyA.copy(ownerId = account.uid))
+                    _    <- repo.addSshKey(sshKeyB.copy(ownerId = account.uid))
+                    keys <- repo.listSshKeys(account.uid).compile.toList
+                } yield keys
+                test.map { keys =>
+                    assertEquals(keys.length, 2, "Expected 2 keys in the key list!")
+                    assert(keys.exists(_.id === sshKeyA.id), "Key A must be in the key list of the user!")
+                    assert(keys.exists(_.id === sshKeyB.id), "Key B must be in the key list of the user!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("markAsValidated must clear the validation token and set the validated column to true".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(ac) =>
-        val account  = ac.copy(validatedEmail = true)
-        val token    = ValidationToken.generate
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAccountManagementRepository[IO](tx)
-        val hash = PasswordHash("Yet another weak password!")
-        val test = for {
-          _    <- createAccount(account, hash, validationToken = token.some)
-          _    <- repo.markAsValidated(account.uid)
-          cols <- loadValidationColumns(account.uid)
-        } yield cols
-        test.map { result =>
-          assert(result === Some((true, None)), "Unexpected result from database!")
+    test("markAsValidated must clear the validation token and set the validated column to true".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(ac) =>
+                val account  = ac.copy(validatedEmail = true)
+                val token    = ValidationToken.generate
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAccountManagementRepository[IO](tx)
+                val hash = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _    <- createAccount(account, hash, validationToken = token.some)
+                    _    <- repo.markAsValidated(account.uid)
+                    cols <- loadValidationColumns(account.uid)
+                } yield cols
+                test.map { result =>
+                    assert(result === Some((true, None)), "Unexpected result from database!")
+                }
         }
     }
-  }
 
-  test("setLanguage must set the language".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val language = genLanguageCode.sample
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAccountManagementRepository[IO](tx)
-        val hash = PasswordHash("Yet another weak password!")
-        val test = for {
-          _               <- createAccount(account, hash)
-          _               <- repo.setLanguage(account.uid, language)
-          modifiedAccount <- loadAccount(account.uid)
-        } yield modifiedAccount
-        test.map { modifiedAccount =>
-          assert(modifiedAccount.exists(_.language === language), "Written language field does not match!")
+    test("setLanguage must set the language".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val language = genLanguageCode.sample
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAccountManagementRepository[IO](tx)
+                val hash = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _               <- createAccount(account, hash)
+                    _               <- repo.setLanguage(account.uid, language)
+                    modifiedAccount <- loadAccount(account.uid)
+                } yield modifiedAccount
+                test.map { modifiedAccount =>
+                    assert(modifiedAccount.exists(_.language === language), "Written language field does not match!")
+                }
         }
     }
-  }
 
-  test("setValidationToken must set the validation token".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val token    = ValidationToken.generate
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAccountManagementRepository[IO](tx)
-        val hash = PasswordHash("Yet another weak password!")
-        val test = for {
-          _    <- createAccount(account, hash)
-          _    <- repo.setValidationToken(account.uid, token)
-          cols <- loadValidationColumns(account.uid)
-        } yield cols
-        test.map { result =>
-          assert(result === Some((account.validatedEmail, Some(token))), "Unexpected result from database!")
+    test("setValidationToken must set the validation token".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val token    = ValidationToken.generate
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAccountManagementRepository[IO](tx)
+                val hash = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _    <- createAccount(account, hash)
+                    _    <- repo.setValidationToken(account.uid, token)
+                    cols <- loadValidationColumns(account.uid)
+                } yield cols
+                test.map { result =>
+                    assert(result === Some((account.validatedEmail, Some(token))), "Unexpected result from database!")
+                }
         }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAuthenticationRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAuthenticationRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAuthenticationRepositoryTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieAuthenticationRepositoryTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -26,498 +26,498 @@
 import org.flywaydb.core.Flyway
 
 final class DoobieAuthenticationRepositoryTest extends BaseSpec {
-  override def beforeEach(context: BeforeEach): Unit = {
-    val dbConfig = configuration.database
-    val flyway: Flyway =
-      DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-    val _ = flyway.migrate()
-  }
-
-  override def afterEach(context: AfterEach): Unit = {
-    val dbConfig = configuration.database
-    val flyway: Flyway =
-      DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
-
-  test("allAccounts must return all accounts from the database".tag(NeedsDatabase)) {
-    genValidAccounts.sample match {
-      case Some(accounts) =>
-        val expected = accounts.map(_.copy(language = None))
+    override def beforeEach(context: BeforeEach): Unit = {
         val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          result <- repo.allAccounts().compile.toList
-        } yield result
-        test.map { result =>
-          assertEquals(result.size, expected.size)
-          assertEquals(result, expected.sortBy(_.name))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("createUserSession must create the user session".tag(NeedsDatabase)) {
-    (genValidSession.sample, genValidAccount.sample) match {
-      case (Some(s), Some(account)) =>
-        val session  = s.copy(uid = account.uid)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          w <- repo.createUserSession(session)
-          o <- repo.findUserSession(session.id)
-        } yield (w, o)
-        test.map { result =>
-          val (written, maybeSession) = result
-          assert(written === 1, "Creating user session must modify one database row!")
-          assert(clue(maybeSession) === clue(Option(session)))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("createUserSession must fail if the user does not exist".tag(NeedsDatabase)) {
-    genValidSession.sample match {
-      case Some(session) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- repo.createUserSession(session)
-          _ <- repo.findUserSession(session.id)
-        } yield ()
-        test.attempt.map(result => assert(result.isLeft))
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("deleteAllUserSessions must delete all sessions of the user".tag(NeedsDatabase)) {
-    (genValidSessions.sample, genValidAccount.sample) match {
-      case (Some(generatedSessions), Some(account)) =>
-        val sessions = generatedSessions.map(_.copy(uid = account.uid))
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _             <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _             <- sessions.traverse(createUserSession)
-          deleted       <- repo.deleteAllUserSessions(account.uid)
-          foundSessions <- sessions.traverse(s => repo.findUserSession(s.id))
-        } yield (deleted, foundSessions)
-        test.map { result =>
-          val (deleted, foundSessions) = result
-          assertEquals(deleted, sessions.size, "Number of deleted sessions differs from number of sessions!")
-          assert(foundSessions.flatten.isEmpty, "Not all sessions were deleted!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("deleteUserSession must delete the session".tag(NeedsDatabase)) {
-    (genValidSession.sample, genValidAccount.sample) match {
-      case (Some(s), Some(account)) =>
-        val session  = s.copy(uid = account.uid)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _       <- createUserSession(session)
-          before  <- repo.findUserSession(session.id)
-          deleted <- repo.deleteUserSession(session.id)
-          after   <- repo.findUserSession(session.id)
-        } yield (before, deleted, after)
-        test.map { result =>
-          val (before, deleted, after) = result
-          assert(before.nonEmpty, "Session must exist before deleting it!")
-          assert(deleted === 1, "Deletion must affect one database row!")
-          assert(after.isEmpty, "Session must not exist after deletion!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findAccount must return an existing account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          o <- repo.findAccount(account.uid)
-        } yield o
-        test.map { result =>
-          assert(result === Option(account))
-        }
-    }
-  }
-
-  test("findAccount must not return a locked account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(
-            account,
-            PasswordHash("I am not a password hash!"),
-            Option(UnlockToken.generate),
-            None
-          )
-          o <- repo.findAccount(account.uid)
-        } yield o
-        test.map { result =>
-          assert(result.isEmpty, "The function must not return locked accounts!")
-        }
-    }
-  }
-
-  test("findAccountByEmail must return an existing account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          o <- repo.findAccountByEmail(account.email)
-        } yield o
-        test.map { result =>
-          assert(result === Option(account))
-        }
-    }
-  }
-
-  test("findAccountByEmail must not return a locked account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(
-            account,
-            PasswordHash("I am not a password hash!"),
-            Option(UnlockToken.generate),
-            None
-          )
-          o <- repo.findAccountByEmail(account.email)
-        } yield o
-        test.map { result =>
-          assert(result.isEmpty, "The function must not return locked accounts!")
-        }
-    }
-  }
-
-  test("findAccountByName must return an existing account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          o <- repo.findAccountByName(account.name)
-        } yield o
-        test.map { result =>
-          assert(result === Option(account))
-        }
-    }
-  }
-
-  test("findAccountByName must not return a locked account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(
-            account,
-            PasswordHash("I am not a password hash!"),
-            Option(UnlockToken.generate),
-            None
-          )
-          o <- repo.findAccountByName(account.name)
-        } yield o
-        test.map { result =>
-          assert(result.isEmpty, "The function must not return locked accounts!")
-        }
-    }
-  }
-
-  test("findLockedAccount must return a locked account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo  = new DoobieAuthenticationRepository[IO](tx)
-        val token = UnlockToken.generate
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), Option(token), None)
-          o <- repo.findLockedAccount(account.name)(token.some)
-        } yield o
-        test.map { result =>
-          assert(result === Option(account))
-        }
-    }
-  }
-
-  test("findLockedAccount must return a locked account if no token is given".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo  = new DoobieAuthenticationRepository[IO](tx)
-        val token = UnlockToken.generate
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), Option(token), None)
-          o <- repo.findLockedAccount(account.name)(None)
-        } yield o
-        test.map { result =>
-          assert(result === Option(account))
-        }
-    }
-  }
-
-  test("findPasswordHashAndAttempts must return correct values".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAuthenticationRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _ <- createAccount(account, hash, None, Option(attempts))
-          o <- repo.findPasswordHashAndAttempts(account.uid)
-        } yield o
-        test.map { result =>
-          result match {
-            case Some((readHash, readAttempts)) =>
-              assert(readHash === hash)
-              assert(readAttempts === attempts)
-            case _ => fail("Unexpected result from database!")
-          }
-        }
-    }
-  }
-
-  test("findPasswordHashAndAttempts must not return values for locked accounts".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAuthenticationRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _ <- createAccount(account, hash, Option(UnlockToken.generate), Option(attempts))
-          o <- repo.findPasswordHashAndAttempts(account.uid)
-        } yield o
-        test.map { result =>
-          assert(result.isEmpty, "The function must not return locked accounts!")
-        }
-    }
-  }
-
-  test("findUserSession must find an existing session".tag(NeedsDatabase)) {
-    (genValidSession.sample, genValidAccount.sample) match {
-      case (Some(s), Some(account)) =>
-        val session  = s.copy(uid = account.uid)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieAuthenticationRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _ <- createUserSession(session)
-          o <- repo.findUserSession(session.id)
-        } yield o
-        test.map { maybeSession =>
-          assert(clue(maybeSession) === clue(Option(session)))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("incrementFailedAttempts must increment failed attempts by 1".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo     = new DoobieAuthenticationRepository[IO](tx)
-        val attempts = scala.util.Random.nextInt(128)
-        val hash     = PasswordHash("Yet another weak password!")
-        val test = for {
-          _      <- createAccount(account, hash, None, Option(attempts))
-          before <- repo.findPasswordHashAndAttempts(account.uid)
-          _      <- repo.incrementFailedAttempts(account.uid)
-          after  <- repo.findPasswordHashAndAttempts(account.uid)
-        } yield (before, after)
-        test.map { result =>
-          result match {
-            case (Some((_, before)), Some((_, after))) =>
-              assert(after - before === 1, "Attempts must be incremented by one!")
-            case _ => fail("Unexpected result from database!")
-          }
-        }
-    }
-  }
-
-  test("lockAccount must lock an account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo  = new DoobieAuthenticationRepository[IO](tx)
-        val token = UnlockToken.generate
-        val test = for {
-          _      <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          before <- repo.findAccount(account.uid)
-          _      <- repo.lockAccount(account.uid)(Option(token))
-          after  <- repo.findAccount(account.uid)
-          locked <- repo.findLockedAccount(account.name)(token.some)
-        } yield (before, after, locked)
-        test.map { result =>
-          result match {
-            case (Some(before), after, Some(locked)) =>
-              assert(after.isEmpty)
-              assert(before === locked)
-            case _ => fail("Unexpected result from database!")
-          }
+        val flyway: Flyway =
+            DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+        val _ = flyway.migrate()
+    }
+
+    override def afterEach(context: AfterEach): Unit = {
+        val dbConfig = configuration.database
+        val flyway: Flyway =
+            DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+    }
+
+    test("allAccounts must return all accounts from the database".tag(NeedsDatabase)) {
+        genValidAccounts.sample match {
+            case Some(accounts) =>
+                val expected = accounts.map(_.copy(language = None))
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    result <- repo.allAccounts().compile.toList
+                } yield result
+                test.map { result =>
+                    assertEquals(result.size, expected.size)
+                    assertEquals(result, expected.sortBy(_.name))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("createUserSession must create the user session".tag(NeedsDatabase)) {
+        (genValidSession.sample, genValidAccount.sample) match {
+            case (Some(s), Some(account)) =>
+                val session  = s.copy(uid = account.uid)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    w <- repo.createUserSession(session)
+                    o <- repo.findUserSession(session.id)
+                } yield (w, o)
+                test.map { result =>
+                    val (written, maybeSession) = result
+                    assert(written === 1, "Creating user session must modify one database row!")
+                    assert(clue(maybeSession) === clue(Option(session)))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("createUserSession must fail if the user does not exist".tag(NeedsDatabase)) {
+        genValidSession.sample match {
+            case Some(session) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- repo.createUserSession(session)
+                    _ <- repo.findUserSession(session.id)
+                } yield ()
+                test.attempt.map(result => assert(result.isLeft))
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("deleteAllUserSessions must delete all sessions of the user".tag(NeedsDatabase)) {
+        (genValidSessions.sample, genValidAccount.sample) match {
+            case (Some(generatedSessions), Some(account)) =>
+                val sessions = generatedSessions.map(_.copy(uid = account.uid))
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _             <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _             <- sessions.traverse(createUserSession)
+                    deleted       <- repo.deleteAllUserSessions(account.uid)
+                    foundSessions <- sessions.traverse(s => repo.findUserSession(s.id))
+                } yield (deleted, foundSessions)
+                test.map { result =>
+                    val (deleted, foundSessions) = result
+                    assertEquals(deleted, sessions.size, "Number of deleted sessions differs from number of sessions!")
+                    assert(foundSessions.flatten.isEmpty, "Not all sessions were deleted!")
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("deleteUserSession must delete the session".tag(NeedsDatabase)) {
+        (genValidSession.sample, genValidAccount.sample) match {
+            case (Some(s), Some(account)) =>
+                val session  = s.copy(uid = account.uid)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _       <- createUserSession(session)
+                    before  <- repo.findUserSession(session.id)
+                    deleted <- repo.deleteUserSession(session.id)
+                    after   <- repo.findUserSession(session.id)
+                } yield (before, deleted, after)
+                test.map { result =>
+                    val (before, deleted, after) = result
+                    assert(before.nonEmpty, "Session must exist before deleting it!")
+                    assert(deleted === 1, "Deletion must affect one database row!")
+                    assert(after.isEmpty, "Session must not exist after deletion!")
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findAccount must return an existing account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    o <- repo.findAccount(account.uid)
+                } yield o
+                test.map { result =>
+                    assert(result === Option(account))
+                }
+        }
+    }
+
+    test("findAccount must not return a locked account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(
+                        account,
+                        PasswordHash("I am not a password hash!"),
+                        Option(UnlockToken.generate),
+                        None
+                    )
+                    o <- repo.findAccount(account.uid)
+                } yield o
+                test.map { result =>
+                    assert(result.isEmpty, "The function must not return locked accounts!")
+                }
+        }
+    }
+
+    test("findAccountByEmail must return an existing account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    o <- repo.findAccountByEmail(account.email)
+                } yield o
+                test.map { result =>
+                    assert(result === Option(account))
+                }
+        }
+    }
+
+    test("findAccountByEmail must not return a locked account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(
+                        account,
+                        PasswordHash("I am not a password hash!"),
+                        Option(UnlockToken.generate),
+                        None
+                    )
+                    o <- repo.findAccountByEmail(account.email)
+                } yield o
+                test.map { result =>
+                    assert(result.isEmpty, "The function must not return locked accounts!")
+                }
+        }
+    }
+
+    test("findAccountByName must return an existing account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    o <- repo.findAccountByName(account.name)
+                } yield o
+                test.map { result =>
+                    assert(result === Option(account))
+                }
+        }
+    }
+
+    test("findAccountByName must not return a locked account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(
+                        account,
+                        PasswordHash("I am not a password hash!"),
+                        Option(UnlockToken.generate),
+                        None
+                    )
+                    o <- repo.findAccountByName(account.name)
+                } yield o
+                test.map { result =>
+                    assert(result.isEmpty, "The function must not return locked accounts!")
+                }
+        }
+    }
+
+    test("findLockedAccount must return a locked account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo  = new DoobieAuthenticationRepository[IO](tx)
+                val token = UnlockToken.generate
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), Option(token), None)
+                    o <- repo.findLockedAccount(account.name)(token.some)
+                } yield o
+                test.map { result =>
+                    assert(result === Option(account))
+                }
+        }
+    }
+
+    test("findLockedAccount must return a locked account if no token is given".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo  = new DoobieAuthenticationRepository[IO](tx)
+                val token = UnlockToken.generate
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), Option(token), None)
+                    o <- repo.findLockedAccount(account.name)(None)
+                } yield o
+                test.map { result =>
+                    assert(result === Option(account))
+                }
+        }
+    }
+
+    test("findPasswordHashAndAttempts must return correct values".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAuthenticationRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _ <- createAccount(account, hash, None, Option(attempts))
+                    o <- repo.findPasswordHashAndAttempts(account.uid)
+                } yield o
+                test.map { result =>
+                    result match {
+                        case Some((readHash, readAttempts)) =>
+                            assert(readHash === hash)
+                            assert(readAttempts === attempts)
+                        case _ => fail("Unexpected result from database!")
+                    }
+                }
+        }
+    }
+
+    test("findPasswordHashAndAttempts must not return values for locked accounts".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAuthenticationRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _ <- createAccount(account, hash, Option(UnlockToken.generate), Option(attempts))
+                    o <- repo.findPasswordHashAndAttempts(account.uid)
+                } yield o
+                test.map { result =>
+                    assert(result.isEmpty, "The function must not return locked accounts!")
+                }
+        }
+    }
+
+    test("findUserSession must find an existing session".tag(NeedsDatabase)) {
+        (genValidSession.sample, genValidAccount.sample) match {
+            case (Some(s), Some(account)) =>
+                val session  = s.copy(uid = account.uid)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieAuthenticationRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _ <- createUserSession(session)
+                    o <- repo.findUserSession(session.id)
+                } yield o
+                test.map { maybeSession =>
+                    assert(clue(maybeSession) === clue(Option(session)))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("incrementFailedAttempts must increment failed attempts by 1".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo     = new DoobieAuthenticationRepository[IO](tx)
+                val attempts = scala.util.Random.nextInt(128)
+                val hash     = PasswordHash("Yet another weak password!")
+                val test = for {
+                    _      <- createAccount(account, hash, None, Option(attempts))
+                    before <- repo.findPasswordHashAndAttempts(account.uid)
+                    _      <- repo.incrementFailedAttempts(account.uid)
+                    after  <- repo.findPasswordHashAndAttempts(account.uid)
+                } yield (before, after)
+                test.map { result =>
+                    result match {
+                        case (Some((_, before)), Some((_, after))) =>
+                            assert(after - before === 1, "Attempts must be incremented by one!")
+                        case _ => fail("Unexpected result from database!")
+                    }
+                }
+        }
+    }
+
+    test("lockAccount must lock an account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo  = new DoobieAuthenticationRepository[IO](tx)
+                val token = UnlockToken.generate
+                val test = for {
+                    _      <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    before <- repo.findAccount(account.uid)
+                    _      <- repo.lockAccount(account.uid)(Option(token))
+                    after  <- repo.findAccount(account.uid)
+                    locked <- repo.findLockedAccount(account.name)(token.some)
+                } yield (before, after, locked)
+                test.map { result =>
+                    result match {
+                        case (Some(before), after, Some(locked)) =>
+                            assert(after.isEmpty)
+                            assert(before === locked)
+                        case _ => fail("Unexpected result from database!")
+                    }
+                }
         }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieResetPasswordRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieResetPasswordRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieResetPasswordRepositoryTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieResetPasswordRepositoryTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -30,262 +30,266 @@
 import org.flywaydb.core.Flyway
 
 final class DoobieResetPasswordRepositoryTest extends BaseSpec {
-  override def beforeEach(context: BeforeEach): Unit = {
-    val dbConfig = configuration.database
-    val flyway: Flyway =
-      DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-    val _ = flyway.migrate()
-  }
-
-  override def afterEach(context: AfterEach): Unit = {
-    val dbConfig = configuration.database
-    val flyway: Flyway =
-      DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
-
-  test("findByNameAndResetPasswordToken must find a matching account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
-        val expected        = user.copy(language = None)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          _      <- repo.removeResetPasswordExpirationDate(user.uid)
-          result <- repo.findByNameAndResetPasswordToken(user.name, token)
-        } yield result
-        test.map { result =>
-          assertEquals(result, Option(expected))
+    override def beforeEach(context: BeforeEach): Unit = {
+        val dbConfig = configuration.database
+        val flyway: Flyway =
+            DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+        val _ = flyway.migrate()
+    }
+
+    override def afterEach(context: AfterEach): Unit = {
+        val dbConfig = configuration.database
+        val flyway: Flyway =
+            DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+    }
+
+    test("findByNameAndResetPasswordToken must find a matching account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token           = ResetToken.generate
+                val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
+                val expected        = user.copy(language = None)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    _      <- repo.removeResetPasswordExpirationDate(user.uid)
+                    result <- repo.findByNameAndResetPasswordToken(user.name, token)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, Option(expected))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findByNameAndResetPasswordToken must not respect tokens with expiration date".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          result <- repo.findByNameAndResetPasswordToken(user.name, token)
-        } yield result
-        test.map { result =>
-          assertEquals(result, None)
+    test("findByNameAndResetPasswordToken must not respect tokens with expiration date".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token           = ResetToken.generate
+                val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    result <- repo.findByNameAndResetPasswordToken(user.name, token)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, None)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findByResetPasswordToken must find a matching account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
-        val expected        = user.copy(language = None)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          result <- repo.findByResetPasswordToken(token)
-        } yield result
-        test.map { result =>
-          assertEquals(result, Option(expected))
+    test("findByResetPasswordToken must find a matching account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token           = ResetToken.generate
+                val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L)
+                val expected        = user.copy(language = None)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    result <- repo.findByResetPasswordToken(token)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, Option(expected))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findByResetPasswordToken must not return accounts with expired tokens".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).minusDays(3L)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          result <- repo.findByResetPasswordToken(token)
-        } yield result
-        test.map { result =>
-          assertEquals(result, None)
+    test("findByResetPasswordToken must not return accounts with expired tokens".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token           = ResetToken.generate
+                val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).minusDays(3L)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    result <- repo.findByResetPasswordToken(token)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, None)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findByResetPasswordToken must not return accounts without expiration date".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).minusDays(3L)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          _      <- repo.removeResetPasswordExpirationDate(user.uid)
-          result <- repo.findByResetPasswordToken(token)
-        } yield result
-        test.map { result =>
-          assertEquals(result, None)
+    test("findByResetPasswordToken must not return accounts without expiration date".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token           = ResetToken.generate
+                val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).minusDays(3L)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    _      <- repo.removeResetPasswordExpirationDate(user.uid)
+                    result <- repo.findByResetPasswordToken(token)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, None)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("removeResetPasswordExpirationDate must remove the expiration date".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L).withNano(0) // Nanos are troublesome!
-        val expected        = ((None, token.some)).some
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          _      <- repo.removeResetPasswordExpirationDate(user.uid)
-          result <- loadResetColumns(user.uid)
-        } yield result
-        test.map { result =>
-          assertEquals(result, expected)
+    test("removeResetPasswordExpirationDate must remove the expiration date".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token = ResetToken.generate
+                val tokenExpiration =
+                    OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L).withNano(0) // Nanos are troublesome!
+                val expected = ((None, token.some)).some
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    _      <- repo.removeResetPasswordExpirationDate(user.uid)
+                    result <- loadResetColumns(user.uid)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, expected)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("removeResetPasswordToken must remove the token and the expiration date".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L).withNano(0) // Nanos are troublesome!
-        val expected        = ((None, None)).some
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          _      <- repo.removeResetPasswordToken(user.uid)
-          result <- loadResetColumns(user.uid)
-        } yield result
-        test.map { result =>
-          assertEquals(result, expected)
+    test("removeResetPasswordToken must remove the token and the expiration date".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token = ResetToken.generate
+                val tokenExpiration =
+                    OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L).withNano(0) // Nanos are troublesome!
+                val expected = ((None, None)).some
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    _      <- repo.removeResetPasswordToken(user.uid)
+                    result <- loadResetColumns(user.uid)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, expected)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("setPassword must set the password correctly".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val expected = Password("This is not the password you're looking for!".getBytes(StandardCharsets.UTF_8)).encode
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setPassword(user.uid)(expected)
-          result <- loadPasswordHash(user.uid)
-        } yield result
-        test.map { result =>
-          assertEquals(result, expected.some)
+    test("setPassword must set the password correctly".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val expected =
+                    Password("This is not the password you're looking for!".getBytes(StandardCharsets.UTF_8)).encode
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setPassword(user.uid)(expected)
+                    result <- loadPasswordHash(user.uid)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, expected.some)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("setResetPasswordToken must set token and expiry date correctly".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(user) =>
-        val token           = ResetToken.generate
-        val tokenExpiration = OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L).withNano(0) // Nanos are troublesome!
-        val expected        = ((tokenExpiration.some, token.some)).some
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieResetPasswordRepository[IO](tx)
-        val test = for {
-          _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
-          _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
-          result <- loadResetColumns(user.uid)
-        } yield result
-        test.map { result =>
-          assertEquals(result, expected)
+    test("setResetPasswordToken must set token and expiry date correctly".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(user) =>
+                val token = ResetToken.generate
+                val tokenExpiration =
+                    OffsetDateTime.now(ZoneOffset.UTC).plusDays(3L).withNano(0) // Nanos are troublesome!
+                val expected = ((tokenExpiration.some, token.some)).some
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieResetPasswordRepository[IO](tx)
+                val test = for {
+                    _      <- createAccount(user, PasswordHash("I am not a password hash!"), None, None)
+                    _      <- repo.setResetPasswordToken(user.uid)(token, tokenExpiration)
+                    result <- loadResetColumns(user.uid)
+                } yield result
+                test.map { result =>
+                    assertEquals(result, expected)
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieSignupRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieSignupRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieSignupRepositoryTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieSignupRepositoryTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -25,138 +25,138 @@
 import doobie.*
 
 final class DoobieSignupRepositoryTest extends BaseSpec {
-  test("createAccount must create a new account".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieSignupRepository[IO](tx)
-        val test = for {
-          w <- repo.createAccount(account, PasswordHash("I am not a password hash!"))
-          o <- loadAccount(account.uid)
-        } yield (w, o)
-        test.map { result =>
-          val (written, loadedAccount) = result
-          assert(written === 1, "1 database row must have been written!")
-          assert(loadedAccount === Option(account), "Saved account differs from expected one!")
+    test("createAccount must create a new account".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieSignupRepository[IO](tx)
+                val test = for {
+                    w <- repo.createAccount(account, PasswordHash("I am not a password hash!"))
+                    o <- loadAccount(account.uid)
+                } yield (w, o)
+                test.map { result =>
+                    val (written, loadedAccount) = result
+                    assert(written === 1, "1 database row must have been written!")
+                    assert(loadedAccount === Option(account), "Saved account differs from expected one!")
+                }
         }
     }
-  }
 
-  test("createAccount must fail if the email already exists".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidAccount.sample) match {
-      case (Some(a), Some(b)) =>
-        val existingAccount = a
-        val newAccount      = b.copy(email = a.email)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieSignupRepository[IO](tx)
-        val test = for {
-          c <- createAccount(existingAccount, PasswordHash("I am not a password hash!"), None, None)
-          w <- repo.createAccount(newAccount, PasswordHash("I am not a password hash!"))
-        } yield (c, w)
-        test.attempt.map {
-          case Left(error) =>
-            assert(
-              error.getMessage.contains("accounts_unique_email"),
-              "Error must be triggered by accounts_unique_email constraint!"
-            )
-          case Right(_) => fail("Creating accounts with already used emails must fail!")
+    test("createAccount must fail if the email already exists".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidAccount.sample) match {
+            case (Some(a), Some(b)) =>
+                val existingAccount = a
+                val newAccount      = b.copy(email = a.email)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieSignupRepository[IO](tx)
+                val test = for {
+                    c <- createAccount(existingAccount, PasswordHash("I am not a password hash!"), None, None)
+                    w <- repo.createAccount(newAccount, PasswordHash("I am not a password hash!"))
+                } yield (c, w)
+                test.attempt.map {
+                    case Left(error) =>
+                        assert(
+                            error.getMessage.contains("accounts_unique_email"),
+                            "Error must be triggered by accounts_unique_email constraint!"
+                        )
+                    case Right(_) => fail("Creating accounts with already used emails must fail!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("createAccount must fail if the username already exists".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidAccount.sample) match {
-      case (Some(a), Some(b)) =>
-        val existingAccount = a
-        val newAccount      = b.copy(name = a.name)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieSignupRepository[IO](tx)
-        val test = for {
-          c <- createAccount(existingAccount, PasswordHash("I am not a password hash!"), None, None)
-          w <- repo.createAccount(newAccount, PasswordHash("I am not a password hash!"))
-        } yield (c, w)
-        test.attempt.map {
-          case Left(error) =>
-            assert(
-              error.getMessage.contains("accounts_unique_name"),
-              "Error must be triggered by accounts_unique_name constraint!"
-            )
-          case Right(_) => fail("Creating accounts with already used names must fail!")
+    test("createAccount must fail if the username already exists".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidAccount.sample) match {
+            case (Some(a), Some(b)) =>
+                val existingAccount = a
+                val newAccount      = b.copy(name = a.name)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieSignupRepository[IO](tx)
+                val test = for {
+                    c <- createAccount(existingAccount, PasswordHash("I am not a password hash!"), None, None)
+                    w <- repo.createAccount(newAccount, PasswordHash("I am not a password hash!"))
+                } yield (c, w)
+                test.attempt.map {
+                    case Left(error) =>
+                        assert(
+                            error.getMessage.contains("accounts_unique_name"),
+                            "Error must be triggered by accounts_unique_name constraint!"
+                        )
+                    case Right(_) => fail("Creating accounts with already used names must fail!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findEmail must return an existing email".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieSignupRepository[IO](tx)
-        val test = for {
-          c <- repo.createAccount(account, PasswordHash("I am not a password hash!"))
-          e <- repo.findEmail(account.email)
-        } yield (c, e)
-        test.map { result =>
-          val (created, email) = result
-          assert(created === 1, "Test account not created!")
-          assert(email === Option(account.email), "Expected email not found!")
+    test("findEmail must return an existing email".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieSignupRepository[IO](tx)
+                val test = for {
+                    c <- repo.createAccount(account, PasswordHash("I am not a password hash!"))
+                    e <- repo.findEmail(account.email)
+                } yield (c, e)
+                test.map { result =>
+                    val (created, email) = result
+                    assert(created === 1, "Test account not created!")
+                    assert(email === Option(account.email), "Expected email not found!")
+                }
         }
     }
-  }
 
-  test("findUsername must return an existing name".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case None => fail("Could not generate data samples!")
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieSignupRepository[IO](tx)
-        val test = for {
-          c <- repo.createAccount(account, PasswordHash("I am not a password hash!"))
-          n <- repo.findUsername(account.name)
-        } yield (c, n)
-        test.map { result =>
-          val (created, name) = result
-          assert(created === 1, "Test account not created!")
-          assert(name === Option(account.name), "Expected name not found!")
+    test("findUsername must return an existing name".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case None => fail("Could not generate data samples!")
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieSignupRepository[IO](tx)
+                val test = for {
+                    c <- repo.createAccount(account, PasswordHash("I am not a password hash!"))
+                    n <- repo.findUsername(account.name)
+                } yield (c, n)
+                test.map { result =>
+                    val (created, name) = result
+                    assert(created === 1, "Test account not created!")
+                    assert(name === Option(account.name), "Expected name not found!")
+                }
         }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/DoobieVcsMetadataRepositoryTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -31,610 +31,630 @@
 
 final class DoobieVcsMetadataRepositoryTest extends BaseSpec {
 
-  /** Find all forks of the original repository with the given ID.
-    *
-    * @param originalRepoId
-    *   The unique ID of the original repo from which was forked.
-    * @return
-    *   A list of ID pairs (original repository id, forked repository id) which may be empty.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.var", "DisableSyntax.while"))
-  protected def findForks(originalRepoId: VcsRepositoryId): IO[Seq[(VcsRepositoryId, VcsRepositoryId)]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement("""SELECT original_repo, forked_repo FROM "hub"."forks" WHERE original_repo = ?""")
-        )
-        _      <- IO.delay(statement.setLong(1, originalRepoId.toLong))
-        result <- IO.delay(statement.executeQuery)
-        forks <- IO.delay {
-          var queue = Queue.empty[(VcsRepositoryId, VcsRepositoryId)]
-          while (result.next())
-            queue = queue :+ (VcsRepositoryId(result.getLong("original_repo")), VcsRepositoryId(
-              result.getLong("forked_repo")
-            ))
-          queue
-        }
-        _ <- IO.delay(statement.close())
-      } yield forks.toList
-    }
-
-  test("createFork must work correctly".tag(NeedsDatabase)) {
-    (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match {
-      case (Some(accounts), Some(repositories)) =>
-        val vcsRepositories = accounts.zip(repositories).map { tuple =>
-          val (account, repo) = tuple
-          repo.copy(owner = account.toVcsRepositoryOwner)
-        }
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          written <- vcsRepositories.traverse(repo.createVcsRepository)
-          original <- vcsRepositories.headOption.traverse(vcsRepository =>
-            loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          )
-          toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
-            loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          }
-          forked <- (original, toFork) match {
-            case (Some(Some(originalId)), forkIds) => forkIds.flatten.traverse(id => repo.createFork(originalId, id))
-            case _                                 => IO.pure(List.empty)
-          }
-          foundForks <- original match {
-            case Some(Some(originalId)) => findForks(originalId)
-            case _                      => IO.pure(List.empty)
-          }
-        } yield (written, forked, foundForks)
-        test.map { result =>
-          val (written, forked, foundForks) = result
-          assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!")
-          assert(forked.sum === vcsRepositories.drop(1).take(5).size, "Number of created forks does not match!")
-          assert(foundForks.size === vcsRepositories.drop(1).take(5).size, "Number of found forks does not match!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("createVcsRepository must create a repository entry".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepository.sample) match {
-      case (Some(account), Some(repository)) =>
-        val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
-        val dbConfig      = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          written <- repo.createVcsRepository(vcsRepository)
-        } yield written
-        test.map { written =>
-          assert(written === 1, "Creating a vcs repository must modify one database row!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("createVcsRepository must fail if the user does not exist".tag(NeedsDatabase)) {
-    genValidVcsRepository.sample match {
-      case Some(repository) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          written <- repo.createVcsRepository(repository)
-        } yield written
-        test.attempt.map(result => assert(result.isLeft))
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("createVcsRepository must fail if a repository with the same name exists".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepository.sample) match {
-      case (Some(account), Some(repository)) =>
-        val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
-        val dbConfig      = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _       <- repo.createVcsRepository(vcsRepository)
-          written <- repo.createVcsRepository(vcsRepository)
-        } yield written
-        test.attempt.map(result => assert(result.isLeft))
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findVcsRepository must return an existing repository".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepository.sample) match {
-      case (Some(account), Some(repository)) =>
-        val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
-        val dbConfig      = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _         <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          written   <- repo.createVcsRepository(vcsRepository)
-          foundRepo <- repo.findVcsRepository(vcsRepository.owner, vcsRepository.name)
-        } yield (written, foundRepo)
-        test.map { result =>
-          val (written, foundRepo) = result
-          assert(written === 1, "Test repository data was not written to database!")
-          foundRepo match {
-            case None       => fail("Repository was not found!")
-            case Some(repo) => assert(repo === vcsRepository)
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findVcsRepositoryBranches must return all branches".tag(NeedsDatabase)) {
-    (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match {
-      case (Some(accounts), Some(repositories)) =>
-        val vcsRepositories = accounts.zip(repositories).map { tuple =>
-          val (account, repo) = tuple
-          repo.copy(owner = account.toVcsRepositoryOwner)
-        }
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          written <- vcsRepositories.traverse(repo.createVcsRepository)
-          original <- vcsRepositories.headOption.traverse(vcsRepository =>
-            loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          )
-          toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
-            loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          }
-          forked <- (original, toFork) match {
-            case (Some(Some(originalId)), forkIds) => forkIds.flatten.traverse(id => repo.createFork(originalId, id))
-            case _                                 => IO.pure(List.empty)
-          }
-          foundForks <- original match {
-            case Some(Some(originalId)) => repo.findVcsRepositoryBranches(originalId).compile.toList
-            case _                      => IO.pure(List.empty)
-          }
-        } yield (written, forked, foundForks)
-        test.map { result =>
-          val (written, forked, foundForks) = result
-          assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!")
-          assert(forked.sum === vcsRepositories.drop(1).take(5).size, "Number of created forks does not match!")
-          assert(foundForks.size === vcsRepositories.drop(1).take(5).size, "Number of found forks does not match!")
-          foundForks.zip(vcsRepositories.drop(1).take(5)).map { tuple =>
-            val ((ownerName, repoName), repo) = tuple
-            assertEquals(ownerName, repo.owner.name)
-            assertEquals(repoName, repo.name)
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("loadVcsRepositoryId must return the id of an existing repository".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepository.sample) match {
-      case (Some(account), Some(repository)) =>
-        val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
-        val dbConfig      = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _           <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          writtenRows <- repo.createVcsRepository(vcsRepository)
-          writtenId   <- loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          foundId     <- repo.findVcsRepositoryId(vcsRepository.owner, vcsRepository.name)
-        } yield (writtenRows, writtenId, foundId)
-        test.map { result =>
-          val (written, writtenId, foundId) = result
-          assert(written === 1, "Test repository data was not written to database!")
-          assert(writtenId.nonEmpty)
-          assert(foundId.nonEmpty)
-          assert(writtenId === foundId)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findVcsRepositoryOwner must return the correct account".tag(NeedsDatabase)) {
-    genValidAccounts.sample match {
-      case Some(accounts) =>
-        val expectedOwner = accounts(scala.util.Random.nextInt(accounts.size)).toVcsRepositoryOwner
-        val dbConfig      = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          written <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          foundOwner <- repo.findVcsRepositoryOwner(expectedOwner.name)
-        } yield (written, foundOwner)
-        test.map { result =>
-          val (written, foundOwner) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was not written to database!"
-          )
-          foundOwner match {
-            case None        => fail("Vcs repository owner not found!")
-            case Some(owner) => assertEquals(owner, expectedOwner)
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findVcsRepositoryParentFork must return the parent repository if it exists".tag(NeedsDatabase)) {
-    (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match {
-      case (Some(accounts), Some(repositories)) =>
-        val vcsRepositories = accounts.zip(repositories).map { tuple =>
-          val (account, repo) = tuple
-          repo.copy(owner = account.toVcsRepositoryOwner)
-        }
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          written <- vcsRepositories.traverse(repo.createVcsRepository)
-          original <- vcsRepositories.headOption.traverse(vcsRepository =>
-            loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          )
-          toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
-            loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
-          }
-          forked <- (original, toFork) match {
-            case (Some(Some(originalId)), forkIds) => forkIds.flatten.traverse(id => repo.createFork(originalId, id))
-            case _                                 => IO.pure(List.empty)
-          }
-          foundParents <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
-            repo.findVcsRepositoryParentFork(vcsRepository.owner, vcsRepository.name)
-          }
-        } yield (written, forked, vcsRepositories.headOption, foundParents)
-        test.map { result =>
-          val (written, forked, original, foundParents) = result
-          assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!")
-          assert(forked.sum === vcsRepositories.drop(1).take(5).size, "Number of created forks does not match!")
-          assert(foundParents.forall(_ === original), "Parent vcs repository not matching!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("listAllRepositories must return only public repositories for guest users".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample) match {
-      case (Some(account), Some(repositories)) =>
-        val vcsRepositories = repositories
-        val accounts = vcsRepositories.map(repo =>
-          Account(
-            repo.owner.uid,
-            repo.owner.name,
-            EmailAddress(s"${repo.owner.name}@example.com"),
-            validatedEmail = true,
-            None
-          )
-        )
-        val expectedRepoList = vcsRepositories.filter(_.isPrivate === false)
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          foundRepos <- repo.listAllRepositories(None)(NameAscending).compile.toList
-        } yield (written, foundRepos)
-        test.map { result =>
-          val (written, foundRepos) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assertEquals(foundRepos.size, expectedRepoList.size)
-        }
-
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("listAllRepositories must return only public repositories of others for any user".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample) match {
-      case (Some(account), Some(repositories)) =>
-        val vcsRepositories = repositories
-        val accounts = vcsRepositories.map(repo =>
-          Account(
-            repo.owner.uid,
-            repo.owner.name,
-            EmailAddress(s"${repo.owner.name}@example.com"),
-            validatedEmail = true,
-            None
-          )
-        )
-        val expectedRepoList = vcsRepositories.filter(_.isPrivate === false)
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          foundRepos <- repo.listAllRepositories(account.some)(NameDescending).compile.toList
-        } yield (written, foundRepos)
-        test.map { result =>
-          val (written, foundRepos) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assertEquals(foundRepos.size, expectedRepoList.size)
-        }
-
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("listAllRepositories must include all private repositories of the user".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample) match {
-      case (Some(account), Some(repositories)) =>
-        val privateRepos =
-          repositories.filter(_.isPrivate === true).map(_.copy(owner = account.toVcsRepositoryOwner))
-        val publicRepos     = repositories.filter(_.isPrivate === false)
-        val vcsRepositories = privateRepos ::: publicRepos
-        val accounts = publicRepos.map(repo =>
-          Account(
-            repo.owner.uid,
-            repo.owner.name,
-            EmailAddress(s"${repo.owner.name}@example.com"),
-            validatedEmail = true,
-            None
-          )
-        )
-        val expectedRepoList = vcsRepositories
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          _ <- accounts.traverse(account =>
-            createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          )
-          written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          foundRepos <- repo.listAllRepositories(account.some)(NameDescending).compile.toList
-        } yield (written, foundRepos)
-        test.map { result =>
-          val (written, foundRepos) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assertEquals(foundRepos.size, expectedRepoList.size)
-        }
-
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("listRepositories must return only public repositories for guest users".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample) match {
-      case (Some(account), Some(repositories)) =>
-        val vcsRepositories  = repositories.map(_.copy(owner = account.toVcsRepositoryOwner))
-        val expectedRepoList = vcsRepositories.filter(_.isPrivate === false).sortBy(_.name)
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _          <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          foundRepos <- repo.listRepositories(None)(account.toVcsRepositoryOwner).compile.toList
-        } yield (written, foundRepos)
-        test.map { result =>
-          val (written, foundRepos) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assertEquals(foundRepos.size, expectedRepoList.size)
-          // We sort again because database sorting might differ slightly from code sorting.
-          assertEquals(foundRepos.sortBy(_.name), expectedRepoList)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("listRepositories must return all repositories for the owner".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample) match {
-      case (Some(account), Some(repositories)) =>
-        val vcsRepositories  = repositories.map(_.copy(owner = account.toVcsRepositoryOwner))
-        val expectedRepoList = vcsRepositories.sortBy(_.name)
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _          <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          foundRepos <- repo.listRepositories(account.some)(account.toVcsRepositoryOwner).compile.toList
-        } yield (written, foundRepos)
-        test.map { result =>
-          val (written, foundRepos) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assertEquals(foundRepos.size, expectedRepoList.size)
-          // We sort again because database sorting might differ slightly from code sorting.
-          assertEquals(foundRepos.sortBy(_.name), expectedRepoList)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("listRepositories must return only public repositories for any user".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample, genValidAccount.sample) match {
-      case (Some(account), Some(repositories), Some(otherAccount)) =>
-        val vcsRepositories  = repositories.map(_.copy(owner = account.toVcsRepositoryOwner))
-        val expectedRepoList = vcsRepositories.filter(_.isPrivate === false).sortBy(_.name)
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _          <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          foundRepos <- repo.listRepositories(otherAccount.some)(account.toVcsRepositoryOwner).compile.toList
-        } yield (written, foundRepos)
-        test.map { result =>
-          val (written, foundRepos) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assertEquals(foundRepos.size, expectedRepoList.size)
-          // We sort again because database sorting might differ slightly from code sorting.
-          assertEquals(foundRepos.sortBy(_.name), expectedRepoList)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("updateVcsRepository must update all columns correctly".tag(NeedsDatabase)) {
-    (genValidAccount.sample, genValidVcsRepositories.sample) match {
-      case (Some(account), Some(repository :: repositories)) =>
-        val vcsRepositories = repository.copy(owner = account.toVcsRepositoryOwner) :: repositories.map(
-          _.copy(owner = account.toVcsRepositoryOwner)
-        )
-        val updatedRepo = repository
-          .copy(owner = account.toVcsRepositoryOwner)
-          .copy(
-            isPrivate = !repository.isPrivate,
-            description = Option(VcsRepositoryDescription("I am a description...")),
-            website = Option(uri"https://updated.example.com")
-          )
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieVcsMetadataRepository[IO](tx)
-        val test = for {
-          _         <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          written   <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
-          updated   <- repo.updateVcsRepository(updatedRepo)
-          persisted <- repo.findVcsRepository(account.toVcsRepositoryOwner, repository.name)
-        } yield (written, updated, persisted)
-        test.map { result =>
-          val (written, updated, persisted) = result
-          assert(
-            written.filter(_ === 1).size === written.size,
-            "Not all test repository data was written to database!"
-          )
-          assert(updated === 1, "Repository was not updated in database!")
-          assert(persisted === Some(updatedRepo))
+    /** Find all forks of the original repository with the given ID.
+      *
+      * @param originalRepoId
+      *   The unique ID of the original repo from which was forked.
+      * @return
+      *   A list of ID pairs (original repository id, forked repository id) which may be empty.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.var", "DisableSyntax.while"))
+    protected def findForks(originalRepoId: VcsRepositoryId): IO[Seq[(VcsRepositoryId, VcsRepositoryId)]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT original_repo, forked_repo FROM "hub"."forks" WHERE original_repo = ?"""
+                    )
+                )
+                _      <- IO.delay(statement.setLong(1, originalRepoId.toLong))
+                result <- IO.delay(statement.executeQuery)
+                forks <- IO.delay {
+                    var queue = Queue.empty[(VcsRepositoryId, VcsRepositoryId)]
+                    while (result.next())
+                        queue = queue :+ (VcsRepositoryId(result.getLong("original_repo")), VcsRepositoryId(
+                            result.getLong("forked_repo")
+                        ))
+                    queue
+                }
+                _ <- IO.delay(statement.close())
+            } yield forks.toList
+        }
+
+    test("createFork must work correctly".tag(NeedsDatabase)) {
+        (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match {
+            case (Some(accounts), Some(repositories)) =>
+                val vcsRepositories = accounts.zip(repositories).map { tuple =>
+                    val (account, repo) = tuple
+                    repo.copy(owner = account.toVcsRepositoryOwner)
+                }
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    written <- vcsRepositories.traverse(repo.createVcsRepository)
+                    original <- vcsRepositories.headOption.traverse(vcsRepository =>
+                        loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    )
+                    toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
+                        loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    }
+                    forked <- (original, toFork) match {
+                        case (Some(Some(originalId)), forkIds) =>
+                            forkIds.flatten.traverse(id => repo.createFork(originalId, id))
+                        case _ => IO.pure(List.empty)
+                    }
+                    foundForks <- original match {
+                        case Some(Some(originalId)) => findForks(originalId)
+                        case _                      => IO.pure(List.empty)
+                    }
+                } yield (written, forked, foundForks)
+                test.map { result =>
+                    val (written, forked, foundForks) = result
+                    assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!")
+                    assert(
+                        forked.sum === vcsRepositories.drop(1).take(5).size,
+                        "Number of created forks does not match!"
+                    )
+                    assert(
+                        foundForks.size === vcsRepositories.drop(1).take(5).size,
+                        "Number of found forks does not match!"
+                    )
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("createVcsRepository must create a repository entry".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepository.sample) match {
+            case (Some(account), Some(repository)) =>
+                val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
+                val dbConfig      = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    written <- repo.createVcsRepository(vcsRepository)
+                } yield written
+                test.map { written =>
+                    assert(written === 1, "Creating a vcs repository must modify one database row!")
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("createVcsRepository must fail if the user does not exist".tag(NeedsDatabase)) {
+        genValidVcsRepository.sample match {
+            case Some(repository) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    written <- repo.createVcsRepository(repository)
+                } yield written
+                test.attempt.map(result => assert(result.isLeft))
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("createVcsRepository must fail if a repository with the same name exists".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepository.sample) match {
+            case (Some(account), Some(repository)) =>
+                val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
+                val dbConfig      = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _       <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _       <- repo.createVcsRepository(vcsRepository)
+                    written <- repo.createVcsRepository(vcsRepository)
+                } yield written
+                test.attempt.map(result => assert(result.isLeft))
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findVcsRepository must return an existing repository".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepository.sample) match {
+            case (Some(account), Some(repository)) =>
+                val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
+                val dbConfig      = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _         <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    written   <- repo.createVcsRepository(vcsRepository)
+                    foundRepo <- repo.findVcsRepository(vcsRepository.owner, vcsRepository.name)
+                } yield (written, foundRepo)
+                test.map { result =>
+                    val (written, foundRepo) = result
+                    assert(written === 1, "Test repository data was not written to database!")
+                    foundRepo match {
+                        case None       => fail("Repository was not found!")
+                        case Some(repo) => assert(repo === vcsRepository)
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findVcsRepositoryBranches must return all branches".tag(NeedsDatabase)) {
+        (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match {
+            case (Some(accounts), Some(repositories)) =>
+                val vcsRepositories = accounts.zip(repositories).map { tuple =>
+                    val (account, repo) = tuple
+                    repo.copy(owner = account.toVcsRepositoryOwner)
+                }
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    written <- vcsRepositories.traverse(repo.createVcsRepository)
+                    original <- vcsRepositories.headOption.traverse(vcsRepository =>
+                        loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    )
+                    toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
+                        loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    }
+                    forked <- (original, toFork) match {
+                        case (Some(Some(originalId)), forkIds) =>
+                            forkIds.flatten.traverse(id => repo.createFork(originalId, id))
+                        case _ => IO.pure(List.empty)
+                    }
+                    foundForks <- original match {
+                        case Some(Some(originalId)) => repo.findVcsRepositoryBranches(originalId).compile.toList
+                        case _                      => IO.pure(List.empty)
+                    }
+                } yield (written, forked, foundForks)
+                test.map { result =>
+                    val (written, forked, foundForks) = result
+                    assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!")
+                    assert(
+                        forked.sum === vcsRepositories.drop(1).take(5).size,
+                        "Number of created forks does not match!"
+                    )
+                    assert(
+                        foundForks.size === vcsRepositories.drop(1).take(5).size,
+                        "Number of found forks does not match!"
+                    )
+                    foundForks.zip(vcsRepositories.drop(1).take(5)).map { tuple =>
+                        val ((ownerName, repoName), repo) = tuple
+                        assertEquals(ownerName, repo.owner.name)
+                        assertEquals(repoName, repo.name)
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("loadVcsRepositoryId must return the id of an existing repository".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepository.sample) match {
+            case (Some(account), Some(repository)) =>
+                val vcsRepository = repository.copy(owner = account.toVcsRepositoryOwner)
+                val dbConfig      = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _           <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    writtenRows <- repo.createVcsRepository(vcsRepository)
+                    writtenId   <- loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    foundId     <- repo.findVcsRepositoryId(vcsRepository.owner, vcsRepository.name)
+                } yield (writtenRows, writtenId, foundId)
+                test.map { result =>
+                    val (written, writtenId, foundId) = result
+                    assert(written === 1, "Test repository data was not written to database!")
+                    assert(writtenId.nonEmpty)
+                    assert(foundId.nonEmpty)
+                    assert(writtenId === foundId)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findVcsRepositoryOwner must return the correct account".tag(NeedsDatabase)) {
+        genValidAccounts.sample match {
+            case Some(accounts) =>
+                val expectedOwner = accounts(scala.util.Random.nextInt(accounts.size)).toVcsRepositoryOwner
+                val dbConfig      = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    written <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    foundOwner <- repo.findVcsRepositoryOwner(expectedOwner.name)
+                } yield (written, foundOwner)
+                test.map { result =>
+                    val (written, foundOwner) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was not written to database!"
+                    )
+                    foundOwner match {
+                        case None        => fail("Vcs repository owner not found!")
+                        case Some(owner) => assertEquals(owner, expectedOwner)
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findVcsRepositoryParentFork must return the parent repository if it exists".tag(NeedsDatabase)) {
+        (genValidAccounts.suchThat(_.size > 4).sample, genValidVcsRepositories.suchThat(_.size > 4).sample) match {
+            case (Some(accounts), Some(repositories)) =>
+                val vcsRepositories = accounts.zip(repositories).map { tuple =>
+                    val (account, repo) = tuple
+                    repo.copy(owner = account.toVcsRepositoryOwner)
+                }
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    written <- vcsRepositories.traverse(repo.createVcsRepository)
+                    original <- vcsRepositories.headOption.traverse(vcsRepository =>
+                        loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    )
+                    toFork <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
+                        loadVcsRepositoryId(vcsRepository.owner.uid, vcsRepository.name)
+                    }
+                    forked <- (original, toFork) match {
+                        case (Some(Some(originalId)), forkIds) =>
+                            forkIds.flatten.traverse(id => repo.createFork(originalId, id))
+                        case _ => IO.pure(List.empty)
+                    }
+                    foundParents <- vcsRepositories.drop(1).take(5).traverse { vcsRepository =>
+                        repo.findVcsRepositoryParentFork(vcsRepository.owner, vcsRepository.name)
+                    }
+                } yield (written, forked, vcsRepositories.headOption, foundParents)
+                test.map { result =>
+                    val (written, forked, original, foundParents) = result
+                    assert(written.sum === vcsRepositories.size, "Test repository data was not written to database!")
+                    assert(
+                        forked.sum === vcsRepositories.drop(1).take(5).size,
+                        "Number of created forks does not match!"
+                    )
+                    assert(foundParents.forall(_ === original), "Parent vcs repository not matching!")
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("listAllRepositories must return only public repositories for guest users".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample) match {
+            case (Some(account), Some(repositories)) =>
+                val vcsRepositories = repositories
+                val accounts = vcsRepositories.map(repo =>
+                    Account(
+                        repo.owner.uid,
+                        repo.owner.name,
+                        EmailAddress(s"${repo.owner.name}@example.com"),
+                        validatedEmail = true,
+                        None
+                    )
+                )
+                val expectedRepoList = vcsRepositories.filter(_.isPrivate === false)
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    foundRepos <- repo.listAllRepositories(None)(NameAscending).compile.toList
+                } yield (written, foundRepos)
+                test.map { result =>
+                    val (written, foundRepos) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assertEquals(foundRepos.size, expectedRepoList.size)
+                }
+
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("listAllRepositories must return only public repositories of others for any user".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample) match {
+            case (Some(account), Some(repositories)) =>
+                val vcsRepositories = repositories
+                val accounts = vcsRepositories.map(repo =>
+                    Account(
+                        repo.owner.uid,
+                        repo.owner.name,
+                        EmailAddress(s"${repo.owner.name}@example.com"),
+                        validatedEmail = true,
+                        None
+                    )
+                )
+                val expectedRepoList = vcsRepositories.filter(_.isPrivate === false)
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    foundRepos <- repo.listAllRepositories(account.some)(NameDescending).compile.toList
+                } yield (written, foundRepos)
+                test.map { result =>
+                    val (written, foundRepos) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assertEquals(foundRepos.size, expectedRepoList.size)
+                }
+
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("listAllRepositories must include all private repositories of the user".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample) match {
+            case (Some(account), Some(repositories)) =>
+                val privateRepos =
+                    repositories.filter(_.isPrivate === true).map(_.copy(owner = account.toVcsRepositoryOwner))
+                val publicRepos     = repositories.filter(_.isPrivate === false)
+                val vcsRepositories = privateRepos ::: publicRepos
+                val accounts = publicRepos.map(repo =>
+                    Account(
+                        repo.owner.uid,
+                        repo.owner.name,
+                        EmailAddress(s"${repo.owner.name}@example.com"),
+                        validatedEmail = true,
+                        None
+                    )
+                )
+                val expectedRepoList = vcsRepositories
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _ <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    _ <- accounts.traverse(account =>
+                        createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    )
+                    written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    foundRepos <- repo.listAllRepositories(account.some)(NameDescending).compile.toList
+                } yield (written, foundRepos)
+                test.map { result =>
+                    val (written, foundRepos) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assertEquals(foundRepos.size, expectedRepoList.size)
+                }
+
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("listRepositories must return only public repositories for guest users".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample) match {
+            case (Some(account), Some(repositories)) =>
+                val vcsRepositories  = repositories.map(_.copy(owner = account.toVcsRepositoryOwner))
+                val expectedRepoList = vcsRepositories.filter(_.isPrivate === false).sortBy(_.name)
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _          <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    foundRepos <- repo.listRepositories(None)(account.toVcsRepositoryOwner).compile.toList
+                } yield (written, foundRepos)
+                test.map { result =>
+                    val (written, foundRepos) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assertEquals(foundRepos.size, expectedRepoList.size)
+                    // We sort again because database sorting might differ slightly from code sorting.
+                    assertEquals(foundRepos.sortBy(_.name), expectedRepoList)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("listRepositories must return all repositories for the owner".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample) match {
+            case (Some(account), Some(repositories)) =>
+                val vcsRepositories  = repositories.map(_.copy(owner = account.toVcsRepositoryOwner))
+                val expectedRepoList = vcsRepositories.sortBy(_.name)
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _          <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    foundRepos <- repo.listRepositories(account.some)(account.toVcsRepositoryOwner).compile.toList
+                } yield (written, foundRepos)
+                test.map { result =>
+                    val (written, foundRepos) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assertEquals(foundRepos.size, expectedRepoList.size)
+                    // We sort again because database sorting might differ slightly from code sorting.
+                    assertEquals(foundRepos.sortBy(_.name), expectedRepoList)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("listRepositories must return only public repositories for any user".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample, genValidAccount.sample) match {
+            case (Some(account), Some(repositories), Some(otherAccount)) =>
+                val vcsRepositories  = repositories.map(_.copy(owner = account.toVcsRepositoryOwner))
+                val expectedRepoList = vcsRepositories.filter(_.isPrivate === false).sortBy(_.name)
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _          <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    written    <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    foundRepos <- repo.listRepositories(otherAccount.some)(account.toVcsRepositoryOwner).compile.toList
+                } yield (written, foundRepos)
+                test.map { result =>
+                    val (written, foundRepos) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assertEquals(foundRepos.size, expectedRepoList.size)
+                    // We sort again because database sorting might differ slightly from code sorting.
+                    assertEquals(foundRepos.sortBy(_.name), expectedRepoList)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("updateVcsRepository must update all columns correctly".tag(NeedsDatabase)) {
+        (genValidAccount.sample, genValidVcsRepositories.sample) match {
+            case (Some(account), Some(repository :: repositories)) =>
+                val vcsRepositories = repository.copy(owner = account.toVcsRepositoryOwner) :: repositories.map(
+                    _.copy(owner = account.toVcsRepositoryOwner)
+                )
+                val updatedRepo = repository
+                    .copy(owner = account.toVcsRepositoryOwner)
+                    .copy(
+                        isPrivate = !repository.isPrivate,
+                        description = Option(VcsRepositoryDescription("I am a description...")),
+                        website = Option(uri"https://updated.example.com")
+                    )
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieVcsMetadataRepository[IO](tx)
+                val test = for {
+                    _         <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    written   <- vcsRepositories.traverse(vcsRepository => repo.createVcsRepository(vcsRepository))
+                    updated   <- repo.updateVcsRepository(updatedRepo)
+                    persisted <- repo.findVcsRepository(account.toVcsRepositoryOwner, repository.name)
+                } yield (written, updated, persisted)
+                test.map { result =>
+                    val (written, updated, persisted) = result
+                    assert(
+                        written.filter(_ === 1).size === written.size,
+                        "Not all test repository data was written to database!"
+                    )
+                    assert(updated === 1, "Repository was not updated in database!")
+                    assert(persisted === Some(updatedRepo))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/forms/FormErrorsTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -25,58 +25,58 @@
 
 final class FormErrorsTest extends FunSuite {
 
-  test("FormErrors.empty must create an empty instance") {
-    val noErrors = FormErrors.empty
-    assert(noErrors.isEmpty)
-  }
+    test("FormErrors.empty must create an empty instance") {
+        val noErrors = FormErrors.empty
+        assert(noErrors.isEmpty)
+    }
 
-  test("FormErrors.fromNec must collapse given instances into a single one") {
-    val a = NonEmptyChain.of(
-      FormErrors.empty + (FormField("a") -> List(FormFieldError("Error a1"))),
-      FormErrors.empty + (FormField("b") -> List(FormFieldError("Error b1")))
-    )
-    val b = NonEmptyChain.of(FormErrors.empty)
-    val c = NonEmptyChain.of(
-      Map(
-        FormField("a") -> List(FormFieldError("Error a2")),
-        FormField("b") -> List(FormFieldError("Error b2"))
-      ),
-      Map(FormField("b") -> List(FormFieldError("Error b3")))
-    )
-    val expected = Map(
-      FormField("a") -> List(FormFieldError("Error a1"), FormFieldError("Error a2")),
-      FormField("b") -> List(
-        FormFieldError("Error b1"),
-        FormFieldError("Error b2"),
-        FormFieldError("Error b3")
-      )
-    )
-    val combined = FormErrors.fromNec(a |+| b |+| c)
-    assertEquals(combined, expected)
-  }
+    test("FormErrors.fromNec must collapse given instances into a single one") {
+        val a = NonEmptyChain.of(
+            FormErrors.empty + (FormField("a") -> List(FormFieldError("Error a1"))),
+            FormErrors.empty + (FormField("b") -> List(FormFieldError("Error b1")))
+        )
+        val b = NonEmptyChain.of(FormErrors.empty)
+        val c = NonEmptyChain.of(
+            Map(
+                FormField("a") -> List(FormFieldError("Error a2")),
+                FormField("b") -> List(FormFieldError("Error b2"))
+            ),
+            Map(FormField("b") -> List(FormFieldError("Error b3")))
+        )
+        val expected = Map(
+            FormField("a") -> List(FormFieldError("Error a1"), FormFieldError("Error a2")),
+            FormField("b") -> List(
+                FormFieldError("Error b1"),
+                FormFieldError("Error b2"),
+                FormFieldError("Error b3")
+            )
+        )
+        val combined = FormErrors.fromNec(a |+| b |+| c)
+        assertEquals(combined, expected)
+    }
 
-  test("FormErrors.fromNel must collapse given instances into a single one") {
-    val a = NonEmptyList.of(
-      FormErrors.empty + (FormField("a") -> List(FormFieldError("Error a1"))),
-      FormErrors.empty + (FormField("b") -> List(FormFieldError("Error b1")))
-    )
-    val b = NonEmptyList.of(FormErrors.empty)
-    val c = NonEmptyList.of(
-      Map(
-        FormField("a") -> List(FormFieldError("Error a2")),
-        FormField("b") -> List(FormFieldError("Error b2"))
-      ),
-      Map(FormField("b") -> List(FormFieldError("Error b3")))
-    )
-    val expected = Map(
-      FormField("a") -> List(FormFieldError("Error a1"), FormFieldError("Error a2")),
-      FormField("b") -> List(
-        FormFieldError("Error b1"),
-        FormFieldError("Error b2"),
-        FormFieldError("Error b3")
-      )
-    )
-    val combined = FormErrors.fromNel(a |+| b |+| c)
-    assertEquals(combined, expected)
-  }
+    test("FormErrors.fromNel must collapse given instances into a single one") {
+        val a = NonEmptyList.of(
+            FormErrors.empty + (FormField("a") -> List(FormFieldError("Error a1"))),
+            FormErrors.empty + (FormField("b") -> List(FormFieldError("Error b1")))
+        )
+        val b = NonEmptyList.of(FormErrors.empty)
+        val c = NonEmptyList.of(
+            Map(
+                FormField("a") -> List(FormFieldError("Error a2")),
+                FormField("b") -> List(FormFieldError("Error b2"))
+            ),
+            Map(FormField("b") -> List(FormFieldError("Error b3")))
+        )
+        val expected = Map(
+            FormField("a") -> List(FormFieldError("Error a1"), FormFieldError("Error a2")),
+            FormField("b") -> List(
+                FormFieldError("Error b1"),
+                FormFieldError("Error b2"),
+                FormFieldError("Error b3")
+            )
+        )
+        val combined = FormErrors.fromNel(a |+| b |+| c)
+        assertEquals(combined, expected)
+    }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala	2025-01-13 17:13:25.060470984 +0000
@@ -33,178 +33,178 @@
 import scala.jdk.CollectionConverters.*
 
 object Generators {
-  val MinimumYear: Int = -4713  // Lowest year supported by PostgreSQL
-  val MaximumYear: Int = 294276 // Highest year supported by PostgreSQL
+    val MinimumYear: Int = -4713  // Lowest year supported by PostgreSQL
+    val MaximumYear: Int = 294276 // Highest year supported by PostgreSQL
 
-  val genLocale: Gen[Locale] = Gen.oneOf(Locale.getAvailableLocales.toList)
-  given Arbitrary[Locale]    = Arbitrary(genLocale)
+    val genLocale: Gen[Locale] = Gen.oneOf(Locale.getAvailableLocales.toList)
+    given Arbitrary[Locale]    = Arbitrary(genLocale)
 
-  val genLanguageCode: Gen[LanguageCode] = genLocale.map(_.getISO3Language).filter(_.nonEmpty).map(LanguageCode.apply)
+    val genLanguageCode: Gen[LanguageCode] = genLocale.map(_.getISO3Language).filter(_.nonEmpty).map(LanguageCode.apply)
 
-  val genFiniteDuration: Gen[FiniteDuration] =
-    Gen.choose(0, Int.MaxValue).map(seconds => FiniteDuration(seconds, SECONDS))
-
-  given Arbitrary[FiniteDuration]         = Arbitrary(genFiniteDuration)
-  given Arbitrary[Option[FiniteDuration]] = Arbitrary(Gen.option(genFiniteDuration))
-
-  val genOffsetDateTime: Gen[OffsetDateTime] =
-    for {
-      year       <- Gen.choose(MinimumYear, MaximumYear)
-      month      <- Gen.choose(1, 12)
-      day        <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
-      hour       <- Gen.choose(0, 23)
-      minute     <- Gen.choose(0, 59)
-      second     <- Gen.choose(0, 59)
-      nanosecond <- Gen.const(0) // Avoid issues with loosing information during saving and loading.
-      offset <- Gen.oneOf(
-        ZoneId.getAvailableZoneIds.asScala.toList.map(ZoneId.of).map(ZonedDateTime.now).map(_.getOffset)
-      )
-    } yield OffsetDateTime.of(year, month, day, hour, minute, second, nanosecond, offset)
-
-  given Arbitrary[OffsetDateTime] = Arbitrary(genOffsetDateTime)
-
-  val genZonedDateTime: Gen[ZonedDateTime] =
-    for {
-      year       <- Gen.choose(MinimumYear, MaximumYear)
-      month      <- Gen.choose(1, 12)
-      day        <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
-      hour       <- Gen.choose(0, 23)
-      minute     <- Gen.choose(0, 59)
-      second     <- Gen.choose(0, 59)
-      nanosecond <- Gen.const(0) // Avoid issues with loosing information during saving and loading.
-      zone       <- Gen.oneOf(ZoneId.getAvailableZoneIds.asScala.toList.map(ZoneId.of))
-    } yield ZonedDateTime.of(year, month, day, hour, minute, second, nanosecond, zone)
-
-  val genSessionId: Gen[SessionId] = Gen.delay(SessionId.generate)
-
-  val genSignAndValidate: Gen[SignAndValidate] = Gen
-    .nonEmptyListOf(Gen.alphaNumChar)
-    .map(cs => PrivateKey(cs.mkString.getBytes(StandardCharsets.UTF_8)))
-    .map(key => new SignAndValidate(key))
-
-  given Arbitrary[SignAndValidate] = Arbitrary(genSignAndValidate)
-
-  val genUserId: Gen[UserId] = Gen.delay(UserId.randomUserId)
-
-  val genUUID: Gen[UUID] = Gen.delay(UUID.randomUUID)
-
-  private val validEmailAddressPrefixChars =
-    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&’'*+/=?^_`{|}~-".toList
-  private val validDomainNameChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-".toList
-
-  val genValidEmail: Gen[EmailAddress] = for {
-    prefix <- Gen.nonEmptyListOf(Gen.oneOf(validEmailAddressPrefixChars)).map(_.take(64).mkString)
-    domain <- Gen.nonEmptyListOf(Gen.oneOf(validDomainNameChars)).map(_.take(32).mkString)
-    topLevelDomain <- Gen
-      .nonEmptyListOf(Gen.oneOf(validDomainNameChars))
-      .suchThat(_.length >= 2)
-      .map(_.take(24).mkString)
-    suffix = s"$domain.$topLevelDomain"
-  } yield EmailAddress(s"$prefix@$suffix")
-
-  val genValidUsername: Gen[Username] = for {
-    length <- Gen.choose(2, 30)
-    prefix <- Gen.alphaChar
-    chars <- Gen
-      .nonEmptyListOf(Gen.alphaNumChar)
-      .map(_.take(length).mkString.toLowerCase(Locale.ROOT))
-  } yield Username(prefix.toString.toLowerCase(Locale.ROOT) + chars)
-
-  val genValidAccount: Gen[Account] = for {
-    id             <- genUserId
-    email          <- genValidEmail
-    name           <- genValidUsername
-    validatedEmail <- Gen.oneOf(List(false, true))
-    language       <- Gen.option(genLanguageCode)
-  } yield Account(uid = id, name = name, email = email, validatedEmail = validatedEmail, language = language)
-
-  given Arbitrary[Account] = Arbitrary(genValidAccount)
-
-  val genValidAccounts: Gen[List[Account]] = Gen
-    .nonEmptyListOf(genValidAccount)
-    .map(_.foldLeft(List.empty[Account]) { (acc, a) =>
-      if (acc.exists(_.name === a.name))
-        acc
-      else
-        a :: acc
-    }) // Ensure distinct user names.
-
-  val genValidSession: Gen[Session] =
-    for {
-      id  <- genSessionId
-      uid <- genUserId
-      cat <- genOffsetDateTime.map(_.withOffsetSameInstant(ZoneOffset.UTC))
-      uat <- genOffsetDateTime.map(_.withOffsetSameInstant(ZoneOffset.UTC))
-    } yield Session(id, uid, cat, uat)
-
-  given Arbitrary[Session] = Arbitrary(genValidSession)
-
-  val genValidSessions: Gen[List[Session]] = Gen.nonEmptyListOf(genValidSession)
-
-  val genValidVcsRepositoryName: Gen[VcsRepositoryName] = Gen
-    .nonEmptyListOf(
-      Gen.oneOf(
-        List(
-          "a",
-          "b",
-          "c",
-          "d",
-          "e",
-          "f",
-          "g",
-          "h",
-          "i",
-          "j",
-          "k",
-          "l",
-          "m",
-          "n",
-          "o",
-          "p",
-          "q",
-          "r",
-          "s",
-          "t",
-          "u",
-          "v",
-          "w",
-          "x",
-          "y",
-          "z",
-          "0",
-          "1",
-          "2",
-          "3",
-          "4",
-          "5",
-          "6",
-          "7",
-          "8",
-          "9",
-          "-",
-          "_"
+    val genFiniteDuration: Gen[FiniteDuration] =
+        Gen.choose(0, Int.MaxValue).map(seconds => FiniteDuration(seconds, SECONDS))
+
+    given Arbitrary[FiniteDuration]         = Arbitrary(genFiniteDuration)
+    given Arbitrary[Option[FiniteDuration]] = Arbitrary(Gen.option(genFiniteDuration))
+
+    val genOffsetDateTime: Gen[OffsetDateTime] =
+        for {
+            year       <- Gen.choose(MinimumYear, MaximumYear)
+            month      <- Gen.choose(1, 12)
+            day        <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
+            hour       <- Gen.choose(0, 23)
+            minute     <- Gen.choose(0, 59)
+            second     <- Gen.choose(0, 59)
+            nanosecond <- Gen.const(0) // Avoid issues with loosing information during saving and loading.
+            offset <- Gen.oneOf(
+                ZoneId.getAvailableZoneIds.asScala.toList.map(ZoneId.of).map(ZonedDateTime.now).map(_.getOffset)
+            )
+        } yield OffsetDateTime.of(year, month, day, hour, minute, second, nanosecond, offset)
+
+    given Arbitrary[OffsetDateTime] = Arbitrary(genOffsetDateTime)
+
+    val genZonedDateTime: Gen[ZonedDateTime] =
+        for {
+            year       <- Gen.choose(MinimumYear, MaximumYear)
+            month      <- Gen.choose(1, 12)
+            day        <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
+            hour       <- Gen.choose(0, 23)
+            minute     <- Gen.choose(0, 59)
+            second     <- Gen.choose(0, 59)
+            nanosecond <- Gen.const(0) // Avoid issues with loosing information during saving and loading.
+            zone       <- Gen.oneOf(ZoneId.getAvailableZoneIds.asScala.toList.map(ZoneId.of))
+        } yield ZonedDateTime.of(year, month, day, hour, minute, second, nanosecond, zone)
+
+    val genSessionId: Gen[SessionId] = Gen.delay(SessionId.generate)
+
+    val genSignAndValidate: Gen[SignAndValidate] = Gen
+        .nonEmptyListOf(Gen.alphaNumChar)
+        .map(cs => PrivateKey(cs.mkString.getBytes(StandardCharsets.UTF_8)))
+        .map(key => new SignAndValidate(key))
+
+    given Arbitrary[SignAndValidate] = Arbitrary(genSignAndValidate)
+
+    val genUserId: Gen[UserId] = Gen.delay(UserId.randomUserId)
+
+    val genUUID: Gen[UUID] = Gen.delay(UUID.randomUUID)
+
+    private val validEmailAddressPrefixChars =
+        "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&’'*+/=?^_`{|}~-".toList
+    private val validDomainNameChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-".toList
+
+    val genValidEmail: Gen[EmailAddress] = for {
+        prefix <- Gen.nonEmptyListOf(Gen.oneOf(validEmailAddressPrefixChars)).map(_.take(64).mkString)
+        domain <- Gen.nonEmptyListOf(Gen.oneOf(validDomainNameChars)).map(_.take(32).mkString)
+        topLevelDomain <- Gen
+            .nonEmptyListOf(Gen.oneOf(validDomainNameChars))
+            .suchThat(_.length >= 2)
+            .map(_.take(24).mkString)
+        suffix = s"$domain.$topLevelDomain"
+    } yield EmailAddress(s"$prefix@$suffix")
+
+    val genValidUsername: Gen[Username] = for {
+        length <- Gen.choose(2, 30)
+        prefix <- Gen.alphaChar
+        chars <- Gen
+            .nonEmptyListOf(Gen.alphaNumChar)
+            .map(_.take(length).mkString.toLowerCase(Locale.ROOT))
+    } yield Username(prefix.toString.toLowerCase(Locale.ROOT) + chars)
+
+    val genValidAccount: Gen[Account] = for {
+        id             <- genUserId
+        email          <- genValidEmail
+        name           <- genValidUsername
+        validatedEmail <- Gen.oneOf(List(false, true))
+        language       <- Gen.option(genLanguageCode)
+    } yield Account(uid = id, name = name, email = email, validatedEmail = validatedEmail, language = language)
+
+    given Arbitrary[Account] = Arbitrary(genValidAccount)
+
+    val genValidAccounts: Gen[List[Account]] = Gen
+        .nonEmptyListOf(genValidAccount)
+        .map(_.foldLeft(List.empty[Account]) { (acc, a) =>
+            if (acc.exists(_.name === a.name))
+                acc
+            else
+                a :: acc
+        }) // Ensure distinct user names.
+
+    val genValidSession: Gen[Session] =
+        for {
+            id  <- genSessionId
+            uid <- genUserId
+            cat <- genOffsetDateTime.map(_.withOffsetSameInstant(ZoneOffset.UTC))
+            uat <- genOffsetDateTime.map(_.withOffsetSameInstant(ZoneOffset.UTC))
+        } yield Session(id, uid, cat, uat)
+
+    given Arbitrary[Session] = Arbitrary(genValidSession)
+
+    val genValidSessions: Gen[List[Session]] = Gen.nonEmptyListOf(genValidSession)
+
+    val genValidVcsRepositoryName: Gen[VcsRepositoryName] = Gen
+        .nonEmptyListOf(
+            Gen.oneOf(
+                List(
+                    "a",
+                    "b",
+                    "c",
+                    "d",
+                    "e",
+                    "f",
+                    "g",
+                    "h",
+                    "i",
+                    "j",
+                    "k",
+                    "l",
+                    "m",
+                    "n",
+                    "o",
+                    "p",
+                    "q",
+                    "r",
+                    "s",
+                    "t",
+                    "u",
+                    "v",
+                    "w",
+                    "x",
+                    "y",
+                    "z",
+                    "0",
+                    "1",
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                    "6",
+                    "7",
+                    "8",
+                    "9",
+                    "-",
+                    "_"
+                )
+            )
         )
-      )
-    )
-    .map(cs => VcsRepositoryName(cs.take(64).mkString))
-
-  val genValidVcsRepositoryOwner = for {
-    uid   <- genUserId
-    name  <- genValidUsername
-    email <- genValidEmail
-  } yield VcsRepositoryOwner(uid, name, email)
-
-  val genValidVcsType = Gen.oneOf(VcsType.values.toList)
-
-  val genValidVcsRepository: Gen[VcsRepository] = for {
-    name           <- genValidVcsRepositoryName
-    owner          <- genValidVcsRepositoryOwner
-    isPrivate      <- Gen.oneOf(List(false, true))
-    description    <- Gen.alphaNumStr.map(VcsRepositoryDescription.from)
-    ticketsEnabled <- Gen.oneOf(List(false, true))
-    vcsType        <- genValidVcsType
-  } yield VcsRepository(name, owner, isPrivate, description, ticketsEnabled, vcsType, None)
+        .map(cs => VcsRepositoryName(cs.take(64).mkString))
 
-  val genValidVcsRepositories: Gen[List[VcsRepository]] = Gen.nonEmptyListOf(genValidVcsRepository)
+    val genValidVcsRepositoryOwner = for {
+        uid   <- genUserId
+        name  <- genValidUsername
+        email <- genValidEmail
+    } yield VcsRepositoryOwner(uid, name, email)
+
+    val genValidVcsType = Gen.oneOf(VcsType.values.toList)
+
+    val genValidVcsRepository: Gen[VcsRepository] = for {
+        name           <- genValidVcsRepositoryName
+        owner          <- genValidVcsRepositoryOwner
+        isPrivate      <- Gen.oneOf(List(false, true))
+        description    <- Gen.alphaNumStr.map(VcsRepositoryDescription.from)
+        ticketsEnabled <- Gen.oneOf(List(false, true))
+        vcsType        <- genValidVcsType
+    } yield VcsRepository(name, owner, isPrivate, description, ticketsEnabled, vcsType, None)
+
+    val genValidVcsRepositories: Gen[List[VcsRepository]] = Gen.nonEmptyListOf(genValidVcsRepository)
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionHelpersTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -38,44 +38,44 @@
 
 final class SessionHelpersTest extends ScalaCheckSuite {
 
-  property("Session.hasReachedAbsoluteTimeout must work correctly") {
-    forAll { (absoluteTimeout: FiniteDuration, currentTime: OffsetDateTime, session: Session) =>
-      val timeouts = AuthenticationTimeouts(absoluteTimeout, absoluteTimeout, absoluteTimeout)
-      val expected = session.createdAt.compareTo(currentTime.minusSeconds(absoluteTimeout.toSeconds)) < 0
-      assertEquals(session.hasReachedAbsoluteTimeout(timeouts)(currentTime), expected)
+    property("Session.hasReachedAbsoluteTimeout must work correctly") {
+        forAll { (absoluteTimeout: FiniteDuration, currentTime: OffsetDateTime, session: Session) =>
+            val timeouts = AuthenticationTimeouts(absoluteTimeout, absoluteTimeout, absoluteTimeout)
+            val expected = session.createdAt.compareTo(currentTime.minusSeconds(absoluteTimeout.toSeconds)) < 0
+            assertEquals(session.hasReachedAbsoluteTimeout(timeouts)(currentTime), expected)
+        }
     }
-  }
 
-  property("SignedToken.toAuthenticationCookie must work correctly") {
-    forAll {
-      (
-          expires: Option[FiniteDuration],
-          nonce: String,
-          secure: Boolean,
-          signAndValidate: SignAndValidate,
-          token: String
-      ) =>
-        val domain      = Option("example.com")
-        val signedToken = signAndValidate.signToken(token)(nonce)
-        val obtained    = signedToken.toAuthenticationCookie(domain)(expires)(secure)
-        assertEquals(obtained.name, Constants.authenticationCookieName.toString)
-        assertEquals(obtained.content, signedToken.toString)
-        assertEquals(obtained.domain, domain)
-        assertEquals(obtained.sameSite, Option(SameSite.Strict))
-        assertEquals(obtained.secure, secure)
-        assertEquals(obtained.httpOnly, true)
-        // If an expires value was passed then the expires field in the cookie must be set!
-        assertEquals(obtained.expires.nonEmpty, expires.nonEmpty)
-        // The function uses `ZonedDateTime.now(ZoneOffset.UTC)` inside, therefore we can only check
-        // if the expiration date stored in the cookie is at least "later" than the current datetime.
-        (expires, obtained.expires) match {
-          case (Some(_), Some(expiration)) =>
-            HttpDate.fromZonedDateTime(ZonedDateTime.now(ZoneOffset.UTC)).foreach { httpDate =>
-              assert(expiration.compareTo(httpDate) > 0)
-            }
-          case _ => // We only check if the values are set.
+    property("SignedToken.toAuthenticationCookie must work correctly") {
+        forAll {
+            (
+                expires: Option[FiniteDuration],
+                nonce: String,
+                secure: Boolean,
+                signAndValidate: SignAndValidate,
+                token: String
+            ) =>
+                val domain      = Option("example.com")
+                val signedToken = signAndValidate.signToken(token)(nonce)
+                val obtained    = signedToken.toAuthenticationCookie(domain)(expires)(secure)
+                assertEquals(obtained.name, Constants.authenticationCookieName.toString)
+                assertEquals(obtained.content, signedToken.toString)
+                assertEquals(obtained.domain, domain)
+                assertEquals(obtained.sameSite, Option(SameSite.Strict))
+                assertEquals(obtained.secure, secure)
+                assertEquals(obtained.httpOnly, true)
+                // If an expires value was passed then the expires field in the cookie must be set!
+                assertEquals(obtained.expires.nonEmpty, expires.nonEmpty)
+                // The function uses `ZonedDateTime.now(ZoneOffset.UTC)` inside, therefore we can only check
+                // if the expiration date stored in the cookie is at least "later" than the current datetime.
+                (expires, obtained.expires) match {
+                    case (Some(_), Some(expiration)) =>
+                        HttpDate.fromZonedDateTime(ZonedDateTime.now(ZoneOffset.UTC)).foreach { httpDate =>
+                            assert(expiration.compareTo(httpDate) > 0)
+                        }
+                    case _ => // We only check if the values are set.
+                }
         }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionIdTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionIdTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionIdTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionIdTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -28,31 +28,31 @@
 import org.scalacheck.*
 
 final class SessionIdTest extends ScalaCheckSuite {
-  private val genKey: Gen[PrivateKey] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(cs => PrivateKey(cs.mkString.getBytes(StandardCharsets.UTF_8)))
-  given Arbitrary[PrivateKey]              = Arbitrary(genKey)
-  private val genSessionId: Gen[SessionId] = Gen.delay(SessionId.generate)
-  given Arbitrary[SessionId]               = Arbitrary(genSessionId)
+    private val genKey: Gen[PrivateKey] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(cs => PrivateKey(cs.mkString.getBytes(StandardCharsets.UTF_8)))
+    given Arbitrary[PrivateKey]              = Arbitrary(genKey)
+    private val genSessionId: Gen[SessionId] = Gen.delay(SessionId.generate)
+    given Arbitrary[SessionId]               = Arbitrary(genSessionId)
 
-  property("from must parse ids created with generate") {
-    forAll { (id: SessionId) =>
-      val obtained = SessionId.from(id.toString)
-      assert(obtained.map(_ === id).getOrElse(false), s"$obtained not parsed into expectd $id")
+    property("from must parse ids created with generate") {
+        forAll { (id: SessionId) =>
+            val obtained = SessionId.from(id.toString)
+            assert(obtained.map(_ === id).getOrElse(false), s"$obtained not parsed into expectd $id")
+        }
     }
-  }
 
-  property("from must not parse random garbage") {
-    forAll { (garbage: String) =>
-      assert(SessionId.from(garbage).isEmpty)
+    property("from must not parse random garbage") {
+        forAll { (garbage: String) =>
+            assert(SessionId.from(garbage).isEmpty)
+        }
     }
-  }
 
-  test("generate must produce sufficiently random session ids") {
-    val builder = Vector.newBuilder[String]
-    (0 until 15000000).foreach { _ =>
-      builder.addOne(SessionId.generate.toString)
+    test("generate must produce sufficiently random session ids") {
+        val builder = Vector.newBuilder[String]
+        (0 until 15000000).foreach { _ =>
+            builder.addOne(SessionId.generate.toString)
+        }
+        val ids = builder.result
+        assertEquals(ids.distinct.length, ids.length, "Duplicate session id found!")
     }
-    val ids = builder.result
-    assertEquals(ids.distinct.length, ids.length, "Duplicate session id found!")
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/SessionTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -27,10 +27,10 @@
 
 final class SessionTest extends ScalaCheckSuite {
 
-  property("Eq must hold for identical sessions") {
-    forAll { (session: Session) =>
-      assert(session === session, "Identical sessions must be considered equal!")
+    property("Eq must hold for identical sessions") {
+        forAll { (session: Session) =>
+            assert(session === session, "Identical sessions must be considered equal!")
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/TestAuthenticationRepository.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/TestAuthenticationRepository.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/TestAuthenticationRepository.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/TestAuthenticationRepository.scala	2025-01-13 17:13:25.060470984 +0000
@@ -38,67 +38,67 @@
 @SuppressWarnings(Array("DisableSyntax.var"))
 class TestAuthenticationRepository[F[_]: Sync](accounts: List[Account], sessions: List[Session])
     extends AuthenticationRepository[F] {
-  val DefaultPassword = "My voice is my passport. Verify me."
+    val DefaultPassword = "My voice is my passport. Verify me."
 
-  private var failed = Map.empty[UserId, Int]
-  private var locked = List.empty[UserId]
+    private var failed = Map.empty[UserId, Int]
+    private var locked = List.empty[UserId]
 
-  /** Return the map of user ids and related failed login attempts.
-    *
-    * @return
-    *   A map of user ids pointing to an integer holding the number of failed login attempts.
-    */
-  def getFailed: F[Map[UserId, Int]] = Sync[F].delay(failed)
+    /** Return the map of user ids and related failed login attempts.
+      *
+      * @return
+      *   A map of user ids pointing to an integer holding the number of failed login attempts.
+      */
+    def getFailed: F[Map[UserId, Int]] = Sync[F].delay(failed)
 
-  /** Return the list of locked user ids.
-    *
-    * @return
-    *   A list with user ids that are locked.
-    */
-  def getLocked: F[List[UserId]] = Sync[F].delay(locked)
+    /** Return the list of locked user ids.
+      *
+      * @return
+      *   A list with user ids that are locked.
+      */
+    def getLocked: F[List[UserId]] = Sync[F].delay(locked)
 
-  override def allAccounts(): Stream[F, Account] = Stream.emits(accounts)
+    override def allAccounts(): Stream[F, Account] = Stream.emits(accounts)
 
-  override def lockAccount(uid: UserId)(token: Option[UnlockToken]): F[Int] = {
-    locked = uid :: locked
-    Sync[F].pure(1)
-  }
+    override def lockAccount(uid: UserId)(token: Option[UnlockToken]): F[Int] = {
+        locked = uid :: locked
+        Sync[F].pure(1)
+    }
 
-  override def findAccountByName(name: Username): F[Option[Account]] = Sync[F].pure(accounts.headOption)
+    override def findAccountByName(name: Username): F[Option[Account]] = Sync[F].pure(accounts.headOption)
 
-  override def resetFailedAttempts(uid: UserId): F[Int] = {
-    failed = failed.filterNot(_._1 === uid)
-    Sync[F].pure(1)
-  }
+    override def resetFailedAttempts(uid: UserId): F[Int] = {
+        failed = failed.filterNot(_._1 === uid)
+        Sync[F].pure(1)
+    }
 
-  override def deleteAllUserSessions(uid: UserId): F[Int] = Sync[F].pure(1)
+    override def deleteAllUserSessions(uid: UserId): F[Int] = Sync[F].pure(1)
 
-  override def createUserSession(session: Session): F[Int] = Sync[F].pure(1)
+    override def createUserSession(session: Session): F[Int] = Sync[F].pure(1)
 
-  override def incrementFailedAttempts(uid: UserId): F[Int] = {
-    failed = failed |+| Map(uid -> 1)
-    Sync[F].pure(1)
-  }
+    override def incrementFailedAttempts(uid: UserId): F[Int] = {
+        failed = failed |+| Map(uid -> 1)
+        Sync[F].pure(1)
+    }
 
-  override def findUserSession(id: SessionId): F[Option[Session]] = Sync[F].pure(sessions.headOption)
+    override def findUserSession(id: SessionId): F[Option[Session]] = Sync[F].pure(sessions.headOption)
 
-  override def findLockedAccount(name: Username)(token: Option[UnlockToken]): F[Option[Account]] =
-    Sync[F].pure(accounts.headOption)
+    override def findLockedAccount(name: Username)(token: Option[UnlockToken]): F[Option[Account]] =
+        Sync[F].pure(accounts.headOption)
 
-  override def findPasswordHashAndAttempts(uid: UserId): F[Option[(PasswordHash, Int)]] =
-    Sync[F].delay(
-      Option((Password(DefaultPassword.getBytes(StandardCharsets.UTF_8)).encode, failed.withDefaultValue(0)(uid)))
-    )
+    override def findPasswordHashAndAttempts(uid: UserId): F[Option[(PasswordHash, Int)]] =
+        Sync[F].delay(
+            Option((Password(DefaultPassword.getBytes(StandardCharsets.UTF_8)).encode, failed.withDefaultValue(0)(uid)))
+        )
 
-  override def findAccount(uid: UserId): F[Option[Account]] = Sync[F].pure(accounts.headOption)
+    override def findAccount(uid: UserId): F[Option[Account]] = Sync[F].pure(accounts.headOption)
 
-  override def findAccountByEmail(email: EmailAddress): F[Option[Account]] = Sync[F].pure(accounts.headOption)
+    override def findAccountByEmail(email: EmailAddress): F[Option[Account]] = Sync[F].pure(accounts.headOption)
 
-  override def unlockAccount(uid: UserId): F[Int] = {
-    locked = locked.filterNot(_ === uid)
-    Sync[F].pure(1)
-  }
+    override def unlockAccount(uid: UserId): F[Int] = {
+        locked = locked.filterNot(_ === uid)
+        Sync[F].pure(1)
+    }
 
-  override def deleteUserSession(id: SessionId): F[Int] = Sync[F].pure(1)
+    override def deleteUserSession(id: SessionId): F[Int] = Sync[F].pure(1)
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/hub/VcsRepositoryPatchMetadataTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/hub/VcsRepositoryPatchMetadataTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/hub/VcsRepositoryPatchMetadataTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/VcsRepositoryPatchMetadataTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -23,62 +23,62 @@
 import org.scalacheck.*
 
 final class VcsRepositoryPatchMetadataTest extends ScalaCheckSuite {
-  private val genVcsPatchFilename: Gen[VcsPatchFilename] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(chars => VcsPatchFilename(chars.mkString))
-  private val genVcsPatchFilenames: Gen[List[VcsPatchFilename]] = Gen.listOf(genVcsPatchFilename)
-  private val genVcsPatchSummaryFileModification: Gen[VcsPatchSummaryFileModification] = for {
-    name    <- genVcsPatchFilename
-    added   <- Gen.choose(0, Int.MaxValue)
-    removed <- Gen.choose(0, Int.MaxValue)
-  } yield VcsPatchSummaryFileModification(added, name, removed)
-  private val genVcsPatchSummaryFileModifications: Gen[List[VcsPatchSummaryFileModification]] =
-    Gen.listOf(genVcsPatchSummaryFileModification)
+    private val genVcsPatchFilename: Gen[VcsPatchFilename] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(chars => VcsPatchFilename(chars.mkString))
+    private val genVcsPatchFilenames: Gen[List[VcsPatchFilename]] = Gen.listOf(genVcsPatchFilename)
+    private val genVcsPatchSummaryFileModification: Gen[VcsPatchSummaryFileModification] = for {
+        name    <- genVcsPatchFilename
+        added   <- Gen.choose(0, Int.MaxValue)
+        removed <- Gen.choose(0, Int.MaxValue)
+    } yield VcsPatchSummaryFileModification(added, name, removed)
+    private val genVcsPatchSummaryFileModifications: Gen[List[VcsPatchSummaryFileModification]] =
+        Gen.listOf(genVcsPatchSummaryFileModification)
 
-  /** Create a darcs xml patch element from the given input for testing purposes.
-    *
-    * @return
-    *   An xml node which holds all information required to mimick a darcs log xml-output patch entry.
-    */
-  private def createPatchXml(
-      added: List[VcsPatchFilename],
-      modified: List[VcsPatchSummaryFileModification],
-      removed: List[VcsPatchFilename]
-  ): scala.xml.Node = {
-    // We need to preserve the xml parts as they are because line breaks etc. would be added to file names!
-    // format: off
-    val added_files   = added.map(filename => <add_file>{filename.toString}</add_file>)
-    val removed_files = removed.map(filename => <remove_file>{filename.toString}</remove_file>)
-    val modified_files = modified.map { summary =>
-      <modify_file>{summary.name.toString}<removed_lines num={summary.removed.toString}/><added_lines num={summary.added.toString}/></modify_file>
+    /** Create a darcs xml patch element from the given input for testing purposes.
+      *
+      * @return
+      *   An xml node which holds all information required to mimick a darcs log xml-output patch entry.
+      */
+    private def createPatchXml(
+        added: List[VcsPatchFilename],
+        modified: List[VcsPatchSummaryFileModification],
+        removed: List[VcsPatchFilename]
+    ): scala.xml.Node = {
+        // We need to preserve the xml parts as they are because line breaks etc. would be added to file names!
+        // format: off
+        val added_files   = added.map(filename => <add_file>{filename.toString}</add_file>)
+        val removed_files = removed.map(filename => <remove_file>{filename.toString}</remove_file>)
+        val modified_files = modified.map { summary =>
+          <modify_file>{summary.name.toString}<removed_lines num={summary.removed.toString}/><added_lines num={summary.added.toString}/></modify_file>
+        }
+        val patch =
+          <patch>
+            <name>TEST</name>
+            <comment>A test comment...</comment>
+            <summary>
+              {added_files}
+              {modified_files}
+              {removed_files}
+            </summary>
+          </patch>
+        // format: on
+        patch
     }
-    val patch =
-      <patch>
-        <name>TEST</name>
-        <comment>A test comment...</comment>
-        <summary>
-          {added_files}
-          {modified_files}
-          {removed_files}
-        </summary>
-      </patch>
-    // format: on
-    patch
-  }
 
-  property("VcsPatchSummary#fromDarcsXmlLog must parse a given XML node correctly") {
-    forAll(genVcsPatchFilenames, genVcsPatchSummaryFileModifications, genVcsPatchFilenames) {
-      (
-          added: List[VcsPatchFilename],
-          modified: List[VcsPatchSummaryFileModification],
-          removed: List[VcsPatchFilename]
-      ) =>
-        val xmlInput = createPatchXml(added, modified, removed)
-        val expected = VcsPatchSummary(added, modified, removed)
-        VcsPatchSummary.fromDarcsXmlLog(xmlInput) match {
-          case None           => fail("VcsPatchSummary could not be generated!")
-          case Some(obtained) => assertEquals(obtained, expected)
+    property("VcsPatchSummary#fromDarcsXmlLog must parse a given XML node correctly") {
+        forAll(genVcsPatchFilenames, genVcsPatchSummaryFileModifications, genVcsPatchFilenames) {
+            (
+                added: List[VcsPatchFilename],
+                modified: List[VcsPatchSummaryFileModification],
+                removed: List[VcsPatchFilename]
+            ) =>
+                val xmlInput = createPatchXml(added, modified, removed)
+                val expected = VcsPatchSummary(added, modified, removed)
+                VcsPatchSummary.fromDarcsXmlLog(xmlInput) match {
+                    case None           => fail("VcsPatchSummary could not be generated!")
+                    case Some(obtained) => assertEquals(obtained, expected)
+                }
         }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/DoobieSshAuthenticationRepositoryTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/DoobieSshAuthenticationRepositoryTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/DoobieSshAuthenticationRepositoryTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/DoobieSshAuthenticationRepositoryTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -26,24 +26,24 @@
 import doobie.*
 
 final class DoobieSshAuthenticationRepositoryTest extends BaseSpec {
-  test("findVcsRepositoryOwner must return the correct owner".tag(NeedsDatabase)) {
-    genValidAccount.sample match {
-      case Some(account) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val repo = new DoobieSshAuthenticationRepository[IO](tx)
-        val test = for {
-          _     <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
-          owner <- repo.findVcsRepositoryOwner(account.name)
-        } yield owner
-        test.assertEquals(account.toVcsRepositoryOwner.some)
-      case _ => fail("Could not generate data samples!")
+    test("findVcsRepositoryOwner must return the correct owner".tag(NeedsDatabase)) {
+        genValidAccount.sample match {
+            case Some(account) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val repo = new DoobieSshAuthenticationRepository[IO](tx)
+                val test = for {
+                    _     <- createAccount(account, PasswordHash("I am not a password hash!"), None, None)
+                    owner <- repo.findVcsRepositoryOwner(account.name)
+                } yield owner
+                test.assertEquals(account.toVcsRepositoryOwner.some)
+            case _ => fail("Could not generate data samples!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/PublicSshKeyTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -26,87 +26,87 @@
 import munit.*
 
 final class PublicSshKeyTest extends FunSuite {
-  test("SshKeyString must work for keys with comment") {
-    val input = scala.io.Source
-      .fromInputStream(
-        getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
-        "UTF-8"
-      )
-      .getLines()
-      .mkString
-    val expected = SshPublicKeyString(input)
-    SshPublicKeyString.from(input) match {
-      case None            => fail("Key could not be parsed!")
-      case Some(keyString) => assertEquals(keyString, expected)
+    test("SshKeyString must work for keys with comment") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        val expected = SshPublicKeyString(input)
+        SshPublicKeyString.from(input) match {
+            case None            => fail("Key could not be parsed!")
+            case Some(keyString) => assertEquals(keyString, expected)
+        }
     }
-  }
 
-  test("SshKeyString must work for keys without comment") {
-    val input = scala.io.Source
-      .fromInputStream(
-        getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
-        "UTF-8"
-      )
-      .getLines()
-      .mkString
-    val expected = SshPublicKeyString(input)
-    SshPublicKeyString.from(input) match {
-      case None            => fail("Key could not be parsed!")
-      case Some(keyString) => assertEquals(keyString, expected)
+    test("SshKeyString must work for keys without comment") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        val expected = SshPublicKeyString(input)
+        SshPublicKeyString.from(input) match {
+            case None            => fail("Key could not be parsed!")
+            case Some(keyString) => assertEquals(keyString, expected)
+        }
     }
-  }
 
-  test("PublicSshKey.from must work for keys with comment") {
-    val input = scala.io.Source
-      .fromInputStream(
-        getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
-        "UTF-8"
-      )
-      .getLines()
-      .mkString
-    val sshKey  = SshPublicKeyString(input)
-    val id      = UUID.randomUUID()
-    val ownerId = UserId.randomUserId
-    PublicSshKey.from(id)(ownerId)(OffsetDateTime.now(ZoneOffset.UTC))(sshKey) match {
-      case Some(key) =>
-        assertEquals(key.comment, Option(KeyComment("Some optional comment...")))
-        assertEquals(key.fingerprint, KeyFingerprint("qduYGwQlx7kMHo7GBNwx6tULMTxbuEbDJ6pdgC88ZSo"))
-        assertEquals(key.id, id)
-        assertEquals(
-          key.keyBytes,
-          EncodedKeyBytes("AAAAC3NzaC1lZDI1NTE5AAAAIH8ZU2xquZvstbesPktthwY2r5sanULBQKuM5bGHVdeP")
-        )
-        assertEquals(key.keyType, SshKeyType.SshEd25519)
-        assertEquals(key.ownerId, ownerId)
-      case _ => fail("PublicSshKey could not be created!")
+    test("PublicSshKey.from must work for keys with comment") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-with-comment.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        val sshKey  = SshPublicKeyString(input)
+        val id      = UUID.randomUUID()
+        val ownerId = UserId.randomUserId
+        PublicSshKey.from(id)(ownerId)(OffsetDateTime.now(ZoneOffset.UTC))(sshKey) match {
+            case Some(key) =>
+                assertEquals(key.comment, Option(KeyComment("Some optional comment...")))
+                assertEquals(key.fingerprint, KeyFingerprint("qduYGwQlx7kMHo7GBNwx6tULMTxbuEbDJ6pdgC88ZSo"))
+                assertEquals(key.id, id)
+                assertEquals(
+                    key.keyBytes,
+                    EncodedKeyBytes("AAAAC3NzaC1lZDI1NTE5AAAAIH8ZU2xquZvstbesPktthwY2r5sanULBQKuM5bGHVdeP")
+                )
+                assertEquals(key.keyType, SshKeyType.SshEd25519)
+                assertEquals(key.ownerId, ownerId)
+            case _ => fail("PublicSshKey could not be created!")
+        }
     }
-  }
 
-  test("PublicSshKey.from must work for keys without comment") {
-    val input = scala.io.Source
-      .fromInputStream(
-        getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
-        "UTF-8"
-      )
-      .getLines()
-      .mkString
-    val sshKey  = SshPublicKeyString(input)
-    val id      = UUID.randomUUID()
-    val ownerId = UserId.randomUserId
-    PublicSshKey.from(id)(ownerId)(OffsetDateTime.now(ZoneOffset.UTC))(sshKey) match {
-      case Some(key) =>
-        assertEquals(key.comment, None)
-        assertEquals(key.fingerprint, KeyFingerprint("tPwxtBT8SN5nq1kbT/vERM/pkJUAtMP6j+sf3m75UqE"))
-        assertEquals(key.id, id)
-        assertEquals(
-          key.keyBytes,
-          EncodedKeyBytes(
-            "AAAAB3NzaC1kc3MAAACBAKn1DHh6DaIg/cN6vNVh1VXvHhH86eKelfsolIfvTPQSb3vkqoWPG3T3DGmrUjbqvrrfzaKILTBRv05KqMCbJKETGR0fuY7G1/Nkd/6dZjw1ngYkGd0fr2ERGuq87+gdd1A3TeIqvdjnl7MG3bEGf+fIEJOrRJraZ+u/tDFlSYq/AAAAFQCAUrv94uu1dVTTiyoagKV4Y4QWuQAAAIAuR5mFFYAgT1+t1u16eRCou1nPO4+q35/6uNNCyXtP0BmZaxXqQw25foJz5OzSQWXjjianfRfUyjsHt5DgM0PAIZaqmxMUiVw7BT7zUTa7ucl9NQmFBexiedCtokVb8++vHVZ7Y42tf2CpqVW8T2lw5b8sWb7rHYGarI935qv2bgAAAIABfRnu0PkvysY6QJhUCD4ZKt3qZ6E1cYDivLhDb4GAZxmxSeN5cFPXU3Gst0oNmNjUW55rsZwZP+KkXi3NwAsTd9dZBxkcc+28m8Dr4hGtPTnPp+4p8wzw/X6Lmyr6RSykCK6xuv9rc2td+1fgNyPoWwcLZZQclDj+OdgQVHWj3A=="
-          )
-        )
-        assertEquals(key.keyType, SshKeyType.SshDsa)
-        assertEquals(key.ownerId, ownerId)
-      case _ => fail("PublicSshKey could not be created!")
+    test("PublicSshKey.from must work for keys without comment") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/ssh-key-without-comment.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        val sshKey  = SshPublicKeyString(input)
+        val id      = UUID.randomUUID()
+        val ownerId = UserId.randomUserId
+        PublicSshKey.from(id)(ownerId)(OffsetDateTime.now(ZoneOffset.UTC))(sshKey) match {
+            case Some(key) =>
+                assertEquals(key.comment, None)
+                assertEquals(key.fingerprint, KeyFingerprint("tPwxtBT8SN5nq1kbT/vERM/pkJUAtMP6j+sf3m75UqE"))
+                assertEquals(key.id, id)
+                assertEquals(
+                    key.keyBytes,
+                    EncodedKeyBytes(
+                        "AAAAB3NzaC1kc3MAAACBAKn1DHh6DaIg/cN6vNVh1VXvHhH86eKelfsolIfvTPQSb3vkqoWPG3T3DGmrUjbqvrrfzaKILTBRv05KqMCbJKETGR0fuY7G1/Nkd/6dZjw1ngYkGd0fr2ERGuq87+gdd1A3TeIqvdjnl7MG3bEGf+fIEJOrRJraZ+u/tDFlSYq/AAAAFQCAUrv94uu1dVTTiyoagKV4Y4QWuQAAAIAuR5mFFYAgT1+t1u16eRCou1nPO4+q35/6uNNCyXtP0BmZaxXqQw25foJz5OzSQWXjjianfRfUyjsHt5DgM0PAIZaqmxMUiVw7BT7zUTa7ucl9NQmFBexiedCtokVb8++vHVZ7Y42tf2CpqVW8T2lw5b8sWb7rHYGarI935qv2bgAAAIABfRnu0PkvysY6QJhUCD4ZKt3qZ6E1cYDivLhDb4GAZxmxSeN5cFPXU3Gst0oNmNjUW55rsZwZP+KkXi3NwAsTd9dZBxkcc+28m8Dr4hGtPTnPp+4p8wzw/X6Lmyr6RSykCK6xuv9rc2td+1fgNyPoWwcLZZQclDj+OdgQVHWj3A=="
+                    )
+                )
+                assertEquals(key.keyType, SshKeyType.SshDsa)
+                assertEquals(key.ownerId, ownerId)
+            case _ => fail("PublicSshKey could not be created!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshKeyTypeTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -20,52 +20,64 @@
 import munit.*
 
 final class SshKeyTypeTest extends FunSuite {
-  test("from must work for ssh-dsa") {
-    val input = scala.io.Source
-      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-dsa.pub"), "UTF-8")
-      .getLines()
-      .mkString
-    assertEquals(
-      SshKeyType.from(input),
-      Some(SshKeyType.SshDsa),
-      s"Incorrect key type extracted from: $input"
-    )
-  }
+    test("from must work for ssh-dsa") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-dsa.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        assertEquals(
+            SshKeyType.from(input),
+            Some(SshKeyType.SshDsa),
+            s"Incorrect key type extracted from: $input"
+        )
+    }
 
-  test("from must work for ssh-ecdsa") {
-    val input = scala.io.Source
-      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ecdsa.pub"), "UTF-8")
-      .getLines()
-      .mkString
-    assertEquals(
-      SshKeyType.from(input),
-      Some(SshKeyType.SshEcDsa),
-      s"Incorrect key type extracted from: $input"
-    )
-  }
+    test("from must work for ssh-ecdsa") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ecdsa.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        assertEquals(
+            SshKeyType.from(input),
+            Some(SshKeyType.SshEcDsa),
+            s"Incorrect key type extracted from: $input"
+        )
+    }
 
-  test("from must work for ssh-ed25519") {
-    val input = scala.io.Source
-      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ed25519.pub"), "UTF-8")
-      .getLines()
-      .mkString
-    assertEquals(
-      SshKeyType.from(input),
-      Some(SshKeyType.SshEd25519),
-      s"Incorrect key type extracted from: $input"
-    )
-  }
+    test("from must work for ssh-ed25519") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-ed25519.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        assertEquals(
+            SshKeyType.from(input),
+            Some(SshKeyType.SshEd25519),
+            s"Incorrect key type extracted from: $input"
+        )
+    }
 
-  test("from must work for ssh-rsa") {
-    val input = scala.io.Source
-      .fromInputStream(getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-rsa.pub"), "UTF-8")
-      .getLines()
-      .mkString
-    assertEquals(
-      SshKeyType.from(input),
-      Some(SshKeyType.SshRsa),
-      s"Incorrect key type extracted from: $input"
-    )
-  }
+    test("from must work for ssh-rsa") {
+        val input = scala.io.Source
+            .fromInputStream(
+                getClass().getClassLoader().getResourceAsStream("de/smederee/ssh/test-ssh-rsa.pub"),
+                "UTF-8"
+            )
+            .getLines()
+            .mkString
+        assertEquals(
+            SshKeyType.from(input),
+            Some(SshKeyType.SshRsa),
+            s"Incorrect key type extracted from: $input"
+        )
+    }
 
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshServerProviderTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshServerProviderTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshServerProviderTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshServerProviderTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -26,45 +26,45 @@
 
 final class SshServerProviderTest extends BaseSpec {
 
-  val repositoriesDirectory = ResourceSuiteLocalFixture(
-    "repositories-directory",
-    Resource.make(IO(Files.createTempDirectory("test-repo-dir")))(path => deleteDirectory(path) *> IO.unit)
-  )
-
-  val serverKeyFile = ResourceSuiteLocalFixture(
-    "server-key-file",
-    Resource.make(
-      for {
-        path <- IO(Files.createTempFile("test-server-", ".key"))
-        _    <- IO(Files.deleteIfExists(path))
-      } yield path
-    )(_ => IO.unit)
-  )
-
-  val freePort = ResourceFixture(Resource.make(IO(findFreePort()))(_ => IO.unit))
-
-  override def munitFixtures = List(repositoriesDirectory, serverKeyFile)
-
-  freePort.test("run() must create and start a server with the given configuration") { port =>
-    port match {
-      case None => fail("Could not find a free port for testing!")
-      case Some(portNumber) =>
-        val keyfile     = serverKeyFile()
-        val darcsConfig = DarcsConfiguration(Paths.get("darcs"), DirectoryPath(repositoriesDirectory()))
-        val sshConfig = SshServerConfiguration(
-          enabled = true,
-          genericUser = SshUsername("darcs"),
-          host = host"localhost",
-          port = portNumber,
-          serverKeyFile = keyfile
-        )
-        val provider = new SshServerProvider(darcsConfig, configuration.database, sshConfig)
-        provider.run().use { server =>
-          assert(server.isStarted(), "Server not started!")
-          assertEquals(server.getPort(), portNumber.toString.toInt)
-          assertEquals(server.getHost(), null) // scalafix:ok
-          IO.unit
+    val repositoriesDirectory = ResourceSuiteLocalFixture(
+        "repositories-directory",
+        Resource.make(IO(Files.createTempDirectory("test-repo-dir")))(path => deleteDirectory(path) *> IO.unit)
+    )
+
+    val serverKeyFile = ResourceSuiteLocalFixture(
+        "server-key-file",
+        Resource.make(
+            for {
+                path <- IO(Files.createTempFile("test-server-", ".key"))
+                _    <- IO(Files.deleteIfExists(path))
+            } yield path
+        )(_ => IO.unit)
+    )
+
+    val freePort = ResourceFixture(Resource.make(IO(findFreePort()))(_ => IO.unit))
+
+    override def munitFixtures = List(repositoriesDirectory, serverKeyFile)
+
+    freePort.test("run() must create and start a server with the given configuration") { port =>
+        port match {
+            case None => fail("Could not find a free port for testing!")
+            case Some(portNumber) =>
+                val keyfile     = serverKeyFile()
+                val darcsConfig = DarcsConfiguration(Paths.get("darcs"), DirectoryPath(repositoriesDirectory()))
+                val sshConfig = SshServerConfiguration(
+                    enabled = true,
+                    genericUser = SshUsername("darcs"),
+                    host = host"localhost",
+                    port = portNumber,
+                    serverKeyFile = keyfile
+                )
+                val provider = new SshServerProvider(darcsConfig, configuration.database, sshConfig)
+                provider.run().use { server =>
+                    assert(server.isStarted(), "Server not started!")
+                    assertEquals(server.getPort(), portNumber.toString.toInt)
+                    assertEquals(server.getHost(), null) // scalafix:ok
+                    IO.unit
+                }
         }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshUsernameTest.scala new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshUsernameTest.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshUsernameTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/ssh/SshUsernameTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -27,47 +27,47 @@
 import org.scalacheck.*
 
 final class SshUsernameTest extends ScalaCheckSuite {
-  val genValidSshUsername: Gen[SshUsername] =
-    for {
-      firstLetter    <- Gen.alphaChar
-      randomAlphaNum <- Gen.nonEmptyListOf(Gen.alphaNumChar).suchThat(_.length > 2)
-      name = s"$firstLetter${randomAlphaNum.take(15).mkString}".toLowerCase(Locale.ROOT)
-    } yield SshUsername(name)
-  given Arbitrary[SshUsername] = Arbitrary(genValidSshUsername)
-
-  property("SshUsername cannot be longer than 16 characters") {
-    forAll { (randomSshUsername: SshUsername) =>
-      val invalid = s"randomSshUsername${List.fill(16)("a").mkString}"
-      assert(SshUsername.from(invalid).isEmpty)
+    val genValidSshUsername: Gen[SshUsername] =
+        for {
+            firstLetter    <- Gen.alphaChar
+            randomAlphaNum <- Gen.nonEmptyListOf(Gen.alphaNumChar).suchThat(_.length > 2)
+            name = s"$firstLetter${randomAlphaNum.take(15).mkString}".toLowerCase(Locale.ROOT)
+        } yield SshUsername(name)
+    given Arbitrary[SshUsername] = Arbitrary(genValidSshUsername)
+
+    property("SshUsername cannot be longer than 16 characters") {
+        forAll { (randomSshUsername: SshUsername) =>
+            val invalid = s"randomSshUsername${List.fill(16)("a").mkString}"
+            assert(SshUsername.from(invalid).isEmpty)
+        }
     }
-  }
 
-  property("SshUsername must start with a letter") {
-    forAll { (randomSshUsername: SshUsername) =>
-      val invalid = s"1${randomSshUsername.toString.take(15)}"
-      assert(SshUsername.from(invalid).isEmpty)
+    property("SshUsername must start with a letter") {
+        forAll { (randomSshUsername: SshUsername) =>
+            val invalid = s"1${randomSshUsername.toString.take(15)}"
+            assert(SshUsername.from(invalid).isEmpty)
+        }
     }
-  }
 
-  property("SshUsername must be at least 3 characters long") {
-    forAll { (randomSshUsername: SshUsername) =>
-      val invalid = randomSshUsername.toString.take(2)
-      assert(SshUsername.from(invalid).isEmpty)
+    property("SshUsername must be at least 3 characters long") {
+        forAll { (randomSshUsername: SshUsername) =>
+            val invalid = randomSshUsername.toString.take(2)
+            assert(SshUsername.from(invalid).isEmpty)
+        }
     }
-  }
 
-  property("SshUsername.from must fail on invalid input") {
-    forAll { (string: String) =>
-      assert(SshUsername.from(string).nonEmpty === SshUsername.Format.matches(string))
+    property("SshUsername.from must fail on invalid input") {
+        forAll { (string: String) =>
+            assert(SshUsername.from(string).nonEmpty === SshUsername.Format.matches(string))
+        }
     }
-  }
 
-  property("SshUsername.fromString must succeed on valid input") {
-    forAll { (randomSshUsername: SshUsername) =>
-      SshUsername.from(randomSshUsername.toString) match {
-        case None           => fail(s"Failed to create from valid username: $randomSshUsername")
-        case Some(username) => assertEquals(username, randomSshUsername)
-      }
+    property("SshUsername.fromString must succeed on valid input") {
+        forAll { (randomSshUsername: SshUsername) =>
+            SshUsername.from(randomSshUsername.toString) match {
+                case None           => fail(s"Failed to create from valid username: $randomSshUsername")
+                case Some(username) => assertEquals(username, randomSshUsername)
+            }
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/hub/src/test/scala/de/smederee/TestTags.scala new-smederee/modules/hub/src/test/scala/de/smederee/TestTags.scala
--- old-smederee/modules/hub/src/test/scala/de/smederee/TestTags.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/hub/src/test/scala/de/smederee/TestTags.scala	2025-01-13 17:13:25.056470978 +0000
@@ -21,5 +21,5 @@
   * connection.
   */
 object TestTags {
-  val NeedsDatabase = new munit.Tag("NeedsDatabase")
+    val NeedsDatabase = new munit.Tag("NeedsDatabase")
 }
diff -rN -u old-smederee/modules/i18n/src/main/scala/de/smederee/i18n/LanguageCode.scala new-smederee/modules/i18n/src/main/scala/de/smederee/i18n/LanguageCode.scala
--- old-smederee/modules/i18n/src/main/scala/de/smederee/i18n/LanguageCode.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/i18n/src/main/scala/de/smederee/i18n/LanguageCode.scala	2025-01-13 17:13:25.060470984 +0000
@@ -23,39 +23,39 @@
 
 opaque type LanguageCode = String
 object LanguageCode {
-  given Eq[LanguageCode] = Eq.fromUniversalEquals
+    given Eq[LanguageCode] = Eq.fromUniversalEquals
 
-  val FormatIso639: Regex = "^[a-z]{2,3}$".r
+    val FormatIso639: Regex = "^[a-z]{2,3}$".r
 
-  /** Create an instance of LanguageCode from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a LanguageCode.
-    * @return
-    *   The appropriate instance of LanguageCode.
-    */
-  def apply(source: String): LanguageCode = source
-
-  /** Try to create an instance of LanguageCode from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a LanguageCode.
-    * @return
-    *   An option to the successfully converted LanguageCode.
-    */
-  def from(source: String): Option[LanguageCode] =
-    Option(source).map(FormatIso639.matches) match {
-      case Some(true) => Option(source)
-      case _          => None
-    }
-
-  extension (code: LanguageCode) {
+    /** Create an instance of LanguageCode from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a LanguageCode.
+      * @return
+      *   The appropriate instance of LanguageCode.
+      */
+    def apply(source: String): LanguageCode = source
 
-    /** Convert the language code into a [[java.util.Locale]] to be used for internationalisation functions.
+    /** Try to create an instance of LanguageCode from the given String.
       *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a LanguageCode.
       * @return
-      *   A locale retrieved via the `forLanguageTag` method of the Java Locale implementation.
+      *   An option to the successfully converted LanguageCode.
       */
-    def toLocale: java.util.Locale = java.util.Locale.forLanguageTag(code.toString)
-  }
+    def from(source: String): Option[LanguageCode] =
+        Option(source).map(FormatIso639.matches) match {
+            case Some(true) => Option(source)
+            case _          => None
+        }
+
+    extension (code: LanguageCode) {
+
+        /** Convert the language code into a [[java.util.Locale]] to be used for internationalisation functions.
+          *
+          * @return
+          *   A locale retrieved via the `forLanguageTag` method of the Java Locale implementation.
+          */
+        def toLocale: java.util.Locale = java.util.Locale.forLanguageTag(code.toString)
+    }
 }
diff -rN -u old-smederee/modules/i18n/src/main/scala/de/smederee/i18n/Messages.scala new-smederee/modules/i18n/src/main/scala/de/smederee/i18n/Messages.scala
--- old-smederee/modules/i18n/src/main/scala/de/smederee/i18n/Messages.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/i18n/src/main/scala/de/smederee/i18n/Messages.scala	2025-01-13 17:13:25.060470984 +0000
@@ -28,76 +28,76 @@
 import scala.util.Try
 
 object Messages {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  val DefaultLocale: Locale = Locale.ENGLISH
-  val Filename: String      = "messages"
+    val DefaultLocale: Locale = Locale.ENGLISH
+    val Filename: String      = "messages"
 
-  /** Provide a way to simply write `Messages("my.key", foo, bar)` to translate stuff. This function will catch possible
-    * exceptions underneath and in case of an error will log an error message and the underlying exception and will
-    * return the given message string instead a translation!
-    *
-    * @param message
-    *   The message to be translated, which should be a valid property key.
-    * @param args
-    *   A list of arguments which will be used in the [[java.text.MessageFormat]] to fill in placeholders.
-    * @param locale
-    *   The implicitly provided locale which determines which resource bundle will be used.
-    * @return
-    *   The translated and formatted (placeholders filled in) string.
-    */
-  def apply(message: String, args: Any*)(using locale: Locale): String =
-    Try {
-      val usedLocale =
-        Option(locale).getOrElse(DefaultLocale) // Fallback to default locale if provided one is null.
-      val formatter = new MessageFormat(getProperty(usedLocale)(message), usedLocale)
-      formatter.format(args.toArray)
-    } match {
-      case Failure(error) =>
-        log.error(s"Error during translation of '$message' for locale $locale!", error)
-        message
-      case Success(translation) => translation
-    }
+    /** Provide a way to simply write `Messages("my.key", foo, bar)` to translate stuff. This function will catch
+      * possible exceptions underneath and in case of an error will log an error message and the underlying exception
+      * and will return the given message string instead a translation!
+      *
+      * @param message
+      *   The message to be translated, which should be a valid property key.
+      * @param args
+      *   A list of arguments which will be used in the [[java.text.MessageFormat]] to fill in placeholders.
+      * @param locale
+      *   The implicitly provided locale which determines which resource bundle will be used.
+      * @return
+      *   The translated and formatted (placeholders filled in) string.
+      */
+    def apply(message: String, args: Any*)(using locale: Locale): String =
+        Try {
+            val usedLocale =
+                Option(locale).getOrElse(DefaultLocale) // Fallback to default locale if provided one is null.
+            val formatter = new MessageFormat(getProperty(usedLocale)(message), usedLocale)
+            formatter.format(args.toArray)
+        } match {
+            case Failure(error) =>
+                log.error(s"Error during translation of '$message' for locale $locale!", error)
+                message
+            case Success(translation) => translation
+        }
 
-  /** Provide a way to simply write `Messages("my.key", foo, bar)` to translate stuff.
-    *
-    * @param message
-    *   The message to be translated, which should be a valid property key.
-    * @param args
-    *   A list of arguments which will be used in the [[java.text.MessageFormat]] to fill in placeholders.
-    * @param locale
-    *   The implicitly provided locale which determines which resource bundle will be used.
-    * @return
-    *   The translated and formatted (placeholders filled in) string.
-    */
-  @throws[java.util.MissingResourceException]("if the message key cannot be found in the resource bundle")
-  def unsafeApply(message: String, args: Any*)(using locale: Locale): String = {
-    val usedLocale =
-      Option(locale).getOrElse(DefaultLocale) // Fallback to default locale if provided one is null.
-    val formatter = new MessageFormat(getProperty(usedLocale)(message), usedLocale)
-    formatter.format(args.toArray)
-  }
+    /** Provide a way to simply write `Messages("my.key", foo, bar)` to translate stuff.
+      *
+      * @param message
+      *   The message to be translated, which should be a valid property key.
+      * @param args
+      *   A list of arguments which will be used in the [[java.text.MessageFormat]] to fill in placeholders.
+      * @param locale
+      *   The implicitly provided locale which determines which resource bundle will be used.
+      * @return
+      *   The translated and formatted (placeholders filled in) string.
+      */
+    @throws[java.util.MissingResourceException]("if the message key cannot be found in the resource bundle")
+    def unsafeApply(message: String, args: Any*)(using locale: Locale): String = {
+        val usedLocale =
+            Option(locale).getOrElse(DefaultLocale) // Fallback to default locale if provided one is null.
+        val formatter = new MessageFormat(getProperty(usedLocale)(message), usedLocale)
+        formatter.format(args.toArray)
+    }
 
-  /** Return the string stored in the properties bundle for the given locale and key.
-    *
-    * If the resource bundle for the desired locale cannot be found then the function falls back to the default one.
-    *
-    * @param locale
-    *   The locale for which the string shall be returned.
-    * @param key
-    *   A key which must be present in the resource bundle of the given locale.
-    * @return
-    *   The string defined in the resource bundle of the locale at the given key.
-    */
-  @throws[java.lang.NullPointerException]("if base filename or locale is null")
-  @throws[java.util.MissingResourceException]("if the message key cannot be found in the resource bundle")
-  @throws[java.lang.ClassCastException]("if the object found for the given key is not a string")
-  def getProperty(locale: Locale)(key: String): String = {
-    val filename = s"messages_${locale.getLanguage.toString}.properties"
-    log.debug("Checking for resource bundle: {}", filename)
-    val usedLocale = Option(getClass.getClassLoader.getResource(filename)).map(_ => locale).getOrElse(DefaultLocale)
-    log.debug("Using resource bundle for locale: {}", usedLocale)
-    val bundle = ResourceBundle.getBundle(Filename, usedLocale)
-    bundle.getString(key)
-  }
+    /** Return the string stored in the properties bundle for the given locale and key.
+      *
+      * If the resource bundle for the desired locale cannot be found then the function falls back to the default one.
+      *
+      * @param locale
+      *   The locale for which the string shall be returned.
+      * @param key
+      *   A key which must be present in the resource bundle of the given locale.
+      * @return
+      *   The string defined in the resource bundle of the locale at the given key.
+      */
+    @throws[java.lang.NullPointerException]("if base filename or locale is null")
+    @throws[java.util.MissingResourceException]("if the message key cannot be found in the resource bundle")
+    @throws[java.lang.ClassCastException]("if the object found for the given key is not a string")
+    def getProperty(locale: Locale)(key: String): String = {
+        val filename = s"messages_${locale.getLanguage.toString}.properties"
+        log.debug("Checking for resource bundle: {}", filename)
+        val usedLocale = Option(getClass.getClassLoader.getResource(filename)).map(_ => locale).getOrElse(DefaultLocale)
+        log.debug("Using resource bundle for locale: {}", usedLocale)
+        val bundle = ResourceBundle.getBundle(Filename, usedLocale)
+        bundle.getString(key)
+    }
 }
diff -rN -u old-smederee/modules/i18n/src/test/scala/de/smederee/i18n/LanguageCodeTest.scala new-smederee/modules/i18n/src/test/scala/de/smederee/i18n/LanguageCodeTest.scala
--- old-smederee/modules/i18n/src/test/scala/de/smederee/i18n/LanguageCodeTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/i18n/src/test/scala/de/smederee/i18n/LanguageCodeTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -25,25 +25,25 @@
 import org.scalacheck.*
 
 final class LanguageCodeTest extends ScalaCheckSuite {
-  val genLocale: Gen[Locale] = Gen.oneOf(Locale.getAvailableLocales.toList.filter(_.getISO3Language.nonEmpty))
-  given Arbitrary[Locale]    = Arbitrary(genLocale)
+    val genLocale: Gen[Locale] = Gen.oneOf(Locale.getAvailableLocales.toList.filter(_.getISO3Language.nonEmpty))
+    given Arbitrary[Locale]    = Arbitrary(genLocale)
 
-  val genLanguageCode: Gen[LanguageCode] = genLocale.map(_.getISO3Language).filter(_.nonEmpty).map(LanguageCode.apply)
-  given Arbitrary[LanguageCode]          = Arbitrary(genLanguageCode)
+    val genLanguageCode: Gen[LanguageCode] = genLocale.map(_.getISO3Language).filter(_.nonEmpty).map(LanguageCode.apply)
+    given Arbitrary[LanguageCode]          = Arbitrary(genLanguageCode)
 
-  property("from must work for correct language codes") {
-    forAll { (locale: Locale) =>
-      LanguageCode.from(locale.getISO3Language) match {
-        case None               => fail(s"No LanguageCode created from given input (locale: $locale)!")
-        case Some(languageCode) => assertEquals(languageCode.toString, locale.getISO3Language)
-      }
+    property("from must work for correct language codes") {
+        forAll { (locale: Locale) =>
+            LanguageCode.from(locale.getISO3Language) match {
+                case None               => fail(s"No LanguageCode created from given input (locale: $locale)!")
+                case Some(languageCode) => assertEquals(languageCode.toString, locale.getISO3Language)
+            }
+        }
     }
-  }
 
-  property("toLocale must return correct locales") {
-    forAll { (code: LanguageCode) =>
-      val expectedLocale = Locale.forLanguageTag(code.toString)
-      assertEquals(code.toLocale, expectedLocale)
+    property("toLocale must return correct locales") {
+        forAll { (code: LanguageCode) =>
+            val expectedLocale = Locale.forLanguageTag(code.toString)
+            assertEquals(code.toLocale, expectedLocale)
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/i18n/src/test/scala/de/smederee/i18n/MessagesTest.scala new-smederee/modules/i18n/src/test/scala/de/smederee/i18n/MessagesTest.scala
--- old-smederee/modules/i18n/src/test/scala/de/smederee/i18n/MessagesTest.scala	2025-01-13 17:13:25.036470950 +0000
+++ new-smederee/modules/i18n/src/test/scala/de/smederee/i18n/MessagesTest.scala	2025-01-13 17:13:25.060470984 +0000
@@ -26,45 +26,45 @@
 import org.scalacheck.*
 
 final class MessagesTest extends ScalaCheckSuite {
-  val genLocale: Gen[Locale] = Gen.oneOf(Locale.getAvailableLocales.toList)
-  given Arbitrary[Locale]    = Arbitrary(genLocale)
+    val genLocale: Gen[Locale] = Gen.oneOf(Locale.getAvailableLocales.toList)
+    given Arbitrary[Locale]    = Arbitrary(genLocale)
 
-  property("Messages must fall back to default locale if expected one is missing") {
-    forAll { (locale: Locale) =>
-      given Locale = locale
-      val message  = "test.main.only"
-      val expected = "This should only exist in the main properties file."
-      val obtained = Messages(message)
-      assertEquals(obtained, expected)
+    property("Messages must fall back to default locale if expected one is missing") {
+        forAll { (locale: Locale) =>
+            given Locale = locale
+            val message  = "test.main.only"
+            val expected = "This should only exist in the main properties file."
+            val obtained = Messages(message)
+            assertEquals(obtained, expected)
+        }
     }
-  }
 
-  test("Messages must translate correctly") {
-    given Locale = Locale.GERMAN
-    val message  = "test.title"
-    val expected = "Das ist eine deutsche Übersetzung!"
-    val obtained = Messages(message)
-    assertEquals(obtained, expected)
-  }
-
-  property("Messages must return the given key if it is not defined in the resource bundle") {
-    forAll { (message: String) =>
-      given Locale    = Messages.DefaultLocale
-      val translation = Messages(message)
-      assertEquals(translation, message)
+    test("Messages must translate correctly") {
+        given Locale = Locale.GERMAN
+        val message  = "test.title"
+        val expected = "Das ist eine deutsche Übersetzung!"
+        val obtained = Messages(message)
+        assertEquals(obtained, expected)
     }
-  }
 
-  property(
-    "Messages.unsafeApply must fail with MissingResourceException if key is not defined in the resource bundle"
-  ) {
-    forAll { (message: String) =>
-      given Locale = Messages.DefaultLocale
-      intercept[MissingResourceException] {
-        Messages.unsafeApply(message)
-      }
-      assert(true) // Workaround for the intercept part.
+    property("Messages must return the given key if it is not defined in the resource bundle") {
+        forAll { (message: String) =>
+            given Locale    = Messages.DefaultLocale
+            val translation = Messages(message)
+            assertEquals(translation, message)
+        }
+    }
+
+    property(
+        "Messages.unsafeApply must fail with MissingResourceException if key is not defined in the resource bundle"
+    ) {
+        forAll { (message: String) =>
+            given Locale = Messages.DefaultLocale
+            intercept[MissingResourceException] {
+                Messages.unsafeApply(message)
+            }
+            assert(true) // Workaround for the intercept part.
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/CsrfToken.scala new-smederee/modules/security/src/main/scala/de/smederee/security/CsrfToken.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/CsrfToken.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/CsrfToken.scala	2025-01-13 17:13:25.060470984 +0000
@@ -20,25 +20,25 @@
 opaque type CsrfToken = String
 object CsrfToken {
 
-  /** Create an instance of CsrfToken from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a CsrfToken.
-    * @return
-    *   The appropriate instance of CsrfToken.
-    */
-  def apply(source: String): CsrfToken = source
+    /** Create an instance of CsrfToken from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a CsrfToken.
+      * @return
+      *   The appropriate instance of CsrfToken.
+      */
+    def apply(source: String): CsrfToken = source
 
-  /** Try to create an instance of CsrfToken from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a CsrfToken.
-    * @return
-    *   An option to the successfully converted CsrfToken.
-    */
-  def from(source: String): Option[CsrfToken] =
-    Option(source).map(_.trim.nonEmpty) match {
-      case Some(true) => Option(source.trim)
-      case _          => None
-    }
+    /** Try to create an instance of CsrfToken from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a CsrfToken.
+      * @return
+      *   An option to the successfully converted CsrfToken.
+      */
+    def from(source: String): Option[CsrfToken] =
+        Option(source).map(_.trim.nonEmpty) match {
+            case Some(true) => Option(source.trim)
+            case _          => None
+        }
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/PasswordHash.scala new-smederee/modules/security/src/main/scala/de/smederee/security/PasswordHash.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/PasswordHash.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/PasswordHash.scala	2025-01-13 17:13:25.064470989 +0000
@@ -21,27 +21,27 @@
 
 opaque type PasswordHash = String
 object PasswordHash {
-  given Eq[PasswordHash] = Eq.fromUniversalEquals
+    given Eq[PasswordHash] = Eq.fromUniversalEquals
 
-  /** Create an instance of PasswordHash from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a PasswordHash.
-    * @return
-    *   The appropriate instance of PasswordHash.
-    */
-  def apply(source: String): PasswordHash = source
+    /** Create an instance of PasswordHash from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a PasswordHash.
+      * @return
+      *   The appropriate instance of PasswordHash.
+      */
+    def apply(source: String): PasswordHash = source
 
-  /** Try to create an instance of PasswordHash from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a PasswordHash.
-    * @return
-    *   An option to the successfully converted PasswordHash.
-    */
-  def from(source: String): Option[PasswordHash] =
-    Option(source).map(_.trim.nonEmpty) match {
-      case Some(true) => Option(source.trim)
-      case _          => None
-    }
+    /** Try to create an instance of PasswordHash from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a PasswordHash.
+      * @return
+      *   An option to the successfully converted PasswordHash.
+      */
+    def from(source: String): Option[PasswordHash] =
+        Option(source).map(_.trim.nonEmpty) match {
+            case Some(true) => Option(source.trim)
+            case _          => None
+        }
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/Password.scala new-smederee/modules/security/src/main/scala/de/smederee/security/Password.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/Password.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/Password.scala	2025-01-13 17:13:25.064470989 +0000
@@ -28,53 +28,53 @@
   */
 opaque type Password = Array[Byte]
 object Password {
-  // The minimum length of a password to be considered valid.
-  val MinimumLength: Int = 12
-  // The maximum length of a password might look contra-productive but it can prevent "long password denial of service attacks".
-  val MaximumLength: Int = 128
+    // The minimum length of a password to be considered valid.
+    val MinimumLength: Int = 12
+    // The maximum length of a password might look contra-productive but it can prevent "long password denial of service attacks".
+    val MaximumLength: Int = 128
 
-  /** Create an instance of Password from the given Array[Byte] type.
-    *
-    * @param source
-    *   An instance of type Array[Byte] which will be returned as a Password.
-    * @return
-    *   The appropriate instance of Password.
-    */
-  def apply(source: Array[Byte]): Password = source
+    /** Create an instance of Password from the given Array[Byte] type.
+      *
+      * @param source
+      *   An instance of type Array[Byte] which will be returned as a Password.
+      * @return
+      *   The appropriate instance of Password.
+      */
+    def apply(source: Array[Byte]): Password = source
 
-  /** Try to create an instance of Password from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a Password.
-    * @return
-    *   An option to the successfully converted Password.
-    */
-  def from(source: String): Option[Password] =
-    Option(source) match {
-      case None         => None
-      case Some(string) => Option(string.getBytes(StandardCharsets.UTF_8))
-    }
+    /** Try to create an instance of Password from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a Password.
+      * @return
+      *   An option to the successfully converted Password.
+      */
+    def from(source: String): Option[Password] =
+        Option(source) match {
+            case None         => None
+            case Some(string) => Option(string.getBytes(StandardCharsets.UTF_8))
+        }
 
-  /** Validate the given String against the criteria for a valid password.
-    *
-    * @param source
-    *   A string which should fulfil the password criteria.
-    * @return
-    *   Either a list of errors or the validated Password.
-    */
-  def validate(source: String): ValidatedNec[String, Password] =
-    Option(source).map(_.trim).filter(_.nonEmpty) match {
-      case Some(password) =>
-        if (password.length < MinimumLength)
-          s"Password must be at least $MinimumLength characters long!".invalidNec
-        else if (password.length > MaximumLength)
-          s"Password must not be longer than $MaximumLength characters!".invalidNec
-        else
-          password.getBytes(StandardCharsets.UTF_8).validNec
-      case _ => "Password must not be empty!".invalidNec
-    }
+    /** Validate the given String against the criteria for a valid password.
+      *
+      * @param source
+      *   A string which should fulfil the password criteria.
+      * @return
+      *   Either a list of errors or the validated Password.
+      */
+    def validate(source: String): ValidatedNec[String, Password] =
+        Option(source).map(_.trim).filter(_.nonEmpty) match {
+            case Some(password) =>
+                if (password.length < MinimumLength)
+                    s"Password must be at least $MinimumLength characters long!".invalidNec
+                else if (password.length > MaximumLength)
+                    s"Password must not be longer than $MaximumLength characters!".invalidNec
+                else
+                    password.getBytes(StandardCharsets.UTF_8).validNec
+            case _ => "Password must not be empty!".invalidNec
+        }
 
-  extension (p: Password) {
-    def toArray: Array[Byte] = p
-  }
+    extension (p: Password) {
+        def toArray: Array[Byte] = p
+    }
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/PrivateKey.scala new-smederee/modules/security/src/main/scala/de/smederee/security/PrivateKey.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/PrivateKey.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/PrivateKey.scala	2025-01-13 17:13:25.064470989 +0000
@@ -20,36 +20,36 @@
 opaque type PrivateKey = Array[Byte]
 object PrivateKey {
 
-  /** Create an instance of PrivateKey from the given Array[Byte] type.
-    *
-    * @param source
-    *   An instance of type Array[Byte] which will be returned as a PrivateKey.
-    * @return
-    *   The appropriate instance of PrivateKey.
-    */
-  def apply(source: Array[Byte]): PrivateKey = source
-
-  /** Try to create an instance of PrivateKey from the given Array[Byte].
-    *
-    * @param source
-    *   A Array[Byte] that should fulfil the requirements to be converted into a PrivateKey.
-    * @return
-    *   An option to the successfully converted PrivateKey.
-    */
-  def from(source: Array[Byte]): Option[PrivateKey] =
-    if (source.isEmpty: Boolean)
-      None
-    else
-      Option(source)
-
-  extension (key: PrivateKey) {
+    /** Create an instance of PrivateKey from the given Array[Byte] type.
+      *
+      * @param source
+      *   An instance of type Array[Byte] which will be returned as a PrivateKey.
+      * @return
+      *   The appropriate instance of PrivateKey.
+      */
+    def apply(source: Array[Byte]): PrivateKey = source
 
-    /** Return the corresponding array of bytes for the private key.
+    /** Try to create an instance of PrivateKey from the given Array[Byte].
       *
+      * @param source
+      *   A Array[Byte] that should fulfil the requirements to be converted into a PrivateKey.
       * @return
-      *   An array of bytes representing the private key.
+      *   An option to the successfully converted PrivateKey.
       */
-    def toArray: Array[Byte] = key
+    def from(source: Array[Byte]): Option[PrivateKey] =
+        if (source.isEmpty: Boolean)
+            None
+        else
+            Option(source)
+
+    extension (key: PrivateKey) {
+
+        /** Return the corresponding array of bytes for the private key.
+          *
+          * @return
+          *   An array of bytes representing the private key.
+          */
+        def toArray: Array[Byte] = key
 
-  }
+    }
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/SignAndValidate.scala new-smederee/modules/security/src/main/scala/de/smederee/security/SignAndValidate.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/SignAndValidate.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/SignAndValidate.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,51 +27,51 @@
 
 extension (bytes: Array[Byte]) {
 
-  /** Convert the byte array into a string containing the hexadecimal representation of the content.
-    *
-    * @return
-    *   The hexadecimal notation representing the content.
-    */
-  def toHexString: String = Hex.encode(bytes).mkString
+    /** Convert the byte array into a string containing the hexadecimal representation of the content.
+      *
+      * @return
+      *   The hexadecimal notation representing the content.
+      */
+    def toHexString: String = Hex.encode(bytes).mkString
 }
 
 extension (string: String) {
 
-  /** Convert the string into an array of bytes, assuming that it follows the notation generated by the corresponding
-    * `toHexString` function.
-    *
-    * @return
-    *   An array containing the bytes represented by the given hexadecimal codes.
-    */
-  def toBytes: Array[Byte] = Hex.decode(string)
+    /** Convert the string into an array of bytes, assuming that it follows the notation generated by the corresponding
+      * `toHexString` function.
+      *
+      * @return
+      *   An array containing the bytes represented by the given hexadecimal codes.
+      */
+    def toBytes: Array[Byte] = Hex.decode(string)
 }
 
 opaque type SignedToken = String
 object SignedToken {
-  val Separator: String = "#" // The separator character to be used.
+    val Separator: String = "#" // The separator character to be used.
 
-  /** Create an instance of SignedToken from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a SignedToken.
-    * @return
-    *   The appropriate instance of SignedToken.
-    */
-  def apply(source: String): SignedToken = source
-
-  /** Try to create an instance of SignedToken from the given String. Please note that this function will only do a
-    * syntactic check!
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SignedToken.
-    * @return
-    *   An option to the successfully converted SignedToken.
-    */
-  def from(source: String): Option[SignedToken] =
-    Option(source).map(_.split(Separator, 3).length) match {
-      case Some(3) => Option(source)
-      case _       => None
-    }
+    /** Create an instance of SignedToken from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a SignedToken.
+      * @return
+      *   The appropriate instance of SignedToken.
+      */
+    def apply(source: String): SignedToken = source
+
+    /** Try to create an instance of SignedToken from the given String. Please note that this function will only do a
+      * syntactic check!
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SignedToken.
+      * @return
+      *   An option to the successfully converted SignedToken.
+      */
+    def from(source: String): Option[SignedToken] =
+        Option(source).map(_.split(Separator, 3).length) match {
+            case Some(3) => Option(source)
+            case _       => None
+        }
 }
 
 /** A class containing a private key and providing functionality for signing and validating messages / tokens.
@@ -80,60 +80,60 @@
   *   A private key used to initialise the underlying cryptographic implementations.
   */
 final case class SignAndValidate(private val privateKey: PrivateKey) {
-  val Algorithm: String = "HmacSHA256"
+    val Algorithm: String = "HmacSHA256"
+
+    /** Sign the given message string and return a string containing the signature in hexadecimal notation.
+      *
+      * @param message
+      *   A string that should be signed.
+      * @return
+      *   The HMAC-SHA256 signature in hexadecimal notation for the given message string.
+      */
+    def sign(message: String): String = {
+        val hmac = Mac.getInstance(Algorithm)
+        hmac.init(new SecretKeySpec(privateKey.toArray, Algorithm))
+        val signature = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8))
+        signature.toHexString
+    }
+
+    /** Sign the given token using the given nonce. The string produced by this function can be validated by the
+      * corresponding [[validate]] function.
+      *
+      * @param token
+      *   A string containing a token that shall be signed.
+      * @param nonce
+      *   A nonce string used to prepend it before the token.
+      * @return
+      *   A string containing the HMAC-SHA256 signature followed by a hash sign and the raw string used for signing
+      *   which contains the nonce a hash sign and the token.
+      */
+    def signToken(token: String)(nonce: String): SignedToken = {
+        val combined = s"$nonce${SignedToken.Separator}$token"
+        SignedToken(s"${sign(combined)}${SignedToken.Separator}$combined")
+    }
 
-  /** Sign the given message string and return a string containing the signature in hexadecimal notation.
-    *
-    * @param message
-    *   A string that should be signed.
-    * @return
-    *   The HMAC-SHA256 signature in hexadecimal notation for the given message string.
-    */
-  def sign(message: String): String = {
-    val hmac = Mac.getInstance(Algorithm)
-    hmac.init(new SecretKeySpec(privateKey.toArray, Algorithm))
-    val signature = hmac.doFinal(message.getBytes(StandardCharsets.UTF_8))
-    signature.toHexString
-  }
-
-  /** Sign the given token using the given nonce. The string produced by this function can be validated by the
-    * corresponding [[validate]] function.
-    *
-    * @param token
-    *   A string containing a token that shall be signed.
-    * @param nonce
-    *   A nonce string used to prepend it before the token.
-    * @return
-    *   A string containing the HMAC-SHA256 signature followed by a hash sign and the raw string used for signing which
-    *   contains the nonce a hash sign and the token.
-    */
-  def signToken(token: String)(nonce: String): SignedToken = {
-    val combined = s"$nonce${SignedToken.Separator}$token"
-    SignedToken(s"${sign(combined)}${SignedToken.Separator}$combined")
-  }
-
-  /** Validate the given string which should contain a signature generated by the corresponding [[signToken]] function.
-    * This function uses the [[java.security.MessageDigest#isEqual]] function which should ensure constant time
-    * comparison.
-    *
-    * @param signedToken
-    *   A string containing the HMAC-SHA256 signature followed by a hash sign and the raw string used for signing which
-    *   contains the nonce a hash sign and the token.
-    * @return
-    *   Option containing the pure token if the validation was successful.
-    */
-  def validate(signedToken: SignedToken): Option[String] = {
-    val parts     = signedToken.toString.split(SignedToken.Separator, 3)
-    val signature = parts.headOption
-    val nonce     = parts.drop(1).headOption
-    val token     = parts.drop(2).headOption
-    val comparison = (signature, nonce, token).mapN { case (signature, nonce, token) =>
-      val reSigned = sign(s"$nonce${SignedToken.Separator}$token")
-      if (MessageDigest.isEqual(signature.toBytes, reSigned.toBytes))
-        Option(token)
-      else
-        None
+    /** Validate the given string which should contain a signature generated by the corresponding [[signToken]]
+      * function. This function uses the [[java.security.MessageDigest#isEqual]] function which should ensure constant
+      * time comparison.
+      *
+      * @param signedToken
+      *   A string containing the HMAC-SHA256 signature followed by a hash sign and the raw string used for signing
+      *   which contains the nonce a hash sign and the token.
+      * @return
+      *   Option containing the pure token if the validation was successful.
+      */
+    def validate(signedToken: SignedToken): Option[String] = {
+        val parts     = signedToken.toString.split(SignedToken.Separator, 3)
+        val signature = parts.headOption
+        val nonce     = parts.drop(1).headOption
+        val token     = parts.drop(2).headOption
+        val comparison = (signature, nonce, token).mapN { case (signature, nonce, token) =>
+            val reSigned = sign(s"$nonce${SignedToken.Separator}$token")
+            if (MessageDigest.isEqual(signature.toBytes, reSigned.toBytes))
+                Option(token)
+            else
+                None
+        }
+        comparison.getOrElse(None) // Could also be flatten...
     }
-    comparison.getOrElse(None) // Could also be flatten...
-  }
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/UserId.scala new-smederee/modules/security/src/main/scala/de/smederee/security/UserId.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/UserId.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/UserId.scala	2025-01-13 17:13:25.064470989 +0000
@@ -28,51 +28,51 @@
   */
 opaque type UserId = UUID
 object UserId {
-  val Format: Regex = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
+    val Format: Regex = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
 
-  given Eq[UserId] = Eq.fromUniversalEquals
+    given Eq[UserId] = Eq.fromUniversalEquals
 
-  /** Create an instance of UserId from the given UUID type.
-    *
-    * @param source
-    *   An instance of type UUID which will be returned as a UserId.
-    * @return
-    *   The appropriate instance of UserId.
-    */
-  def apply(source: UUID): UserId = source
-
-  /** Try to create an instance of UserId from the given UUID.
-    *
-    * @param source
-    *   A UUID that should fulfil the requirements to be converted into a UserId.
-    * @return
-    *   An option to the successfully converted UserId.
-    */
-  def from(source: UUID): Option[UserId] = Option(source)
-
-  /** Try to create an instance of UserId from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a UserId.
-    * @return
-    *   An option to the successfully converted UserId.
-    */
-  def fromString(source: String): Either[String, UserId] =
-    Option(source)
-      .filter(s => Format.matches(s))
-      .flatMap { uuidString =>
-        Either.catchNonFatal(UUID.fromString(uuidString)).toOption
-      }
-      .toRight("Illegal value for UserId!")
-
-  /** Generate a new random user id.
-    *
-    * @return
-    *   A user id which is pseudo randomly generated.
-    */
-  def randomUserId: UserId = UUID.randomUUID
-
-  extension (uid: UserId) {
-    def toUUID: UUID = uid
-  }
+    /** Create an instance of UserId from the given UUID type.
+      *
+      * @param source
+      *   An instance of type UUID which will be returned as a UserId.
+      * @return
+      *   The appropriate instance of UserId.
+      */
+    def apply(source: UUID): UserId = source
+
+    /** Try to create an instance of UserId from the given UUID.
+      *
+      * @param source
+      *   A UUID that should fulfil the requirements to be converted into a UserId.
+      * @return
+      *   An option to the successfully converted UserId.
+      */
+    def from(source: UUID): Option[UserId] = Option(source)
+
+    /** Try to create an instance of UserId from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a UserId.
+      * @return
+      *   An option to the successfully converted UserId.
+      */
+    def fromString(source: String): Either[String, UserId] =
+        Option(source)
+            .filter(s => Format.matches(s))
+            .flatMap { uuidString =>
+                Either.catchNonFatal(UUID.fromString(uuidString)).toOption
+            }
+            .toRight("Illegal value for UserId!")
+
+    /** Generate a new random user id.
+      *
+      * @return
+      *   A user id which is pseudo randomly generated.
+      */
+    def randomUserId: UserId = UUID.randomUUID
+
+    extension (uid: UserId) {
+        def toUUID: UUID = uid
+    }
 }
diff -rN -u old-smederee/modules/security/src/main/scala/de/smederee/security/Username.scala new-smederee/modules/security/src/main/scala/de/smederee/security/Username.scala
--- old-smederee/modules/security/src/main/scala/de/smederee/security/Username.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/main/scala/de/smederee/security/Username.scala	2025-01-13 17:13:25.064470989 +0000
@@ -29,61 +29,61 @@
   */
 opaque type Username = String
 object Username {
-  given Eq[Username]       = Eq.fromUniversalEquals
-  given Ordering[Username] = (x: Username, y: Username) => x.compareTo(y)
-  given Order[Username]    = Order.fromOrdering
+    given Eq[Username]       = Eq.fromUniversalEquals
+    given Ordering[Username] = (x: Username, y: Username) => x.compareTo(y)
+    given Order[Username]    = Order.fromOrdering
 
-  val MinimumLength: Int    = 2
-  val MaximumLength: Int    = 31
-  val isAlphanumeric: Regex = "^[a-z][a-z0-9]+$".r
+    val MinimumLength: Int    = 2
+    val MaximumLength: Int    = 31
+    val isAlphanumeric: Regex = "^[a-z][a-z0-9]+$".r
 
-  /** Create an instance of Username from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a Username.
-    * @return
-    *   The appropriate instance of Username.
-    */
-  def apply(source: String): Username = source
+    /** Create an instance of Username from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a Username.
+      * @return
+      *   The appropriate instance of Username.
+      */
+    def apply(source: String): Username = source
 
-  /** Try to create an instance of Username from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a Username.
-    * @return
-    *   An option to the successfully converted Username.
-    */
-  def from(s: String): Option[Username] = validate(s).toOption
+    /** Try to create an instance of Username from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a Username.
+      * @return
+      *   An option to the successfully converted Username.
+      */
+    def from(s: String): Option[Username] = validate(s).toOption
 
-  /** Validate the given string and return either the validated username or a list of errors.
-    *
-    * @param s
-    *   An arbitrary string which should be a username.
-    * @return
-    *   Either a list of errors or the validated username.
-    */
-  def validate(s: String): ValidatedNec[String, Username] =
-    Option(s).map(_.trim.nonEmpty) match {
-      case Some(true) =>
-        val input = s.trim
-        val miniumLength =
-          if (input.length >= MinimumLength)
-            input.validNec
-          else
-            s"Username too short (min. $MinimumLength characters)!".invalidNec
-        val maximumLength =
-          if (input.length <= MaximumLength)
-            input.validNec
-          else
-            s"Username too long (max. $MaximumLength characters)!".invalidNec
-        val alphanumeric =
-          if (isAlphanumeric.matches(input))
-            input.validNec
-          else
-            "Username must be all lowercase alphanumeric characters and start with a character.".invalidNec
-        (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
-          name
+    /** Validate the given string and return either the validated username or a list of errors.
+      *
+      * @param s
+      *   An arbitrary string which should be a username.
+      * @return
+      *   Either a list of errors or the validated username.
+      */
+    def validate(s: String): ValidatedNec[String, Username] =
+        Option(s).map(_.trim.nonEmpty) match {
+            case Some(true) =>
+                val input = s.trim
+                val miniumLength =
+                    if (input.length >= MinimumLength)
+                        input.validNec
+                    else
+                        s"Username too short (min. $MinimumLength characters)!".invalidNec
+                val maximumLength =
+                    if (input.length <= MaximumLength)
+                        input.validNec
+                    else
+                        s"Username too long (max. $MaximumLength characters)!".invalidNec
+                val alphanumeric =
+                    if (isAlphanumeric.matches(input))
+                        input.validNec
+                    else
+                        "Username must be all lowercase alphanumeric characters and start with a character.".invalidNec
+                (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
+                    name
+                }
+            case _ => "Username must not be empty!".invalidNec
         }
-      case _ => "Username must not be empty!".invalidNec
-    }
 }
diff -rN -u old-smederee/modules/security/src/test/scala/de/smederee/security/PasswordTest.scala new-smederee/modules/security/src/test/scala/de/smederee/security/PasswordTest.scala
--- old-smederee/modules/security/src/test/scala/de/smederee/security/PasswordTest.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/test/scala/de/smederee/security/PasswordTest.scala	2025-01-13 17:13:25.064470989 +0000
@@ -25,41 +25,41 @@
 import org.scalacheck.*
 
 final class PasswordTest extends ScalaCheckSuite {
-  property("Password.from(null) must always be None") {
-    forAll { (_: String) =>
-      assertEquals(Password.from(null), None) // scalafix:ok
+    property("Password.from(null) must always be None") {
+        forAll { (_: String) =>
+            assertEquals(Password.from(null), None) // scalafix:ok
+        }
     }
-  }
 
-  property("Password.from(string) must always be UTF8 bytes") {
-    forAll { (s: String) =>
-      val expected = s.getBytes(StandardCharsets.UTF_8)
-      Password.from(s) match {
-        case None => fail("Must produce valid byte array from non null string!")
-        case Some(p) =>
-          assert(p.toArray.sameElements(expected), "Generated byte array differs from expected one!")
-      }
+    property("Password.from(string) must always be UTF8 bytes") {
+        forAll { (s: String) =>
+            val expected = s.getBytes(StandardCharsets.UTF_8)
+            Password.from(s) match {
+                case None => fail("Must produce valid byte array from non null string!")
+                case Some(p) =>
+                    assert(p.toArray.sameElements(expected), "Generated byte array differs from expected one!")
+            }
+        }
     }
-  }
 
-  property(s"Password.validate must fail on trimmed string that is too long") {
-    forAll { (input: String) =>
-      if (input.trim.length > Password.MaximumLength) {
-        assert(
-          Password.validate(input).isInvalid,
-          s"Passwords with more than ${Password.MaximumLength} characters must be invalid!"
-        )
-      }
+    property(s"Password.validate must fail on trimmed string that is too long") {
+        forAll { (input: String) =>
+            if (input.trim.length > Password.MaximumLength) {
+                assert(
+                    Password.validate(input).isInvalid,
+                    s"Passwords with more than ${Password.MaximumLength} characters must be invalid!"
+                )
+            }
+        }
     }
-  }
 
-  property(s"Password.validate must fail on trimmed string that is too short") {
-    forAll { (s: String) =>
-      val input = s.take(Password.MinimumLength).drop(1)
-      assert(
-        Password.validate(input).isInvalid,
-        s"Passwords with less than ${Password.MinimumLength} characters must be invalid!"
-      )
+    property(s"Password.validate must fail on trimmed string that is too short") {
+        forAll { (s: String) =>
+            val input = s.take(Password.MinimumLength).drop(1)
+            assert(
+                Password.validate(input).isInvalid,
+                s"Passwords with less than ${Password.MinimumLength} characters must be invalid!"
+            )
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/security/src/test/scala/de/smederee/security/SignAndValidateTest.scala new-smederee/modules/security/src/test/scala/de/smederee/security/SignAndValidateTest.scala
--- old-smederee/modules/security/src/test/scala/de/smederee/security/SignAndValidateTest.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/test/scala/de/smederee/security/SignAndValidateTest.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,41 +27,41 @@
 import org.scalacheck.*
 
 final class SignAndValidateTest extends ScalaCheckSuite {
-  private val genKey: Gen[PrivateKey] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(cs => PrivateKey(cs.mkString.getBytes(StandardCharsets.UTF_8)))
-  given Arbitrary[PrivateKey] = Arbitrary(genKey)
+    private val genKey: Gen[PrivateKey] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(cs => PrivateKey(cs.mkString.getBytes(StandardCharsets.UTF_8)))
+    given Arbitrary[PrivateKey] = Arbitrary(genKey)
 
-  property("SignedToken.from must work for input with correct syntax") {
-    forAll { (a: String, b: String, c: String) =>
-      val input = s"$a${SignedToken.Separator}$b${SignedToken.Separator}$c"
-      assert(SignedToken.from(input).nonEmpty)
+    property("SignedToken.from must work for input with correct syntax") {
+        forAll { (a: String, b: String, c: String) =>
+            val input = s"$a${SignedToken.Separator}$b${SignedToken.Separator}$c"
+            assert(SignedToken.from(input).nonEmpty)
+        }
     }
-  }
 
-  property("SignedToken.from must permit input with incorrect syntax") {
-    forAll { (input: String) =>
-      if (input.split(SignedToken.Separator, 3).size < 3)
-        assert(SignedToken.from(input).isEmpty)
-      else
-        assert(SignedToken.from(input).nonEmpty)
+    property("SignedToken.from must permit input with incorrect syntax") {
+        forAll { (input: String) =>
+            if (input.split(SignedToken.Separator, 3).size < 3)
+                assert(SignedToken.from(input).isEmpty)
+            else
+                assert(SignedToken.from(input).nonEmpty)
+        }
     }
-  }
 
-  property("toHexString and toBytes must be commutative") {
-    forAll { (bytes: Array[Byte]) =>
-      assert(
-        java.util.Arrays.equals(bytes, bytes.toHexString.toBytes),
-        "Encoded and decoded value differs from original input!"
-      )
+    property("toHexString and toBytes must be commutative") {
+        forAll { (bytes: Array[Byte]) =>
+            assert(
+                java.util.Arrays.equals(bytes, bytes.toHexString.toBytes),
+                "Encoded and decoded value differs from original input!"
+            )
+        }
     }
-  }
 
-  property("signToken and validate must be commutative") {
-    forAll { (key: PrivateKey, token: String, nonce: String) =>
-      val sav       = SignAndValidate(key)
-      val signed    = sav.signToken(token)(nonce)
-      val validated = sav.validate(signed)
-      assert(validated.map(_ === token).getOrElse(false), "Validated token does not match original input!")
+    property("signToken and validate must be commutative") {
+        forAll { (key: PrivateKey, token: String, nonce: String) =>
+            val sav       = SignAndValidate(key)
+            val signed    = sav.signToken(token)(nonce)
+            val validated = sav.validate(signed)
+            assert(validated.map(_ === token).getOrElse(false), "Validated token does not match original input!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/security/src/test/scala/de/smederee/security/UserIdTest.scala new-smederee/modules/security/src/test/scala/de/smederee/security/UserIdTest.scala
--- old-smederee/modules/security/src/test/scala/de/smederee/security/UserIdTest.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/security/src/test/scala/de/smederee/security/UserIdTest.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,28 +27,28 @@
 import org.scalacheck.*
 
 final class UserIdTest extends ScalaCheckSuite {
-  private val genUUID: Gen[UUID] = Gen.delay(UUID.randomUUID)
-  given Arbitrary[UUID]          = Arbitrary(genUUID)
+    private val genUUID: Gen[UUID] = Gen.delay(UUID.randomUUID)
+    given Arbitrary[UUID]          = Arbitrary(genUUID)
 
-  property("UserId.fromString must fail on invalid input") {
-    forAll { (input: String) =>
-      assert(UserId.fromString(input).isLeft)
+    property("UserId.fromString must fail on invalid input") {
+        forAll { (input: String) =>
+            assert(UserId.fromString(input).isLeft)
+        }
     }
-  }
 
-  property("UserId.fromString must succeed on valid input") {
-    forAll { (uuid: UUID) =>
-      assert(UserId.fromString(uuid.toString).isRight)
+    property("UserId.fromString must succeed on valid input") {
+        forAll { (uuid: UUID) =>
+            assert(UserId.fromString(uuid.toString).isRight)
+        }
     }
-  }
 
-  property("Eq must hold") {
-    forAll { (id1: UUID, id2: UUID) =>
-      val uid1 = UserId(id1)
-      val uid2 = UserId(id2)
+    property("Eq must hold") {
+        forAll { (id1: UUID, id2: UUID) =>
+            val uid1 = UserId(id1)
+            val uid2 = UserId(id2)
 
-      assert(uid1 === uid1, "Identical user ids must be considered equal!")
-      assert(uid1 =!= uid2, "Different user ids must not be considered equal!")
+            assert(uid1 === uid1, "Identical user ids must be considered equal!")
+            assert(uid1 =!= uid2, "Different user ids must not be considered equal!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Assignee.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Assignee.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Assignee.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Assignee.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,53 +27,53 @@
   */
 opaque type AssigneeId = UUID
 object AssigneeId {
-  val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
+    val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
 
-  given Eq[AssigneeId] = Eq.fromUniversalEquals
+    given Eq[AssigneeId] = Eq.fromUniversalEquals
 
-  /** Create an instance of AssigneeId from the given UUID type.
-    *
-    * @param source
-    *   An instance of type UUID which will be returned as a AssigneeId.
-    * @return
-    *   The appropriate instance of AssigneeId.
-    */
-  def apply(source: UUID): AssigneeId = source
-
-  /** Try to create an instance of AssigneeId from the given UUID.
-    *
-    * @param source
-    *   A UUID that should fulfil the requirements to be converted into a AssigneeId.
-    * @return
-    *   An option to the successfully converted AssigneeId.
-    */
-  def from(source: UUID): Option[AssigneeId] = Option(source)
-
-  /** Try to create an instance of AssigneeId from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a AssigneeId.
-    * @return
-    *   An option to the successfully converted AssigneeId.
-    */
-  def fromString(source: String): Either[String, AssigneeId] =
-    Option(source)
-      .filter(s => Format.matches(s))
-      .flatMap { uuidString =>
-        Either.catchNonFatal(UUID.fromString(uuidString)).toOption
-      }
-      .toRight("Illegal value for AssigneeId!")
-
-  /** Generate a new random user id.
-    *
-    * @return
-    *   A user id which is pseudo randomly generated.
-    */
-  def randomAssigneeId: AssigneeId = UUID.randomUUID
-
-  extension (uid: AssigneeId) {
-    def toUUID: UUID = uid
-  }
+    /** Create an instance of AssigneeId from the given UUID type.
+      *
+      * @param source
+      *   An instance of type UUID which will be returned as a AssigneeId.
+      * @return
+      *   The appropriate instance of AssigneeId.
+      */
+    def apply(source: UUID): AssigneeId = source
+
+    /** Try to create an instance of AssigneeId from the given UUID.
+      *
+      * @param source
+      *   A UUID that should fulfil the requirements to be converted into a AssigneeId.
+      * @return
+      *   An option to the successfully converted AssigneeId.
+      */
+    def from(source: UUID): Option[AssigneeId] = Option(source)
+
+    /** Try to create an instance of AssigneeId from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a AssigneeId.
+      * @return
+      *   An option to the successfully converted AssigneeId.
+      */
+    def fromString(source: String): Either[String, AssigneeId] =
+        Option(source)
+            .filter(s => Format.matches(s))
+            .flatMap { uuidString =>
+                Either.catchNonFatal(UUID.fromString(uuidString)).toOption
+            }
+            .toRight("Illegal value for AssigneeId!")
+
+    /** Generate a new random user id.
+      *
+      * @return
+      *   A user id which is pseudo randomly generated.
+      */
+    def randomAssigneeId: AssigneeId = UUID.randomUUID
+
+    extension (uid: AssigneeId) {
+        def toUUID: UUID = uid
+    }
 }
 
 /** A submitter name has to obey several restrictions which are similiar to the ones found for Unix usernames. It must
@@ -82,73 +82,73 @@
   */
 opaque type AssigneeName = String
 object AssigneeName {
-  given Eq[AssigneeName]       = Eq.fromUniversalEquals
-  given Order[AssigneeName]    = Order.from((x: AssigneeName, y: AssigneeName) => x.compareTo(y))
-  given Ordering[AssigneeName] = implicitly[Order[AssigneeName]].toOrdering
-
-  val isAlphanumeric = "^[a-z][a-z0-9]+$".r
-
-  /** Create an instance of AssigneeName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a AssigneeName.
-    * @return
-    *   The appropriate instance of AssigneeName.
-    */
-  def apply(source: String): AssigneeName = source
-
-  /** Try to create an instance of AssigneeName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a AssigneeName.
-    * @return
-    *   An option to the successfully converted AssigneeName.
-    */
-  def from(s: String): Option[AssigneeName] = validate(s).toOption
-
-  /** Validate the given string and return either the validated username or a list of errors.
-    *
-    * @param s
-    *   An arbitrary string which should be a username.
-    * @return
-    *   Either a list of errors or the validated username.
-    */
-  def validate(s: String): ValidatedNec[String, AssigneeName] =
-    Option(s).map(_.trim.nonEmpty) match {
-      case Some(true) =>
-        val input = s.trim
-        val miniumLength =
-          if (input.length >= 2)
-            input.validNec
-          else
-            "AssigneeName too short (min. 2 characters)!".invalidNec
-        val maximumLength =
-          if (input.length < 32)
-            input.validNec
-          else
-            "AssigneeName too long (max. 31 characters)!".invalidNec
-        val alphanumeric =
-          if (isAlphanumeric.matches(input))
-            input.validNec
-          else
-            "AssigneeName must be all lowercase alphanumeric characters and start with a character.".invalidNec
-        (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
-          name
+    given Eq[AssigneeName]       = Eq.fromUniversalEquals
+    given Order[AssigneeName]    = Order.from((x: AssigneeName, y: AssigneeName) => x.compareTo(y))
+    given Ordering[AssigneeName] = implicitly[Order[AssigneeName]].toOrdering
+
+    val isAlphanumeric = "^[a-z][a-z0-9]+$".r
+
+    /** Create an instance of AssigneeName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a AssigneeName.
+      * @return
+      *   The appropriate instance of AssigneeName.
+      */
+    def apply(source: String): AssigneeName = source
+
+    /** Try to create an instance of AssigneeName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a AssigneeName.
+      * @return
+      *   An option to the successfully converted AssigneeName.
+      */
+    def from(s: String): Option[AssigneeName] = validate(s).toOption
+
+    /** Validate the given string and return either the validated username or a list of errors.
+      *
+      * @param s
+      *   An arbitrary string which should be a username.
+      * @return
+      *   Either a list of errors or the validated username.
+      */
+    def validate(s: String): ValidatedNec[String, AssigneeName] =
+        Option(s).map(_.trim.nonEmpty) match {
+            case Some(true) =>
+                val input = s.trim
+                val miniumLength =
+                    if (input.length >= 2)
+                        input.validNec
+                    else
+                        "AssigneeName too short (min. 2 characters)!".invalidNec
+                val maximumLength =
+                    if (input.length < 32)
+                        input.validNec
+                    else
+                        "AssigneeName too long (max. 31 characters)!".invalidNec
+                val alphanumeric =
+                    if (isAlphanumeric.matches(input))
+                        input.validNec
+                    else
+                        "AssigneeName must be all lowercase alphanumeric characters and start with a character.".invalidNec
+                (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
+                    name
+                }
+            case _ => "AssigneeName must not be empty!".invalidNec
         }
-      case _ => "AssigneeName must not be empty!".invalidNec
-    }
 }
 
 /** Extractor to retrieve an AssigneeName from a path parameter.
   */
 object AssigneeNamePathParameter {
-  def unapply(str: String): Option[AssigneeName] =
-    Option(str).flatMap { string =>
-      if (string.startsWith("~"))
-        AssigneeName.from(string.drop(1))
-      else
-        None
-    }
+    def unapply(str: String): Option[AssigneeName] =
+        Option(str).flatMap { string =>
+            if (string.startsWith("~"))
+                AssigneeName.from(string.drop(1))
+            else
+                None
+        }
 }
 
 /** The assignee for a ticket i.e. the person supposed to be working on it.
@@ -161,5 +161,5 @@
 final case class Assignee(id: AssigneeId, name: AssigneeName)
 
 object Assignee {
-  given Eq[Assignee] = Eq.fromUniversalEquals
+    given Eq[Assignee] = Eq.fromUniversalEquals
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/ConfigurationPath.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/ConfigurationPath.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/ConfigurationPath.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/ConfigurationPath.scala	2025-01-13 17:13:25.064470989 +0000
@@ -23,23 +23,23 @@
 opaque type ConfigurationPath = String
 object ConfigurationPath {
 
-  given Conversion[ConfigurationPath, String] = _.toString
+    given Conversion[ConfigurationPath, String] = _.toString
 
-  /** Create an instance of ConfigurationPath from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ConfigurationPath.
-    * @return
-    *   The appropriate instance of ConfigurationPath.
-    */
-  def apply(source: String): ConfigurationPath = source
+    /** Create an instance of ConfigurationPath from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ConfigurationPath.
+      * @return
+      *   The appropriate instance of ConfigurationPath.
+      */
+    def apply(source: String): ConfigurationPath = source
 
-  /** Try to create an instance of ConfigurationPath from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ConfigurationPath.
-    * @return
-    *   An option to the successfully converted ConfigurationPath.
-    */
-  def from(source: String): Option[ConfigurationPath] = Option(source).map(_.trim).filter(_.nonEmpty)
+    /** Try to create an instance of ConfigurationPath from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ConfigurationPath.
+      * @return
+      *   An option to the successfully converted ConfigurationPath.
+      */
+    def from(source: String): Option[ConfigurationPath] = Option(source).map(_.trim).filter(_.nonEmpty)
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseConfig.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseConfig.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseConfig.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseConfig.scala	2025-01-13 17:13:25.064470989 +0000
@@ -33,5 +33,5 @@
 final case class DatabaseConfig(driver: String, url: String, user: String, pass: String)
 
 object DatabaseConfig {
-  given ConfigReader[DatabaseConfig] = ConfigReader.forProduct4("driver", "url", "user", "pass")(DatabaseConfig.apply)
+    given ConfigReader[DatabaseConfig] = ConfigReader.forProduct4("driver", "url", "user", "pass")(DatabaseConfig.apply)
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseMigrator.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseMigrator.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseMigrator.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/DatabaseMigrator.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,40 +27,44 @@
   */
 final class DatabaseMigrator[F[_]: Sync] {
 
-  /** Apply pending migrations to the database if needed using the underlying Flyway library.
-    *
-    * @param url
-    *   The JDBC connection URL **without** username and password.
-    * @param user
-    *   The username (login) needed to authenticate against the database.
-    * @param pass
-    *   The password needed to authenticate against the database.
-    * @return
-    *   A migrate result object holding information about executed migrations and the schema. See the Java-Doc of Flyway
-    *   for details.
-    */
-  def migrate(url: String, user: String, pass: String): F[MigrateResult] =
-    for {
-      flyway <- Sync[F].delay(DatabaseMigrator.configureFlyway(url, user, pass).load())
-      result <- Sync[F].delay(flyway.migrate())
-    } yield result
+    /** Apply pending migrations to the database if needed using the underlying Flyway library.
+      *
+      * @param url
+      *   The JDBC connection URL **without** username and password.
+      * @param user
+      *   The username (login) needed to authenticate against the database.
+      * @param pass
+      *   The password needed to authenticate against the database.
+      * @return
+      *   A migrate result object holding information about executed migrations and the schema. See the Java-Doc of
+      *   Flyway for details.
+      */
+    def migrate(url: String, user: String, pass: String): F[MigrateResult] =
+        for {
+            flyway <- Sync[F].delay(DatabaseMigrator.configureFlyway(url, user, pass).load())
+            result <- Sync[F].delay(flyway.migrate())
+        } yield result
 }
 
 object DatabaseMigrator {
 
-  /** Configure an instance of flyway with our defaults and the given credentials for a database connection. The
-    * returned instance must be activated by calling the `.load()` method.
-    *
-    * @param url
-    *   The JDBC connection URL **without** username and password.
-    * @param user
-    *   The username (login) needed to authenticate against the database.
-    * @param pass
-    *   The password needed to authenticate against the database.
-    * @return
-    *   An instance configuration which can be turned into a flyway database migrator by calling the `.load()` method.
-    */
-  def configureFlyway(url: String, user: String, pass: String): FluentConfiguration =
-    Flyway.configure().defaultSchema("tickets").locations("classpath:db/migration/tickets").dataSource(url, user, pass)
+    /** Configure an instance of flyway with our defaults and the given credentials for a database connection. The
+      * returned instance must be activated by calling the `.load()` method.
+      *
+      * @param url
+      *   The JDBC connection URL **without** username and password.
+      * @param user
+      *   The username (login) needed to authenticate against the database.
+      * @param pass
+      *   The password needed to authenticate against the database.
+      * @return
+      *   An instance configuration which can be turned into a flyway database migrator by calling the `.load()` method.
+      */
+    def configureFlyway(url: String, user: String, pass: String): FluentConfiguration =
+        Flyway
+            .configure()
+            .defaultSchema("tickets")
+            .locations("classpath:db/migration/tickets")
+            .dataSource(url, user, pass)
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/SmedereeTicketsConfiguration.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/SmedereeTicketsConfiguration.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/SmedereeTicketsConfiguration.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/SmedereeTicketsConfiguration.scala	2025-01-13 17:13:25.064470989 +0000
@@ -35,14 +35,14 @@
 final case class CsrfProtectionConfiguration(host: Host, port: Option[Port], scheme: Uri.Scheme)
 
 object CsrfProtectionConfiguration {
-  given Eq[CsrfProtectionConfiguration] = Eq.fromUniversalEquals
+    given Eq[CsrfProtectionConfiguration] = Eq.fromUniversalEquals
 
-  given ConfigReader[Host]       = ConfigReader.fromStringOpt[Host](Host.fromString)
-  given ConfigReader[Port]       = ConfigReader.fromStringOpt[Port](Port.fromString)
-  given ConfigReader[Uri.Scheme] = ConfigReader.fromStringOpt(s => Uri.Scheme.fromString(s).toOption)
+    given ConfigReader[Host]       = ConfigReader.fromStringOpt[Host](Host.fromString)
+    given ConfigReader[Port]       = ConfigReader.fromStringOpt[Port](Port.fromString)
+    given ConfigReader[Uri.Scheme] = ConfigReader.fromStringOpt(s => Uri.Scheme.fromString(s).toOption)
 
-  given ConfigReader[CsrfProtectionConfiguration] =
-    ConfigReader.forProduct3("host", "port", "scheme")(CsrfProtectionConfiguration.apply)
+    given ConfigReader[CsrfProtectionConfiguration] =
+        ConfigReader.forProduct3("host", "port", "scheme")(CsrfProtectionConfiguration.apply)
 }
 
 /** Configuration regarding the integration with the hub service.
@@ -53,9 +53,9 @@
 final case class HubIntegrationConfiguration(baseUri: Uri)
 
 object HubIntegrationConfiguration {
-  given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
-  given ConfigReader[HubIntegrationConfiguration] =
-    ConfigReader.forProduct1("base-uri")(HubIntegrationConfiguration.apply)
+    given ConfigReader[Uri] = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+    given ConfigReader[HubIntegrationConfiguration] =
+        ConfigReader.forProduct1("base-uri")(HubIntegrationConfiguration.apply)
 }
 
 /** Generic service configuration determining how the service will be run.
@@ -68,9 +68,9 @@
 final case class ServiceConfiguration(host: Host, port: Port)
 
 object ServiceConfiguration {
-  given ConfigReader[Host]                 = ConfigReader.fromStringOpt[Host](Host.fromString)
-  given ConfigReader[Port]                 = ConfigReader.fromStringOpt[Port](Port.fromString)
-  given ConfigReader[ServiceConfiguration] = ConfigReader.forProduct2("host", "port")(ServiceConfiguration.apply)
+    given ConfigReader[Host]                 = ConfigReader.fromStringOpt[Host](Host.fromString)
+    given ConfigReader[Port]                 = ConfigReader.fromStringOpt[Port](Port.fromString)
+    given ConfigReader[ServiceConfiguration] = ConfigReader.forProduct2("host", "port")(ServiceConfiguration.apply)
 }
 
 /** Wrapper class for the confiuration of the Smederee tickets module.
@@ -96,18 +96,18 @@
 )
 
 object SmedereeTicketsConfiguration {
-  val location: ConfigurationPath = ConfigurationPath("tickets")
+    val location: ConfigurationPath = ConfigurationPath("tickets")
 
-  given ConfigReader[Host]       = ConfigReader.fromStringOpt[Host](Host.fromString)
-  given ConfigReader[Port]       = ConfigReader.fromStringOpt[Port](Port.fromString)
-  given ConfigReader[Uri]        = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
-  given ConfigReader[Uri.Scheme] = ConfigReader.fromStringOpt(s => Uri.Scheme.fromString(s).toOption)
-
-  given ConfigReader[ExternalUrlConfiguration] =
-    ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply)
-
-  given ConfigReader[SmedereeTicketsConfiguration] =
-    ConfigReader.forProduct5("csrf-protection", "database", "external-url", "hub-integration", "service")(
-      SmedereeTicketsConfiguration.apply
-    )
+    given ConfigReader[Host]       = ConfigReader.fromStringOpt[Host](Host.fromString)
+    given ConfigReader[Port]       = ConfigReader.fromStringOpt[Port](Port.fromString)
+    given ConfigReader[Uri]        = ConfigReader.fromStringOpt(s => Uri.fromString(s).toOption)
+    given ConfigReader[Uri.Scheme] = ConfigReader.fromStringOpt(s => Uri.Scheme.fromString(s).toOption)
+
+    given ConfigReader[ExternalUrlConfiguration] =
+        ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply)
+
+    given ConfigReader[SmedereeTicketsConfiguration] =
+        ConfigReader.forProduct5("csrf-protection", "database", "external-url", "hub-integration", "service")(
+            SmedereeTicketsConfiguration.apply
+        )
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieLabelRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieLabelRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieLabelRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieLabelRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -24,24 +24,24 @@
 import org.slf4j.LoggerFactory
 
 final class DoobieLabelRepository[F[_]: Sync](tx: Transactor[F]) extends LabelRepository[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
+    given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
 
-  given Meta[ColourCode]       = Meta[String].timap(ColourCode.apply)(_.toString)
-  given Meta[LabelDescription] = Meta[String].timap(LabelDescription.apply)(_.toString)
-  given Meta[LabelId]          = Meta[Long].timap(LabelId.apply)(_.toLong)
-  given Meta[LabelName]        = Meta[String].timap(LabelName.apply)(_.toString)
-  given Meta[ProjectId]        = Meta[Long].timap(ProjectId.apply)(_.toLong)
-
-  override def allLabels(projectId: ProjectId): Stream[F, Label] =
-    sql"""SELECT id, name, description, colour FROM "tickets"."labels" WHERE project = $projectId ORDER BY name ASC"""
-      .query[Label]
-      .stream
-      .transact(tx)
+    given Meta[ColourCode]       = Meta[String].timap(ColourCode.apply)(_.toString)
+    given Meta[LabelDescription] = Meta[String].timap(LabelDescription.apply)(_.toString)
+    given Meta[LabelId]          = Meta[Long].timap(LabelId.apply)(_.toLong)
+    given Meta[LabelName]        = Meta[String].timap(LabelName.apply)(_.toString)
+    given Meta[ProjectId]        = Meta[Long].timap(ProjectId.apply)(_.toLong)
+
+    override def allLabels(projectId: ProjectId): Stream[F, Label] =
+        sql"""SELECT id, name, description, colour FROM "tickets"."labels" WHERE project = $projectId ORDER BY name ASC"""
+            .query[Label]
+            .stream
+            .transact(tx)
 
-  override def createLabel(projectId: ProjectId)(label: Label): F[Int] =
-    sql"""INSERT INTO "tickets"."labels"
+    override def createLabel(projectId: ProjectId)(label: Label): F[Int] =
+        sql"""INSERT INTO "tickets"."labels"
           (
             project,
             name,
@@ -55,28 +55,28 @@
             ${label.colour}
           )""".update.run.transact(tx)
 
-  override def deleteLabel(label: Label): F[Int] =
-    label.id match {
-      case None => Sync[F].pure(0)
-      case Some(id) =>
-        sql"""DELETE FROM "tickets"."labels" WHERE id = $id""".update.run.transact(tx)
-    }
-
-  override def findLabel(projectId: ProjectId)(name: LabelName): F[Option[Label]] =
-    sql"""SELECT id, name, description, colour FROM "tickets"."labels" WHERE project = $projectId AND name = $name LIMIT 1"""
-      .query[Label]
-      .option
-      .transact(tx)
-
-  override def updateLabel(label: Label): F[Int] =
-    label.id match {
-      case None => Sync[F].pure(0)
-      case Some(id) =>
-        sql"""UPDATE "tickets"."labels" 
+    override def deleteLabel(label: Label): F[Int] =
+        label.id match {
+            case None => Sync[F].pure(0)
+            case Some(id) =>
+                sql"""DELETE FROM "tickets"."labels" WHERE id = $id""".update.run.transact(tx)
+        }
+
+    override def findLabel(projectId: ProjectId)(name: LabelName): F[Option[Label]] =
+        sql"""SELECT id, name, description, colour FROM "tickets"."labels" WHERE project = $projectId AND name = $name LIMIT 1"""
+            .query[Label]
+            .option
+            .transact(tx)
+
+    override def updateLabel(label: Label): F[Int] =
+        label.id match {
+            case None => Sync[F].pure(0)
+            case Some(id) =>
+                sql"""UPDATE "tickets"."labels" 
               SET name = ${label.name},
               description = ${label.description},
               colour = ${label.colour}
               WHERE id = $id""".update.run.transact(tx)
-    }
+        }
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieMilestoneRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -29,31 +29,31 @@
 import org.slf4j.LoggerFactory
 
 final class DoobieMilestoneRepository[F[_]: Sync](tx: Transactor[F]) extends MilestoneRepository[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
+    given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
 
-  given Meta[AssigneeId]           = Meta[UUID].timap(AssigneeId.apply)(_.toUUID)
-  given Meta[AssigneeName]         = Meta[String].timap(AssigneeName.apply)(_.toString)
-  given Meta[ColourCode]           = Meta[String].timap(ColourCode.apply)(_.toString)
-  given Meta[LabelDescription]     = Meta[String].timap(LabelDescription.apply)(_.toString)
-  given Meta[LabelId]              = Meta[Long].timap(LabelId.apply)(_.toLong)
-  given Meta[LabelName]            = Meta[String].timap(LabelName.apply)(_.toString)
-  given Meta[MilestoneDescription] = Meta[String].timap(MilestoneDescription.apply)(_.toString)
-  given Meta[MilestoneId]          = Meta[Long].timap(MilestoneId.apply)(_.toLong)
-  given Meta[MilestoneTitle]       = Meta[String].timap(MilestoneTitle.apply)(_.toString)
-  given Meta[ProjectId]            = Meta[Long].timap(ProjectId.apply)(_.toLong)
-  given Meta[SubmitterId]          = Meta[UUID].timap(SubmitterId.apply)(_.toUUID)
-  given Meta[SubmitterName]        = Meta[String].timap(SubmitterName.apply)(_.toString)
-  given Meta[TicketContent]        = Meta[String].timap(TicketContent.apply)(_.toString)
-  given Meta[TicketId]             = Meta[Long].timap(TicketId.apply)(_.toLong)
-  given Meta[TicketNumber]         = Meta[Int].timap(TicketNumber.apply)(_.toInt)
-  given Meta[TicketResolution]     = Meta[String].timap(TicketResolution.valueOf)(_.toString)
-  given Meta[TicketStatus]         = Meta[String].timap(TicketStatus.valueOf)(_.toString)
-  given Meta[TicketTitle]          = Meta[String].timap(TicketTitle.apply)(_.toString)
+    given Meta[AssigneeId]           = Meta[UUID].timap(AssigneeId.apply)(_.toUUID)
+    given Meta[AssigneeName]         = Meta[String].timap(AssigneeName.apply)(_.toString)
+    given Meta[ColourCode]           = Meta[String].timap(ColourCode.apply)(_.toString)
+    given Meta[LabelDescription]     = Meta[String].timap(LabelDescription.apply)(_.toString)
+    given Meta[LabelId]              = Meta[Long].timap(LabelId.apply)(_.toLong)
+    given Meta[LabelName]            = Meta[String].timap(LabelName.apply)(_.toString)
+    given Meta[MilestoneDescription] = Meta[String].timap(MilestoneDescription.apply)(_.toString)
+    given Meta[MilestoneId]          = Meta[Long].timap(MilestoneId.apply)(_.toLong)
+    given Meta[MilestoneTitle]       = Meta[String].timap(MilestoneTitle.apply)(_.toString)
+    given Meta[ProjectId]            = Meta[Long].timap(ProjectId.apply)(_.toLong)
+    given Meta[SubmitterId]          = Meta[UUID].timap(SubmitterId.apply)(_.toUUID)
+    given Meta[SubmitterName]        = Meta[String].timap(SubmitterName.apply)(_.toString)
+    given Meta[TicketContent]        = Meta[String].timap(TicketContent.apply)(_.toString)
+    given Meta[TicketId]             = Meta[Long].timap(TicketId.apply)(_.toLong)
+    given Meta[TicketNumber]         = Meta[Int].timap(TicketNumber.apply)(_.toInt)
+    given Meta[TicketResolution]     = Meta[String].timap(TicketResolution.valueOf)(_.toString)
+    given Meta[TicketStatus]         = Meta[String].timap(TicketStatus.valueOf)(_.toString)
+    given Meta[TicketTitle]          = Meta[String].timap(TicketTitle.apply)(_.toString)
 
-  private val selectTicketColumns =
-    fr"""SELECT
+    private val selectTicketColumns =
+        fr"""SELECT
             "tickets".number AS number,
             "tickets".title AS title,
             "tickets".content AS content,
@@ -67,40 +67,40 @@
           LEFT OUTER JOIN "tickets"."users" AS "submitters"
           ON "tickets".submitter = "submitters".uid"""
 
-  override def allMilestones(projectId: ProjectId): Stream[F, Milestone] =
-    sql"""SELECT id, title, description, due_date, closed FROM "tickets"."milestones" WHERE project = $projectId ORDER BY due_date ASC, title ASC"""
-      .query[Milestone]
-      .stream
-      .transact(tx)
-
-  override def allTickets(filter: Option[TicketFilter])(milestoneId: MilestoneId): Stream[F, Ticket] = {
-    val milestoneFilter =
-      fr""""tickets".id IN (SELECT ticket FROM "tickets".milestone_tickets AS "milestone_tickets" WHERE milestone = $milestoneId)"""
-    val tickets = filter match {
-      case None => selectTicketColumns ++ whereAnd(milestoneFilter)
-      case Some(filter) =>
-        val numberFilter = filter.number.toNel.map(numbers => Fragments.in(fr""""tickets".number""", numbers))
-        val statusFilter = filter.status.toNel.map(status => Fragments.in(fr""""tickets".status""", status))
-        val resolutionFilter =
-          filter.resolution.toNel.map(resolutions => Fragments.in(fr""""tickets".resolution""", resolutions))
-        val submitterFilter =
-          filter.submitter.toNel.map(submitters => Fragments.in(fr""""submitters".name""", submitters))
-        selectTicketColumns ++ whereAndOpt(
-          milestoneFilter.some,
-          numberFilter,
-          statusFilter,
-          resolutionFilter,
-          submitterFilter
-        )
+    override def allMilestones(projectId: ProjectId): Stream[F, Milestone] =
+        sql"""SELECT id, title, description, due_date, closed FROM "tickets"."milestones" WHERE project = $projectId ORDER BY due_date ASC, title ASC"""
+            .query[Milestone]
+            .stream
+            .transact(tx)
+
+    override def allTickets(filter: Option[TicketFilter])(milestoneId: MilestoneId): Stream[F, Ticket] = {
+        val milestoneFilter =
+            fr""""tickets".id IN (SELECT ticket FROM "tickets".milestone_tickets AS "milestone_tickets" WHERE milestone = $milestoneId)"""
+        val tickets = filter match {
+            case None => selectTicketColumns ++ whereAnd(milestoneFilter)
+            case Some(filter) =>
+                val numberFilter = filter.number.toNel.map(numbers => Fragments.in(fr""""tickets".number""", numbers))
+                val statusFilter = filter.status.toNel.map(status => Fragments.in(fr""""tickets".status""", status))
+                val resolutionFilter =
+                    filter.resolution.toNel.map(resolutions => Fragments.in(fr""""tickets".resolution""", resolutions))
+                val submitterFilter =
+                    filter.submitter.toNel.map(submitters => Fragments.in(fr""""submitters".name""", submitters))
+                selectTicketColumns ++ whereAndOpt(
+                    milestoneFilter.some,
+                    numberFilter,
+                    statusFilter,
+                    resolutionFilter,
+                    submitterFilter
+                )
+        }
+        tickets.query[Ticket].stream.transact(tx)
     }
-    tickets.query[Ticket].stream.transact(tx)
-  }
 
-  override def closeMilestone(milestoneId: MilestoneId): F[Int] =
-    sql"""UPDATE "tickets"."milestones" SET closed = TRUE WHERE id = $milestoneId""".update.run.transact(tx)
+    override def closeMilestone(milestoneId: MilestoneId): F[Int] =
+        sql"""UPDATE "tickets"."milestones" SET closed = TRUE WHERE id = $milestoneId""".update.run.transact(tx)
 
-  override def createMilestone(projectId: ProjectId)(milestone: Milestone): F[Int] =
-    sql"""INSERT INTO "tickets"."milestones"
+    override def createMilestone(projectId: ProjectId)(milestone: Milestone): F[Int] =
+        sql"""INSERT INTO "tickets"."milestones"
           (
             project,
             title,
@@ -116,30 +116,30 @@
             ${milestone.closed}
           )""".update.run.transact(tx)
 
-  override def deleteMilestone(milestone: Milestone): F[Int] =
-    milestone.id match {
-      case None     => Sync[F].pure(0)
-      case Some(id) => sql"""DELETE FROM "tickets"."milestones" WHERE id = $id""".update.run.transact(tx)
-    }
-
-  override def findMilestone(projectId: ProjectId)(title: MilestoneTitle): F[Option[Milestone]] =
-    sql"""SELECT id, title, description, due_date, closed FROM "tickets"."milestones" WHERE project = $projectId AND title = $title LIMIT 1"""
-      .query[Milestone]
-      .option
-      .transact(tx)
-
-  override def openMilestone(milestoneId: MilestoneId): F[Int] =
-    sql"""UPDATE "tickets"."milestones" SET closed = FALSE WHERE id = $milestoneId""".update.run.transact(tx)
-
-  override def updateMilestone(milestone: Milestone): F[Int] =
-    milestone.id match {
-      case None => Sync[F].pure(0)
-      case Some(id) =>
-        sql"""UPDATE "tickets"."milestones"
+    override def deleteMilestone(milestone: Milestone): F[Int] =
+        milestone.id match {
+            case None     => Sync[F].pure(0)
+            case Some(id) => sql"""DELETE FROM "tickets"."milestones" WHERE id = $id""".update.run.transact(tx)
+        }
+
+    override def findMilestone(projectId: ProjectId)(title: MilestoneTitle): F[Option[Milestone]] =
+        sql"""SELECT id, title, description, due_date, closed FROM "tickets"."milestones" WHERE project = $projectId AND title = $title LIMIT 1"""
+            .query[Milestone]
+            .option
+            .transact(tx)
+
+    override def openMilestone(milestoneId: MilestoneId): F[Int] =
+        sql"""UPDATE "tickets"."milestones" SET closed = FALSE WHERE id = $milestoneId""".update.run.transact(tx)
+
+    override def updateMilestone(milestone: Milestone): F[Int] =
+        milestone.id match {
+            case None => Sync[F].pure(0)
+            case Some(id) =>
+                sql"""UPDATE "tickets"."milestones"
               SET title = ${milestone.title},
                 due_date = ${milestone.dueDate},
                 description = ${milestone.description}
               WHERE id = $id""".update.run.transact(tx)
-    }
+        }
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieProjectRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieProjectRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieProjectRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieProjectRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,20 +27,20 @@
 import org.slf4j.LoggerFactory
 
 final class DoobieProjectRepository[F[_]: Sync](tx: Transactor[F]) extends ProjectRepository[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
+    given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
 
-  given Meta[EmailAddress]       = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[ProjectDescription] = Meta[String].timap(ProjectDescription.apply)(_.toString)
-  given Meta[ProjectId]          = Meta[Long].timap(ProjectId.apply)(_.toLong)
-  given Meta[ProjectName]        = Meta[String].timap(ProjectName.apply)(_.toString)
-  given Meta[ProjectOwnerId]     = Meta[UUID].timap(ProjectOwnerId.apply)(_.toUUID)
-  given Meta[ProjectOwnerName]   = Meta[String].timap(ProjectOwnerName.apply)(_.toString)
-  given Meta[TicketNumber]       = Meta[Int].timap(TicketNumber.apply)(_.toInt)
+    given Meta[EmailAddress]       = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[ProjectDescription] = Meta[String].timap(ProjectDescription.apply)(_.toString)
+    given Meta[ProjectId]          = Meta[Long].timap(ProjectId.apply)(_.toLong)
+    given Meta[ProjectName]        = Meta[String].timap(ProjectName.apply)(_.toString)
+    given Meta[ProjectOwnerId]     = Meta[UUID].timap(ProjectOwnerId.apply)(_.toUUID)
+    given Meta[ProjectOwnerName]   = Meta[String].timap(ProjectOwnerName.apply)(_.toString)
+    given Meta[TicketNumber]       = Meta[Int].timap(TicketNumber.apply)(_.toInt)
 
-  override def createProject(project: Project): F[Int] =
-    sql"""INSERT INTO "tickets"."projects" (name, owner, is_private, description, created_at, updated_at)
+    override def createProject(project: Project): F[Int] =
+        sql"""INSERT INTO "tickets"."projects" (name, owner, is_private, description, created_at, updated_at)
           VALUES (
             ${project.name},
             ${project.owner.uid},
@@ -50,12 +50,12 @@
             NOW()
           )""".update.run.transact(tx)
 
-  override def deleteProject(project: Project): F[Int] =
-    sql"""DELETE FROM "tickets"."projects" WHERE owner = ${project.owner.uid} AND name = ${project.name}""".update.run
-      .transact(tx)
+    override def deleteProject(project: Project): F[Int] =
+        sql"""DELETE FROM "tickets"."projects" WHERE owner = ${project.owner.uid} AND name = ${project.name}""".update.run
+            .transact(tx)
 
-  override def findProject(owner: ProjectOwner, name: ProjectName): F[Option[Project]] =
-    sql"""SELECT
+    override def findProject(owner: ProjectOwner, name: ProjectName): F[Option[Project]] =
+        sql"""SELECT
             "users".uid AS owner_id,
             "users".name AS owner_name,
             "users".email AS owner_email,
@@ -70,8 +70,8 @@
           AND
             "projects".name = $name""".query[Project].option.transact(tx)
 
-  override def findProjectId(owner: ProjectOwner, name: ProjectName): F[Option[ProjectId]] =
-    sql"""SELECT
+    override def findProjectId(owner: ProjectOwner, name: ProjectName): F[Option[ProjectId]] =
+        sql"""SELECT
             "projects".id
           FROM "tickets"."projects" AS "projects"
           JOIN "tickets"."users" AS "users"
@@ -81,17 +81,17 @@
           AND
             "projects".name = $name""".query[ProjectId].option.transact(tx)
 
-  override def findProjectOwner(name: ProjectOwnerName): F[Option[ProjectOwner]] =
-    sql"""SELECT
+    override def findProjectOwner(name: ProjectOwnerName): F[Option[ProjectOwner]] =
+        sql"""SELECT
             "users".uid,
             "users".name,
             "users".email
           FROM "tickets"."users"
           WHERE name = $name""".query[ProjectOwner].option.transact(tx)
 
-  override def incrementNextTicketNumber(projectId: ProjectId): F[TicketNumber] = {
-    // TODO: Find out which of the queries is more reliable and more performant.
-    val sqlQuery1 = sql"""UPDATE "tickets"."projects" AS alias1
+    override def incrementNextTicketNumber(projectId: ProjectId): F[TicketNumber] = {
+        // TODO: Find out which of the queries is more reliable and more performant.
+        val sqlQuery1 = sql"""UPDATE "tickets"."projects" AS alias1
                             SET next_ticket_number = alias2.next_ticket_number + 1
                           FROM (
                             SELECT
@@ -102,7 +102,7 @@
                           ) AS alias2
                           WHERE alias1.id = alias2.id
                           RETURNING alias2.next_ticket_number AS next_ticket_number"""
-    val sqlQuery2 = sql"""WITH old_number AS (
+        val sqlQuery2 = sql"""WITH old_number AS (
                             SELECT next_ticket_number FROM "tickets"."projects" WHERE id = $projectId
                           )
                           UPDATE "tickets"."projects"
@@ -111,11 +111,11 @@
                           RETURNING (
                             SELECT next_ticket_number FROM old_number
                           )"""
-    sqlQuery2.query[TicketNumber].unique.transact(tx)
-  }
+        sqlQuery2.query[TicketNumber].unique.transact(tx)
+    }
 
-  override def updateProject(project: Project): F[Int] =
-    sql"""UPDATE "tickets"."projects" SET
+    override def updateProject(project: Project): F[Int] =
+        sql"""UPDATE "tickets"."projects" SET
             is_private = ${project.isPrivate},
             description = ${project.description},
             updated_at = NOW()
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -31,31 +31,31 @@
 import org.slf4j.LoggerFactory
 
 final class DoobieTicketRepository[F[_]: Sync](tx: Transactor[F]) extends TicketRepository[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
+    given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
 
-  given Meta[AssigneeId]           = Meta[UUID].timap(AssigneeId.apply)(_.toUUID)
-  given Meta[AssigneeName]         = Meta[String].timap(AssigneeName.apply)(_.toString)
-  given Meta[ColourCode]           = Meta[String].timap(ColourCode.apply)(_.toString)
-  given Meta[LabelDescription]     = Meta[String].timap(LabelDescription.apply)(_.toString)
-  given Meta[LabelId]              = Meta[Long].timap(LabelId.apply)(_.toLong)
-  given Meta[LabelName]            = Meta[String].timap(LabelName.apply)(_.toString)
-  given Meta[MilestoneDescription] = Meta[String].timap(MilestoneDescription.apply)(_.toString)
-  given Meta[MilestoneId]          = Meta[Long].timap(MilestoneId.apply)(_.toLong)
-  given Meta[MilestoneTitle]       = Meta[String].timap(MilestoneTitle.apply)(_.toString)
-  given Meta[ProjectId]            = Meta[Long].timap(ProjectId.apply)(_.toLong)
-  given Meta[SubmitterId]          = Meta[UUID].timap(SubmitterId.apply)(_.toUUID)
-  given Meta[SubmitterName]        = Meta[String].timap(SubmitterName.apply)(_.toString)
-  given Meta[TicketContent]        = Meta[String].timap(TicketContent.apply)(_.toString)
-  given Meta[TicketId]             = Meta[Long].timap(TicketId.apply)(_.toLong)
-  given Meta[TicketNumber]         = Meta[Int].timap(TicketNumber.apply)(_.toInt)
-  given Meta[TicketResolution]     = Meta[String].timap(TicketResolution.valueOf)(_.toString)
-  given Meta[TicketStatus]         = Meta[String].timap(TicketStatus.valueOf)(_.toString)
-  given Meta[TicketTitle]          = Meta[String].timap(TicketTitle.apply)(_.toString)
+    given Meta[AssigneeId]           = Meta[UUID].timap(AssigneeId.apply)(_.toUUID)
+    given Meta[AssigneeName]         = Meta[String].timap(AssigneeName.apply)(_.toString)
+    given Meta[ColourCode]           = Meta[String].timap(ColourCode.apply)(_.toString)
+    given Meta[LabelDescription]     = Meta[String].timap(LabelDescription.apply)(_.toString)
+    given Meta[LabelId]              = Meta[Long].timap(LabelId.apply)(_.toLong)
+    given Meta[LabelName]            = Meta[String].timap(LabelName.apply)(_.toString)
+    given Meta[MilestoneDescription] = Meta[String].timap(MilestoneDescription.apply)(_.toString)
+    given Meta[MilestoneId]          = Meta[Long].timap(MilestoneId.apply)(_.toLong)
+    given Meta[MilestoneTitle]       = Meta[String].timap(MilestoneTitle.apply)(_.toString)
+    given Meta[ProjectId]            = Meta[Long].timap(ProjectId.apply)(_.toLong)
+    given Meta[SubmitterId]          = Meta[UUID].timap(SubmitterId.apply)(_.toUUID)
+    given Meta[SubmitterName]        = Meta[String].timap(SubmitterName.apply)(_.toString)
+    given Meta[TicketContent]        = Meta[String].timap(TicketContent.apply)(_.toString)
+    given Meta[TicketId]             = Meta[Long].timap(TicketId.apply)(_.toLong)
+    given Meta[TicketNumber]         = Meta[Int].timap(TicketNumber.apply)(_.toInt)
+    given Meta[TicketResolution]     = Meta[String].timap(TicketResolution.valueOf)(_.toString)
+    given Meta[TicketStatus]         = Meta[String].timap(TicketStatus.valueOf)(_.toString)
+    given Meta[TicketTitle]          = Meta[String].timap(TicketTitle.apply)(_.toString)
 
-  private val selectTicketColumns =
-    fr"""SELECT
+    private val selectTicketColumns =
+        fr"""SELECT
             "tickets".number AS number,
             "tickets".title AS title,
             "tickets".content AS content,
@@ -69,25 +69,25 @@
           LEFT OUTER JOIN "tickets"."users" AS "submitters"
           ON "tickets".submitter = "submitters".uid"""
 
-  /** Construct a query fragment that fetches the internal unique ticket id from the tickets table via the given project
-    * id and ticket number. The fetched id can be referenced like this `SELECT id FROM ticket_id`.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @return
-    *   A query fragment useable within other queries which defines a common table expression using the `WITH` clause.
-    */
-  private def withTicketId(projectId: ProjectId, ticketNumber: TicketNumber): Fragment =
-    fr"""WITH ticket_id AS (
+    /** Construct a query fragment that fetches the internal unique ticket id from the tickets table via the given
+      * project id and ticket number. The fetched id can be referenced like this `SELECT id FROM ticket_id`.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @return
+      *   A query fragment useable within other queries which defines a common table expression using the `WITH` clause.
+      */
+    private def withTicketId(projectId: ProjectId, ticketNumber: TicketNumber): Fragment =
+        fr"""WITH ticket_id AS (
            SELECT id AS id
            FROM "tickets"."tickets" AS "tickets"
            WHERE "tickets".project = $projectId
            AND "tickets".number = $ticketNumber)"""
 
-  override def addAssignee(projectId: ProjectId)(ticketNumber: TicketNumber)(assignee: Assignee): F[Int] =
-    sql"""INSERT INTO "tickets"."ticket_assignees" (
+    override def addAssignee(projectId: ProjectId)(ticketNumber: TicketNumber)(assignee: Assignee): F[Int] =
+        sql"""INSERT INTO "tickets"."ticket_assignees" (
             ticket,
             assignee
           ) SELECT 
@@ -97,11 +97,11 @@
           WHERE project = $projectId
           AND number = $ticketNumber""".update.run.transact(tx)
 
-  override def addLabel(projectId: ProjectId)(ticketNumber: TicketNumber)(label: Label): F[Int] =
-    label.id match {
-      case None => Sync[F].pure(0)
-      case Some(labelId) =>
-        sql"""INSERT INTO "tickets"."ticket_labels" (
+    override def addLabel(projectId: ProjectId)(ticketNumber: TicketNumber)(label: Label): F[Int] =
+        label.id match {
+            case None => Sync[F].pure(0)
+            case Some(labelId) =>
+                sql"""INSERT INTO "tickets"."ticket_labels" (
             ticket,
             label
           ) SELECT
@@ -110,13 +110,13 @@
           FROM "tickets"."tickets"
           WHERE project = $projectId
           AND number = $ticketNumber""".update.run.transact(tx)
-    }
+        }
 
-  override def addMilestone(projectId: ProjectId)(ticketNumber: TicketNumber)(milestone: Milestone): F[Int] =
-    milestone.id match {
-      case None => Sync[F].pure(0)
-      case Some(milestoneId) =>
-        sql"""INSERT INTO "tickets"."milestone_tickets" (
+    override def addMilestone(projectId: ProjectId)(ticketNumber: TicketNumber)(milestone: Milestone): F[Int] =
+        milestone.id match {
+            case None => Sync[F].pure(0)
+            case Some(milestoneId) =>
+                sql"""INSERT INTO "tickets"."milestone_tickets" (
                 ticket,
                 milestone
               ) SELECT
@@ -125,32 +125,32 @@
           FROM "tickets"."tickets"
           WHERE project = $projectId
           AND number = $ticketNumber""".update.run.transact(tx)
-    }
+        }
 
-  override def allTickets(filter: Option[TicketFilter])(projectId: ProjectId): Stream[F, Ticket] = {
-    val projectFilter = fr""""tickets".project = $projectId"""
-    val tickets = filter match {
-      case None => selectTicketColumns ++ whereAnd(projectFilter)
-      case Some(filter) =>
-        val numberFilter = filter.number.toNel.map(numbers => Fragments.in(fr""""tickets".number""", numbers))
-        val statusFilter = filter.status.toNel.map(status => Fragments.in(fr""""tickets".status""", status))
-        val resolutionFilter =
-          filter.resolution.toNel.map(resolutions => Fragments.in(fr""""tickets".resolution""", resolutions))
-        val submitterFilter =
-          filter.submitter.toNel.map(submitters => Fragments.in(fr""""submitters".name""", submitters))
-        selectTicketColumns ++ whereAndOpt(
-          projectFilter.some,
-          numberFilter,
-          statusFilter,
-          resolutionFilter,
-          submitterFilter
-        )
+    override def allTickets(filter: Option[TicketFilter])(projectId: ProjectId): Stream[F, Ticket] = {
+        val projectFilter = fr""""tickets".project = $projectId"""
+        val tickets = filter match {
+            case None => selectTicketColumns ++ whereAnd(projectFilter)
+            case Some(filter) =>
+                val numberFilter = filter.number.toNel.map(numbers => Fragments.in(fr""""tickets".number""", numbers))
+                val statusFilter = filter.status.toNel.map(status => Fragments.in(fr""""tickets".status""", status))
+                val resolutionFilter =
+                    filter.resolution.toNel.map(resolutions => Fragments.in(fr""""tickets".resolution""", resolutions))
+                val submitterFilter =
+                    filter.submitter.toNel.map(submitters => Fragments.in(fr""""submitters".name""", submitters))
+                selectTicketColumns ++ whereAndOpt(
+                    projectFilter.some,
+                    numberFilter,
+                    statusFilter,
+                    resolutionFilter,
+                    submitterFilter
+                )
+        }
+        tickets.query[Ticket].stream.transact(tx)
     }
-    tickets.query[Ticket].stream.transact(tx)
-  }
 
-  override def createTicket(projectId: ProjectId)(ticket: Ticket): F[Int] =
-    sql"""INSERT INTO "tickets"."tickets" (
+    override def createTicket(projectId: ProjectId)(ticket: Ticket): F[Int] =
+        sql"""INSERT INTO "tickets"."tickets" (
             project,
             number,
             title,
@@ -172,39 +172,39 @@
             NOW()
           )""".update.run.transact(tx)
 
-  override def deleteTicket(projectId: ProjectId)(ticket: Ticket): F[Int] =
-    sql"""DELETE FROM "tickets"."tickets"
+    override def deleteTicket(projectId: ProjectId)(ticket: Ticket): F[Int] =
+        sql"""DELETE FROM "tickets"."tickets"
           WHERE project = $projectId
           AND number = ${ticket.number}""".update.run.transact(tx)
 
-  override def findTicket(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[Ticket]] = {
-    val projectFilter = fr"""project = $projectId"""
-    val numberFilter  = fr"""number = $ticketNumber"""
-    val ticket        = selectTicketColumns ++ whereAnd(projectFilter, numberFilter) ++ fr"""LIMIT 1"""
-    ticket.query[Ticket].option.transact(tx)
-  }
-
-  override def findTicketId(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[TicketId]] =
-    sql"""SELECT id FROM "tickets"."tickets" WHERE project = $projectId AND number = $ticketNumber"""
-      .query[TicketId]
-      .option
-      .transact(tx)
-
-  override def loadAssignees(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Assignee] = {
-    val sqlQuery = withTicketId(projectId, ticketNumber) ++
-      fr"""SELECT
+    override def findTicket(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[Ticket]] = {
+        val projectFilter = fr"""project = $projectId"""
+        val numberFilter  = fr"""number = $ticketNumber"""
+        val ticket        = selectTicketColumns ++ whereAnd(projectFilter, numberFilter) ++ fr"""LIMIT 1"""
+        ticket.query[Ticket].option.transact(tx)
+    }
+
+    override def findTicketId(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[TicketId]] =
+        sql"""SELECT id FROM "tickets"."tickets" WHERE project = $projectId AND number = $ticketNumber"""
+            .query[TicketId]
+            .option
+            .transact(tx)
+
+    override def loadAssignees(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Assignee] = {
+        val sqlQuery = withTicketId(projectId, ticketNumber) ++
+            fr"""SELECT
              "users".uid AS uid,
              "users".name AS name
            FROM "tickets"."ticket_assignees" AS "assignees"
            JOIN "tickets"."users"            AS "users"
              ON "assignees".assignee = "users".uid
            WHERE "assignees".ticket = (SELECT id FROM ticket_id)"""
-    sqlQuery.query[Assignee].stream.transact(tx)
-  }
+        sqlQuery.query[Assignee].stream.transact(tx)
+    }
 
-  override def loadLabels(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Label] = {
-    val sqlQuery = withTicketId(projectId, ticketNumber) ++
-      fr"""SELECT
+    override def loadLabels(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Label] = {
+        val sqlQuery = withTicketId(projectId, ticketNumber) ++
+            fr"""SELECT
              "labels".id          AS id,
              "labels".name        AS name,
              "labels".description AS description,
@@ -213,12 +213,12 @@
            JOIN "tickets"."ticket_labels" AS "ticket_labels"
              ON "labels".id = "ticket_labels".label
            WHERE "ticket_labels".ticket = (SELECT id FROM ticket_id)"""
-    sqlQuery.query[Label].stream.transact(tx)
-  }
+        sqlQuery.query[Label].stream.transact(tx)
+    }
 
-  override def loadMilestones(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Milestone] = {
-    val sqlQuery = withTicketId(projectId, ticketNumber) ++
-      fr"""SELECT
+    override def loadMilestones(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Milestone] = {
+        val sqlQuery = withTicketId(projectId, ticketNumber) ++
+            fr"""SELECT
              "milestones".id AS id,
              "milestones".title AS title,
              "milestones".description AS description,
@@ -229,41 +229,41 @@
              ON "milestones".id = "milestone_tickets"."milestone"
            WHERE "milestone_tickets".ticket = (SELECT id FROM ticket_id)
            ORDER BY "milestones".due_date ASC, "milestones".title ASC"""
-    sqlQuery.query[Milestone].stream.transact(tx)
-  }
+        sqlQuery.query[Milestone].stream.transact(tx)
+    }
 
-  override def removeAssignee(projectId: ProjectId)(ticket: Ticket)(assignee: Assignee): F[Int] = {
-    val sqlQuery = withTicketId(projectId, ticket.number) ++
-      fr"""DELETE FROM "tickets"."ticket_assignees" AS "ticket_assignees"
+    override def removeAssignee(projectId: ProjectId)(ticket: Ticket)(assignee: Assignee): F[Int] = {
+        val sqlQuery = withTicketId(projectId, ticket.number) ++
+            fr"""DELETE FROM "tickets"."ticket_assignees" AS "ticket_assignees"
            WHERE "ticket_assignees".ticket = (SELECT id FROM ticket_id)
            AND "ticket_assignees".assignee = ${assignee.id}"""
-    sqlQuery.update.run.transact(tx)
-  }
+        sqlQuery.update.run.transact(tx)
+    }
 
-  override def removeLabel(projectId: ProjectId)(ticket: Ticket)(label: Label): F[Int] =
-    label.id match {
-      case None => Sync[F].pure(0)
-      case Some(labelId) =>
-        val sqlQuery = withTicketId(projectId, ticket.number) ++
-          fr"""DELETE FROM "tickets"."ticket_labels" AS "labels"
+    override def removeLabel(projectId: ProjectId)(ticket: Ticket)(label: Label): F[Int] =
+        label.id match {
+            case None => Sync[F].pure(0)
+            case Some(labelId) =>
+                val sqlQuery = withTicketId(projectId, ticket.number) ++
+                    fr"""DELETE FROM "tickets"."ticket_labels" AS "labels"
                WHERE "labels".ticket = (SELECT id FROM ticket_id)
                AND "labels".label = $labelId"""
-        sqlQuery.update.run.transact(tx)
-    }
+                sqlQuery.update.run.transact(tx)
+        }
 
-  override def removeMilestone(projectId: ProjectId)(ticket: Ticket)(milestone: Milestone): F[Int] =
-    milestone.id match {
-      case None => Sync[F].pure(0)
-      case Some(milestoneId) =>
-        val sqlQuery = withTicketId(projectId, ticket.number) ++
-          fr"""DELETE FROM "tickets"."milestone_tickets" AS "milestone_tickets"
+    override def removeMilestone(projectId: ProjectId)(ticket: Ticket)(milestone: Milestone): F[Int] =
+        milestone.id match {
+            case None => Sync[F].pure(0)
+            case Some(milestoneId) =>
+                val sqlQuery = withTicketId(projectId, ticket.number) ++
+                    fr"""DELETE FROM "tickets"."milestone_tickets" AS "milestone_tickets"
                WHERE "milestone_tickets".ticket = (SELECT id FROM ticket_id)
                AND "milestone_tickets".milestone = $milestoneId"""
-        sqlQuery.update.run.transact(tx)
-    }
+                sqlQuery.update.run.transact(tx)
+        }
 
-  override def updateTicket(projectId: ProjectId)(ticket: Ticket): F[Int] =
-    sql"""UPDATE "tickets"."tickets" SET
+    override def updateTicket(projectId: ProjectId)(ticket: Ticket): F[Int] =
+        sql"""UPDATE "tickets"."tickets" SET
             title = ${ticket.title},
             content = ${ticket.content},
             status = ${ticket.status},
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketServiceApi.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketServiceApi.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketServiceApi.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/DoobieTicketServiceApi.scala	2025-01-13 17:13:25.064470989 +0000
@@ -30,17 +30,17 @@
 import org.slf4j.LoggerFactory
 
 final class DoobieTicketServiceApi[F[_]: Sync](tx: Transactor[F]) extends TicketServiceApi[F] {
-  private val log = LoggerFactory.getLogger(getClass)
+    private val log = LoggerFactory.getLogger(getClass)
 
-  given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
+    given LogHandler[F] = Slf4jLogHandler.createLogHandler[F](log)
 
-  given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
-  given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
-  given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
-  given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
+    given Meta[EmailAddress] = Meta[String].timap(EmailAddress.apply)(_.toString)
+    given Meta[LanguageCode] = Meta[String].timap(LanguageCode.apply)(_.toString)
+    given Meta[UserId]       = Meta[UUID].timap(UserId.apply)(_.toUUID)
+    given Meta[Username]     = Meta[String].timap(Username.apply)(_.toString)
 
-  override def createOrUpdateUser(user: TicketsUser): F[Int] =
-    sql"""INSERT INTO "tickets"."users" (uid, name, email, language, created_at, updated_at)
+    override def createOrUpdateUser(user: TicketsUser): F[Int] =
+        sql"""INSERT INTO "tickets"."users" (uid, name, email, language, created_at, updated_at)
           VALUES (
             ${user.uid},
             ${user.name},
@@ -54,7 +54,7 @@
             language = EXCLUDED.language,
             updated_at = EXCLUDED.updated_at""".update.run.transact(tx)
 
-  override def deleteUser(uid: UserId): F[Int] =
-    sql"""DELETE FROM "tickets"."users" WHERE uid = $uid""".update.run.transact(tx)
+    override def deleteUser(uid: UserId): F[Int] =
+        sql"""DELETE FROM "tickets"."users" WHERE uid = $uid""".update.run.transact(tx)
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/FormValidator.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/FormValidator.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/FormValidator.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/FormValidator.scala	2025-01-13 17:13:25.064470989 +0000
@@ -34,21 +34,21 @@
   *   The concrete type of the validated form output.
   */
 abstract class FormValidator[T] {
-  final val fieldGlobal: FormField = FormValidator.fieldGlobal
+    final val fieldGlobal: FormField = FormValidator.fieldGlobal
 
-  /** Validate the submitted form data which is received as stringified map and return either a validated `T` or a list
-    * of [[de.smederee.tickets.forms.types.FormErrors]].
-    *
-    * @param data
-    *   The stringified map which was submitted.
-    * @return
-    *   Either the validated form as concrete type T or a list of form errors.
-    */
-  def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, T]
+    /** Validate the submitted form data which is received as stringified map and return either a validated `T` or a
+      * list of [[de.smederee.tickets.forms.types.FormErrors]].
+      *
+      * @param data
+      *   The stringified map which was submitted.
+      * @return
+      *   Either the validated form as concrete type T or a list of form errors.
+      */
+    def validate(data: Map[String, Chain[String]]): ValidatedNec[FormErrors, T]
 
 }
 
 object FormValidator {
-  // A constant for the field name used for global errors.
-  val fieldGlobal: FormField = FormField("global")
+    // A constant for the field name used for global errors.
+    val fieldGlobal: FormField = FormField("global")
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/types.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/types.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/types.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/forms/types.scala	2025-01-13 17:13:25.064470989 +0000
@@ -22,85 +22,85 @@
 
 object types {
 
-  type FormErrors = Map[FormField, List[FormFieldError]]
-  object FormErrors {
-    val empty: FormErrors = Map.empty[FormField, List[FormFieldError]]
-
-    /** Create a single FormErrors instance from a given non empty chain of FormErrors which is usually returned from
-      * validators.
-      *
-      * @param errors
-      *   A non empty chain of FormErrors.
-      * @return
-      *   A single FormErrors instance containing all the errors.
-      */
-    def fromNec(errors: NonEmptyChain[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
-
-    /** Create a single FormErrors instance from a given non empty list of FormErrors which is usually returned from
-      * validators.
-      *
-      * @param errors
-      *   A non empty list of FormErrors.
-      * @return
-      *   A single FormErrors instance containing all the errors.
-      */
-    def fromNel(errors: NonEmptyList[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
-  }
-
-  opaque type FormField = String
-  object FormField {
-
-    /** Create an instance of FormField from the given String type.
-      *
-      * @param source
-      *   An instance of type String which will be returned as a FormField.
-      * @return
-      *   The appropriate instance of FormField.
-      */
-    def apply(source: String): FormField = source
-
-    /** Try to create an instance of FormField from the given String.
-      *
-      * @param source
-      *   A String that should fulfil the requirements to be converted into a FormField.
-      * @return
-      *   An option to the successfully converted FormField.
-      */
-    def from(source: String): Option[FormField] =
-      Option(source).map(_.trim.nonEmpty) match {
-        case Some(true) => Option(source.trim)
-        case _          => None
-      }
-  }
-
-  given Conversion[FormField, String] = _.toString
-
-  opaque type FormFieldError = String
-  object FormFieldError {
-
-    /** Create an instance of FormFieldError from the given String type.
-      *
-      * @param source
-      *   An instance of type String which will be returned as a FormFieldError.
-      * @return
-      *   The appropriate instance of FormFieldError.
-      */
-    def apply(source: String): FormFieldError = source
-
-    /** Try to create an instance of FormFieldError from the given String.
-      *
-      * @param source
-      *   A String that should fulfil the requirements to be converted into a FormFieldError.
-      * @return
-      *   An option to the successfully converted FormFieldError.
-      */
-    def from(source: String): Option[FormFieldError] =
-      Option(source).map(_.trim.nonEmpty) match {
-        case Some(true) => Option(source.trim)
-        case _          => None
-      }
-  }
+    type FormErrors = Map[FormField, List[FormFieldError]]
+    object FormErrors {
+        val empty: FormErrors = Map.empty[FormField, List[FormFieldError]]
+
+        /** Create a single FormErrors instance from a given non empty chain of FormErrors which is usually returned
+          * from validators.
+          *
+          * @param errors
+          *   A non empty chain of FormErrors.
+          * @return
+          *   A single FormErrors instance containing all the errors.
+          */
+        def fromNec(errors: NonEmptyChain[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
+
+        /** Create a single FormErrors instance from a given non empty list of FormErrors which is usually returned from
+          * validators.
+          *
+          * @param errors
+          *   A non empty list of FormErrors.
+          * @return
+          *   A single FormErrors instance containing all the errors.
+          */
+        def fromNel(errors: NonEmptyList[FormErrors]): FormErrors = errors.toList.fold(FormErrors.empty)(_ combine _)
+    }
+
+    opaque type FormField = String
+    object FormField {
+
+        /** Create an instance of FormField from the given String type.
+          *
+          * @param source
+          *   An instance of type String which will be returned as a FormField.
+          * @return
+          *   The appropriate instance of FormField.
+          */
+        def apply(source: String): FormField = source
+
+        /** Try to create an instance of FormField from the given String.
+          *
+          * @param source
+          *   A String that should fulfil the requirements to be converted into a FormField.
+          * @return
+          *   An option to the successfully converted FormField.
+          */
+        def from(source: String): Option[FormField] =
+            Option(source).map(_.trim.nonEmpty) match {
+                case Some(true) => Option(source.trim)
+                case _          => None
+            }
+    }
+
+    given Conversion[FormField, String] = _.toString
+
+    opaque type FormFieldError = String
+    object FormFieldError {
+
+        /** Create an instance of FormFieldError from the given String type.
+          *
+          * @param source
+          *   An instance of type String which will be returned as a FormFieldError.
+          * @return
+          *   The appropriate instance of FormFieldError.
+          */
+        def apply(source: String): FormFieldError = source
+
+        /** Try to create an instance of FormFieldError from the given String.
+          *
+          * @param source
+          *   A String that should fulfil the requirements to be converted into a FormFieldError.
+          * @return
+          *   An option to the successfully converted FormFieldError.
+          */
+        def from(source: String): Option[FormFieldError] =
+            Option(source).map(_.trim.nonEmpty) match {
+                case Some(true) => Option(source.trim)
+                case _          => None
+            }
+    }
 
-  given Conversion[FormFieldError, String] = _.toString
+    given Conversion[FormFieldError, String] = _.toString
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/LabelRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/LabelRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/LabelRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/LabelRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -26,53 +26,53 @@
   */
 abstract class LabelRepository[F[_]] {
 
-  /** Return all labels associated with the given repository.
-    *
-    * @param projectId
-    *   The unique internal ID of a vcs repository metadata entry for which all labels shall be returned.
-    * @return
-    *   A stream of labels associated with the vcs repository which may be empty.
-    */
-  def allLabels(projectId: ProjectId): Stream[F, Label]
-
-  /** Create a database entry for the given label definition.
-    *
-    * @param projectId
-    *   The unique internal ID of a vcs repository metadata entry to which the label belongs.
-    * @param label
-    *   The label definition that shall be written to the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createLabel(projectId: ProjectId)(label: Label): F[Int]
-
-  /** Delete the label from the database.
-    *
-    * @param label
-    *   The label definition that shall be deleted from the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteLabel(label: Label): F[Int]
-
-  /** Find the label with the given name for the given vcs repository.
-    *
-    * @param projectId
-    *   The unique internal ID of a vcs repository metadata entry to which the label belongs.
-    * @param name
-    *   The name of the label which is must be unique in the context of the repository.
-    * @return
-    *   An option to the found label.
-    */
-  def findLabel(projectId: ProjectId)(name: LabelName): F[Option[Label]]
-
-  /** Update the database entry for the given label.
-    *
-    * @param label
-    *   The label definition that shall be updated within the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def updateLabel(label: Label): F[Int]
+    /** Return all labels associated with the given repository.
+      *
+      * @param projectId
+      *   The unique internal ID of a vcs repository metadata entry for which all labels shall be returned.
+      * @return
+      *   A stream of labels associated with the vcs repository which may be empty.
+      */
+    def allLabels(projectId: ProjectId): Stream[F, Label]
+
+    /** Create a database entry for the given label definition.
+      *
+      * @param projectId
+      *   The unique internal ID of a vcs repository metadata entry to which the label belongs.
+      * @param label
+      *   The label definition that shall be written to the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createLabel(projectId: ProjectId)(label: Label): F[Int]
+
+    /** Delete the label from the database.
+      *
+      * @param label
+      *   The label definition that shall be deleted from the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteLabel(label: Label): F[Int]
+
+    /** Find the label with the given name for the given vcs repository.
+      *
+      * @param projectId
+      *   The unique internal ID of a vcs repository metadata entry to which the label belongs.
+      * @param name
+      *   The name of the label which is must be unique in the context of the repository.
+      * @return
+      *   An option to the found label.
+      */
+    def findLabel(projectId: ProjectId)(name: LabelName): F[Option[Label]]
+
+    /** Update the database entry for the given label.
+      *
+      * @param label
+      *   The label definition that shall be updated within the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def updateLabel(label: Label): F[Int]
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Label.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Label.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Label.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Label.scala	2025-01-13 17:13:25.064470989 +0000
@@ -24,48 +24,48 @@
 
 opaque type LabelId = Long
 object LabelId {
-  given Eq[LabelId]       = Eq.fromUniversalEquals
-  given Ordering[LabelId] = (x: LabelId, y: LabelId) => x.compareTo(y)
-  given Order[LabelId]    = Order.fromOrdering
-
-  val Format: Regex = "^-?\\d+$".r
-
-  /** Create an instance of LabelId from the given Long type.
-    *
-    * @param source
-    *   An instance of type Long which will be returned as a LabelId.
-    * @return
-    *   The appropriate instance of LabelId.
-    */
-  def apply(source: Long): LabelId = source
-
-  /** Try to create an instance of LabelId from the given Long.
-    *
-    * @param source
-    *   A Long that should fulfil the requirements to be converted into a LabelId.
-    * @return
-    *   An option to the successfully converted LabelId.
-    */
-  def from(source: Long): Option[LabelId] = Option(source)
-
-  /** Try to create an instance of LabelId from the given String.
-    *
-    * @param source
-    *   A string that should fulfil the requirements to be converted into a LabelId.
-    * @return
-    *   An option to the successfully converted LabelId.
-    */
-  def fromString(source: String): Option[LabelId] = Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
-
-  extension (id: LabelId) {
-    def toLong: Long = id
-  }
+    given Eq[LabelId]       = Eq.fromUniversalEquals
+    given Ordering[LabelId] = (x: LabelId, y: LabelId) => x.compareTo(y)
+    given Order[LabelId]    = Order.fromOrdering
+
+    val Format: Regex = "^-?\\d+$".r
+
+    /** Create an instance of LabelId from the given Long type.
+      *
+      * @param source
+      *   An instance of type Long which will be returned as a LabelId.
+      * @return
+      *   The appropriate instance of LabelId.
+      */
+    def apply(source: Long): LabelId = source
+
+    /** Try to create an instance of LabelId from the given Long.
+      *
+      * @param source
+      *   A Long that should fulfil the requirements to be converted into a LabelId.
+      * @return
+      *   An option to the successfully converted LabelId.
+      */
+    def from(source: Long): Option[LabelId] = Option(source)
+
+    /** Try to create an instance of LabelId from the given String.
+      *
+      * @param source
+      *   A string that should fulfil the requirements to be converted into a LabelId.
+      * @return
+      *   An option to the successfully converted LabelId.
+      */
+    def fromString(source: String): Option[LabelId] = Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
+
+    extension (id: LabelId) {
+        def toLong: Long = id
+    }
 }
 
 /** Extractor to retrieve an LabelId from a path parameter.
   */
 object LabelIdPathParameter {
-  def unapply(str: String): Option[LabelId] = Option(str).flatMap(LabelId.fromString)
+    def unapply(str: String): Option[LabelId] = Option(str).flatMap(LabelId.fromString)
 }
 
 /** A short descriptive name for the label which is supposed to be unique in a project context. It must not be empty and
@@ -73,65 +73,65 @@
   */
 opaque type LabelName = String
 object LabelName {
-  given Eq[LabelName]       = Eq.fromUniversalEquals
-  given Ordering[LabelName] = (x: LabelName, y: LabelName) => x.compareTo(y)
-  given Order[LabelName]    = Order.fromOrdering[LabelName]
-
-  val MaxLength: Int = 40
-
-  /** Create an instance of LabelName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a LabelName.
-    * @return
-    *   The appropriate instance of LabelName.
-    */
-  def apply(source: String): LabelName = source
-
-  /** Try to create an instance of LabelName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a LabelName.
-    * @return
-    *   An option to the successfully converted LabelName.
-    */
-  def from(source: String): Option[LabelName] =
-    Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
+    given Eq[LabelName]       = Eq.fromUniversalEquals
+    given Ordering[LabelName] = (x: LabelName, y: LabelName) => x.compareTo(y)
+    given Order[LabelName]    = Order.fromOrdering[LabelName]
+
+    val MaxLength: Int = 40
+
+    /** Create an instance of LabelName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a LabelName.
+      * @return
+      *   The appropriate instance of LabelName.
+      */
+    def apply(source: String): LabelName = source
+
+    /** Try to create an instance of LabelName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a LabelName.
+      * @return
+      *   An option to the successfully converted LabelName.
+      */
+    def from(source: String): Option[LabelName] =
+        Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
 
 }
 
 /** Extractor to retrieve an LabelName from a path parameter.
   */
 object LabelNamePathParameter {
-  def unapply(str: String): Option[LabelName] = Option(str).flatMap(LabelName.from)
+    def unapply(str: String): Option[LabelName] = Option(str).flatMap(LabelName.from)
 }
 
 /** A maybe needed description of a label which must not be empty and not exceed 254 characters in length.
   */
 opaque type LabelDescription = String
 object LabelDescription {
-  given Eq[LabelDescription] = Eq.fromUniversalEquals
+    given Eq[LabelDescription] = Eq.fromUniversalEquals
 
-  val MaxLength: Int = 254
+    val MaxLength: Int = 254
 
-  /** Create an instance of LabelDescription from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a LabelDescription.
-    * @return
-    *   The appropriate instance of LabelDescription.
-    */
-  def apply(source: String): LabelDescription = source
-
-  /** Try to create an instance of LabelDescription from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a LabelDescription.
-    * @return
-    *   An option to the successfully converted LabelDescription.
-    */
-  def from(source: String): Option[LabelDescription] =
-    Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
+    /** Create an instance of LabelDescription from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a LabelDescription.
+      * @return
+      *   The appropriate instance of LabelDescription.
+      */
+    def apply(source: String): LabelDescription = source
+
+    /** Try to create an instance of LabelDescription from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a LabelDescription.
+      * @return
+      *   An option to the successfully converted LabelDescription.
+      */
+    def from(source: String): Option[LabelDescription] =
+        Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
 }
 
 /** An HTML colour code in hexadecimal notation e.g. `#12ab3f`. It must match the format of starting with a hashtag
@@ -139,27 +139,27 @@
   */
 opaque type ColourCode = String
 object ColourCode {
-  given Eq[ColourCode] = Eq.instance((a, b) => a.equalsIgnoreCase(b))
+    given Eq[ColourCode] = Eq.instance((a, b) => a.equalsIgnoreCase(b))
 
-  val Format: Regex = "^#[0-9a-fA-F]{6}$".r
+    val Format: Regex = "^#[0-9a-fA-F]{6}$".r
 
-  /** Create an instance of ColourCode from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ColourCode.
-    * @return
-    *   The appropriate instance of ColourCode.
-    */
-  def apply(source: String): ColourCode = source
-
-  /** Try to create an instance of ColourCode from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ColourCode.
-    * @return
-    *   An option to the successfully converted ColourCode.
-    */
-  def from(source: String): Option[ColourCode] = Option(source).filter(string => Format.matches(string))
+    /** Create an instance of ColourCode from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ColourCode.
+      * @return
+      *   The appropriate instance of ColourCode.
+      */
+    def apply(source: String): ColourCode = source
+
+    /** Try to create an instance of ColourCode from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ColourCode.
+      * @return
+      *   An option to the successfully converted ColourCode.
+      */
+    def from(source: String): Option[ColourCode] = Option(source).filter(string => Format.matches(string))
 
 }
 
@@ -177,11 +177,11 @@
 final case class Label(id: Option[LabelId], name: LabelName, description: Option[LabelDescription], colour: ColourCode)
 
 object Label {
-  given Eq[Label] =
-    Eq.instance((thisLabel, thatLabel) =>
-      thisLabel.id === thatLabel.id &&
-        thisLabel.name === thatLabel.name &&
-        thisLabel.description === thatLabel.description &&
-        thisLabel.colour === thatLabel.colour
-    )
+    given Eq[Label] =
+        Eq.instance((thisLabel, thatLabel) =>
+            thisLabel.id === thatLabel.id &&
+                thisLabel.name === thatLabel.name &&
+                thisLabel.description === thatLabel.description &&
+                thisLabel.colour === thatLabel.colour
+        )
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/MilestoneRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -26,82 +26,82 @@
   */
 abstract class MilestoneRepository[F[_]] {
 
-  /** Return all milestones associated with the given repository.
-    *
-    * @param projectId
-    *   The unique internal ID of a vcs repository metadata entry for which all milestones shall be returned.
-    * @return
-    *   A stream of milestones associated with the vcs repository which may be empty.
-    */
-  def allMilestones(projectId: ProjectId): Stream[F, Milestone]
-
-  /** Return all tickets associated with the given milestone.
-    *
-    * @param filter
-    *   A ticket filter containing possible values which will be used to filter the list of tickets.
-    * @param milestoneId
-    *   The unique internal ID of a milestone for which all tickets shall be returned.
-    * @return
-    *   A stream of tickets associated with the vcs repository which may be empty.
-    */
-  def allTickets(filter: Option[TicketFilter])(milestoneId: MilestoneId): Stream[F, Ticket]
-
-  /** Change the milestone status with the given id to closed.
-    *
-    * @param milestoneId
-    *   The unique internal ID of a milestone for which all tickets shall be returned.
-    * @return
-    *   The number of affected database rows.
-    */
-  def closeMilestone(milestoneId: MilestoneId): F[Int]
-
-  /** Create a database entry for the given milestone definition.
-    *
-    * @param projectId
-    *   The unique internal ID of a vcs repository metadata entry to which the milestone belongs.
-    * @param milestone
-    *   The milestone definition that shall be written to the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createMilestone(projectId: ProjectId)(milestone: Milestone): F[Int]
-
-  /** Delete the milestone from the database.
-    *
-    * @param milestone
-    *   The milestone definition that shall be deleted from the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteMilestone(milestone: Milestone): F[Int]
-
-  /** Find the milestone with the given title for the given vcs repository.
-    *
-    * @param projectId
-    *   The unique internal ID of a vcs repository metadata entry to which the milestone belongs.
-    * @param title
-    *   The title of the milestone which is must be unique in the context of the repository.
-    * @return
-    *   An option to the found milestone.
-    */
-  def findMilestone(projectId: ProjectId)(title: MilestoneTitle): F[Option[Milestone]]
-
-  /** Change the milestone status with the given id to open.
-    *
-    * @param milestoneId
-    *   The unique internal ID of a milestone for which all tickets shall be returned.
-    * @return
-    *   The number of affected database rows.
-    */
-  def openMilestone(milestoneId: MilestoneId): F[Int]
-
-  /** Update the database entry for the given milestone.
-    *
-    * @param milestone
-    *   The milestone definition that shall be updated within the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def updateMilestone(milestone: Milestone): F[Int]
+    /** Return all milestones associated with the given repository.
+      *
+      * @param projectId
+      *   The unique internal ID of a vcs repository metadata entry for which all milestones shall be returned.
+      * @return
+      *   A stream of milestones associated with the vcs repository which may be empty.
+      */
+    def allMilestones(projectId: ProjectId): Stream[F, Milestone]
+
+    /** Return all tickets associated with the given milestone.
+      *
+      * @param filter
+      *   A ticket filter containing possible values which will be used to filter the list of tickets.
+      * @param milestoneId
+      *   The unique internal ID of a milestone for which all tickets shall be returned.
+      * @return
+      *   A stream of tickets associated with the vcs repository which may be empty.
+      */
+    def allTickets(filter: Option[TicketFilter])(milestoneId: MilestoneId): Stream[F, Ticket]
+
+    /** Change the milestone status with the given id to closed.
+      *
+      * @param milestoneId
+      *   The unique internal ID of a milestone for which all tickets shall be returned.
+      * @return
+      *   The number of affected database rows.
+      */
+    def closeMilestone(milestoneId: MilestoneId): F[Int]
+
+    /** Create a database entry for the given milestone definition.
+      *
+      * @param projectId
+      *   The unique internal ID of a vcs repository metadata entry to which the milestone belongs.
+      * @param milestone
+      *   The milestone definition that shall be written to the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createMilestone(projectId: ProjectId)(milestone: Milestone): F[Int]
+
+    /** Delete the milestone from the database.
+      *
+      * @param milestone
+      *   The milestone definition that shall be deleted from the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteMilestone(milestone: Milestone): F[Int]
+
+    /** Find the milestone with the given title for the given vcs repository.
+      *
+      * @param projectId
+      *   The unique internal ID of a vcs repository metadata entry to which the milestone belongs.
+      * @param title
+      *   The title of the milestone which is must be unique in the context of the repository.
+      * @return
+      *   An option to the found milestone.
+      */
+    def findMilestone(projectId: ProjectId)(title: MilestoneTitle): F[Option[Milestone]]
+
+    /** Change the milestone status with the given id to open.
+      *
+      * @param milestoneId
+      *   The unique internal ID of a milestone for which all tickets shall be returned.
+      * @return
+      *   The number of affected database rows.
+      */
+    def openMilestone(milestoneId: MilestoneId): F[Int]
+
+    /** Update the database entry for the given milestone.
+      *
+      * @param milestone
+      *   The milestone definition that shall be updated within the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def updateMilestone(milestone: Milestone): F[Int]
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Milestone.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Milestone.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Milestone.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Milestone.scala	2025-01-13 17:13:25.064470989 +0000
@@ -26,47 +26,47 @@
 
 opaque type MilestoneId = Long
 object MilestoneId {
-  given Eq[MilestoneId] = Eq.fromUniversalEquals
+    given Eq[MilestoneId] = Eq.fromUniversalEquals
 
-  val Format: Regex = "^-?\\d+$".r
+    val Format: Regex = "^-?\\d+$".r
 
-  /** Create an instance of MilestoneId from the given Long type.
-    *
-    * @param source
-    *   An instance of type Long which will be returned as a MilestoneId.
-    * @return
-    *   The appropriate instance of MilestoneId.
-    */
-  def apply(source: Long): MilestoneId = source
-
-  /** Try to create an instance of MilestoneId from the given Long.
-    *
-    * @param source
-    *   A Long that should fulfil the requirements to be converted into a MilestoneId.
-    * @return
-    *   An option to the successfully converted MilestoneId.
-    */
-  def from(source: Long): Option[MilestoneId] = Option(source)
-
-  /** Try to create an instance of MilestoneId from the given String.
-    *
-    * @param source
-    *   A string that should fulfil the requirements to be converted into a MilestoneId.
-    * @return
-    *   An option to the successfully converted MilestoneId.
-    */
-  def fromString(source: String): Option[MilestoneId] =
-    Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
-
-  extension (id: MilestoneId) {
-    def toLong: Long = id
-  }
+    /** Create an instance of MilestoneId from the given Long type.
+      *
+      * @param source
+      *   An instance of type Long which will be returned as a MilestoneId.
+      * @return
+      *   The appropriate instance of MilestoneId.
+      */
+    def apply(source: Long): MilestoneId = source
+
+    /** Try to create an instance of MilestoneId from the given Long.
+      *
+      * @param source
+      *   A Long that should fulfil the requirements to be converted into a MilestoneId.
+      * @return
+      *   An option to the successfully converted MilestoneId.
+      */
+    def from(source: Long): Option[MilestoneId] = Option(source)
+
+    /** Try to create an instance of MilestoneId from the given String.
+      *
+      * @param source
+      *   A string that should fulfil the requirements to be converted into a MilestoneId.
+      * @return
+      *   An option to the successfully converted MilestoneId.
+      */
+    def fromString(source: String): Option[MilestoneId] =
+        Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
+
+    extension (id: MilestoneId) {
+        def toLong: Long = id
+    }
 }
 
 /** Extractor to retrieve an MilestoneId from a path parameter.
   */
 object MilestoneIdPathParameter {
-  def unapply(str: String): Option[MilestoneId] = Option(str).flatMap(MilestoneId.fromString)
+    def unapply(str: String): Option[MilestoneId] = Option(str).flatMap(MilestoneId.fromString)
 }
 
 /** A title for a milestone, usually a version number, a word or a short phrase that is supposed to be unique within a
@@ -74,62 +74,62 @@
   */
 opaque type MilestoneTitle = String
 object MilestoneTitle {
-  given Eq[MilestoneTitle]       = Eq.fromUniversalEquals
-  given Ordering[MilestoneTitle] = (x: MilestoneTitle, y: MilestoneTitle) => x.compareTo(y)
-  given Order[MilestoneTitle]    = Order.fromOrdering[MilestoneTitle]
-
-  val MaxLength: Int = 64
-
-  /** Create an instance of MilestoneTitle from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a MilestoneTitle.
-    * @return
-    *   The appropriate instance of MilestoneTitle.
-    */
-  def apply(source: String): MilestoneTitle = source
-
-  /** Try to create an instance of MilestoneTitle from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a MilestoneTitle.
-    * @return
-    *   An option to the successfully converted MilestoneTitle.
-    */
-  def from(source: String): Option[MilestoneTitle] =
-    Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
+    given Eq[MilestoneTitle]       = Eq.fromUniversalEquals
+    given Ordering[MilestoneTitle] = (x: MilestoneTitle, y: MilestoneTitle) => x.compareTo(y)
+    given Order[MilestoneTitle]    = Order.fromOrdering[MilestoneTitle]
+
+    val MaxLength: Int = 64
+
+    /** Create an instance of MilestoneTitle from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a MilestoneTitle.
+      * @return
+      *   The appropriate instance of MilestoneTitle.
+      */
+    def apply(source: String): MilestoneTitle = source
+
+    /** Try to create an instance of MilestoneTitle from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a MilestoneTitle.
+      * @return
+      *   An option to the successfully converted MilestoneTitle.
+      */
+    def from(source: String): Option[MilestoneTitle] =
+        Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
 
 }
 
 /** Extractor to retrieve an MilestoneTitle from a path parameter.
   */
 object MilestoneTitlePathParameter {
-  def unapply(str: String): Option[MilestoneTitle] = Option(str).flatMap(MilestoneTitle.from)
+    def unapply(str: String): Option[MilestoneTitle] = Option(str).flatMap(MilestoneTitle.from)
 }
 
 /** A longer detailed description of a project milestone which must not be empty.
   */
 opaque type MilestoneDescription = String
 object MilestoneDescription {
-  given Eq[MilestoneDescription] = Eq.fromUniversalEquals
+    given Eq[MilestoneDescription] = Eq.fromUniversalEquals
 
-  /** Create an instance of MilestoneDescription from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a MilestoneDescription.
-    * @return
-    *   The appropriate instance of MilestoneDescription.
-    */
-  def apply(source: String): MilestoneDescription = source
-
-  /** Try to create an instance of MilestoneDescription from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a MilestoneDescription.
-    * @return
-    *   An option to the successfully converted MilestoneDescription.
-    */
-  def from(source: String): Option[MilestoneDescription] = Option(source).map(_.trim).filter(_.nonEmpty)
+    /** Create an instance of MilestoneDescription from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a MilestoneDescription.
+      * @return
+      *   The appropriate instance of MilestoneDescription.
+      */
+    def apply(source: String): MilestoneDescription = source
+
+    /** Try to create an instance of MilestoneDescription from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a MilestoneDescription.
+      * @return
+      *   An option to the successfully converted MilestoneDescription.
+      */
+    def from(source: String): Option[MilestoneDescription] = Option(source).map(_.trim).filter(_.nonEmpty)
 
 }
 
@@ -157,11 +157,11 @@
 
 object Milestone {
 
-  given Eq[LocalDate] = Eq.instance((a, b) => a.compareTo(b) === 0)
+    given Eq[LocalDate] = Eq.instance((a, b) => a.compareTo(b) === 0)
 
-  given Eq[Milestone] =
-    Eq.instance((a, b) =>
-      a.id === b.id && a.title === b.title && a.dueDate === b.dueDate && a.description === b.description
-    )
+    given Eq[Milestone] =
+        Eq.instance((a, b) =>
+            a.id === b.id && a.title === b.title && a.dueDate === b.dueDate && a.description === b.description
+        )
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/ProjectRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/ProjectRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/ProjectRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/ProjectRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -24,73 +24,73 @@
   */
 abstract class ProjectRepository[F[_]] {
 
-  /** Create the given project within the database.
-    *
-    * @param project
-    *   The project that shall be created.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createProject(project: Project): F[Int]
-
-  /** Delete the given project from the database.
-    *
-    * @param project
-    *   The project that shall be deleted.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteProject(project: Project): F[Int]
-
-  /** Search for the project entry with the given owner and name.
-    *
-    * @param owner
-    *   Data about the owner of the project containing information needed to query the database.
-    * @param name
-    *   The project name which must be unique in regard to the owner.
-    * @return
-    *   An option to the successfully found project entry.
-    */
-  def findProject(owner: ProjectOwner, name: ProjectName): F[Option[Project]]
-
-  /** Search for the internal database specific (auto generated) ID of the given owner / project combination which
-    * serves as a primary key for the database table.
-    *
-    * @param owner
-    *   Data about the owner of the project containing information needed to query the database.
-    * @param name
-    *   The project name which must be unique in regard to the owner.
-    * @return
-    *   An option to the internal database ID.
-    */
-  def findProjectId(owner: ProjectOwner, name: ProjectName): F[Option[ProjectId]]
-
-  /** Search for a project owner of whom we only know the name.
-    *
-    * @param name
-    *   The name of the project owner which is the username of the actual owners account.
-    * @return
-    *   An option to successfully found project owner.
-    */
-  def findProjectOwner(name: ProjectOwnerName): F[Option[ProjectOwner]]
-
-  /** Increment the counter column for the next ticket number and return the old value (i.e. the value _before_ it was
-    * incremented).
-    *
-    * @param projectId
-    *   The internal database id of the project.
-    * @return
-    *   The ticket number _before_ it was incremented.
-    */
-  def incrementNextTicketNumber(projectId: ProjectId): F[TicketNumber]
-
-  /** Update the database entry for the given project.
-    *
-    * @param project
-    *   The project that shall be updated within the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def updateProject(project: Project): F[Int]
+    /** Create the given project within the database.
+      *
+      * @param project
+      *   The project that shall be created.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createProject(project: Project): F[Int]
+
+    /** Delete the given project from the database.
+      *
+      * @param project
+      *   The project that shall be deleted.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteProject(project: Project): F[Int]
+
+    /** Search for the project entry with the given owner and name.
+      *
+      * @param owner
+      *   Data about the owner of the project containing information needed to query the database.
+      * @param name
+      *   The project name which must be unique in regard to the owner.
+      * @return
+      *   An option to the successfully found project entry.
+      */
+    def findProject(owner: ProjectOwner, name: ProjectName): F[Option[Project]]
+
+    /** Search for the internal database specific (auto generated) ID of the given owner / project combination which
+      * serves as a primary key for the database table.
+      *
+      * @param owner
+      *   Data about the owner of the project containing information needed to query the database.
+      * @param name
+      *   The project name which must be unique in regard to the owner.
+      * @return
+      *   An option to the internal database ID.
+      */
+    def findProjectId(owner: ProjectOwner, name: ProjectName): F[Option[ProjectId]]
+
+    /** Search for a project owner of whom we only know the name.
+      *
+      * @param name
+      *   The name of the project owner which is the username of the actual owners account.
+      * @return
+      *   An option to successfully found project owner.
+      */
+    def findProjectOwner(name: ProjectOwnerName): F[Option[ProjectOwner]]
+
+    /** Increment the counter column for the next ticket number and return the old value (i.e. the value _before_ it was
+      * incremented).
+      *
+      * @param projectId
+      *   The internal database id of the project.
+      * @return
+      *   The ticket number _before_ it was incremented.
+      */
+    def incrementNextTicketNumber(projectId: ProjectId): F[TicketNumber]
+
+    /** Update the database entry for the given project.
+      *
+      * @param project
+      *   The project that shall be updated within the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def updateProject(project: Project): F[Int]
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Project.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Project.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Project.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Project.scala	2025-01-13 17:13:25.064470989 +0000
@@ -31,198 +31,198 @@
 
 opaque type ProjectDescription = String
 object ProjectDescription {
-  val MaximumLength: Int = 8192
+    val MaximumLength: Int = 8192
 
-  /** Create an instance of ProjectDescription from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ProjectDescription.
-    * @return
-    *   The appropriate instance of ProjectDescription.
-    */
-  def apply(source: String): ProjectDescription = source
-
-  /** Try to create an instance of ProjectDescription from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ProjectDescription.
-    * @return
-    *   An option to the successfully converted ProjectDescription.
-    */
-  def from(source: String): Option[ProjectDescription] = Option(source).map(_.take(MaximumLength))
+    /** Create an instance of ProjectDescription from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ProjectDescription.
+      * @return
+      *   The appropriate instance of ProjectDescription.
+      */
+    def apply(source: String): ProjectDescription = source
+
+    /** Try to create an instance of ProjectDescription from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ProjectDescription.
+      * @return
+      *   An option to the successfully converted ProjectDescription.
+      */
+    def from(source: String): Option[ProjectDescription] = Option(source).map(_.take(MaximumLength))
 
 }
 
 opaque type ProjectId = Long
 object ProjectId {
-  given Eq[ProjectId] = Eq.fromUniversalEquals
+    given Eq[ProjectId] = Eq.fromUniversalEquals
+
+    val Format: Regex = "^-?\\d+$".r
+
+    /** Create an instance of ProjectId from the given Long type.
+      *
+      * @param source
+      *   An instance of type Long which will be returned as a ProjectId.
+      * @return
+      *   The appropriate instance of ProjectId.
+      */
+    def apply(source: Long): ProjectId = source
 
-  val Format: Regex = "^-?\\d+$".r
+    /** Try to create an instance of ProjectId from the given Long.
+      *
+      * @param source
+      *   A Long that should fulfil the requirements to be converted into a ProjectId.
+      * @return
+      *   An option to the successfully converted ProjectId.
+      */
+    def from(source: Long): Option[ProjectId] = Option(source)
+
+    /** Try to create an instance of ProjectId from the given String.
+      *
+      * @param source
+      *   A string that should fulfil the requirements to be converted into a ProjectId.
+      * @return
+      *   An option to the successfully converted ProjectId.
+      */
+    def fromString(source: String): Option[ProjectId] =
+        Option(source).filter(Format.matches).flatMap(string => Try(string.toLong).toOption).flatMap(from)
 
-  /** Create an instance of ProjectId from the given Long type.
-    *
-    * @param source
-    *   An instance of type Long which will be returned as a ProjectId.
-    * @return
-    *   The appropriate instance of ProjectId.
-    */
-  def apply(source: Long): ProjectId = source
-
-  /** Try to create an instance of ProjectId from the given Long.
-    *
-    * @param source
-    *   A Long that should fulfil the requirements to be converted into a ProjectId.
-    * @return
-    *   An option to the successfully converted ProjectId.
-    */
-  def from(source: Long): Option[ProjectId] = Option(source)
-
-  /** Try to create an instance of ProjectId from the given String.
-    *
-    * @param source
-    *   A string that should fulfil the requirements to be converted into a ProjectId.
-    * @return
-    *   An option to the successfully converted ProjectId.
-    */
-  def fromString(source: String): Option[ProjectId] =
-    Option(source).filter(Format.matches).flatMap(string => Try(string.toLong).toOption).flatMap(from)
-
-  extension (id: ProjectId) {
-    def toLong: Long = id
-  }
+    extension (id: ProjectId) {
+        def toLong: Long = id
+    }
 }
 
 opaque type ProjectName = String
 object ProjectName {
 
-  given Eq[ProjectName] = Eq.fromUniversalEquals
+    given Eq[ProjectName] = Eq.fromUniversalEquals
+
+    given Order[ProjectName] = Order.from((a, b) => a.toString.compareTo(b.toString))
 
-  given Order[ProjectName] = Order.from((a, b) => a.toString.compareTo(b.toString))
+    // TODO: Can we rewrite this in a Scala-3 way (i.e. without implicitly)?
+    given Ordering[ProjectName] = implicitly[Order[ProjectName]].toOrdering
 
-  // TODO: Can we rewrite this in a Scala-3 way (i.e. without implicitly)?
-  given Ordering[ProjectName] = implicitly[Order[ProjectName]].toOrdering
+    val Format: Regex = "^[a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63}$".r
 
-  val Format: Regex = "^[a-zA-Z0-9][a-zA-Z0-9\\-_]{1,63}$".r
+    /** Create an instance of ProjectName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ProjectName.
+      * @return
+      *   The appropriate instance of ProjectName.
+      */
+    def apply(source: String): ProjectName = source
+
+    /** Try to create an instance of ProjectName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ProjectName.
+      * @return
+      *   An option to the successfully converted ProjectName.
+      */
+    def from(source: String): Option[ProjectName] = validate(source).toOption
 
-  /** Create an instance of ProjectName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ProjectName.
-    * @return
-    *   The appropriate instance of ProjectName.
-    */
-  def apply(source: String): ProjectName = source
-
-  /** Try to create an instance of ProjectName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ProjectName.
-    * @return
-    *   An option to the successfully converted ProjectName.
-    */
-  def from(source: String): Option[ProjectName] = validate(source).toOption
-
-  /** Validate the given string and return either the validated repository name or a list of errors.
-    *
-    * @param s
-    *   An arbitrary string which should be a repository name.
-    * @return
-    *   Either a list of errors or the validated repository name.
-    */
-  def validate(s: String): ValidatedNec[String, ProjectName] =
-    Option(s).map(_.trim.nonEmpty) match {
-      case Some(true) =>
-        val input = s.trim
-        val miniumLength =
-          if (input.length > 1)
-            input.validNec
-          else
-            "Repository name too short (min. 2 characters)!".invalidNec
-        val maximumLength =
-          if (input.length < 65)
-            input.validNec
-          else
-            "Repository name too long (max. 64 characters)!".invalidNec
-        val validFormat =
-          if (Format.matches(input))
-            input.validNec
-          else
-            "Repository name must start with a letter or number and is only allowed to contain alphanumeric characters plus .".invalidNec
-        (miniumLength, maximumLength, validFormat).mapN { case (_, _, name) =>
-          name
+    /** Validate the given string and return either the validated repository name or a list of errors.
+      *
+      * @param s
+      *   An arbitrary string which should be a repository name.
+      * @return
+      *   Either a list of errors or the validated repository name.
+      */
+    def validate(s: String): ValidatedNec[String, ProjectName] =
+        Option(s).map(_.trim.nonEmpty) match {
+            case Some(true) =>
+                val input = s.trim
+                val miniumLength =
+                    if (input.length > 1)
+                        input.validNec
+                    else
+                        "Repository name too short (min. 2 characters)!".invalidNec
+                val maximumLength =
+                    if (input.length < 65)
+                        input.validNec
+                    else
+                        "Repository name too long (max. 64 characters)!".invalidNec
+                val validFormat =
+                    if (Format.matches(input))
+                        input.validNec
+                    else
+                        "Repository name must start with a letter or number and is only allowed to contain alphanumeric characters plus .".invalidNec
+                (miniumLength, maximumLength, validFormat).mapN { case (_, _, name) =>
+                    name
+                }
+            case _ => "Repository name must not be empty!".invalidNec
         }
-      case _ => "Repository name must not be empty!".invalidNec
-    }
 }
 
 /** Extractor to retrieve a ProjectName from a path parameter.
   */
 object ProjectNamePathParameter {
-  def unapply(str: String): Option[ProjectName] = Option(str).flatMap(ProjectName.from)
+    def unapply(str: String): Option[ProjectName] = Option(str).flatMap(ProjectName.from)
 }
 
 /** A project owner id is supposed to be globally unique and maps to the [[java.util.UUID]] type beneath.
   */
 opaque type ProjectOwnerId = UUID
 object ProjectOwnerId {
-  val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
+    val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
 
-  given Eq[ProjectOwnerId] = Eq.fromUniversalEquals
+    given Eq[ProjectOwnerId] = Eq.fromUniversalEquals
 
-  // given Conversion[UserId, ProjectOwnerId] = ProjectOwnerId.fromUserId
+    // given Conversion[UserId, ProjectOwnerId] = ProjectOwnerId.fromUserId
+
+    /** Create an instance of ProjectOwnerId from the given UUID type.
+      *
+      * @param source
+      *   An instance of type UUID which will be returned as a ProjectOwnerId.
+      * @return
+      *   The appropriate instance of ProjectOwnerId.
+      */
+    def apply(source: UUID): ProjectOwnerId = source
 
-  /** Create an instance of ProjectOwnerId from the given UUID type.
-    *
-    * @param source
-    *   An instance of type UUID which will be returned as a ProjectOwnerId.
-    * @return
-    *   The appropriate instance of ProjectOwnerId.
-    */
-  def apply(source: UUID): ProjectOwnerId = source
-
-  /** Try to create an instance of ProjectOwnerId from the given UUID.
-    *
-    * @param source
-    *   A UUID that should fulfil the requirements to be converted into a ProjectOwnerId.
-    * @return
-    *   An option to the successfully converted ProjectOwnerId.
-    */
-  def from(source: UUID): Option[ProjectOwnerId] = Option(source)
-
-  /** Try to create an instance of ProjectOwnerId from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ProjectOwnerId.
-    * @return
-    *   An option to the successfully converted ProjectOwnerId.
-    */
-  def fromString(source: String): Either[String, ProjectOwnerId] =
-    Option(source)
-      .filter(s => Format.matches(s))
-      .flatMap { uuidString =>
-        Either.catchNonFatal(UUID.fromString(uuidString)).toOption
-      }
-      .toRight("Illegal value for ProjectOwnerId!")
-
-  /** Create an instance of ProjectOwnerId from the given UserId type.
-    *
-    * @param uid
-    *   An instance of type UserId which will be returned as a ProjectOwnerId.
-    * @return
-    *   The appropriate instance of ProjectOwnerId.
-    */
-  def fromUserId(uid: UserId): ProjectOwnerId = uid.toUUID
-
-  /** Generate a new random project owner id.
-    *
-    * @return
-    *   A project owner id which is pseudo randomly generated.
-    */
-  def randomProjectOwnerId: ProjectOwnerId = UUID.randomUUID
-
-  extension (uid: ProjectOwnerId) {
-    def toUUID: UUID = uid
-  }
+    /** Try to create an instance of ProjectOwnerId from the given UUID.
+      *
+      * @param source
+      *   A UUID that should fulfil the requirements to be converted into a ProjectOwnerId.
+      * @return
+      *   An option to the successfully converted ProjectOwnerId.
+      */
+    def from(source: UUID): Option[ProjectOwnerId] = Option(source)
+
+    /** Try to create an instance of ProjectOwnerId from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ProjectOwnerId.
+      * @return
+      *   An option to the successfully converted ProjectOwnerId.
+      */
+    def fromString(source: String): Either[String, ProjectOwnerId] =
+        Option(source)
+            .filter(s => Format.matches(s))
+            .flatMap { uuidString =>
+                Either.catchNonFatal(UUID.fromString(uuidString)).toOption
+            }
+            .toRight("Illegal value for ProjectOwnerId!")
+
+    /** Create an instance of ProjectOwnerId from the given UserId type.
+      *
+      * @param uid
+      *   An instance of type UserId which will be returned as a ProjectOwnerId.
+      * @return
+      *   The appropriate instance of ProjectOwnerId.
+      */
+    def fromUserId(uid: UserId): ProjectOwnerId = uid.toUUID
+
+    /** Generate a new random project owner id.
+      *
+      * @return
+      *   A project owner id which is pseudo randomly generated.
+      */
+    def randomProjectOwnerId: ProjectOwnerId = UUID.randomUUID
+
+    extension (uid: ProjectOwnerId) {
+        def toUUID: UUID = uid
+    }
 }
 
 /** A project owner name for an account has to obey several restrictions which are similiar to the ones found for Unix
@@ -231,92 +231,92 @@
   */
 opaque type ProjectOwnerName = String
 object ProjectOwnerName {
-  given Eq[ProjectOwnerName] = Eq.fromUniversalEquals
+    given Eq[ProjectOwnerName] = Eq.fromUniversalEquals
 
-  given Conversion[Username, ProjectOwnerName] = ProjectOwnerName.fromUsername
+    given Conversion[Username, ProjectOwnerName] = ProjectOwnerName.fromUsername
 
-  val isAlphanumeric = "^[a-z][a-z0-9]+$".r
+    val isAlphanumeric = "^[a-z][a-z0-9]+$".r
 
-  /** Create an instance of ProjectOwnerName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a ProjectOwnerName.
-    * @return
-    *   The appropriate instance of ProjectOwnerName.
-    */
-  def apply(source: String): ProjectOwnerName = source
-
-  /** Try to create an instance of ProjectOwnerName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a ProjectOwnerName.
-    * @return
-    *   An option to the successfully converted ProjectOwnerName.
-    */
-  def from(s: String): Option[ProjectOwnerName] = validate(s).toOption
-
-  /** Create an instance of ProjectOwnerName from the given Username type.
-    *
-    * @param username
-    *   An instance of the type Username which will be returned as a ProjectOwnerName.
-    * @return
-    *   The appropriate instance of ProjectOwnerName.
-    */
-  def fromUsername(username: Username): ProjectOwnerName = username.toString
-
-  /** Validate the given string and return either the validated project owner name or a list of errors.
-    *
-    * @param s
-    *   An arbitrary string which should be a project owner name.
-    * @return
-    *   Either a list of errors or the validated project owner name.
-    */
-  def validate(s: String): ValidatedNec[String, ProjectOwnerName] =
-    Option(s).map(_.trim.nonEmpty) match {
-      case Some(true) =>
-        val input = s.trim
-        val miniumLength =
-          if (input.length >= 2)
-            input.validNec
-          else
-            "ProjectOwnerName too short (min. 2 characters)!".invalidNec
-        val maximumLength =
-          if (input.length < 32)
-            input.validNec
-          else
-            "ProjectOwnerName too long (max. 31 characters)!".invalidNec
-        val alphanumeric =
-          if (isAlphanumeric.matches(input))
-            input.validNec
-          else
-            "ProjectOwnerName must be all lowercase alphanumeric characters and start with a character.".invalidNec
-        (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
-          name
-        }
-      case _ => "ProjectOwnerName must not be empty!".invalidNec
-    }
+    /** Create an instance of ProjectOwnerName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a ProjectOwnerName.
+      * @return
+      *   The appropriate instance of ProjectOwnerName.
+      */
+    def apply(source: String): ProjectOwnerName = source
 
-  extension (ownername: ProjectOwnerName) {
+    /** Try to create an instance of ProjectOwnerName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a ProjectOwnerName.
+      * @return
+      *   An option to the successfully converted ProjectOwnerName.
+      */
+    def from(s: String): Option[ProjectOwnerName] = validate(s).toOption
 
-    /** Convert this project owner name into a username.
+    /** Create an instance of ProjectOwnerName from the given Username type.
       *
+      * @param username
+      *   An instance of the type Username which will be returned as a ProjectOwnerName.
       * @return
-      *   A syntactically valid username.
+      *   The appropriate instance of ProjectOwnerName.
       */
-    def toUsername: Username = Username(ownername.toString)
-  }
+    def fromUsername(username: Username): ProjectOwnerName = username.toString
+
+    /** Validate the given string and return either the validated project owner name or a list of errors.
+      *
+      * @param s
+      *   An arbitrary string which should be a project owner name.
+      * @return
+      *   Either a list of errors or the validated project owner name.
+      */
+    def validate(s: String): ValidatedNec[String, ProjectOwnerName] =
+        Option(s).map(_.trim.nonEmpty) match {
+            case Some(true) =>
+                val input = s.trim
+                val miniumLength =
+                    if (input.length >= 2)
+                        input.validNec
+                    else
+                        "ProjectOwnerName too short (min. 2 characters)!".invalidNec
+                val maximumLength =
+                    if (input.length < 32)
+                        input.validNec
+                    else
+                        "ProjectOwnerName too long (max. 31 characters)!".invalidNec
+                val alphanumeric =
+                    if (isAlphanumeric.matches(input))
+                        input.validNec
+                    else
+                        "ProjectOwnerName must be all lowercase alphanumeric characters and start with a character.".invalidNec
+                (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
+                    name
+                }
+            case _ => "ProjectOwnerName must not be empty!".invalidNec
+        }
+
+    extension (ownername: ProjectOwnerName) {
+
+        /** Convert this project owner name into a username.
+          *
+          * @return
+          *   A syntactically valid username.
+          */
+        def toUsername: Username = Username(ownername.toString)
+    }
 }
 
 /** Extractor to retrieve an ProjectOwnerName from a path parameter.
   */
 object ProjectOwnerNamePathParameter {
-  def unapply(str: String): Option[ProjectOwnerName] =
-    Option(str).flatMap { string =>
-      if (string.startsWith("~"))
-        ProjectOwnerName.from(string.drop(1))
-      else
-        None
-    }
+    def unapply(str: String): Option[ProjectOwnerName] =
+        Option(str).flatMap { string =>
+            if (string.startsWith("~"))
+                ProjectOwnerName.from(string.drop(1))
+            else
+                None
+        }
 }
 
 /** Descriptive information about the owner of a project.
@@ -331,7 +331,7 @@
 final case class ProjectOwner(uid: ProjectOwnerId, name: ProjectOwnerName, email: EmailAddress)
 
 object ProjectOwner {
-  given Eq[ProjectOwner] = Eq.fromUniversalEquals
+    given Eq[ProjectOwner] = Eq.fromUniversalEquals
 }
 
 /** A project is the base entity for tracking tickets.
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Slf4jLogHandler.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Slf4jLogHandler.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Slf4jLogHandler.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Slf4jLogHandler.scala	2025-01-13 17:13:25.064470989 +0000
@@ -22,31 +22,31 @@
 import org.slf4j.Logger
 
 object Slf4jLogHandler {
-  private val RedactArguments: Boolean = true // This SHALL only be set to `false` when debugging issues!
+    private val RedactArguments: Boolean = true // This SHALL only be set to `false` when debugging issues!
 
-  private val sqlArgumentsToLogString: List[Any] => String = arguments =>
-    if (RedactArguments)
-      arguments.map(_ => "redacted").mkString(", ")
-    else
-      arguments.mkString(", ")
+    private val sqlArgumentsToLogString: List[Any] => String = arguments =>
+        if (RedactArguments)
+            arguments.map(_ => "redacted").mkString(", ")
+        else
+            arguments.mkString(", ")
 
-  private val sqlQueryToLogString: String => String = _.linesIterator.dropWhile(_.trim.isEmpty).mkString("\n  ")
+    private val sqlQueryToLogString: String => String = _.linesIterator.dropWhile(_.trim.isEmpty).mkString("\n  ")
 
-  /** Create a [[doobie.util.log.LogHandler]] for logging doobie queries and errors. For convenience it is best to
-    * simply return the return value of this method to a given (implicit) instance.
-    *
-    * @param log
-    *   A logger which provides an slf4j interface.
-    * @return
-    *   A log handler as expected by doobie.
-    */
-  def createLogHandler[F[_]: Sync](log: Logger): LogHandler[F] =
-    new LogHandler[F] {
-      def run(logEvent: LogEvent): F[Unit] =
-        Sync[F].delay {
-          logEvent match {
-            case Success(sqlQuery, arguments, label, executionTime, processingTime) =>
-              log.debug(s"""SQL command successful:
+    /** Create a [[doobie.util.log.LogHandler]] for logging doobie queries and errors. For convenience it is best to
+      * simply return the return value of this method to a given (implicit) instance.
+      *
+      * @param log
+      *   A logger which provides an slf4j interface.
+      * @return
+      *   A log handler as expected by doobie.
+      */
+    def createLogHandler[F[_]: Sync](log: Logger): LogHandler[F] =
+        new LogHandler[F] {
+            def run(logEvent: LogEvent): F[Unit] =
+                Sync[F].delay {
+                    logEvent match {
+                        case Success(sqlQuery, arguments, label, executionTime, processingTime) =>
+                            log.debug(s"""SQL command successful:
                            |
                            | ${sqlQueryToLogString(sqlQuery)}
                            |
@@ -57,9 +57,9 @@
                            | processing time: ${processingTime.toMillis} ms
                            | total time     : ${(executionTime + processingTime).toMillis} ms
                            |""".stripMargin)
-            case ProcessingFailure(sqlQuery, arguments, label, executionTime, processingTime, failure) =>
-              log.error(
-                s"""SQL PROCESSING FAILURE:
+                        case ProcessingFailure(sqlQuery, arguments, label, executionTime, processingTime, failure) =>
+                            log.error(
+                                s"""SQL PROCESSING FAILURE:
                    |
                    | ${sqlQueryToLogString(sqlQuery)}
                    |
@@ -70,11 +70,11 @@
                    | processing time: ${processingTime.toMillis} ms
                    | total time     : ${(executionTime + processingTime).toMillis} ms
                    |""".stripMargin,
-                failure
-              )
-            case ExecFailure(sqlQuery, arguments, label, executionTime, failure) =>
-              log.error(
-                s"""SQL EXECUTION FAILURE:
+                                failure
+                            )
+                        case ExecFailure(sqlQuery, arguments, label, executionTime, failure) =>
+                            log.error(
+                                s"""SQL EXECUTION FAILURE:
                    |
                    | ${sqlQueryToLogString(sqlQuery)}
                    |
@@ -83,9 +83,9 @@
                    |
                    | execution time : ${executionTime.toMillis} ms
                    |""".stripMargin,
-                failure
-              )
-          }
+                                failure
+                            )
+                    }
+                }
         }
-    }
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Submitter.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Submitter.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Submitter.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Submitter.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,53 +27,53 @@
   */
 opaque type SubmitterId = UUID
 object SubmitterId {
-  val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
+    val Format = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$".r
 
-  given Eq[SubmitterId] = Eq.fromUniversalEquals
+    given Eq[SubmitterId] = Eq.fromUniversalEquals
 
-  /** Create an instance of SubmitterId from the given UUID type.
-    *
-    * @param source
-    *   An instance of type UUID which will be returned as a SubmitterId.
-    * @return
-    *   The appropriate instance of SubmitterId.
-    */
-  def apply(source: UUID): SubmitterId = source
-
-  /** Try to create an instance of SubmitterId from the given UUID.
-    *
-    * @param source
-    *   A UUID that should fulfil the requirements to be converted into a SubmitterId.
-    * @return
-    *   An option to the successfully converted SubmitterId.
-    */
-  def from(source: UUID): Option[SubmitterId] = Option(source)
-
-  /** Try to create an instance of SubmitterId from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SubmitterId.
-    * @return
-    *   An option to the successfully converted SubmitterId.
-    */
-  def fromString(source: String): Either[String, SubmitterId] =
-    Option(source)
-      .filter(s => Format.matches(s))
-      .flatMap { uuidString =>
-        Either.catchNonFatal(UUID.fromString(uuidString)).toOption
-      }
-      .toRight("Illegal value for SubmitterId!")
-
-  /** Generate a new random user id.
-    *
-    * @return
-    *   A user id which is pseudo randomly generated.
-    */
-  def randomSubmitterId: SubmitterId = UUID.randomUUID
-
-  extension (uid: SubmitterId) {
-    def toUUID: UUID = uid
-  }
+    /** Create an instance of SubmitterId from the given UUID type.
+      *
+      * @param source
+      *   An instance of type UUID which will be returned as a SubmitterId.
+      * @return
+      *   The appropriate instance of SubmitterId.
+      */
+    def apply(source: UUID): SubmitterId = source
+
+    /** Try to create an instance of SubmitterId from the given UUID.
+      *
+      * @param source
+      *   A UUID that should fulfil the requirements to be converted into a SubmitterId.
+      * @return
+      *   An option to the successfully converted SubmitterId.
+      */
+    def from(source: UUID): Option[SubmitterId] = Option(source)
+
+    /** Try to create an instance of SubmitterId from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SubmitterId.
+      * @return
+      *   An option to the successfully converted SubmitterId.
+      */
+    def fromString(source: String): Either[String, SubmitterId] =
+        Option(source)
+            .filter(s => Format.matches(s))
+            .flatMap { uuidString =>
+                Either.catchNonFatal(UUID.fromString(uuidString)).toOption
+            }
+            .toRight("Illegal value for SubmitterId!")
+
+    /** Generate a new random user id.
+      *
+      * @return
+      *   A user id which is pseudo randomly generated.
+      */
+    def randomSubmitterId: SubmitterId = UUID.randomUUID
+
+    extension (uid: SubmitterId) {
+        def toUUID: UUID = uid
+    }
 }
 
 /** A submitter name has to obey several restrictions which are similiar to the ones found for Unix usernames. It must
@@ -82,71 +82,71 @@
   */
 opaque type SubmitterName = String
 object SubmitterName {
-  given Eq[SubmitterName] = Eq.fromUniversalEquals
+    given Eq[SubmitterName] = Eq.fromUniversalEquals
 
-  val isAlphanumeric = "^[a-z][a-z0-9]+$".r
+    val isAlphanumeric = "^[a-z][a-z0-9]+$".r
 
-  /** Create an instance of SubmitterName from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a SubmitterName.
-    * @return
-    *   The appropriate instance of SubmitterName.
-    */
-  def apply(source: String): SubmitterName = source
-
-  /** Try to create an instance of SubmitterName from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a SubmitterName.
-    * @return
-    *   An option to the successfully converted SubmitterName.
-    */
-  def from(s: String): Option[SubmitterName] = validate(s).toOption
-
-  /** Validate the given string and return either the validated username or a list of errors.
-    *
-    * @param s
-    *   An arbitrary string which should be a username.
-    * @return
-    *   Either a list of errors or the validated username.
-    */
-  def validate(s: String): ValidatedNec[String, SubmitterName] =
-    Option(s).map(_.trim.nonEmpty) match {
-      case Some(true) =>
-        val input = s.trim
-        val miniumLength =
-          if (input.length >= 2)
-            input.validNec
-          else
-            "SubmitterName too short (min. 2 characters)!".invalidNec
-        val maximumLength =
-          if (input.length < 32)
-            input.validNec
-          else
-            "SubmitterName too long (max. 31 characters)!".invalidNec
-        val alphanumeric =
-          if (isAlphanumeric.matches(input))
-            input.validNec
-          else
-            "SubmitterName must be all lowercase alphanumeric characters and start with a character.".invalidNec
-        (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
-          name
+    /** Create an instance of SubmitterName from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a SubmitterName.
+      * @return
+      *   The appropriate instance of SubmitterName.
+      */
+    def apply(source: String): SubmitterName = source
+
+    /** Try to create an instance of SubmitterName from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a SubmitterName.
+      * @return
+      *   An option to the successfully converted SubmitterName.
+      */
+    def from(s: String): Option[SubmitterName] = validate(s).toOption
+
+    /** Validate the given string and return either the validated username or a list of errors.
+      *
+      * @param s
+      *   An arbitrary string which should be a username.
+      * @return
+      *   Either a list of errors or the validated username.
+      */
+    def validate(s: String): ValidatedNec[String, SubmitterName] =
+        Option(s).map(_.trim.nonEmpty) match {
+            case Some(true) =>
+                val input = s.trim
+                val miniumLength =
+                    if (input.length >= 2)
+                        input.validNec
+                    else
+                        "SubmitterName too short (min. 2 characters)!".invalidNec
+                val maximumLength =
+                    if (input.length < 32)
+                        input.validNec
+                    else
+                        "SubmitterName too long (max. 31 characters)!".invalidNec
+                val alphanumeric =
+                    if (isAlphanumeric.matches(input))
+                        input.validNec
+                    else
+                        "SubmitterName must be all lowercase alphanumeric characters and start with a character.".invalidNec
+                (miniumLength, maximumLength, alphanumeric).mapN { case (_, _, name) =>
+                    name
+                }
+            case _ => "SubmitterName must not be empty!".invalidNec
         }
-      case _ => "SubmitterName must not be empty!".invalidNec
-    }
 }
 
 /** Extractor to retrieve an SubmitterName from a path parameter.
   */
 object SubmitterNamePathParameter {
-  def unapply(str: String): Option[SubmitterName] =
-    Option(str).flatMap { string =>
-      if (string.startsWith("~"))
-        SubmitterName.from(string.drop(1))
-      else
-        None
-    }
+    def unapply(str: String): Option[SubmitterName] =
+        Option(str).flatMap { string =>
+            if (string.startsWith("~"))
+                SubmitterName.from(string.drop(1))
+            else
+                None
+        }
 }
 
 /** The submitter for a ticket i.e. the person supposed to be working on it.
@@ -159,5 +159,5 @@
 final case class Submitter(id: SubmitterId, name: SubmitterName)
 
 object Submitter {
-  given Eq[Submitter] = Eq.fromUniversalEquals
+    given Eq[Submitter] = Eq.fromUniversalEquals
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketRepository.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketRepository.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketRepository.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketRepository.scala	2025-01-13 17:13:25.064470989 +0000
@@ -26,182 +26,182 @@
   */
 abstract class TicketRepository[F[_]] {
 
-  /** Add the given assignee to the ticket of the given repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @param assignee
-    *   The assignee to be added to the ticket.
-    * @return
-    *   The number of affected database rows.
-    */
-  def addAssignee(projectId: ProjectId)(ticketNumber: TicketNumber)(assignee: Assignee): F[Int]
-
-  /** Add the given label to the ticket of the given repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @param label
-    *   The label to be added to the ticket.
-    * @return
-    *   The number of affected database rows.
-    */
-  def addLabel(projectId: ProjectId)(ticketNumber: TicketNumber)(label: Label): F[Int]
-
-  /** Add the given milestone to the ticket of the given repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @param milestone
-    *   The milestone to be added to the ticket.
-    * @return
-    *   The number of affected database rows.
-    */
-  def addMilestone(projectId: ProjectId)(ticketNumber: TicketNumber)(milestone: Milestone): F[Int]
-
-  /** Return all tickets associated with the given repository.
-    *
-    * @param filter
-    *   A ticket filter containing possible values which will be used to filter the list of tickets.
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project for which all tickets shall be returned.
-    * @return
-    *   A stream of tickets associated with the vcs repository which may be empty.
-    */
-  def allTickets(filter: Option[TicketFilter])(projectId: ProjectId): Stream[F, Ticket]
-
-  /** Create a database entry for the given ticket definition within the scope of the repository with the given id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticket
-    *   The ticket definition that shall be written to the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createTicket(projectId: ProjectId)(ticket: Ticket): F[Int]
-
-  /** Delete the ticket of the repository with the given id from the database.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticket
-    *   The ticket definition that shall be deleted from the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteTicket(projectId: ProjectId)(ticket: Ticket): F[Int]
-
-  /** Find the ticket with the given number of the repository with the given id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @return
-    *   An option to the found ticket.
-    */
-  def findTicket(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[Ticket]]
-
-  /** Find the ticket with the given number of the project with the given id and return the internal unique id of the
-    * ticket.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @return
-    *   An option to the found ticket.
-    */
-  def findTicketId(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[TicketId]]
-
-  /** Load all assignees that are assigned to the ticket with the given number and repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @return
-    *   A stream of assigness that may be empty.
-    */
-  def loadAssignees(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Assignee]
-
-  /** Load all labels that are attached to the ticket with the given number and repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @return
-    *   A stream of labels that may be empty.
-    */
-  def loadLabels(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Label]
-
-  /** Load all milestones that are attached to the ticket with the given number and repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @return
-    *   A stream of milestones that may be empty.
-    */
-  def loadMilestones(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Milestone]
-
-  /** Remove the given assignee from the ticket of the given repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @param assignee
-    *   The assignee to be removed from the ticket.
-    * @return
-    *   The number of affected database rows.
-    */
-  def removeAssignee(projectId: ProjectId)(ticket: Ticket)(assignee: Assignee): F[Int]
-
-  /** Remove the given label from the ticket of the given repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @param label
-    *   The label to be removed from the ticket.
-    * @return
-    *   The number of affected database rows.
-    */
-  def removeLabel(projectId: ProjectId)(ticket: Ticket)(label: Label): F[Int]
-
-  /** Remove the given milestone from the ticket of the given repository id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticketNumber
-    *   The unique identifier of a ticket within the project scope is its number.
-    * @param milestone
-    *   The milestone to be removed from the ticket.
-    * @return
-    *   The number of affected database rows.
-    */
-  def removeMilestone(projectId: ProjectId)(ticket: Ticket)(milestone: Milestone): F[Int]
-
-  /** Update the database entry for the given ticket within the scope of the repository with the given id.
-    *
-    * @param projectId
-    *   The unique internal ID of a ticket tracking project.
-    * @param ticket
-    *   The ticket definition that shall be updated within the database.
-    * @return
-    *   The number of affected database rows.
-    */
-  def updateTicket(projectId: ProjectId)(ticket: Ticket): F[Int]
+    /** Add the given assignee to the ticket of the given repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @param assignee
+      *   The assignee to be added to the ticket.
+      * @return
+      *   The number of affected database rows.
+      */
+    def addAssignee(projectId: ProjectId)(ticketNumber: TicketNumber)(assignee: Assignee): F[Int]
+
+    /** Add the given label to the ticket of the given repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @param label
+      *   The label to be added to the ticket.
+      * @return
+      *   The number of affected database rows.
+      */
+    def addLabel(projectId: ProjectId)(ticketNumber: TicketNumber)(label: Label): F[Int]
+
+    /** Add the given milestone to the ticket of the given repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @param milestone
+      *   The milestone to be added to the ticket.
+      * @return
+      *   The number of affected database rows.
+      */
+    def addMilestone(projectId: ProjectId)(ticketNumber: TicketNumber)(milestone: Milestone): F[Int]
+
+    /** Return all tickets associated with the given repository.
+      *
+      * @param filter
+      *   A ticket filter containing possible values which will be used to filter the list of tickets.
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project for which all tickets shall be returned.
+      * @return
+      *   A stream of tickets associated with the vcs repository which may be empty.
+      */
+    def allTickets(filter: Option[TicketFilter])(projectId: ProjectId): Stream[F, Ticket]
+
+    /** Create a database entry for the given ticket definition within the scope of the repository with the given id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticket
+      *   The ticket definition that shall be written to the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createTicket(projectId: ProjectId)(ticket: Ticket): F[Int]
+
+    /** Delete the ticket of the repository with the given id from the database.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticket
+      *   The ticket definition that shall be deleted from the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteTicket(projectId: ProjectId)(ticket: Ticket): F[Int]
+
+    /** Find the ticket with the given number of the repository with the given id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @return
+      *   An option to the found ticket.
+      */
+    def findTicket(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[Ticket]]
+
+    /** Find the ticket with the given number of the project with the given id and return the internal unique id of the
+      * ticket.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @return
+      *   An option to the found ticket.
+      */
+    def findTicketId(projectId: ProjectId)(ticketNumber: TicketNumber): F[Option[TicketId]]
+
+    /** Load all assignees that are assigned to the ticket with the given number and repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @return
+      *   A stream of assigness that may be empty.
+      */
+    def loadAssignees(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Assignee]
+
+    /** Load all labels that are attached to the ticket with the given number and repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @return
+      *   A stream of labels that may be empty.
+      */
+    def loadLabels(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Label]
+
+    /** Load all milestones that are attached to the ticket with the given number and repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @return
+      *   A stream of milestones that may be empty.
+      */
+    def loadMilestones(projectId: ProjectId)(ticketNumber: TicketNumber): Stream[F, Milestone]
+
+    /** Remove the given assignee from the ticket of the given repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @param assignee
+      *   The assignee to be removed from the ticket.
+      * @return
+      *   The number of affected database rows.
+      */
+    def removeAssignee(projectId: ProjectId)(ticket: Ticket)(assignee: Assignee): F[Int]
+
+    /** Remove the given label from the ticket of the given repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @param label
+      *   The label to be removed from the ticket.
+      * @return
+      *   The number of affected database rows.
+      */
+    def removeLabel(projectId: ProjectId)(ticket: Ticket)(label: Label): F[Int]
+
+    /** Remove the given milestone from the ticket of the given repository id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticketNumber
+      *   The unique identifier of a ticket within the project scope is its number.
+      * @param milestone
+      *   The milestone to be removed from the ticket.
+      * @return
+      *   The number of affected database rows.
+      */
+    def removeMilestone(projectId: ProjectId)(ticket: Ticket)(milestone: Milestone): F[Int]
+
+    /** Update the database entry for the given ticket within the scope of the repository with the given id.
+      *
+      * @param projectId
+      *   The unique internal ID of a ticket tracking project.
+      * @param ticket
+      *   The ticket definition that shall be updated within the database.
+      * @return
+      *   The number of affected database rows.
+      */
+    def updateTicket(projectId: ProjectId)(ticket: Ticket): F[Int]
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/Ticket.scala	2025-01-13 17:13:25.064470989 +0000
@@ -32,111 +32,111 @@
 opaque type TicketContent = String
 object TicketContent {
 
-  /** Create an instance of TicketContent from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a TicketContent.
-    * @return
-    *   The appropriate instance of TicketContent.
-    */
-  def apply(source: String): TicketContent = source
-
-  /** Try to create an instance of TicketContent from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a TicketContent.
-    * @return
-    *   An option to the successfully converted TicketContent.
-    */
-  def from(source: String): Option[TicketContent] = Option(source).filter(_.nonEmpty)
+    /** Create an instance of TicketContent from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a TicketContent.
+      * @return
+      *   The appropriate instance of TicketContent.
+      */
+    def apply(source: String): TicketContent = source
+
+    /** Try to create an instance of TicketContent from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a TicketContent.
+      * @return
+      *   An option to the successfully converted TicketContent.
+      */
+    def from(source: String): Option[TicketContent] = Option(source).filter(_.nonEmpty)
 
 }
 
 opaque type TicketId = Long
 object TicketId {
-  given Eq[TicketId] = Eq.fromUniversalEquals
+    given Eq[TicketId] = Eq.fromUniversalEquals
+
+    val Format: Regex = "^-?\\d+$".r
 
-  val Format: Regex = "^-?\\d+$".r
+    /** Create an instance of TicketId from the given Long type.
+      *
+      * @param source
+      *   An instance of type Long which will be returned as a TicketId.
+      * @return
+      *   The appropriate instance of TicketId.
+      */
+    def apply(source: Long): TicketId = source
 
-  /** Create an instance of TicketId from the given Long type.
-    *
-    * @param source
-    *   An instance of type Long which will be returned as a TicketId.
-    * @return
-    *   The appropriate instance of TicketId.
-    */
-  def apply(source: Long): TicketId = source
-
-  /** Try to create an instance of TicketId from the given Long.
-    *
-    * @param source
-    *   A Long that should fulfil the requirements to be converted into a TicketId.
-    * @return
-    *   An option to the successfully converted TicketId.
-    */
-  def from(source: Long): Option[TicketId] = Option(source)
-
-  /** Try to create an instance of TicketId from the given String.
-    *
-    * @param source
-    *   A string that should fulfil the requirements to be converted into a TicketId.
-    * @return
-    *   An option to the successfully converted TicketId.
-    */
-  def fromString(source: String): Option[TicketId] = Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
-
-  extension (id: TicketId) {
-    def toLong: Long = id
-  }
+    /** Try to create an instance of TicketId from the given Long.
+      *
+      * @param source
+      *   A Long that should fulfil the requirements to be converted into a TicketId.
+      * @return
+      *   An option to the successfully converted TicketId.
+      */
+    def from(source: Long): Option[TicketId] = Option(source)
+
+    /** Try to create an instance of TicketId from the given String.
+      *
+      * @param source
+      *   A string that should fulfil the requirements to be converted into a TicketId.
+      * @return
+      *   An option to the successfully converted TicketId.
+      */
+    def fromString(source: String): Option[TicketId] = Option(source).filter(Format.matches).map(_.toLong).flatMap(from)
+
+    extension (id: TicketId) {
+        def toLong: Long = id
+    }
 }
 
 /** A ticket number maps to an integer beneath and has the requirement to be greater than zero.
   */
 opaque type TicketNumber = Int
 object TicketNumber {
-  given Eq[TicketNumber]       = Eq.fromUniversalEquals
-  given Ordering[TicketNumber] = (x: TicketNumber, y: TicketNumber) => x.compareTo(y)
-  given Order[TicketNumber]    = Order.fromOrdering
-
-  val Format: Regex = "^-?\\d+$".r
-
-  /** Create an instance of TicketNumber from the given Int type.
-    *
-    * @param source
-    *   An instance of type Int which will be returned as a TicketNumber.
-    * @return
-    *   The appropriate instance of TicketNumber.
-    */
-  def apply(source: Int): TicketNumber = source
-
-  /** Try to create an instance of TicketNumber from the given Int.
-    *
-    * @param source
-    *   A Int that should fulfil the requirements to be converted into a TicketNumber.
-    * @return
-    *   An option to the successfully converted TicketNumber.
-    */
-  def from(source: Int): Option[TicketNumber] = Option(source).filter(_ > 0)
-
-  /** Try to create an instance of TicketNumber from the given String.
-    *
-    * @param source
-    *   A string that should fulfil the requirements to be converted into a TicketNumber.
-    * @return
-    *   An option to the successfully converted TicketNumber.
-    */
-  def fromString(source: String): Option[TicketNumber] =
-    Option(source).filter(Format.matches).map(_.toInt).flatMap(from)
-
-  extension (number: TicketNumber) {
-    def toInt: Int = number.toInt
-  }
+    given Eq[TicketNumber]       = Eq.fromUniversalEquals
+    given Ordering[TicketNumber] = (x: TicketNumber, y: TicketNumber) => x.compareTo(y)
+    given Order[TicketNumber]    = Order.fromOrdering
+
+    val Format: Regex = "^-?\\d+$".r
+
+    /** Create an instance of TicketNumber from the given Int type.
+      *
+      * @param source
+      *   An instance of type Int which will be returned as a TicketNumber.
+      * @return
+      *   The appropriate instance of TicketNumber.
+      */
+    def apply(source: Int): TicketNumber = source
+
+    /** Try to create an instance of TicketNumber from the given Int.
+      *
+      * @param source
+      *   A Int that should fulfil the requirements to be converted into a TicketNumber.
+      * @return
+      *   An option to the successfully converted TicketNumber.
+      */
+    def from(source: Int): Option[TicketNumber] = Option(source).filter(_ > 0)
+
+    /** Try to create an instance of TicketNumber from the given String.
+      *
+      * @param source
+      *   A string that should fulfil the requirements to be converted into a TicketNumber.
+      * @return
+      *   An option to the successfully converted TicketNumber.
+      */
+    def fromString(source: String): Option[TicketNumber] =
+        Option(source).filter(Format.matches).map(_.toInt).flatMap(from)
+
+    extension (number: TicketNumber) {
+        def toInt: Int = number.toInt
+    }
 }
 
 /** Extractor to retrieve a TicketNumber from a path parameter.
   */
 object TicketNumberPathParameter {
-  def unapply(str: String): Option[TicketNumber] = Option(str).flatMap(TicketNumber.fromString)
+    def unapply(str: String): Option[TicketNumber] = Option(str).flatMap(TicketNumber.fromString)
 }
 
 /** Possible states of a ticket which shall model the life cycle from ticket creation until it is closed. To keep things
@@ -144,92 +144,93 @@
   */
 enum TicketStatus {
 
-  /** The ticket is considered to be a valid ticket / ready to be worked on e.g. a bug has been confirmed to be present.
-    */
-  case Confirmed
-
-  /** The ticket is being worked on i.e. it is in progress.
-    */
-  case InProgress
-
-  /** The ticket is pending and cannot be processed right now. It may be moved to another state or closed depending on
-    * the circumstances. This could be used to model the "blocked" state of Kanban.
-    */
-  case Pending
-
-  /** The ticket is resolved (i.e. closed) and considered done.
-    */
-  case Resolved
-
-  /** The ticket was reported and is open for further investigation. This might map to what is often called "Backlog"
-    * nowadays.
-    */
-  case Submitted
+    /** The ticket is considered to be a valid ticket / ready to be worked on e.g. a bug has been confirmed to be
+      * present.
+      */
+    case Confirmed
+
+    /** The ticket is being worked on i.e. it is in progress.
+      */
+    case InProgress
+
+    /** The ticket is pending and cannot be processed right now. It may be moved to another state or closed depending on
+      * the circumstances. This could be used to model the "blocked" state of Kanban.
+      */
+    case Pending
+
+    /** The ticket is resolved (i.e. closed) and considered done.
+      */
+    case Resolved
+
+    /** The ticket was reported and is open for further investigation. This might map to what is often called "Backlog"
+      * nowadays.
+      */
+    case Submitted
 }
 
 object TicketStatus {
-  given Eq[TicketStatus] = Eq.fromUniversalEquals
+    given Eq[TicketStatus] = Eq.fromUniversalEquals
 
-  /** Try to parse a ticket status instance from the given string without throwin an exception like `valueOf`.
-    *
-    * @param source
-    *   A string that should contain the name of a ticket status.
-    * @return
-    *   An option to the successfully deserialised instance.
-    */
-  def fromString(source: String): Option[TicketStatus] =
-    TicketStatus.values.map(_.toString).find(_ === source).map(TicketStatus.valueOf)
+    /** Try to parse a ticket status instance from the given string without throwin an exception like `valueOf`.
+      *
+      * @param source
+      *   A string that should contain the name of a ticket status.
+      * @return
+      *   An option to the successfully deserialised instance.
+      */
+    def fromString(source: String): Option[TicketStatus] =
+        TicketStatus.values.map(_.toString).find(_ === source).map(TicketStatus.valueOf)
 }
 
 /** Possible types of "resolved states" of a ticket.
   */
 enum TicketResolution {
 
-  /** The behaviour / scenario described in the ticket is caused by the design of the application and not considered to
-    * be a bug.
-    */
-  case ByDesign
-
-  /** The ticket is finally closed and considered done.
-    *
-    * This state can be used to model a review process e.g. a developer can move a ticket to `Fixed` and reviewer and
-    * tester can later move the ticket to `Closed`.
-    */
-  case Closed
-
-  /** The ticket is a duplicate of an already existing one.
-    */
-  case Duplicate
-
-  /** The bug described in the ticket was fixed.
-    */
-  case Fixed
-
-  /** The feature described in the ticket was implemented.
-    */
-  case Implemented
-
-  /** The ticket is considered to be invalid.
-    */
-  case Invalid
-
-  /** The issue described in the ticket will not be fixed.
-    */
-  case WontFix
+    /** The behaviour / scenario described in the ticket is caused by the design of the application and not considered
+      * to be a bug.
+      */
+    case ByDesign
+
+    /** The ticket is finally closed and considered done.
+      *
+      * This state can be used to model a review process e.g. a developer can move a ticket to `Fixed` and reviewer and
+      * tester can later move the ticket to `Closed`.
+      */
+    case Closed
+
+    /** The ticket is a duplicate of an already existing one.
+      */
+    case Duplicate
+
+    /** The bug described in the ticket was fixed.
+      */
+    case Fixed
+
+    /** The feature described in the ticket was implemented.
+      */
+    case Implemented
+
+    /** The ticket is considered to be invalid.
+      */
+    case Invalid
+
+    /** The issue described in the ticket will not be fixed.
+      */
+    case WontFix
 }
 
 object TicketResolution {
-  given Eq[TicketResolution] = Eq.fromUniversalEquals
+    given Eq[TicketResolution] = Eq.fromUniversalEquals
 
-  /** Try to parse a ticket resolution instance from the given string without throwin an exception like `valueOf`.
-    *
-    * @param source
-    *   A string that should contain the name of a ticket resolution.
-    * @return
-    *   An option to the successfully deserialised instance.
-    */
-  def fromString(source: String): Option[TicketResolution] =
-    TicketResolution.values.map(_.toString).find(_ === source).map(TicketResolution.valueOf)
+    /** Try to parse a ticket resolution instance from the given string without throwin an exception like `valueOf`.
+      *
+      * @param source
+      *   A string that should contain the name of a ticket resolution.
+      * @return
+      *   An option to the successfully deserialised instance.
+      */
+    def fromString(source: String): Option[TicketResolution] =
+        TicketResolution.values.map(_.toString).find(_ === source).map(TicketResolution.valueOf)
 }
 
 /** A concise and short description of the ticket which should not exceed 80 characters.
@@ -237,26 +238,26 @@
 opaque type TicketTitle = String
 object TicketTitle {
 
-  val MaxLength: Int = 72
+    val MaxLength: Int = 72
+
+    /** Create an instance of TicketTitle from the given String type.
+      *
+      * @param source
+      *   An instance of type String which will be returned as a TicketTitle.
+      * @return
+      *   The appropriate instance of TicketTitle.
+      */
+    def apply(source: String): TicketTitle = source
 
-  /** Create an instance of TicketTitle from the given String type.
-    *
-    * @param source
-    *   An instance of type String which will be returned as a TicketTitle.
-    * @return
-    *   The appropriate instance of TicketTitle.
-    */
-  def apply(source: String): TicketTitle = source
-
-  /** Try to create an instance of TicketTitle from the given String.
-    *
-    * @param source
-    *   A String that should fulfil the requirements to be converted into a TicketTitle.
-    * @return
-    *   An option to the successfully converted TicketTitle.
-    */
-  def from(source: String): Option[TicketTitle] =
-    Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
+    /** Try to create an instance of TicketTitle from the given String.
+      *
+      * @param source
+      *   A String that should fulfil the requirements to be converted into a TicketTitle.
+      * @return
+      *   An option to the successfully converted TicketTitle.
+      */
+    def from(source: String): Option[TicketTitle] =
+        Option(source).filter(string => string.nonEmpty && string.length <= MaxLength)
 }
 
 /** An ticket used to describe a problem or a task (e.g. implement a concrete feature) within the scope of a project.
@@ -309,117 +310,117 @@
 )
 
 object TicketFilter {
-  given QueryParamDecoder[TicketFilter] = QueryParamDecoder[String].map(TicketFilter.fromQueryParameter)
-  given QueryParamEncoder[TicketFilter] = QueryParamEncoder[String].contramap(_.toQueryParameter)
+    given QueryParamDecoder[TicketFilter] = QueryParamDecoder[String].map(TicketFilter.fromQueryParameter)
+    given QueryParamEncoder[TicketFilter] = QueryParamEncoder[String].contramap(_.toQueryParameter)
+
+    /** Decode an optional possibly existing query parameter into a `TicketFilter`.
+      *
+      * Usage: `case GET -> Root / "..." :? OptionalUrlParamter(maybeFilter) => ...`
+      */
+    object OptionalUrlParameter extends OptionalQueryParamDecoderMatcher[TicketFilter]("q")
+
+    // Only "open" tickets.
+    val OpenTicketsOnly = TicketFilter(
+        number = Nil,
+        status = TicketStatus.values.filterNot(_ === TicketStatus.Resolved).toList,
+        resolution = Nil,
+        submitter = Nil
+    )
+    // Only resolved (closed) tickets.
+    val ResolvedTicketsOnly =
+        TicketFilter(number = Nil, status = List(TicketStatus.Resolved), resolution = Nil, submitter = Nil)
+
+    /** Parse the given query string which must contain a serialised ticket filter instance and return a ticket filter
+      * with the successfully parsed filters.
+      *
+      * @param queryString
+      *   A query string parameter passed via an URL.
+      * @return
+      *   A ticket filter instance which may be empty.
+      */
+    def fromQueryParameter(queryString: String): TicketFilter = {
+        val number =
+            if (queryString.contains("numbers: "))
+                queryString
+                    .drop(queryString.indexOf("numbers: ") + 9)
+                    .takeWhile(char => !char.isWhitespace)
+                    .split(",")
+                    .map(TicketNumber.fromString)
+                    .flatten
+                    .toList
+            else
+                Nil
+        val status =
+            if (queryString.contains("status: "))
+                queryString
+                    .drop(queryString.indexOf("status: ") + 8)
+                    .takeWhile(char => !char.isWhitespace)
+                    .split(",")
+                    .map(TicketStatus.fromString)
+                    .flatten
+                    .toList
+            else
+                Nil
+        val resolution =
+            if (queryString.contains("resolution: "))
+                queryString
+                    .drop(queryString.indexOf("resolution: ") + 12)
+                    .takeWhile(char => !char.isWhitespace)
+                    .split(",")
+                    .map(TicketResolution.fromString)
+                    .flatten
+                    .toList
+            else
+                Nil
+        val submitter =
+            if (queryString.contains("by: "))
+                queryString
+                    .drop(queryString.indexOf("by: ") + 4)
+                    .takeWhile(char => !char.isWhitespace)
+                    .split(",")
+                    .map(SubmitterName.from)
+                    .flatten
+                    .toList
+            else
+                Nil
+        TicketFilter(number, status, resolution, submitter)
+    }
+
+    extension (filter: TicketFilter) {
 
-  /** Decode an optional possibly existing query parameter into a `TicketFilter`.
-    *
-    * Usage: `case GET -> Root / "..." :? OptionalUrlParamter(maybeFilter) => ...`
-    */
-  object OptionalUrlParameter extends OptionalQueryParamDecoderMatcher[TicketFilter]("q")
-
-  // Only "open" tickets.
-  val OpenTicketsOnly = TicketFilter(
-    number = Nil,
-    status = TicketStatus.values.filterNot(_ === TicketStatus.Resolved).toList,
-    resolution = Nil,
-    submitter = Nil
-  )
-  // Only resolved (closed) tickets.
-  val ResolvedTicketsOnly =
-    TicketFilter(number = Nil, status = List(TicketStatus.Resolved), resolution = Nil, submitter = Nil)
-
-  /** Parse the given query string which must contain a serialised ticket filter instance and return a ticket filter
-    * with the successfully parsed filters.
-    *
-    * @param queryString
-    *   A query string parameter passed via an URL.
-    * @return
-    *   A ticket filter instance which may be empty.
-    */
-  def fromQueryParameter(queryString: String): TicketFilter = {
-    val number =
-      if (queryString.contains("numbers: "))
-        queryString
-          .drop(queryString.indexOf("numbers: ") + 9)
-          .takeWhile(char => !char.isWhitespace)
-          .split(",")
-          .map(TicketNumber.fromString)
-          .flatten
-          .toList
-      else
-        Nil
-    val status =
-      if (queryString.contains("status: "))
-        queryString
-          .drop(queryString.indexOf("status: ") + 8)
-          .takeWhile(char => !char.isWhitespace)
-          .split(",")
-          .map(TicketStatus.fromString)
-          .flatten
-          .toList
-      else
-        Nil
-    val resolution =
-      if (queryString.contains("resolution: "))
-        queryString
-          .drop(queryString.indexOf("resolution: ") + 12)
-          .takeWhile(char => !char.isWhitespace)
-          .split(",")
-          .map(TicketResolution.fromString)
-          .flatten
-          .toList
-      else
-        Nil
-    val submitter =
-      if (queryString.contains("by: "))
-        queryString
-          .drop(queryString.indexOf("by: ") + 4)
-          .takeWhile(char => !char.isWhitespace)
-          .split(",")
-          .map(SubmitterName.from)
-          .flatten
-          .toList
-      else
-        Nil
-    TicketFilter(number, status, resolution, submitter)
-  }
-
-  extension (filter: TicketFilter) {
-
-    /** Convert this ticket filter instance into a query string representation that can be passed as query parameter in
-      * a URL and parsed back again.
-      *
-      * @return
-      *   A string containing a serialised form of the ticket filter that can be used as a URL query parameter.
-      */
-    def toQueryParameter: String = {
-      val numbers =
-        if (filter.number.isEmpty)
-          None
-        else
-          filter.number.map(_.toString).mkString(",").some
-      val status =
-        if (filter.status.isEmpty)
-          None
-        else
-          filter.status.map(_.toString).mkString(",").some
-      val resolution =
-        if (filter.resolution.isEmpty)
-          None
-        else
-          filter.resolution.map(_.toString).mkString(",").some
-      val submitter =
-        if (filter.submitter.isEmpty)
-          None
-        else
-          filter.submitter.map(_.toString).mkString(",").some
-      List(
-        numbers.map(string => s"numbers: $string"),
-        status.map(string => s"status: $string"),
-        resolution.map(string => s"resolution: $string"),
-        submitter.map(string => s"by: $string")
-      ).flatten.mkString(" ")
+        /** Convert this ticket filter instance into a query string representation that can be passed as query parameter
+          * in a URL and parsed back again.
+          *
+          * @return
+          *   A string containing a serialised form of the ticket filter that can be used as a URL query parameter.
+          */
+        def toQueryParameter: String = {
+            val numbers =
+                if (filter.number.isEmpty)
+                    None
+                else
+                    filter.number.map(_.toString).mkString(",").some
+            val status =
+                if (filter.status.isEmpty)
+                    None
+                else
+                    filter.status.map(_.toString).mkString(",").some
+            val resolution =
+                if (filter.resolution.isEmpty)
+                    None
+                else
+                    filter.resolution.map(_.toString).mkString(",").some
+            val submitter =
+                if (filter.submitter.isEmpty)
+                    None
+                else
+                    filter.submitter.map(_.toString).mkString(",").some
+            List(
+                numbers.map(string => s"numbers: $string"),
+                status.map(string => s"status: $string"),
+                resolution.map(string => s"resolution: $string"),
+                submitter.map(string => s"by: $string")
+            ).flatten.mkString(" ")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketServiceApi.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketServiceApi.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketServiceApi.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketServiceApi.scala	2025-01-13 17:13:25.064470989 +0000
@@ -27,22 +27,22 @@
   */
 abstract class TicketServiceApi[F[_]] {
 
-  /** Create a user in the ticket service or update an existing one if an account with the unique id already exists.
-    *
-    * @param user
-    *   The user account that shall be created.
-    * @return
-    *   The number of affected database rows.
-    */
-  def createOrUpdateUser(user: TicketsUser): F[Int]
+    /** Create a user in the ticket service or update an existing one if an account with the unique id already exists.
+      *
+      * @param user
+      *   The user account that shall be created.
+      * @return
+      *   The number of affected database rows.
+      */
+    def createOrUpdateUser(user: TicketsUser): F[Int]
 
-  /** Delete the given user from the ticket service.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   The number of affected database rows.
-    */
-  def deleteUser(uid: UserId): F[Int]
+    /** Delete the given user from the ticket service.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   The number of affected database rows.
+      */
+    def deleteUser(uid: UserId): F[Int]
 
 }
diff -rN -u old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketsUser.scala new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketsUser.scala
--- old-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketsUser.scala	2025-01-13 17:13:25.040470955 +0000
+++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/TicketsUser.scala	2025-01-13 17:13:25.064470989 +0000
@@ -37,5 +37,5 @@
 final case class TicketsUser(uid: UserId, name: Username, email: EmailAddress, language: Option[LanguageCode])
 
 object TicketsUser {
-  given Eq[TicketsUser] = Eq.fromUniversalEquals
+    given Eq[TicketsUser] = Eq.fromUniversalEquals
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/TestTags.scala new-smederee/modules/tickets/src/test/scala/de/smederee/TestTags.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/TestTags.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/TestTags.scala	2025-01-13 17:13:25.068470995 +0000
@@ -21,5 +21,5 @@
   * connection.
   */
 object TestTags {
-  val NeedsDatabase = new munit.Tag("NeedsDatabase")
+    val NeedsDatabase = new munit.Tag("NeedsDatabase")
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/BaseSpec.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/BaseSpec.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/BaseSpec.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/BaseSpec.scala	2025-01-13 17:13:25.068470995 +0000
@@ -42,324 +42,328 @@
   * configured **will be deleted**!
   */
 abstract class BaseSpec extends CatsEffectSuite {
-  protected final val configuration: SmedereeTicketsConfiguration =
-    ConfigSource
-      .fromConfig(ConfigFactory.load(getClass.getClassLoader))
-      .at(SmedereeTicketsConfiguration.location)
-      .loadOrThrow[SmedereeTicketsConfiguration]
-
-  protected final val flyway: Flyway =
-    DatabaseMigrator
-      .configureFlyway(configuration.database.url, configuration.database.user, configuration.database.pass)
-      .cleanDisabled(false)
-      .load()
-
-  /** Connect to the DBMS using the generic "template1" database which should always be present.
-    *
-    * @param dbConfig
-    *   The database configuration.
-    * @return
-    *   The connection to the database ("template1").
-    */
-  private def connect(dbConfig: DatabaseConfig): IO[java.sql.Connection] =
-    for {
-      _        <- IO(Class.forName(dbConfig.driver))
-      database <- IO(dbConfig.url.split("/").reverse.take(1).mkString)
-      connection <- IO(
-        java.sql.DriverManager
-          .getConnection(dbConfig.url.replace(database, "template1"), dbConfig.user, dbConfig.pass)
-      )
-    } yield connection
-
-  @nowarn("msg=discarded non-Unit value.*")
-  override def beforeAll(): Unit = {
-    // Extract the database name from the URL.
-    val database = configuration.database.url.split("/").reverse.take(1).mkString
-    val db       = Resource.make(connect(configuration.database))(con => IO(con.close()))
-    // Create the test database if it does not already exist.
-    db.use { connection =>
-      for {
-        statement <- IO(connection.createStatement())
-        exists <- IO(
-          statement.executeQuery(
-            s"""SELECT datname FROM pg_catalog.pg_database WHERE datname = '$database'"""
-          )
-        )
-        _ <- IO {
-          if (!exists.next())
-            statement.execute(s"""CREATE DATABASE "$database"""")
-        }
-        _ <- IO(exists.close)
-        _ <- IO(statement.close)
-      } yield ()
-    }.unsafeRunSync()
-  }
-
-  override def afterAll(): Unit = {
-    // Extract the database name from the URL.
-    val database = configuration.database.url.split("/").reverse.take(1).mkString
-    val db       = Resource.make(connect(configuration.database))(con => IO(con.close()))
-    // Drop the test database after all tests have been run.
-    db.use { connection =>
-      for {
-        statement <- IO(connection.createStatement())
-        _         <- IO(statement.execute(s"""DROP DATABASE IF EXISTS $database"""))
-        _         <- IO(statement.close)
-      } yield ()
-    }.unsafeRunSync()
-  }
-
-  override def beforeEach(context: BeforeEach): Unit = {
-    val _ = flyway.migrate()
-  }
-
-  override def afterEach(context: AfterEach): Unit = {
-    val _ = flyway.clean()
-  }
-
-  /** Find and return a free port on the local machine by starting a server socket and closing it. The port number used
-    * by the socket is marked to allow reuse, considered free and returned.
-    *
-    * @return
-    *   An optional port number if a free one can be found.
-    */
-  protected def findFreePort(): Option[Port] = {
-    val socket = new ServerSocket(0)
-    val port   = socket.getLocalPort
-    socket.setReuseAddress(true) // Allow instant rebinding of the socket.
-    socket.close()               // Free the socket for further use by closing it.
-    Port.fromInt(port)
-  }
-
-  /** Provide a resource with a database connection to allow db operations and proper resource release later.
-    *
-    * @param cfg
-    *   The application configuration.
-    * @return
-    *   A cats resource encapsulation a database connection as defined within the given configuration.
-    */
-  protected def connectToDb(cfg: SmedereeTicketsConfiguration): Resource[IO, java.sql.Connection] =
-    Resource.make(
-      IO.delay(java.sql.DriverManager.getConnection(cfg.database.url, cfg.database.user, cfg.database.pass))
-    )(c => IO.delay(c.close()))
-
-  /** Create a project for ticket tracking in the database.
-    *
-    * @param project
-    *   The project to be created.
-    * @return
-    *   The number of affected database rows.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def createTicketsProject(project: Project): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay {
-          con.prepareStatement(
-            """INSERT INTO "tickets"."projects" (name, owner, is_private, description, created_at, updated_at) VALUES(?, ?, ?, ?, NOW(), NOW())"""
-          )
-        }
-        _ <- IO.delay(statement.setString(1, project.name.toString))
-        _ <- IO.delay(statement.setObject(2, project.owner.uid))
-        _ <- IO.delay(statement.setBoolean(3, project.isPrivate))
-        _ <- IO.delay(
-          project.description.fold(statement.setNull(4, java.sql.Types.VARCHAR))(descr =>
-            statement.setString(4, descr.toString)
-          )
-        )
-        r <- IO.delay(statement.executeUpdate())
-        _ <- IO.delay(statement.close())
-      } yield r
+    protected final val configuration: SmedereeTicketsConfiguration =
+        ConfigSource
+            .fromConfig(ConfigFactory.load(getClass.getClassLoader))
+            .at(SmedereeTicketsConfiguration.location)
+            .loadOrThrow[SmedereeTicketsConfiguration]
+
+    protected final val flyway: Flyway =
+        DatabaseMigrator
+            .configureFlyway(configuration.database.url, configuration.database.user, configuration.database.pass)
+            .cleanDisabled(false)
+            .load()
+
+    /** Connect to the DBMS using the generic "template1" database which should always be present.
+      *
+      * @param dbConfig
+      *   The database configuration.
+      * @return
+      *   The connection to the database ("template1").
+      */
+    private def connect(dbConfig: DatabaseConfig): IO[java.sql.Connection] =
+        for {
+            _        <- IO(Class.forName(dbConfig.driver))
+            database <- IO(dbConfig.url.split("/").reverse.take(1).mkString)
+            connection <- IO(
+                java.sql.DriverManager
+                    .getConnection(dbConfig.url.replace(database, "template1"), dbConfig.user, dbConfig.pass)
+            )
+        } yield connection
+
+    @nowarn("msg=discarded non-Unit value.*")
+    override def beforeAll(): Unit = {
+        // Extract the database name from the URL.
+        val database = configuration.database.url.split("/").reverse.take(1).mkString
+        val db       = Resource.make(connect(configuration.database))(con => IO(con.close()))
+        // Create the test database if it does not already exist.
+        db.use { connection =>
+            for {
+                statement <- IO(connection.createStatement())
+                exists <- IO(
+                    statement.executeQuery(
+                        s"""SELECT datname FROM pg_catalog.pg_database WHERE datname = '$database'"""
+                    )
+                )
+                _ <- IO {
+                    if (!exists.next())
+                        statement.execute(s"""CREATE DATABASE "$database"""")
+                }
+                _ <- IO(exists.close)
+                _ <- IO(statement.close)
+            } yield ()
+        }.unsafeRunSync()
     }
 
-  /** Create a user account from a ticket submitter in the database.
-    *
-    * @param submitter
-    *   The submitter for which the account shall be created.
-    * @return
-    *   The number of affected database rows.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def createTicketsSubmitter(submitter: Submitter): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay {
-          con.prepareStatement(
-            """INSERT INTO "tickets"."users" (uid, name, email, created_at, updated_at) VALUES(?, ?, ?, NOW(), NOW())"""
-          )
-        }
-        _ <- IO.delay(statement.setObject(1, submitter.id))
-        _ <- IO.delay(statement.setString(2, submitter.name.toString))
-        _ <- IO.delay(statement.setString(3, s"email-${submitter.id.toString}@example.com"))
-        r <- IO.delay(statement.executeUpdate())
-        _ <- IO.delay(statement.close())
-      } yield r
+    override def afterAll(): Unit = {
+        // Extract the database name from the URL.
+        val database = configuration.database.url.split("/").reverse.take(1).mkString
+        val db       = Resource.make(connect(configuration.database))(con => IO(con.close()))
+        // Drop the test database after all tests have been run.
+        db.use { connection =>
+            for {
+                statement <- IO(connection.createStatement())
+                _         <- IO(statement.execute(s"""DROP DATABASE IF EXISTS $database"""))
+                _         <- IO(statement.close)
+            } yield ()
+        }.unsafeRunSync()
     }
 
-  /** Create a tickets user account in the database.
-    *
-    * @param owner
-    *   The user to be created.
-    * @return
-    *   The number of affected database rows.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def createProjectOwner(owner: ProjectOwner): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay {
-          con.prepareStatement(
-            """INSERT INTO "tickets"."users" (uid, name, email, created_at, updated_at) VALUES(?, ?, ?, NOW(), NOW())"""
-          )
-        }
-        _ <- IO.delay(statement.setObject(1, owner.uid))
-        _ <- IO.delay(statement.setString(2, owner.name.toString))
-        _ <- IO.delay(statement.setString(3, owner.email.toString))
-        r <- IO.delay(statement.executeUpdate())
-        _ <- IO.delay(statement.close())
-      } yield r
+    override def beforeEach(context: BeforeEach): Unit = {
+        val _ = flyway.migrate()
     }
 
-  /** Create a tickets user account in the database.
-    *
-    * @param user
-    *   The user to be created.
-    * @return
-    *   The number of affected database rows.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def createTicketsUser(user: TicketsUser): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay {
-          con.prepareStatement(
-            """INSERT INTO "tickets"."users" (uid, name, email, created_at, updated_at) VALUES(?, ?, ?, NOW(), NOW())"""
-          )
-        }
-        _ <- IO.delay(statement.setObject(1, user.uid))
-        _ <- IO.delay(statement.setString(2, user.name.toString))
-        _ <- IO.delay(statement.setString(3, user.email.toString))
-        r <- IO.delay(statement.executeUpdate())
-        _ <- IO.delay(statement.close())
-      } yield r
+    override def afterEach(context: AfterEach): Unit = {
+        val _ = flyway.clean()
     }
 
-  /** Return the next ticket number for the given project.
-    *
-    * @param projectId
-    *   The internal database ID of the project.
-    * @return
-    *   The next ticket number.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def loadNextTicketNumber(projectId: ProjectId): IO[Int] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT next_ticket_number FROM "tickets"."projects" WHERE id = ?"""
-          )
-        )
-        _      <- IO.delay(statement.setLong(1, projectId.toLong))
-        result <- IO.delay(statement.executeQuery)
-        number <- IO.delay {
-          result.next()
-          result.getInt("next_ticket_number")
-        }
-        _ <- IO(statement.close())
-      } yield number
+    /** Find and return a free port on the local machine by starting a server socket and closing it. The port number
+      * used by the socket is marked to allow reuse, considered free and returned.
+      *
+      * @return
+      *   An optional port number if a free one can be found.
+      */
+    protected def findFreePort(): Option[Port] = {
+        val socket = new ServerSocket(0)
+        val port   = socket.getLocalPort
+        socket.setReuseAddress(true) // Allow instant rebinding of the socket.
+        socket.close()               // Free the socket for further use by closing it.
+        Port.fromInt(port)
     }
 
-  /** Find the project ID for the given owner and project name.
-    *
-    * @param owner
-    *   The unique ID of the user account that owns the project.
-    * @param name
-    *   The project name which must be unique in regard to the owner.
-    * @return
-    *   An option to the internal database ID.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def loadProjectId(owner: ProjectOwnerId, name: ProjectName): IO[Option[ProjectId]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT id FROM "tickets"."projects" WHERE owner = ? AND name = ? LIMIT 1"""
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, owner))
-        _      <- IO.delay(statement.setString(2, name.toString))
-        result <- IO.delay(statement.executeQuery)
-        projectId <- IO.delay {
-          if (result.next()) {
-            ProjectId.from(result.getLong("id"))
-          } else {
-            None
-          }
+    /** Provide a resource with a database connection to allow db operations and proper resource release later.
+      *
+      * @param cfg
+      *   The application configuration.
+      * @return
+      *   A cats resource encapsulation a database connection as defined within the given configuration.
+      */
+    protected def connectToDb(cfg: SmedereeTicketsConfiguration): Resource[IO, java.sql.Connection] =
+        Resource.make(
+            IO.delay(java.sql.DriverManager.getConnection(cfg.database.url, cfg.database.user, cfg.database.pass))
+        )(c => IO.delay(c.close()))
+
+    /** Create a project for ticket tracking in the database.
+      *
+      * @param project
+      *   The project to be created.
+      * @return
+      *   The number of affected database rows.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def createTicketsProject(project: Project): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay {
+                    con.prepareStatement(
+                        """INSERT INTO "tickets"."projects" (name, owner, is_private, description, created_at, updated_at) VALUES(?, ?, ?, ?, NOW(), NOW())"""
+                    )
+                }
+                _ <- IO.delay(statement.setString(1, project.name.toString))
+                _ <- IO.delay(statement.setObject(2, project.owner.uid))
+                _ <- IO.delay(statement.setBoolean(3, project.isPrivate))
+                _ <- IO.delay(
+                    project.description.fold(statement.setNull(4, java.sql.Types.VARCHAR))(descr =>
+                        statement.setString(4, descr.toString)
+                    )
+                )
+                r <- IO.delay(statement.executeUpdate())
+                _ <- IO.delay(statement.close())
+            } yield r
         }
-        _ <- IO(statement.close())
-      } yield projectId
-    }
 
-  /** Find the ticket ID for the given project ID and ticket number.
-    *
-    * @param projectId
-    *   The unique internal project id.
-    * @param ticketNumber
-    *   The ticket number.
-    * @return
-    *   An option to the internal database ID of the ticket.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def loadTicketId(project: ProjectId, number: TicketNumber): IO[Option[TicketId]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT id FROM "tickets"."tickets" WHERE project = ? AND number = ? LIMIT 1"""
-          )
-        )
-        _      <- IO.delay(statement.setLong(1, project.toLong))
-        _      <- IO.delay(statement.setInt(2, number.toInt))
-        result <- IO.delay(statement.executeQuery)
-        ticketId <- IO.delay {
-          if (result.next()) {
-            TicketId.from(result.getLong("id"))
-          } else {
-            None
-          }
+    /** Create a user account from a ticket submitter in the database.
+      *
+      * @param submitter
+      *   The submitter for which the account shall be created.
+      * @return
+      *   The number of affected database rows.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def createTicketsSubmitter(submitter: Submitter): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay {
+                    con.prepareStatement(
+                        """INSERT INTO "tickets"."users" (uid, name, email, created_at, updated_at) VALUES(?, ?, ?, NOW(), NOW())"""
+                    )
+                }
+                _ <- IO.delay(statement.setObject(1, submitter.id))
+                _ <- IO.delay(statement.setString(2, submitter.name.toString))
+                _ <- IO.delay(statement.setString(3, s"email-${submitter.id.toString}@example.com"))
+                r <- IO.delay(statement.executeUpdate())
+                _ <- IO.delay(statement.close())
+            } yield r
         }
-        _ <- IO(statement.close())
-      } yield ticketId
-    }
 
-  /** Find the ticket service user with the given user id.
-    *
-    * @param uid
-    *   The unique id of the user account.
-    * @return
-    *   An option to the loaded user.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def loadTicketsUser(uid: UserId): IO[Option[TicketsUser]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement("""SELECT uid, name, email, language FROM "tickets"."users" WHERE uid = ?""")
-        )
-        _      <- IO.delay(statement.setObject(1, uid.toUUID))
-        result <- IO.delay(statement.executeQuery())
-        user <- IO.delay {
-          if (result.next()) {
-            val language = LanguageCode.from(result.getString("language"))
-            (uid.some, Username.from(result.getString("name")), EmailAddress.from(result.getString("email"))).mapN {
-              case (uid, name, email) => TicketsUser(uid, name, email, language)
-            }
-          } else {
-            None
-          }
+    /** Create a tickets user account in the database.
+      *
+      * @param owner
+      *   The user to be created.
+      * @return
+      *   The number of affected database rows.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def createProjectOwner(owner: ProjectOwner): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay {
+                    con.prepareStatement(
+                        """INSERT INTO "tickets"."users" (uid, name, email, created_at, updated_at) VALUES(?, ?, ?, NOW(), NOW())"""
+                    )
+                }
+                _ <- IO.delay(statement.setObject(1, owner.uid))
+                _ <- IO.delay(statement.setString(2, owner.name.toString))
+                _ <- IO.delay(statement.setString(3, owner.email.toString))
+                r <- IO.delay(statement.executeUpdate())
+                _ <- IO.delay(statement.close())
+            } yield r
+        }
+
+    /** Create a tickets user account in the database.
+      *
+      * @param user
+      *   The user to be created.
+      * @return
+      *   The number of affected database rows.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def createTicketsUser(user: TicketsUser): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay {
+                    con.prepareStatement(
+                        """INSERT INTO "tickets"."users" (uid, name, email, created_at, updated_at) VALUES(?, ?, ?, NOW(), NOW())"""
+                    )
+                }
+                _ <- IO.delay(statement.setObject(1, user.uid))
+                _ <- IO.delay(statement.setString(2, user.name.toString))
+                _ <- IO.delay(statement.setString(3, user.email.toString))
+                r <- IO.delay(statement.executeUpdate())
+                _ <- IO.delay(statement.close())
+            } yield r
+        }
+
+    /** Return the next ticket number for the given project.
+      *
+      * @param projectId
+      *   The internal database ID of the project.
+      * @return
+      *   The next ticket number.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def loadNextTicketNumber(projectId: ProjectId): IO[Int] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT next_ticket_number FROM "tickets"."projects" WHERE id = ?"""
+                    )
+                )
+                _      <- IO.delay(statement.setLong(1, projectId.toLong))
+                result <- IO.delay(statement.executeQuery)
+                number <- IO.delay {
+                    result.next()
+                    result.getInt("next_ticket_number")
+                }
+                _ <- IO(statement.close())
+            } yield number
+        }
+
+    /** Find the project ID for the given owner and project name.
+      *
+      * @param owner
+      *   The unique ID of the user account that owns the project.
+      * @param name
+      *   The project name which must be unique in regard to the owner.
+      * @return
+      *   An option to the internal database ID.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def loadProjectId(owner: ProjectOwnerId, name: ProjectName): IO[Option[ProjectId]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT id FROM "tickets"."projects" WHERE owner = ? AND name = ? LIMIT 1"""
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, owner))
+                _      <- IO.delay(statement.setString(2, name.toString))
+                result <- IO.delay(statement.executeQuery)
+                projectId <- IO.delay {
+                    if (result.next()) {
+                        ProjectId.from(result.getLong("id"))
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO(statement.close())
+            } yield projectId
+        }
+
+    /** Find the ticket ID for the given project ID and ticket number.
+      *
+      * @param projectId
+      *   The unique internal project id.
+      * @param ticketNumber
+      *   The ticket number.
+      * @return
+      *   An option to the internal database ID of the ticket.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def loadTicketId(project: ProjectId, number: TicketNumber): IO[Option[TicketId]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT id FROM "tickets"."tickets" WHERE project = ? AND number = ? LIMIT 1"""
+                    )
+                )
+                _      <- IO.delay(statement.setLong(1, project.toLong))
+                _      <- IO.delay(statement.setInt(2, number.toInt))
+                result <- IO.delay(statement.executeQuery)
+                ticketId <- IO.delay {
+                    if (result.next()) {
+                        TicketId.from(result.getLong("id"))
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO(statement.close())
+            } yield ticketId
+        }
+
+    /** Find the ticket service user with the given user id.
+      *
+      * @param uid
+      *   The unique id of the user account.
+      * @return
+      *   An option to the loaded user.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def loadTicketsUser(uid: UserId): IO[Option[TicketsUser]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement("""SELECT uid, name, email, language FROM "tickets"."users" WHERE uid = ?""")
+                )
+                _      <- IO.delay(statement.setObject(1, uid.toUUID))
+                result <- IO.delay(statement.executeQuery())
+                user <- IO.delay {
+                    if (result.next()) {
+                        val language = LanguageCode.from(result.getString("language"))
+                        (
+                            uid.some,
+                            Username.from(result.getString("name")),
+                            EmailAddress.from(result.getString("email"))
+                        ).mapN { case (uid, name, email) =>
+                            TicketsUser(uid, name, email, language)
+                        }
+                    } else {
+                        None
+                    }
+                }
+            } yield user
         }
-      } yield user
-    }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/ColourCodeTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/ColourCodeTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/ColourCodeTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/ColourCodeTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,18 +25,18 @@
 import org.scalacheck.*
 
 final class ColourCodeTest extends ScalaCheckSuite {
-  given Arbitrary[ColourCode] = Arbitrary(genColourCode)
+    given Arbitrary[ColourCode] = Arbitrary(genColourCode)
 
-  property("ColourCode.from must fail on invalid input") {
-    forAll { (input: String) =>
-      assertEquals(ColourCode.from(input), None)
+    property("ColourCode.from must fail on invalid input") {
+        forAll { (input: String) =>
+            assertEquals(ColourCode.from(input), None)
+        }
     }
-  }
 
-  property("ColourCode.from must succeed on valid input") {
-    forAll { (colourCode: ColourCode) =>
-      val input = colourCode.toString
-      assertEquals(ColourCode.from(input), Option(colourCode))
+    property("ColourCode.from must succeed on valid input") {
+        forAll { (colourCode: ColourCode) =>
+            val input = colourCode.toString
+            assertEquals(ColourCode.from(input), Option(colourCode))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/DatabaseMigratorTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/DatabaseMigratorTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/DatabaseMigratorTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/DatabaseMigratorTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -24,42 +24,42 @@
 import org.flywaydb.core.Flyway
 
 final class DatabaseMigratorTest extends BaseSpec {
-  override def beforeEach(context: BeforeEach): Unit = {
-    val dbConfig = configuration.database
-    val flyway: Flyway =
-      DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
+    override def beforeEach(context: BeforeEach): Unit = {
+        val dbConfig = configuration.database
+        val flyway: Flyway =
+            DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+    }
 
-  override def afterEach(context: AfterEach): Unit = {
-    val dbConfig = configuration.database
-    val flyway: Flyway =
-      DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
-    val _ = flyway.migrate()
-    val _ = flyway.clean()
-  }
+    override def afterEach(context: AfterEach): Unit = {
+        val dbConfig = configuration.database
+        val flyway: Flyway =
+            DatabaseMigrator.configureFlyway(dbConfig.url, dbConfig.user, dbConfig.pass).cleanDisabled(false).load()
+        val _ = flyway.migrate()
+        val _ = flyway.clean()
+    }
 
-  test("DatabaseMigrator must update available outdated database".tag(NeedsDatabase)) {
-    val dbConfig = configuration.database
-    val migrator = new DatabaseMigrator[IO]
-    val test     = migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
-    test.map(result => assert(result.migrationsExecuted > 0))
-  }
+    test("DatabaseMigrator must update available outdated database".tag(NeedsDatabase)) {
+        val dbConfig = configuration.database
+        val migrator = new DatabaseMigrator[IO]
+        val test     = migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
+        test.map(result => assert(result.migrationsExecuted > 0))
+    }
 
-  test("DatabaseMigrator must not update an up to date database".tag(NeedsDatabase)) {
-    val dbConfig = configuration.database
-    val migrator = new DatabaseMigrator[IO]
-    val test = for {
-      _ <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
-      r <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
-    } yield r
-    test.map(result => assert(result.migrationsExecuted === 0))
-  }
+    test("DatabaseMigrator must not update an up to date database".tag(NeedsDatabase)) {
+        val dbConfig = configuration.database
+        val migrator = new DatabaseMigrator[IO]
+        val test = for {
+            _ <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
+            r <- migrator.migrate(dbConfig.url, dbConfig.user, dbConfig.pass)
+        } yield r
+        test.map(result => assert(result.migrationsExecuted === 0))
+    }
 
-  test("DatabaseMigrator must throw an exception if the database is not available".tag(NeedsDatabase)) {
-    val migrator = new DatabaseMigrator[IO]
-    val test     = migrator.migrate("jdbc:nodriver://", "", "")
-    test.attempt.map(r => assert(r.isLeft))
-  }
+    test("DatabaseMigrator must throw an exception if the database is not available".tag(NeedsDatabase)) {
+        val migrator = new DatabaseMigrator[IO]
+        val test     = migrator.migrate("jdbc:nodriver://", "", "")
+        test.attempt.map(r => assert(r.isLeft))
+    }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/SmedereeTicketsConfigurationTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/SmedereeTicketsConfigurationTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/SmedereeTicketsConfigurationTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/SmedereeTicketsConfigurationTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,46 +25,46 @@
 import munit.*
 
 final class SmedereeTicketsConfigurationTest extends FunSuite {
-  val rawDefaultConfig = new Fixture[Config]("defaultConfig") {
-    def apply() = ConfigFactory.load(getClass.getClassLoader)
-  }
+    val rawDefaultConfig = new Fixture[Config]("defaultConfig") {
+        def apply() = ConfigFactory.load(getClass.getClassLoader)
+    }
 
-  override def munitFixtures = List(rawDefaultConfig)
+    override def munitFixtures = List(rawDefaultConfig)
 
-  test("must load from the default configuration successfully") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeTicketsConfiguration.location.toString}")
-      .load[SmedereeTicketsConfiguration] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(_)     => assert(true)
+    test("must load from the default configuration successfully") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeTicketsConfiguration.location.toString}")
+            .load[SmedereeTicketsConfiguration] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(_)     => assert(true)
+            }
     }
-  }
 
-  test("default values for external linking must be setup for local development") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeTicketsConfiguration.location.toString}")
-      .load[SmedereeTicketsConfiguration] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(cfg) =>
-        val externalCfg = cfg.externalUrl
-        assertEquals(externalCfg.host, cfg.service.host)
-        assertEquals(externalCfg.port, Option(cfg.service.port))
-        assert(externalCfg.path.isEmpty)
-        assertEquals(externalCfg.scheme, Uri.Scheme.http)
+    test("default values for external linking must be setup for local development") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeTicketsConfiguration.location.toString}")
+            .load[SmedereeTicketsConfiguration] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(cfg) =>
+                    val externalCfg = cfg.externalUrl
+                    assertEquals(externalCfg.host, cfg.service.host)
+                    assertEquals(externalCfg.port, Option(cfg.service.port))
+                    assert(externalCfg.path.isEmpty)
+                    assertEquals(externalCfg.scheme, Uri.Scheme.http)
+            }
     }
-  }
 
-  test("default values for hub service integration must be setup for local development") {
-    ConfigSource
-      .fromConfig(rawDefaultConfig())
-      .at(s"${SmedereeTicketsConfiguration.location.toString}")
-      .load[SmedereeTicketsConfiguration] match {
-      case Left(errors) => fail(errors.toList.mkString(", "))
-      case Right(cfg) =>
-        val expectedUri = uri"http://localhost:8080"
-        assertEquals(cfg.hub.baseUri, expectedUri)
+    test("default values for hub service integration must be setup for local development") {
+        ConfigSource
+            .fromConfig(rawDefaultConfig())
+            .at(s"${SmedereeTicketsConfiguration.location.toString}")
+            .load[SmedereeTicketsConfiguration] match {
+                case Left(errors) => fail(errors.toList.mkString(", "))
+                case Right(cfg) =>
+                    val expectedUri = uri"http://localhost:8080"
+                    assertEquals(cfg.hub.baseUri, expectedUri)
+            }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieLabelRepositoryTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieLabelRepositoryTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieLabelRepositoryTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieLabelRepositoryTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,288 +25,288 @@
 
 final class DoobieLabelRepositoryTest extends BaseSpec {
 
-  /** Find the label ID for the given project and label name.
-    *
-    * @param owner
-    *   The unique ID of the user account that owns the project.
-    * @param vcsRepoName
-    *   The project name which must be unique in regard to the owner.
-    * @param labelName
-    *   The label name which must be unique in the project context.
-    * @return
-    *   An option to the internal database ID.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def findLabelId(owner: ProjectOwnerId, vcsRepoName: ProjectName, labelName: LabelName): IO[Option[Long]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT "labels".id
+    /** Find the label ID for the given project and label name.
+      *
+      * @param owner
+      *   The unique ID of the user account that owns the project.
+      * @param vcsRepoName
+      *   The project name which must be unique in regard to the owner.
+      * @param labelName
+      *   The label name which must be unique in the project context.
+      * @return
+      *   An option to the internal database ID.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def findLabelId(owner: ProjectOwnerId, vcsRepoName: ProjectName, labelName: LabelName): IO[Option[Long]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT "labels".id
               |FROM "tickets"."labels" AS "labels"
               |JOIN "tickets"."projects" AS "projects"
               |ON "labels".project = "projects".id
               |WHERE "projects".owner = ?
               |AND "projects".name = ?
               |AND "labels".name = ?""".stripMargin
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, owner))
-        _      <- IO.delay(statement.setString(2, vcsRepoName.toString))
-        _      <- IO.delay(statement.setString(3, labelName.toString))
-        result <- IO.delay(statement.executeQuery)
-        account <- IO.delay {
-          if (result.next()) {
-            Option(result.getLong("id"))
-          } else {
-            None
-          }
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, owner))
+                _      <- IO.delay(statement.setString(2, vcsRepoName.toString))
+                _      <- IO.delay(statement.setString(3, labelName.toString))
+                result <- IO.delay(statement.executeQuery)
+                account <- IO.delay {
+                    if (result.next()) {
+                        Option(result.getLong("id"))
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO(statement.close())
+            } yield account
         }
-        _ <- IO(statement.close())
-      } yield account
-    }
 
-  test("allLabels must return all labels".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabels.sample) match {
-      case (Some(owner), Some(generatedProject), Some(labels)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          createdLabels <- projectId match {
-            case None            => IO.pure(List.empty)
-            case Some(projectId) => labels.traverse(label => labelRepo.createLabel(projectId)(label))
-          }
-          foundLabels <- projectId match {
-            case None            => IO.pure(List.empty)
-            case Some(projectId) => labelRepo.allLabels(projectId).compile.toList
-          }
-        } yield foundLabels
-        test.map { foundLabels =>
-          assert(foundLabels.size === labels.size, "Different number of labels!")
-          foundLabels.sortBy(_.name).zip(labels.sortBy(_.name)).map { (found, expected) =>
-            assertEquals(found.copy(id = expected.id), expected)
-          }
+    test("allLabels must return all labels".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabels.sample) match {
+            case (Some(owner), Some(generatedProject), Some(labels)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    createdLabels <- projectId match {
+                        case None            => IO.pure(List.empty)
+                        case Some(projectId) => labels.traverse(label => labelRepo.createLabel(projectId)(label))
+                    }
+                    foundLabels <- projectId match {
+                        case None            => IO.pure(List.empty)
+                        case Some(projectId) => labelRepo.allLabels(projectId).compile.toList
+                    }
+                } yield foundLabels
+                test.map { foundLabels =>
+                    assert(foundLabels.size === labels.size, "Different number of labels!")
+                    foundLabels.sortBy(_.name).zip(labels.sortBy(_.name)).map { (found, expected) =>
+                        assertEquals(found.copy(id = expected.id), expected)
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("createLabel must create the label".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(label)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _               <- createProjectOwner(owner)
-          createdProjects <- createTicketsProject(project)
-          projectId       <- loadProjectId(owner.uid, project.name)
-          createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
-          foundLabel      <- projectId.traverse(id => labelRepo.findLabel(id)(label.name))
-        } yield (createdProjects, projectId, createdLabels, foundLabel)
-        test.map { tuple =>
-          val (createdProjects, projectId, createdLabels, foundLabel) = tuple
-          assert(createdProjects === 1, "Test project was not created!")
-          assert(projectId.nonEmpty, "No project id found!")
-          assert(createdLabels.exists(_ === 1), "Test label was not created!")
-          foundLabel.getOrElse(None) match {
-            case None => fail("Created label not found!")
-            case Some(foundLabel) =>
-              assert(foundLabel.id.nonEmpty, "Label ID must not be empty!")
-              assertEquals(foundLabel.name, label.name)
-              assertEquals(foundLabel.description, label.description)
-              assertEquals(foundLabel.colour, label.colour)
-          }
+    test("createLabel must create the label".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(label)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _               <- createProjectOwner(owner)
+                    createdProjects <- createTicketsProject(project)
+                    projectId       <- loadProjectId(owner.uid, project.name)
+                    createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
+                    foundLabel      <- projectId.traverse(id => labelRepo.findLabel(id)(label.name))
+                } yield (createdProjects, projectId, createdLabels, foundLabel)
+                test.map { tuple =>
+                    val (createdProjects, projectId, createdLabels, foundLabel) = tuple
+                    assert(createdProjects === 1, "Test project was not created!")
+                    assert(projectId.nonEmpty, "No project id found!")
+                    assert(createdLabels.exists(_ === 1), "Test label was not created!")
+                    foundLabel.getOrElse(None) match {
+                        case None => fail("Created label not found!")
+                        case Some(foundLabel) =>
+                            assert(foundLabel.id.nonEmpty, "Label ID must not be empty!")
+                            assertEquals(foundLabel.name, label.name)
+                            assertEquals(foundLabel.description, label.description)
+                            assertEquals(foundLabel.colour, label.colour)
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("createLabel must fail if the label name already exists".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(label)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _               <- createProjectOwner(owner)
-          createdProjects <- createTicketsProject(project)
-          projectId       <- loadProjectId(owner.uid, project.name)
-          createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
-          _               <- projectId.traverse(id => labelRepo.createLabel(id)(label))
-        } yield (createdProjects, projectId, createdLabels)
-        test.attempt.map(r => assert(r.isLeft, "Creating a label with a duplicate name must fail!"))
-      case _ => fail("Could not generate data samples!")
+    test("createLabel must fail if the label name already exists".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(label)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _               <- createProjectOwner(owner)
+                    createdProjects <- createTicketsProject(project)
+                    projectId       <- loadProjectId(owner.uid, project.name)
+                    createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
+                    _               <- projectId.traverse(id => labelRepo.createLabel(id)(label))
+                } yield (createdProjects, projectId, createdLabels)
+                test.attempt.map(r => assert(r.isLeft, "Creating a label with a duplicate name must fail!"))
+            case _ => fail("Could not generate data samples!")
+        }
     }
-  }
 
-  test("deleteLabel must delete an existing label".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(label)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _               <- createProjectOwner(owner)
-          createdProjects <- createTicketsProject(project)
-          projectId       <- loadProjectId(owner.uid, project.name)
-          createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
-          labelId         <- findLabelId(owner.uid, project.name, label.name)
-          deletedLabels   <- labelRepo.deleteLabel(label.copy(id = labelId.flatMap(LabelId.from)))
-          foundLabel      <- projectId.traverse(id => labelRepo.findLabel(id)(label.name))
-        } yield (createdProjects, projectId, createdLabels, deletedLabels, foundLabel)
-        test.map { tuple =>
-          val (createdProjects, projectId, createdLabels, deletedLabels, foundLabel) = tuple
-          assert(createdProjects === 1, "Test vcs project was not created!")
-          assert(projectId.nonEmpty, "No vcs project id found!")
-          assert(createdLabels.exists(_ === 1), "Test label was not created!")
-          assert(deletedLabels === 1, "Test label was not deleted!")
-          assert(foundLabel.getOrElse(None).isEmpty, "Test label was not deleted!")
+    test("deleteLabel must delete an existing label".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(label)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _               <- createProjectOwner(owner)
+                    createdProjects <- createTicketsProject(project)
+                    projectId       <- loadProjectId(owner.uid, project.name)
+                    createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
+                    labelId         <- findLabelId(owner.uid, project.name, label.name)
+                    deletedLabels   <- labelRepo.deleteLabel(label.copy(id = labelId.flatMap(LabelId.from)))
+                    foundLabel      <- projectId.traverse(id => labelRepo.findLabel(id)(label.name))
+                } yield (createdProjects, projectId, createdLabels, deletedLabels, foundLabel)
+                test.map { tuple =>
+                    val (createdProjects, projectId, createdLabels, deletedLabels, foundLabel) = tuple
+                    assert(createdProjects === 1, "Test vcs project was not created!")
+                    assert(projectId.nonEmpty, "No vcs project id found!")
+                    assert(createdLabels.exists(_ === 1), "Test label was not created!")
+                    assert(deletedLabels === 1, "Test label was not deleted!")
+                    assert(foundLabel.getOrElse(None).isEmpty, "Test label was not deleted!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findLabel must find existing labels".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabels.sample) match {
-      case (Some(owner), Some(generatedProject), Some(labels)) =>
-        val project       = generatedProject.copy(owner = owner)
-        val dbConfig      = configuration.database
-        val expectedLabel = labels(scala.util.Random.nextInt(labels.size))
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _               <- createProjectOwner(owner)
-          createdProjects <- createTicketsProject(project)
-          projectId       <- loadProjectId(owner.uid, project.name)
-          createdLabels <- projectId match {
-            case None            => IO.pure(List.empty)
-            case Some(projectId) => labels.traverse(label => labelRepo.createLabel(projectId)(label))
-          }
-          foundLabel <- projectId.traverse(id => labelRepo.findLabel(id)(expectedLabel.name))
-        } yield foundLabel.flatten
-        test.map { foundLabel =>
-          assertEquals(foundLabel.map(_.copy(id = expectedLabel.id)), Option(expectedLabel))
+    test("findLabel must find existing labels".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabels.sample) match {
+            case (Some(owner), Some(generatedProject), Some(labels)) =>
+                val project       = generatedProject.copy(owner = owner)
+                val dbConfig      = configuration.database
+                val expectedLabel = labels(scala.util.Random.nextInt(labels.size))
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _               <- createProjectOwner(owner)
+                    createdProjects <- createTicketsProject(project)
+                    projectId       <- loadProjectId(owner.uid, project.name)
+                    createdLabels <- projectId match {
+                        case None            => IO.pure(List.empty)
+                        case Some(projectId) => labels.traverse(label => labelRepo.createLabel(projectId)(label))
+                    }
+                    foundLabel <- projectId.traverse(id => labelRepo.findLabel(id)(expectedLabel.name))
+                } yield foundLabel.flatten
+                test.map { foundLabel =>
+                    assertEquals(foundLabel.map(_.copy(id = expectedLabel.id)), Option(expectedLabel))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("updateLabel must update an existing label".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(label)) =>
-        val updatedLabel = label.copy(
-          name = LabelName("updated label"),
-          description = Option(LabelDescription("I am an updated label description...")),
-          colour = ColourCode("#abcdef")
-        )
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _               <- createProjectOwner(owner)
-          createdProjects <- createTicketsProject(project)
-          projectId       <- loadProjectId(owner.uid, project.name)
-          createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
-          labelId         <- findLabelId(owner.uid, project.name, label.name)
-          updatedLabels   <- labelRepo.updateLabel(updatedLabel.copy(id = labelId.map(LabelId.apply)))
-          foundLabel      <- projectId.traverse(id => labelRepo.findLabel(id)(updatedLabel.name))
-        } yield (createdProjects, projectId, createdLabels, updatedLabels, foundLabel.flatten)
-        test.map { tuple =>
-          val (createdProjects, projectId, createdLabels, updatedLabels, foundLabel) = tuple
-          assert(createdProjects === 1, "Test vcs project was not created!")
-          assert(projectId.nonEmpty, "No vcs project id found!")
-          assert(createdLabels.exists(_ === 1), "Test label was not created!")
-          assert(updatedLabels === 1, "Test label was not updated!")
-          assert(foundLabel.nonEmpty, "Updated label not found!")
-          foundLabel.map { label =>
-            assertEquals(label, updatedLabel.copy(id = label.id))
-          }
+    test("updateLabel must update an existing label".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(label)) =>
+                val updatedLabel = label.copy(
+                    name = LabelName("updated label"),
+                    description = Option(LabelDescription("I am an updated label description...")),
+                    colour = ColourCode("#abcdef")
+                )
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _               <- createProjectOwner(owner)
+                    createdProjects <- createTicketsProject(project)
+                    projectId       <- loadProjectId(owner.uid, project.name)
+                    createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
+                    labelId         <- findLabelId(owner.uid, project.name, label.name)
+                    updatedLabels   <- labelRepo.updateLabel(updatedLabel.copy(id = labelId.map(LabelId.apply)))
+                    foundLabel      <- projectId.traverse(id => labelRepo.findLabel(id)(updatedLabel.name))
+                } yield (createdProjects, projectId, createdLabels, updatedLabels, foundLabel.flatten)
+                test.map { tuple =>
+                    val (createdProjects, projectId, createdLabels, updatedLabels, foundLabel) = tuple
+                    assert(createdProjects === 1, "Test vcs project was not created!")
+                    assert(projectId.nonEmpty, "No vcs project id found!")
+                    assert(createdLabels.exists(_ === 1), "Test label was not created!")
+                    assert(updatedLabels === 1, "Test label was not updated!")
+                    assert(foundLabel.nonEmpty, "Updated label not found!")
+                    foundLabel.map { label =>
+                        assertEquals(label, updatedLabel.copy(id = label.id))
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("updateLabel must do nothing if id attribute is empty".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(label)) =>
-        val updatedLabel = label.copy(
-          id = None,
-          name = LabelName("updated label"),
-          description = Option(LabelDescription("I am an updated label description...")),
-          colour = ColourCode("#abcdef")
-        )
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo = new DoobieLabelRepository[IO](tx)
-        val test = for {
-          _               <- createProjectOwner(owner)
-          createdProjects <- createTicketsProject(project)
-          projectId       <- loadProjectId(owner.uid, project.name)
-          createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
-          labelId         <- findLabelId(owner.uid, project.name, label.name)
-          updatedLabels   <- labelRepo.updateLabel(updatedLabel)
-        } yield (createdProjects, projectId, createdLabels, updatedLabels)
-        test.map { tuple =>
-          val (createdProjects, projectId, createdLabels, updatedLabels) = tuple
-          assert(createdProjects === 1, "Test vcs project was not created!")
-          assert(projectId.nonEmpty, "No vcs project id found!")
-          assert(createdLabels.exists(_ === 1), "Test label was not created!")
-          assert(updatedLabels === 0, "Label with empty id must not be updated!")
+    test("updateLabel must do nothing if id attribute is empty".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(label)) =>
+                val updatedLabel = label.copy(
+                    id = None,
+                    name = LabelName("updated label"),
+                    description = Option(LabelDescription("I am an updated label description...")),
+                    colour = ColourCode("#abcdef")
+                )
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo = new DoobieLabelRepository[IO](tx)
+                val test = for {
+                    _               <- createProjectOwner(owner)
+                    createdProjects <- createTicketsProject(project)
+                    projectId       <- loadProjectId(owner.uid, project.name)
+                    createdLabels   <- projectId.traverse(id => labelRepo.createLabel(id)(label))
+                    labelId         <- findLabelId(owner.uid, project.name, label.name)
+                    updatedLabels   <- labelRepo.updateLabel(updatedLabel)
+                } yield (createdProjects, projectId, createdLabels, updatedLabels)
+                test.map { tuple =>
+                    val (createdProjects, projectId, createdLabels, updatedLabels) = tuple
+                    assert(createdProjects === 1, "Test vcs project was not created!")
+                    assert(projectId.nonEmpty, "No vcs project id found!")
+                    assert(createdLabels.exists(_ === 1), "Test label was not created!")
+                    assert(updatedLabels === 0, "Label with empty id must not be updated!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieMilestoneRepositoryTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -27,410 +27,420 @@
 
 final class DoobieMilestoneRepositoryTest extends BaseSpec {
 
-  /** Find the milestone ID for the given repository and milestone title.
-    *
-    * @param owner
-    *   The unique ID of the user owner that owns the repository.
-    * @param projectName
-    *   The project name which must be unique in regard to the owner.
-    * @param title
-    *   The milestone title which must be unique in the repository context.
-    * @return
-    *   An option to the internal database ID.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  protected def findMilestoneId(
-      owner: ProjectOwnerId,
-      projectName: ProjectName,
-      title: MilestoneTitle
-  ): IO[Option[Long]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT "milestones".id FROM "tickets"."milestones" AS "milestones" JOIN "tickets"."projects" AS "projects" ON "milestones".project = "projects".id WHERE "projects".owner = ? AND "projects".name = ? AND "milestones".title = ?"""
-          )
-        )
-        _      <- IO.delay(statement.setObject(1, owner))
-        _      <- IO.delay(statement.setString(2, projectName.toString))
-        _      <- IO.delay(statement.setString(3, title.toString))
-        result <- IO.delay(statement.executeQuery)
-        owner <- IO.delay {
-          if (result.next()) {
-            Option(result.getLong("id"))
-          } else {
-            None
-          }
+    /** Find the milestone ID for the given repository and milestone title.
+      *
+      * @param owner
+      *   The unique ID of the user owner that owns the repository.
+      * @param projectName
+      *   The project name which must be unique in regard to the owner.
+      * @param title
+      *   The milestone title which must be unique in the repository context.
+      * @return
+      *   An option to the internal database ID.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    protected def findMilestoneId(
+        owner: ProjectOwnerId,
+        projectName: ProjectName,
+        title: MilestoneTitle
+    ): IO[Option[Long]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT "milestones".id FROM "tickets"."milestones" AS "milestones" JOIN "tickets"."projects" AS "projects" ON "milestones".project = "projects".id WHERE "projects".owner = ? AND "projects".name = ? AND "milestones".title = ?"""
+                    )
+                )
+                _      <- IO.delay(statement.setObject(1, owner))
+                _      <- IO.delay(statement.setString(2, projectName.toString))
+                _      <- IO.delay(statement.setString(3, title.toString))
+                result <- IO.delay(statement.executeQuery)
+                owner <- IO.delay {
+                    if (result.next()) {
+                        Option(result.getLong("id"))
+                    } else {
+                        None
+                    }
+                }
+                _ <- IO(statement.close())
+            } yield owner
         }
-        _ <- IO(statement.close())
-      } yield owner
-    }
 
-  test("allMilestones must return all milestones".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestones.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestones)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          createdRepos <- createTicketsProject(project)
-          repoId       <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId match {
-            case None         => IO.pure(List.empty)
-            case Some(repoId) => milestones.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
-          }
-          foundMilestones <- repoId match {
-            case None         => IO.pure(List.empty)
-            case Some(repoId) => milestoneRepo.allMilestones(repoId).compile.toList
-          }
-        } yield foundMilestones
-        test.map { foundMilestones =>
-          assert(foundMilestones.size === milestones.size, "Different number of milestones!")
-          foundMilestones.sortBy(_.title).zip(milestones.sortBy(_.title)).map { (found, expected) =>
-            assertEquals(found.copy(id = expected.id), expected)
-          }
+    test("allMilestones must return all milestones".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestones.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestones)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    createdRepos <- createTicketsProject(project)
+                    repoId       <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId match {
+                        case None => IO.pure(List.empty)
+                        case Some(repoId) =>
+                            milestones.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
+                    }
+                    foundMilestones <- repoId match {
+                        case None         => IO.pure(List.empty)
+                        case Some(repoId) => milestoneRepo.allMilestones(repoId).compile.toList
+                    }
+                } yield foundMilestones
+                test.map { foundMilestones =>
+                    assert(foundMilestones.size === milestones.size, "Different number of milestones!")
+                    foundMilestones.sortBy(_.title).zip(milestones.sortBy(_.title)).map { (found, expected) =>
+                        assertEquals(found.copy(id = expected.id), expected)
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("allTickets must return all tickets associated with the milestone".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestone), Some(rawTickets)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val tickets  = rawTickets.map(_.copy(submitter = None))
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val ticketRepo    = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _      <- createProjectOwner(owner)
-          _      <- createTicketsProject(project)
-          repoId <- loadProjectId(owner.uid, project.name)
-          foundTickets <- repoId match {
-            case None => IO.pure(List.empty)
-            case Some(projectId) =>
-              for {
-                _                <- milestoneRepo.createMilestone(projectId)(milestone)
-                _                <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                _ <- createdMilestone match {
-                  case None => IO.pure(List.empty)
-                  case Some(milestone) =>
-                    tickets.traverse(ticket => ticketRepo.addMilestone(projectId)(ticket.number)(milestone))
+    test("allTickets must return all tickets associated with the milestone".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestone), Some(rawTickets)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val tickets  = rawTickets.map(_.copy(submitter = None))
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val ticketRepo    = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _      <- createProjectOwner(owner)
+                    _      <- createTicketsProject(project)
+                    repoId <- loadProjectId(owner.uid, project.name)
+                    foundTickets <- repoId match {
+                        case None => IO.pure(List.empty)
+                        case Some(projectId) =>
+                            for {
+                                _ <- milestoneRepo.createMilestone(projectId)(milestone)
+                                _ <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                                _ <- createdMilestone match {
+                                    case None => IO.pure(List.empty)
+                                    case Some(milestone) =>
+                                        tickets.traverse(ticket =>
+                                            ticketRepo.addMilestone(projectId)(ticket.number)(milestone)
+                                        )
+                                }
+                                foundTickets <- createdMilestone.map(_.id).getOrElse(None) match {
+                                    case None              => IO.pure(List.empty)
+                                    case Some(milestoneId) => milestoneRepo.allTickets(None)(milestoneId).compile.toList
+                                }
+                            } yield foundTickets
+                    }
+                } yield foundTickets
+                test.map { foundTickets =>
+                    assertEquals(foundTickets.size, tickets.size, "Different number of tickets!")
+                    foundTickets.sortBy(_.number).zip(tickets.sortBy(_.number)).map { (found, expected) =>
+                        assertEquals(
+                            found.copy(createdAt = expected.createdAt, updatedAt = expected.updatedAt),
+                            expected
+                        )
+                    }
                 }
-                foundTickets <- createdMilestone.map(_.id).getOrElse(None) match {
-                  case None              => IO.pure(List.empty)
-                  case Some(milestoneId) => milestoneRepo.allTickets(None)(milestoneId).compile.toList
-                }
-              } yield foundTickets
-          }
-        } yield foundTickets
-        test.map { foundTickets =>
-          assertEquals(foundTickets.size, tickets.size, "Different number of tickets!")
-          foundTickets.sortBy(_.number).zip(tickets.sortBy(_.number)).map { (found, expected) =>
-            assertEquals(found.copy(createdAt = expected.createdAt, updatedAt = expected.updatedAt), expected)
-          }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("closeMilestone must set the closed flag correctly".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedMilestone)) =>
-        val milestone = generatedMilestone.copy(closed = false)
-        val project   = generatedProject.copy(owner = owner)
-        val dbConfig  = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          createdRepos <- createTicketsProject(project)
-          repoId       <- loadProjectId(owner.uid, project.name)
-          milestones <- repoId match {
-            case None => IO.pure((None, None))
-            case Some(projectId) =>
-              for {
-                _      <- milestoneRepo.createMilestone(projectId)(milestone)
-                before <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                _      <- before.flatMap(_.id).traverse(milestoneRepo.closeMilestone)
-                after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
-              } yield (before, after)
-          }
-        } yield milestones
-        test.map { result =>
-          val (before, after) = result
-          val expected        = before.map(m => milestone.copy(id = m.id))
-          assertEquals(before, expected, "Test milestone not properly initialised!")
-          assertEquals(after, before.map(_.copy(closed = true)), "Test milestone not properly closed!")
+    test("closeMilestone must set the closed flag correctly".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedMilestone)) =>
+                val milestone = generatedMilestone.copy(closed = false)
+                val project   = generatedProject.copy(owner = owner)
+                val dbConfig  = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    createdRepos <- createTicketsProject(project)
+                    repoId       <- loadProjectId(owner.uid, project.name)
+                    milestones <- repoId match {
+                        case None => IO.pure((None, None))
+                        case Some(projectId) =>
+                            for {
+                                _      <- milestoneRepo.createMilestone(projectId)(milestone)
+                                before <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                                _      <- before.flatMap(_.id).traverse(milestoneRepo.closeMilestone)
+                                after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                            } yield (before, after)
+                    }
+                } yield milestones
+                test.map { result =>
+                    val (before, after) = result
+                    val expected        = before.map(m => milestone.copy(id = m.id))
+                    assertEquals(before, expected, "Test milestone not properly initialised!")
+                    assertEquals(after, before.map(_.copy(closed = true)), "Test milestone not properly closed!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("createMilestone must create the milestone".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestone)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _                 <- createProjectOwner(owner)
-          createdRepos      <- createTicketsProject(project)
-          repoId            <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-          foundMilestone    <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
-        } yield (createdRepos, repoId, createdMilestones, foundMilestone)
-        test.map { tuple =>
-          val (createdRepos, repoId, createdMilestones, foundMilestone) = tuple
-          assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-          assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-          assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-          foundMilestone.getOrElse(None) match {
-            case None                 => fail("Created milestone not found!")
-            case Some(foundMilestone) => assertEquals(foundMilestone, milestone.copy(id = foundMilestone.id))
-          }
+    test("createMilestone must create the milestone".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestone)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _                 <- createProjectOwner(owner)
+                    createdRepos      <- createTicketsProject(project)
+                    repoId            <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                    foundMilestone    <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
+                } yield (createdRepos, repoId, createdMilestones, foundMilestone)
+                test.map { tuple =>
+                    val (createdRepos, repoId, createdMilestones, foundMilestone) = tuple
+                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                    foundMilestone.getOrElse(None) match {
+                        case None => fail("Created milestone not found!")
+                        case Some(foundMilestone) =>
+                            assertEquals(foundMilestone, milestone.copy(id = foundMilestone.id))
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("createMilestone must fail if the milestone name already exists".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestone)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _                 <- createProjectOwner(owner)
-          createdRepos      <- createTicketsProject(project)
-          repoId            <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-          _                 <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-        } yield (createdRepos, repoId, createdMilestones)
-        test.attempt.map(r => assert(r.isLeft, "Creating a milestone with a duplicate name must fail!"))
-      case _ => fail("Could not generate data samples!")
+    test("createMilestone must fail if the milestone name already exists".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestone)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _                 <- createProjectOwner(owner)
+                    createdRepos      <- createTicketsProject(project)
+                    repoId            <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                    _                 <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                } yield (createdRepos, repoId, createdMilestones)
+                test.attempt.map(r => assert(r.isLeft, "Creating a milestone with a duplicate name must fail!"))
+            case _ => fail("Could not generate data samples!")
+        }
     }
-  }
 
-  test("deleteMilestone must delete an existing milestone".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestone)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _                 <- createProjectOwner(owner)
-          createdRepos      <- createTicketsProject(project)
-          repoId            <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-          milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
-          deletedMilestones <- milestoneRepo.deleteMilestone(milestone.copy(id = milestoneId.flatMap(MilestoneId.from)))
-          foundMilestone    <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
-        } yield (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone)
-        test.map { tuple =>
-          val (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone) = tuple
-          assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-          assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-          assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-          assert(deletedMilestones === 1, "Test milestone was not deleted!")
-          assert(foundMilestone.getOrElse(None).isEmpty, "Test milestone was not deleted!")
+    test("deleteMilestone must delete an existing milestone".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestone)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _                 <- createProjectOwner(owner)
+                    createdRepos      <- createTicketsProject(project)
+                    repoId            <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                    milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
+                    deletedMilestones <- milestoneRepo.deleteMilestone(
+                        milestone.copy(id = milestoneId.flatMap(MilestoneId.from))
+                    )
+                    foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(milestone.title))
+                } yield (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone)
+                test.map { tuple =>
+                    val (createdRepos, repoId, createdMilestones, deletedMilestones, foundMilestone) = tuple
+                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                    assert(deletedMilestones === 1, "Test milestone was not deleted!")
+                    assert(foundMilestone.getOrElse(None).isEmpty, "Test milestone was not deleted!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("findMilestone must find existing milestones".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestones.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestones)) =>
-        val project           = generatedProject.copy(owner = owner)
-        val dbConfig          = configuration.database
-        val expectedMilestone = milestones(scala.util.Random.nextInt(milestones.size))
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          createdRepos <- createTicketsProject(project)
-          repoId       <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId match {
-            case None         => IO.pure(List.empty)
-            case Some(repoId) => milestones.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
-          }
-          foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(expectedMilestone.title))
-        } yield foundMilestone.flatten
-        test.map { foundMilestone =>
-          assertEquals(foundMilestone.map(_.copy(id = expectedMilestone.id)), Option(expectedMilestone))
+    test("findMilestone must find existing milestones".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestones.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestones)) =>
+                val project           = generatedProject.copy(owner = owner)
+                val dbConfig          = configuration.database
+                val expectedMilestone = milestones(scala.util.Random.nextInt(milestones.size))
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    createdRepos <- createTicketsProject(project)
+                    repoId       <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId match {
+                        case None => IO.pure(List.empty)
+                        case Some(repoId) =>
+                            milestones.traverse(milestone => milestoneRepo.createMilestone(repoId)(milestone))
+                    }
+                    foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(expectedMilestone.title))
+                } yield foundMilestone.flatten
+                test.map { foundMilestone =>
+                    assertEquals(foundMilestone.map(_.copy(id = expectedMilestone.id)), Option(expectedMilestone))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("openMilestone must reset the closed flag correctly".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedMilestone)) =>
-        val milestone = generatedMilestone.copy(closed = true)
-        val project   = generatedProject.copy(owner = owner)
-        val dbConfig  = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          createdRepos <- createTicketsProject(project)
-          repoId       <- loadProjectId(owner.uid, project.name)
-          milestones <- repoId match {
-            case None => IO.pure((None, None))
-            case Some(projectId) =>
-              for {
-                _      <- milestoneRepo.createMilestone(projectId)(milestone)
-                before <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                _      <- before.flatMap(_.id).traverse(milestoneRepo.openMilestone)
-                after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
-              } yield (before, after)
-          }
-        } yield milestones
-        test.map { result =>
-          val (before, after) = result
-          val expected        = before.map(m => milestone.copy(id = m.id))
-          assertEquals(before, expected, "Test milestone not properly initialised!")
-          assertEquals(after, before.map(_.copy(closed = false)), "Test milestone not properly opened!")
+    test("openMilestone must reset the closed flag correctly".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedMilestone)) =>
+                val milestone = generatedMilestone.copy(closed = true)
+                val project   = generatedProject.copy(owner = owner)
+                val dbConfig  = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    createdRepos <- createTicketsProject(project)
+                    repoId       <- loadProjectId(owner.uid, project.name)
+                    milestones <- repoId match {
+                        case None => IO.pure((None, None))
+                        case Some(projectId) =>
+                            for {
+                                _      <- milestoneRepo.createMilestone(projectId)(milestone)
+                                before <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                                _      <- before.flatMap(_.id).traverse(milestoneRepo.openMilestone)
+                                after  <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                            } yield (before, after)
+                    }
+                } yield milestones
+                test.map { result =>
+                    val (before, after) = result
+                    val expected        = before.map(m => milestone.copy(id = m.id))
+                    assertEquals(before, expected, "Test milestone not properly initialised!")
+                    assertEquals(after, before.map(_.copy(closed = false)), "Test milestone not properly opened!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("updateMilestone must update an existing milestone".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestone)) =>
-        val updatedMilestone = milestone.copy(
-          title = MilestoneTitle("updated milestone"),
-          description = Option(MilestoneDescription("I am an updated milestone description...")),
-          dueDate = Option(LocalDate.of(1879, 3, 14))
-        )
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _                 <- createProjectOwner(owner)
-          createdRepos      <- createTicketsProject(project)
-          repoId            <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-          milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
-          updatedMilestones <- milestoneRepo.updateMilestone(
-            updatedMilestone.copy(id = milestoneId.map(MilestoneId.apply))
-          )
-          foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(updatedMilestone.title))
-        } yield (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone.flatten)
-        test.map { tuple =>
-          val (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone) = tuple
-          assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-          assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-          assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-          assert(updatedMilestones === 1, "Test milestone was not updated!")
-          assert(foundMilestone.nonEmpty, "Updated milestone not found!")
-          foundMilestone.map { milestone =>
-            assertEquals(milestone, updatedMilestone.copy(id = milestone.id))
-          }
+    test("updateMilestone must update an existing milestone".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestone)) =>
+                val updatedMilestone = milestone.copy(
+                    title = MilestoneTitle("updated milestone"),
+                    description = Option(MilestoneDescription("I am an updated milestone description...")),
+                    dueDate = Option(LocalDate.of(1879, 3, 14))
+                )
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _                 <- createProjectOwner(owner)
+                    createdRepos      <- createTicketsProject(project)
+                    repoId            <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                    milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
+                    updatedMilestones <- milestoneRepo.updateMilestone(
+                        updatedMilestone.copy(id = milestoneId.map(MilestoneId.apply))
+                    )
+                    foundMilestone <- repoId.traverse(id => milestoneRepo.findMilestone(id)(updatedMilestone.title))
+                } yield (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone.flatten)
+                test.map { tuple =>
+                    val (createdRepos, repoId, createdMilestones, updatedMilestones, foundMilestone) = tuple
+                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                    assert(updatedMilestones === 1, "Test milestone was not updated!")
+                    assert(foundMilestone.nonEmpty, "Updated milestone not found!")
+                    foundMilestone.map { milestone =>
+                        assertEquals(milestone, updatedMilestone.copy(id = milestone.id))
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
-  test("updateMilestone must do nothing if id attribute is empty".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(milestone)) =>
-        val updatedMilestone = milestone.copy(
-          id = None,
-          title = MilestoneTitle("updated milestone"),
-          description = Option(MilestoneDescription("I am an updated milestone description...")),
-          dueDate = Option(LocalDate.of(1879, 3, 14))
-        )
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val test = for {
-          _                 <- createProjectOwner(owner)
-          createdRepos      <- createTicketsProject(project)
-          repoId            <- loadProjectId(owner.uid, project.name)
-          createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
-          milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
-          updatedMilestones <- milestoneRepo.updateMilestone(updatedMilestone)
-        } yield (createdRepos, repoId, createdMilestones, updatedMilestones)
-        test.map { tuple =>
-          val (createdRepos, repoId, createdMilestones, updatedMilestones) = tuple
-          assert(createdRepos === 1, "Test vcs generatedProject was not created!")
-          assert(repoId.nonEmpty, "No vcs generatedProject id found!")
-          assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
-          assert(updatedMilestones === 0, "Milestone with empty id must not be updated!")
+    test("updateMilestone must do nothing if id attribute is empty".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(milestone)) =>
+                val updatedMilestone = milestone.copy(
+                    id = None,
+                    title = MilestoneTitle("updated milestone"),
+                    description = Option(MilestoneDescription("I am an updated milestone description...")),
+                    dueDate = Option(LocalDate.of(1879, 3, 14))
+                )
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val test = for {
+                    _                 <- createProjectOwner(owner)
+                    createdRepos      <- createTicketsProject(project)
+                    repoId            <- loadProjectId(owner.uid, project.name)
+                    createdMilestones <- repoId.traverse(id => milestoneRepo.createMilestone(id)(milestone))
+                    milestoneId       <- findMilestoneId(owner.uid, project.name, milestone.title)
+                    updatedMilestones <- milestoneRepo.updateMilestone(updatedMilestone)
+                } yield (createdRepos, repoId, createdMilestones, updatedMilestones)
+                test.map { tuple =>
+                    val (createdRepos, repoId, createdMilestones, updatedMilestones) = tuple
+                    assert(createdRepos === 1, "Test vcs generatedProject was not created!")
+                    assert(repoId.nonEmpty, "No vcs generatedProject id found!")
+                    assert(createdMilestones.exists(_ === 1), "Test milestone was not created!")
+                    assert(updatedMilestones === 0, "Milestone with empty id must not be updated!")
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieProjectRepositoryTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieProjectRepositoryTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieProjectRepositoryTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieProjectRepositoryTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -24,199 +24,204 @@
 import doobie.*
 
 final class DoobieProjectRepositoryTest extends BaseSpec {
-  test("createProject must create a project".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample) match {
-      case (Some(owner), Some(generatedProject)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          _            <- projectRepo.createProject(project)
-          foundProject <- projectRepo.findProject(owner, project.name)
-        } yield foundProject
-        test.map { foundProject =>
-          assertEquals(foundProject, Some(project))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("deleteProject must delete a project".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample) match {
-      case (Some(owner), Some(generatedProject)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          _            <- createTicketsProject(project)
-          deleted      <- projectRepo.deleteProject(project)
-          foundProject <- projectRepo.findProject(owner, project.name)
-        } yield (deleted, foundProject)
-        test.map { result =>
-          val (deleted, foundProject) = result
-          assert(deleted > 0, "Rows not deleted from database!")
-          assert(foundProject.isEmpty, "Project not deleted from database!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findProject must return the matching project".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProjects.sample) match {
-      case (Some(owner), Some(generatedProject :: projects)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _ <- createProjectOwner(owner)
-          _ <- createTicketsProject(project)
-          _ <- projects.filterNot(_.name === project.name).traverse(p => createTicketsProject(p.copy(owner = owner)))
-          foundProject <- projectRepo.findProject(owner, project.name)
-        } yield foundProject
-        test.map { foundProject =>
-          assertEquals(foundProject, Some(project))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findProjectId must return the matching id".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProjects.sample) match {
-      case (Some(owner), Some(generatedProject :: projects)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _ <- createProjectOwner(owner)
-          _ <- createTicketsProject(project)
-          _ <- projects.filterNot(_.name === project.name).traverse(p => createTicketsProject(p.copy(owner = owner)))
-          foundProjectId <- projectRepo.findProjectId(owner, project.name)
-          projectId      <- loadProjectId(owner.uid, project.name)
-        } yield (foundProjectId, projectId)
-        test.map { result =>
-          val (foundProjectId, projectId) = result
-          assertEquals(foundProjectId, projectId)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findProjectOwner must return the matching project owner".tag(NeedsDatabase)) {
-    genProjectOwners.sample match {
-      case Some(owner :: owners) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _          <- createProjectOwner(owner)
-          _          <- owners.filterNot(_.name === owner.name).traverse(createProjectOwner)
-          foundOwner <- projectRepo.findProjectOwner(owner.name)
-        } yield foundOwner
-        test.map { foundOwner =>
-          assert(foundOwner.exists(_ === owner))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("incrementNextTicketNumber must return and increment the old value".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample) match {
-      case (Some(owner), Some(firstProject)) =>
-        val project  = firstProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- projectRepo.createProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => fail("Project was not created!")
-            case Some(projectId) =>
-              for {
-                before <- loadNextTicketNumber(projectId)
-                number <- projectRepo.incrementNextTicketNumber(projectId)
-                after  <- loadNextTicketNumber(projectId)
-              } yield (TicketNumber(before), number, TicketNumber(after))
-          }
-        } yield result
-        test.map { result =>
-          val (before, number, after) = result
-          assertEquals(before, number)
-          assertEquals(after, TicketNumber(number.toInt + 1))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("updateProject must update a project".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genProject.sample) match {
-      case (Some(owner), Some(firstProject), Some(secondProject)) =>
-        val project        = firstProject.copy(owner = owner)
-        val updatedProject = project.copy(description = secondProject.description, isPrivate = secondProject.isPrivate)
-        val dbConfig       = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val projectRepo = new DoobieProjectRepository[IO](tx)
-        val test = for {
-          _            <- createProjectOwner(owner)
-          _            <- projectRepo.createProject(project)
-          written      <- projectRepo.updateProject(updatedProject)
-          foundProject <- projectRepo.findProject(owner, project.name)
-        } yield (written, foundProject)
-        test.map { result =>
-          val (written, foundProject) = result
-          assert(written > 0, "Rows not updated in database!")
-          assertEquals(foundProject, Some(updatedProject))
+    test("createProject must create a project".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample) match {
+            case (Some(owner), Some(generatedProject)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    _            <- projectRepo.createProject(project)
+                    foundProject <- projectRepo.findProject(owner, project.name)
+                } yield foundProject
+                test.map { foundProject =>
+                    assertEquals(foundProject, Some(project))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("deleteProject must delete a project".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample) match {
+            case (Some(owner), Some(generatedProject)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    _            <- createTicketsProject(project)
+                    deleted      <- projectRepo.deleteProject(project)
+                    foundProject <- projectRepo.findProject(owner, project.name)
+                } yield (deleted, foundProject)
+                test.map { result =>
+                    val (deleted, foundProject) = result
+                    assert(deleted > 0, "Rows not deleted from database!")
+                    assert(foundProject.isEmpty, "Project not deleted from database!")
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findProject must return the matching project".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProjects.sample) match {
+            case (Some(owner), Some(generatedProject :: projects)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _ <- createProjectOwner(owner)
+                    _ <- createTicketsProject(project)
+                    _ <- projects
+                        .filterNot(_.name === project.name)
+                        .traverse(p => createTicketsProject(p.copy(owner = owner)))
+                    foundProject <- projectRepo.findProject(owner, project.name)
+                } yield foundProject
+                test.map { foundProject =>
+                    assertEquals(foundProject, Some(project))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findProjectId must return the matching id".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProjects.sample) match {
+            case (Some(owner), Some(generatedProject :: projects)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _ <- createProjectOwner(owner)
+                    _ <- createTicketsProject(project)
+                    _ <- projects
+                        .filterNot(_.name === project.name)
+                        .traverse(p => createTicketsProject(p.copy(owner = owner)))
+                    foundProjectId <- projectRepo.findProjectId(owner, project.name)
+                    projectId      <- loadProjectId(owner.uid, project.name)
+                } yield (foundProjectId, projectId)
+                test.map { result =>
+                    val (foundProjectId, projectId) = result
+                    assertEquals(foundProjectId, projectId)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findProjectOwner must return the matching project owner".tag(NeedsDatabase)) {
+        genProjectOwners.sample match {
+            case Some(owner :: owners) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _          <- createProjectOwner(owner)
+                    _          <- owners.filterNot(_.name === owner.name).traverse(createProjectOwner)
+                    foundOwner <- projectRepo.findProjectOwner(owner.name)
+                } yield foundOwner
+                test.map { foundOwner =>
+                    assert(foundOwner.exists(_ === owner))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("incrementNextTicketNumber must return and increment the old value".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample) match {
+            case (Some(owner), Some(firstProject)) =>
+                val project  = firstProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- projectRepo.createProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => fail("Project was not created!")
+                        case Some(projectId) =>
+                            for {
+                                before <- loadNextTicketNumber(projectId)
+                                number <- projectRepo.incrementNextTicketNumber(projectId)
+                                after  <- loadNextTicketNumber(projectId)
+                            } yield (TicketNumber(before), number, TicketNumber(after))
+                    }
+                } yield result
+                test.map { result =>
+                    val (before, number, after) = result
+                    assertEquals(before, number)
+                    assertEquals(after, TicketNumber(number.toInt + 1))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("updateProject must update a project".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genProject.sample) match {
+            case (Some(owner), Some(firstProject), Some(secondProject)) =>
+                val project = firstProject.copy(owner = owner)
+                val updatedProject =
+                    project.copy(description = secondProject.description, isPrivate = secondProject.isPrivate)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val projectRepo = new DoobieProjectRepository[IO](tx)
+                val test = for {
+                    _            <- createProjectOwner(owner)
+                    _            <- projectRepo.createProject(project)
+                    written      <- projectRepo.updateProject(updatedProject)
+                    foundProject <- projectRepo.findProject(owner, project.name)
+                } yield (written, foundProject)
+                test.map { result =>
+                    val (written, foundProject) = result
+                    assert(written > 0, "Rows not updated in database!")
+                    assertEquals(foundProject, Some(updatedProject))
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketRepositoryTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketRepositoryTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketRepositoryTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketRepositoryTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -29,831 +29,871 @@
 
 final class DoobieTicketRepositoryTest extends BaseSpec {
 
-  /** Return the internal ids of all lables associated with the given ticket number and project id.
-    *
-    * @param projectId
-    *   The unique internal project id.
-    * @param ticketNumber
-    *   The ticket number.
-    * @return
-    *   A list of label ids that may be empty.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.var", "DisableSyntax.while"))
-  protected def loadTicketLabelIds(projectId: ProjectId, ticketNumber: TicketNumber): IO[List[LabelId]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT label FROM "tickets"."ticket_labels" AS "ticket_labels" JOIN "tickets"."tickets" ON "ticket_labels".ticket = "tickets".id WHERE "tickets".project = ? AND "tickets".number = ?"""
-          )
-        )
-        _      <- IO.delay(statement.setLong(1, projectId.toLong))
-        _      <- IO.delay(statement.setInt(2, ticketNumber.toInt))
-        result <- IO.delay(statement.executeQuery)
-        labelIds <- IO.delay {
-          var queue = Queue.empty[LabelId]
-          while (result.next())
-            queue = queue :+ LabelId(result.getLong("label"))
-          queue.toList
-        }
-        _ <- IO(statement.close())
-      } yield labelIds
-    }
-
-  /** Return the internal ids of all milestones associated with the given ticket number and project id.
-    *
-    * @param projectId
-    *   The unique internal project id.
-    * @param ticketNumber
-    *   The ticket number.
-    * @return
-    *   A list of milestone ids that may be empty.
-    */
-  @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
-  @SuppressWarnings(Array("DisableSyntax.var", "DisableSyntax.while"))
-  protected def loadTicketMilestoneIds(projectId: ProjectId, ticketNumber: TicketNumber): IO[List[MilestoneId]] =
-    connectToDb(configuration).use { con =>
-      for {
-        statement <- IO.delay(
-          con.prepareStatement(
-            """SELECT milestone FROM "tickets"."milestone_tickets" AS "milestone_tickets" JOIN "tickets"."tickets" ON "milestone_tickets".ticket = "tickets".id WHERE "tickets".project = ? AND "tickets".number = ?"""
-          )
-        )
-        _      <- IO.delay(statement.setLong(1, projectId.toLong))
-        _      <- IO.delay(statement.setInt(2, ticketNumber.toInt))
-        result <- IO.delay(statement.executeQuery)
-        milestoneIds <- IO.delay {
-          var queue = Queue.empty[MilestoneId]
-          while (result.next())
-            queue = queue :+ MilestoneId(result.getLong("milestone"))
-          queue.toList
-        }
-        _ <- IO(statement.close())
-      } yield milestoneIds
-    }
-
-  test("addAssignee must save the assignee relation to the database".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicketsUser.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(user)) =>
-        val assignee = Assignee(AssigneeId(user.uid.toUUID), AssigneeName(user.name.toString))
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsUser(user)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          _         <- ticket.submitter.traverse(createTicketsSubmitter)
-          _         <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
-          _         <- projectId.traverse(projectId => ticketRepo.addAssignee(projectId)(ticket.number)(assignee))
-          foundAssignees <- projectId.traverse(projectId =>
-            ticketRepo.loadAssignees(projectId)(ticket.number).compile.toList
-          )
-        } yield foundAssignees.getOrElse(Nil)
-        test.map { foundAssignees =>
-          assertEquals(foundAssignees, List(assignee))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("addLabel must save the label relation to the database".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(label)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo  = new DoobieLabelRepository[IO](tx)
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _            <- ticket.submitter.traverse(createTicketsSubmitter)
-                _            <- labelRepo.createLabel(projectId)(label)
-                createdLabel <- labelRepo.findLabel(projectId)(label.name)
-                _            <- ticketRepo.createTicket(projectId)(ticket)
-                _            <- createdLabel.traverse(cl => ticketRepo.addLabel(projectId)(ticket.number)(cl))
-                foundLabels  <- loadTicketLabelIds(projectId, ticket.number)
-              } yield (createdLabel, foundLabels)
-          }
-        } yield result
-        test.map { result =>
-          val (createdLabel, foundLabels) = result
-          assert(createdLabel.nonEmpty, "Test label not created!")
-          createdLabel.flatMap(_.id) match {
-            case None          => fail("Test label has no ID!")
-            case Some(labelId) => assert(foundLabels.exists(_ === labelId))
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("addMilestone must save the milestone relation to the database".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(milestone)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val ticketRepo    = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _                <- ticket.submitter.traverse(createTicketsSubmitter)
-                _                <- milestoneRepo.createMilestone(projectId)(milestone)
-                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                _                <- ticketRepo.createTicket(projectId)(ticket)
-                _ <- createdMilestone.traverse(cl => ticketRepo.addMilestone(projectId)(ticket.number)(cl))
-                foundMilestones <- loadTicketMilestoneIds(projectId, ticket.number)
-              } yield (createdMilestone, foundMilestones)
-          }
-        } yield result
-        test.map { result =>
-          val (createdMilestone, foundMilestones) = result
-          assert(createdMilestone.nonEmpty, "Test milestone not created!")
-          createdMilestone.flatMap(_.id) match {
-            case None              => fail("Test milestone has no ID!")
-            case Some(milestoneId) => assert(foundMilestones.exists(_ === milestoneId))
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("allTickets must return all tickets for the project".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
-        val defaultTimestamp = OffsetDateTime.now()
-        val tickets =
-          generatedTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
-        val submitters = tickets.map(_.submitter).flatten
-        val project    = generatedProject.copy(owner = owner)
-        val dbConfig   = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => IO.pure((0, Nil))
-            case Some(projectId) =>
-              for {
-                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                foundTickets   <- ticketRepo.allTickets(filter = None)(projectId).compile.toList
-              } yield (writtenTickets.sum, foundTickets)
-          }
-        } yield result
-        test.map { result =>
-          val (writtenTickets, foundTickets) = result
-          assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
-          assertEquals(
-            foundTickets.size,
-            writtenTickets,
-            "Number of returned tickets differs from number of created tickets!"
-          )
-          assertEquals(
-            foundTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
-            tickets.sortBy(_.number)
-          )
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("allTickets must respect given filters for numbers".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
-        val defaultTimestamp = OffsetDateTime.now()
-        val tickets =
-          generatedTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
-        val expectedTickets = tickets.take(tickets.size / 2)
-        val filter          = TicketFilter(number = expectedTickets.map(_.number), Nil, Nil, Nil)
-        val submitters      = tickets.map(_.submitter).flatten
-        val project         = generatedProject.copy(owner = owner)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => IO.pure((0, Nil))
-            case Some(projectId) =>
-              for {
-                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
-              } yield (writtenTickets.sum, foundTickets)
-          }
-        } yield result
-        test.map { result =>
-          val (writtenTickets, foundTickets) = result
-          assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
-          assertEquals(
-            foundTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
-            expectedTickets.sortBy(_.number)
-          )
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("allTickets must respect given filters for status".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
-        val defaultTimestamp = OffsetDateTime.now()
-        val tickets =
-          generatedTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
-        val statusFlags     = tickets.map(_.status).distinct.take(2)
-        val expectedTickets = tickets.filter(t => statusFlags.exists(_ === t.status))
-        val filter          = TicketFilter(Nil, status = statusFlags, Nil, Nil)
-        val submitters      = tickets.map(_.submitter).flatten
-        val project         = generatedProject.copy(owner = owner)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => IO.pure((0, Nil))
-            case Some(projectId) =>
-              for {
-                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
-              } yield (writtenTickets.sum, foundTickets)
-          }
-        } yield result
-        test.map { result =>
-          val (writtenTickets, foundTickets) = result
-          assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
-          assertEquals(
-            foundTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
-            expectedTickets.sortBy(_.number)
-          )
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("allTickets must respect given filters for resolution".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
-        val defaultTimestamp = OffsetDateTime.now()
-        val tickets =
-          generatedTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
-        val resolutions     = tickets.map(_.resolution).flatten.distinct.take(2)
-        val expectedTickets = tickets.filter(t => t.resolution.exists(r => resolutions.exists(_ === r)))
-        val filter          = TicketFilter(Nil, Nil, resolution = resolutions, Nil)
-        val submitters      = tickets.map(_.submitter).flatten
-        val project         = generatedProject.copy(owner = owner)
-        val dbConfig        = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => IO.pure((0, Nil))
-            case Some(projectId) =>
-              for {
-                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
-              } yield (writtenTickets.sum, foundTickets)
-          }
-        } yield result
-        test.map { result =>
-          val (writtenTickets, foundTickets) = result
-          assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
-          assertEquals(
-            foundTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
-            expectedTickets.sortBy(_.number)
-          )
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("allTickets must respect given filters for submitter".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
-        val defaultTimestamp = OffsetDateTime.now()
-        val tickets =
-          generatedTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
-        val submitters       = tickets.map(_.submitter).flatten
-        val wantedSubmitters = submitters.take(submitters.size / 2)
-        val expectedTickets  = tickets.filter(t => t.submitter.exists(s => wantedSubmitters.exists(_ === s)))
-        val filter           = TicketFilter(Nil, Nil, Nil, submitter = wantedSubmitters.map(_.name))
-        val project          = generatedProject.copy(owner = owner)
-        val dbConfig         = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => IO.pure((0, Nil))
-            case Some(projectId) =>
-              for {
-                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
-              } yield (writtenTickets.sum, foundTickets)
-          }
-        } yield result
-        test.map { result =>
-          val (writtenTickets, foundTickets) = result
-          assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
-          assertEquals(
-            foundTickets.sortBy(_.number).map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
-            expectedTickets.sortBy(_.number)
-          )
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("createTicket must save the ticket to the database".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _           <- createProjectOwner(owner)
-          _           <- ticket.submitter.traverse(createTicketsSubmitter)
-          _           <- createTicketsProject(project)
-          projectId   <- loadProjectId(owner.uid, project.name)
-          _           <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
-          foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(ticket.number))
-        } yield foundTicket.getOrElse(None)
-        test.map { foundTicket =>
-          foundTicket match {
-            case None => fail("Created ticket not found!")
-            case Some(foundTicket) =>
-              assertEquals(
-                foundTicket,
-                ticket.copy(createdAt = foundTicket.createdAt, updatedAt = foundTicket.updatedAt)
-              )
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("deleteTicket must remove the ticket from the database".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _           <- createProjectOwner(owner)
-          _           <- ticket.submitter.traverse(createTicketsSubmitter)
-          _           <- createTicketsProject(project)
-          projectId   <- loadProjectId(owner.uid, project.name)
-          _           <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
-          _           <- projectId.traverse(projectId => ticketRepo.deleteTicket(projectId)(ticket))
-          foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(ticket.number))
-        } yield foundTicket.getOrElse(None)
-        test.map { foundTicket =>
-          assertEquals(foundTicket, None, "Ticket was not deleted from database!")
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findTicket must find existing tickets".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(tickets)) =>
-        val expectedTicket = tickets(scala.util.Random.nextInt(tickets.size))
-        val submitters     = tickets.map(_.submitter).flatten
-        val project        = generatedProject.copy(owner = owner)
-        val dbConfig       = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          _ <- projectId match {
-            case None            => IO.pure(Nil)
-            case Some(projectId) => tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-          }
-          foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(expectedTicket.number))
-        } yield foundTicket.getOrElse(None)
-        test.map { foundTicket =>
-          foundTicket match {
-            case None => fail("Ticket not found!")
-            case Some(foundTicket) =>
-              assertEquals(
-                foundTicket,
-                expectedTicket.copy(createdAt = foundTicket.createdAt, updatedAt = foundTicket.updatedAt)
-              )
-          }
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("findTicketId must find the unique internal id of existing tickets".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
-      case (Some(owner), Some(generatedProject), Some(tickets)) =>
-        val expectedTicket = tickets(scala.util.Random.nextInt(tickets.size))
-        val submitters     = tickets.map(_.submitter).flatten
-        val project        = generatedProject.copy(owner = owner)
-        val dbConfig       = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- submitters.traverse(createTicketsSubmitter)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => IO.pure((None, None))
-            case Some(projectId) =>
-              for {
-                _                <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
-                expectedTicketId <- loadTicketId(projectId, expectedTicket.number)
-                foundTicketId    <- ticketRepo.findTicketId(projectId)(expectedTicket.number)
-              } yield (expectedTicketId, foundTicketId)
-          }
-        } yield result
-        test.map { result =>
-          val (expectedTicketId, foundTicketId) = result
-          assert(expectedTicketId.nonEmpty, "Expected ticket id not found!")
-          assertEquals(foundTicketId, expectedTicketId)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("loadAssignees must return all assignees of a ticket".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicketsUsers.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(users)) =>
-        val assignees = users.map(user => Assignee(AssigneeId(user.uid.toUUID), AssigneeName(user.name.toString)))
-        val project   = generatedProject.copy(owner = owner)
-        val dbConfig  = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- users.traverse(createTicketsUser)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          _         <- ticket.submitter.traverse(createTicketsSubmitter)
-          foundAssignees <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _ <- ticketRepo.createTicket(projectId)(ticket)
-                _ <- assignees.traverse(assignee => ticketRepo.addAssignee(projectId)(ticket.number)(assignee))
-                foundAssignees <- ticketRepo.loadAssignees(projectId)(ticket.number).compile.toList
-              } yield foundAssignees
-          }
-        } yield foundAssignees
-        test.map { foundAssignees =>
-          assertEquals(foundAssignees.sortBy(_.name), assignees.sortBy(_.name))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("loadLabels must return all labels of a ticket".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genLabels.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(labels)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo  = new DoobieLabelRepository[IO](tx)
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _             <- ticket.submitter.traverse(createTicketsSubmitter)
-                _             <- ticketRepo.createTicket(projectId)(ticket)
-                _             <- labels.traverse(label => labelRepo.createLabel(projectId)(label))
-                createdLabels <- labelRepo.allLabels(projectId).compile.toList
-                _             <- createdLabels.traverse(cl => ticketRepo.addLabel(projectId)(ticket.number)(cl))
-                foundLabels   <- ticketRepo.loadLabels(projectId)(ticket.number).compile.toList
-              } yield (createdLabels, foundLabels)
-          }
-        } yield result
-        test.map { result =>
-          val (createdLabels, foundLabels) = result
-          assertEquals(foundLabels.sortBy(_.id), createdLabels.sortBy(_.id))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("loadMilestones must return all milestones of a ticket".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genMilestones.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(milestones)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val ticketRepo    = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          result <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _ <- ticket.submitter.traverse(createTicketsSubmitter)
-                _ <- ticketRepo.createTicket(projectId)(ticket)
-                _ <- milestones.traverse(milestone => milestoneRepo.createMilestone(projectId)(milestone))
-                createdMilestones <- milestoneRepo.allMilestones(projectId).compile.toList
-                _ <- createdMilestones.traverse(cm => ticketRepo.addMilestone(projectId)(ticket.number)(cm))
-                foundMilestones <- ticketRepo.loadMilestones(projectId)(ticket.number).compile.toList
-              } yield (createdMilestones, foundMilestones)
-          }
-        } yield result
-        test.map { result =>
-          val (createdMilestones, foundMilestones) = result
-          assertEquals(foundMilestones.sortBy(_.title), createdMilestones.sortBy(_.title))
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("removeAssignee must remove the assignees from the ticket".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicketsUser.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(user)) =>
-        val assignee = Assignee(AssigneeId(user.uid.toUUID), AssigneeName(user.name.toString))
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsUser(user)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          _         <- ticket.submitter.traverse(createTicketsSubmitter)
-          foundAssignees <- projectId match {
-            case None => IO.pure(Nil)
-            case Some(projectId) =>
-              for {
-                _              <- ticketRepo.createTicket(projectId)(ticket)
-                _              <- ticketRepo.addAssignee(projectId)(ticket.number)(assignee)
-                _              <- ticketRepo.removeAssignee(projectId)(ticket)(assignee)
-                foundAssignees <- ticketRepo.loadAssignees(projectId)(ticket.number).compile.toList
-              } yield foundAssignees
-          }
-        } yield foundAssignees
-        test.map { foundAssignees =>
-          assertEquals(foundAssignees, Nil)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("removeLabel must remove the label from the ticket".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genLabel.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(label)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val labelRepo  = new DoobieLabelRepository[IO](tx)
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          foundLabels <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _            <- ticket.submitter.traverse(createTicketsSubmitter)
-                _            <- labelRepo.createLabel(projectId)(label)
-                createdLabel <- labelRepo.findLabel(projectId)(label.name)
-                _            <- ticketRepo.createTicket(projectId)(ticket)
-                _            <- createdLabel.traverse(cl => ticketRepo.addLabel(projectId)(ticket.number)(cl))
-                _            <- createdLabel.traverse(cl => ticketRepo.removeLabel(projectId)(ticket)(cl))
-                foundLabels  <- loadTicketLabelIds(projectId, ticket.number)
-              } yield foundLabels
-          }
-        } yield foundLabels
-        test.map { foundLabels =>
-          assertEquals(foundLabels, Nil)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("removeMilestone must remove the milestone from the ticket".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genMilestone.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(milestone)) =>
-        val project  = generatedProject.copy(owner = owner)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
-        val ticketRepo    = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _         <- createProjectOwner(owner)
-          _         <- createTicketsProject(project)
-          projectId <- loadProjectId(owner.uid, project.name)
-          foundMilestones <- projectId match {
-            case None => fail("Project ID not found in database!")
-            case Some(projectId) =>
-              for {
-                _                <- ticket.submitter.traverse(createTicketsSubmitter)
-                _                <- milestoneRepo.createMilestone(projectId)(milestone)
-                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
-                _                <- ticketRepo.createTicket(projectId)(ticket)
-                _ <- createdMilestone.traverse(ms => ticketRepo.addMilestone(projectId)(ticket.number)(ms))
-                _ <- createdMilestone.traverse(ms => ticketRepo.removeMilestone(projectId)(ticket)(ms))
-                foundMilestones <- loadTicketMilestoneIds(projectId, ticket.number)
-              } yield foundMilestones
-          }
-        } yield foundMilestones
-        test.map { foundMilestones =>
-          assertEquals(foundMilestones, Nil)
-        }
-      case _ => fail("Could not generate data samples!")
-    }
-  }
-
-  test("updateTicket must update the ticket in the database".tag(NeedsDatabase)) {
-    (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicket.sample) match {
-      case (Some(owner), Some(generatedProject), Some(ticket), Some(anotherTicket)) =>
-        val project = generatedProject.copy(owner = owner)
-        val updatedTicket =
-          ticket.copy(title = anotherTicket.title, content = anotherTicket.content, submitter = anotherTicket.submitter)
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val ticketRepo = new DoobieTicketRepository[IO](tx)
-        val test = for {
-          _           <- createProjectOwner(owner)
-          _           <- ticket.submitter.traverse(createTicketsSubmitter)
-          _           <- updatedTicket.submitter.traverse(createTicketsSubmitter)
-          _           <- createTicketsProject(project)
-          projectId   <- loadProjectId(owner.uid, project.name)
-          _           <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
-          _           <- projectId.traverse(projectId => ticketRepo.updateTicket(projectId)(updatedTicket))
-          foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(ticket.number))
-        } yield foundTicket.getOrElse(None)
-        test.map { foundTicket =>
-          foundTicket match {
-            case None => fail("Created ticket not found!")
-            case Some(foundTicket) =>
-              assertEquals(
-                foundTicket,
-                updatedTicket.copy(createdAt = foundTicket.createdAt, updatedAt = foundTicket.updatedAt)
-              )
-          }
+    /** Return the internal ids of all lables associated with the given ticket number and project id.
+      *
+      * @param projectId
+      *   The unique internal project id.
+      * @param ticketNumber
+      *   The ticket number.
+      * @return
+      *   A list of label ids that may be empty.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.var", "DisableSyntax.while"))
+    protected def loadTicketLabelIds(projectId: ProjectId, ticketNumber: TicketNumber): IO[List[LabelId]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT label FROM "tickets"."ticket_labels" AS "ticket_labels" JOIN "tickets"."tickets" ON "ticket_labels".ticket = "tickets".id WHERE "tickets".project = ? AND "tickets".number = ?"""
+                    )
+                )
+                _      <- IO.delay(statement.setLong(1, projectId.toLong))
+                _      <- IO.delay(statement.setInt(2, ticketNumber.toInt))
+                result <- IO.delay(statement.executeQuery)
+                labelIds <- IO.delay {
+                    var queue = Queue.empty[LabelId]
+                    while (result.next())
+                        queue = queue :+ LabelId(result.getLong("label"))
+                    queue.toList
+                }
+                _ <- IO(statement.close())
+            } yield labelIds
+        }
+
+    /** Return the internal ids of all milestones associated with the given ticket number and project id.
+      *
+      * @param projectId
+      *   The unique internal project id.
+      * @param ticketNumber
+      *   The ticket number.
+      * @return
+      *   A list of milestone ids that may be empty.
+      */
+    @throws[java.sql.SQLException]("Errors from the underlying SQL procedures will throw exceptions!")
+    @SuppressWarnings(Array("DisableSyntax.var", "DisableSyntax.while"))
+    protected def loadTicketMilestoneIds(projectId: ProjectId, ticketNumber: TicketNumber): IO[List[MilestoneId]] =
+        connectToDb(configuration).use { con =>
+            for {
+                statement <- IO.delay(
+                    con.prepareStatement(
+                        """SELECT milestone FROM "tickets"."milestone_tickets" AS "milestone_tickets" JOIN "tickets"."tickets" ON "milestone_tickets".ticket = "tickets".id WHERE "tickets".project = ? AND "tickets".number = ?"""
+                    )
+                )
+                _      <- IO.delay(statement.setLong(1, projectId.toLong))
+                _      <- IO.delay(statement.setInt(2, ticketNumber.toInt))
+                result <- IO.delay(statement.executeQuery)
+                milestoneIds <- IO.delay {
+                    var queue = Queue.empty[MilestoneId]
+                    while (result.next())
+                        queue = queue :+ MilestoneId(result.getLong("milestone"))
+                    queue.toList
+                }
+                _ <- IO(statement.close())
+            } yield milestoneIds
+        }
+
+    test("addAssignee must save the assignee relation to the database".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicketsUser.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(user)) =>
+                val assignee = Assignee(AssigneeId(user.uid.toUUID), AssigneeName(user.name.toString))
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsUser(user)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    _         <- ticket.submitter.traverse(createTicketsSubmitter)
+                    _         <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
+                    _ <- projectId.traverse(projectId => ticketRepo.addAssignee(projectId)(ticket.number)(assignee))
+                    foundAssignees <- projectId.traverse(projectId =>
+                        ticketRepo.loadAssignees(projectId)(ticket.number).compile.toList
+                    )
+                } yield foundAssignees.getOrElse(Nil)
+                test.map { foundAssignees =>
+                    assertEquals(foundAssignees, List(assignee))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("addLabel must save the label relation to the database".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(label)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo  = new DoobieLabelRepository[IO](tx)
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _            <- ticket.submitter.traverse(createTicketsSubmitter)
+                                _            <- labelRepo.createLabel(projectId)(label)
+                                createdLabel <- labelRepo.findLabel(projectId)(label.name)
+                                _            <- ticketRepo.createTicket(projectId)(ticket)
+                                _ <- createdLabel.traverse(cl => ticketRepo.addLabel(projectId)(ticket.number)(cl))
+                                foundLabels <- loadTicketLabelIds(projectId, ticket.number)
+                            } yield (createdLabel, foundLabels)
+                    }
+                } yield result
+                test.map { result =>
+                    val (createdLabel, foundLabels) = result
+                    assert(createdLabel.nonEmpty, "Test label not created!")
+                    createdLabel.flatMap(_.id) match {
+                        case None          => fail("Test label has no ID!")
+                        case Some(labelId) => assert(foundLabels.exists(_ === labelId))
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("addMilestone must save the milestone relation to the database".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(milestone)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val ticketRepo    = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _                <- ticket.submitter.traverse(createTicketsSubmitter)
+                                _                <- milestoneRepo.createMilestone(projectId)(milestone)
+                                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                                _                <- ticketRepo.createTicket(projectId)(ticket)
+                                _ <- createdMilestone.traverse(cl =>
+                                    ticketRepo.addMilestone(projectId)(ticket.number)(cl)
+                                )
+                                foundMilestones <- loadTicketMilestoneIds(projectId, ticket.number)
+                            } yield (createdMilestone, foundMilestones)
+                    }
+                } yield result
+                test.map { result =>
+                    val (createdMilestone, foundMilestones) = result
+                    assert(createdMilestone.nonEmpty, "Test milestone not created!")
+                    createdMilestone.flatMap(_.id) match {
+                        case None              => fail("Test milestone has no ID!")
+                        case Some(milestoneId) => assert(foundMilestones.exists(_ === milestoneId))
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("allTickets must return all tickets for the project".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
+                val defaultTimestamp = OffsetDateTime.now()
+                val tickets =
+                    generatedTickets
+                        .sortBy(_.number)
+                        .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
+                val submitters = tickets.map(_.submitter).flatten
+                val project    = generatedProject.copy(owner = owner)
+                val dbConfig   = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => IO.pure((0, Nil))
+                        case Some(projectId) =>
+                            for {
+                                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                foundTickets   <- ticketRepo.allTickets(filter = None)(projectId).compile.toList
+                            } yield (writtenTickets.sum, foundTickets)
+                    }
+                } yield result
+                test.map { result =>
+                    val (writtenTickets, foundTickets) = result
+                    assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
+                    assertEquals(
+                        foundTickets.size,
+                        writtenTickets,
+                        "Number of returned tickets differs from number of created tickets!"
+                    )
+                    assertEquals(
+                        foundTickets
+                            .sortBy(_.number)
+                            .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
+                        tickets.sortBy(_.number)
+                    )
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("allTickets must respect given filters for numbers".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
+                val defaultTimestamp = OffsetDateTime.now()
+                val tickets =
+                    generatedTickets
+                        .sortBy(_.number)
+                        .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
+                val expectedTickets = tickets.take(tickets.size / 2)
+                val filter          = TicketFilter(number = expectedTickets.map(_.number), Nil, Nil, Nil)
+                val submitters      = tickets.map(_.submitter).flatten
+                val project         = generatedProject.copy(owner = owner)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => IO.pure((0, Nil))
+                        case Some(projectId) =>
+                            for {
+                                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
+                            } yield (writtenTickets.sum, foundTickets)
+                    }
+                } yield result
+                test.map { result =>
+                    val (writtenTickets, foundTickets) = result
+                    assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
+                    assertEquals(
+                        foundTickets
+                            .sortBy(_.number)
+                            .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
+                        expectedTickets.sortBy(_.number)
+                    )
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("allTickets must respect given filters for status".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
+                val defaultTimestamp = OffsetDateTime.now()
+                val tickets =
+                    generatedTickets
+                        .sortBy(_.number)
+                        .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
+                val statusFlags     = tickets.map(_.status).distinct.take(2)
+                val expectedTickets = tickets.filter(t => statusFlags.exists(_ === t.status))
+                val filter          = TicketFilter(Nil, status = statusFlags, Nil, Nil)
+                val submitters      = tickets.map(_.submitter).flatten
+                val project         = generatedProject.copy(owner = owner)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => IO.pure((0, Nil))
+                        case Some(projectId) =>
+                            for {
+                                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
+                            } yield (writtenTickets.sum, foundTickets)
+                    }
+                } yield result
+                test.map { result =>
+                    val (writtenTickets, foundTickets) = result
+                    assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
+                    assertEquals(
+                        foundTickets
+                            .sortBy(_.number)
+                            .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
+                        expectedTickets.sortBy(_.number)
+                    )
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("allTickets must respect given filters for resolution".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
+                val defaultTimestamp = OffsetDateTime.now()
+                val tickets =
+                    generatedTickets
+                        .sortBy(_.number)
+                        .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
+                val resolutions     = tickets.map(_.resolution).flatten.distinct.take(2)
+                val expectedTickets = tickets.filter(t => t.resolution.exists(r => resolutions.exists(_ === r)))
+                val filter          = TicketFilter(Nil, Nil, resolution = resolutions, Nil)
+                val submitters      = tickets.map(_.submitter).flatten
+                val project         = generatedProject.copy(owner = owner)
+                val dbConfig        = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => IO.pure((0, Nil))
+                        case Some(projectId) =>
+                            for {
+                                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
+                            } yield (writtenTickets.sum, foundTickets)
+                    }
+                } yield result
+                test.map { result =>
+                    val (writtenTickets, foundTickets) = result
+                    assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
+                    assertEquals(
+                        foundTickets
+                            .sortBy(_.number)
+                            .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
+                        expectedTickets.sortBy(_.number)
+                    )
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("allTickets must respect given filters for submitter".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(generatedTickets)) =>
+                val defaultTimestamp = OffsetDateTime.now()
+                val tickets =
+                    generatedTickets
+                        .sortBy(_.number)
+                        .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp))
+                val submitters       = tickets.map(_.submitter).flatten
+                val wantedSubmitters = submitters.take(submitters.size / 2)
+                val expectedTickets  = tickets.filter(t => t.submitter.exists(s => wantedSubmitters.exists(_ === s)))
+                val filter           = TicketFilter(Nil, Nil, Nil, submitter = wantedSubmitters.map(_.name))
+                val project          = generatedProject.copy(owner = owner)
+                val dbConfig         = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => IO.pure((0, Nil))
+                        case Some(projectId) =>
+                            for {
+                                writtenTickets <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                foundTickets   <- ticketRepo.allTickets(filter.some)(projectId).compile.toList
+                            } yield (writtenTickets.sum, foundTickets)
+                    }
+                } yield result
+                test.map { result =>
+                    val (writtenTickets, foundTickets) = result
+                    assertEquals(writtenTickets, tickets.size, "Wrong number of tickets created in the database!")
+                    assertEquals(
+                        foundTickets
+                            .sortBy(_.number)
+                            .map(_.copy(createdAt = defaultTimestamp, updatedAt = defaultTimestamp)),
+                        expectedTickets.sortBy(_.number)
+                    )
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("createTicket must save the ticket to the database".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _           <- createProjectOwner(owner)
+                    _           <- ticket.submitter.traverse(createTicketsSubmitter)
+                    _           <- createTicketsProject(project)
+                    projectId   <- loadProjectId(owner.uid, project.name)
+                    _           <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
+                    foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(ticket.number))
+                } yield foundTicket.getOrElse(None)
+                test.map { foundTicket =>
+                    foundTicket match {
+                        case None => fail("Created ticket not found!")
+                        case Some(foundTicket) =>
+                            assertEquals(
+                                foundTicket,
+                                ticket.copy(createdAt = foundTicket.createdAt, updatedAt = foundTicket.updatedAt)
+                            )
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("deleteTicket must remove the ticket from the database".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _           <- createProjectOwner(owner)
+                    _           <- ticket.submitter.traverse(createTicketsSubmitter)
+                    _           <- createTicketsProject(project)
+                    projectId   <- loadProjectId(owner.uid, project.name)
+                    _           <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
+                    _           <- projectId.traverse(projectId => ticketRepo.deleteTicket(projectId)(ticket))
+                    foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(ticket.number))
+                } yield foundTicket.getOrElse(None)
+                test.map { foundTicket =>
+                    assertEquals(foundTicket, None, "Ticket was not deleted from database!")
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findTicket must find existing tickets".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(tickets)) =>
+                val expectedTicket = tickets(scala.util.Random.nextInt(tickets.size))
+                val submitters     = tickets.map(_.submitter).flatten
+                val project        = generatedProject.copy(owner = owner)
+                val dbConfig       = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    _ <- projectId match {
+                        case None            => IO.pure(Nil)
+                        case Some(projectId) => tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                    }
+                    foundTicket <- projectId.traverse(projectId =>
+                        ticketRepo.findTicket(projectId)(expectedTicket.number)
+                    )
+                } yield foundTicket.getOrElse(None)
+                test.map { foundTicket =>
+                    foundTicket match {
+                        case None => fail("Ticket not found!")
+                        case Some(foundTicket) =>
+                            assertEquals(
+                                foundTicket,
+                                expectedTicket.copy(
+                                    createdAt = foundTicket.createdAt,
+                                    updatedAt = foundTicket.updatedAt
+                                )
+                            )
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("findTicketId must find the unique internal id of existing tickets".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTickets.sample) match {
+            case (Some(owner), Some(generatedProject), Some(tickets)) =>
+                val expectedTicket = tickets(scala.util.Random.nextInt(tickets.size))
+                val submitters     = tickets.map(_.submitter).flatten
+                val project        = generatedProject.copy(owner = owner)
+                val dbConfig       = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- submitters.traverse(createTicketsSubmitter)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => IO.pure((None, None))
+                        case Some(projectId) =>
+                            for {
+                                _ <- tickets.traverse(ticket => ticketRepo.createTicket(projectId)(ticket))
+                                expectedTicketId <- loadTicketId(projectId, expectedTicket.number)
+                                foundTicketId    <- ticketRepo.findTicketId(projectId)(expectedTicket.number)
+                            } yield (expectedTicketId, foundTicketId)
+                    }
+                } yield result
+                test.map { result =>
+                    val (expectedTicketId, foundTicketId) = result
+                    assert(expectedTicketId.nonEmpty, "Expected ticket id not found!")
+                    assertEquals(foundTicketId, expectedTicketId)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("loadAssignees must return all assignees of a ticket".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicketsUsers.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(users)) =>
+                val assignees =
+                    users.map(user => Assignee(AssigneeId(user.uid.toUUID), AssigneeName(user.name.toString)))
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- users.traverse(createTicketsUser)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    _         <- ticket.submitter.traverse(createTicketsSubmitter)
+                    foundAssignees <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _ <- ticketRepo.createTicket(projectId)(ticket)
+                                _ <- assignees.traverse(assignee =>
+                                    ticketRepo.addAssignee(projectId)(ticket.number)(assignee)
+                                )
+                                foundAssignees <- ticketRepo.loadAssignees(projectId)(ticket.number).compile.toList
+                            } yield foundAssignees
+                    }
+                } yield foundAssignees
+                test.map { foundAssignees =>
+                    assertEquals(foundAssignees.sortBy(_.name), assignees.sortBy(_.name))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("loadLabels must return all labels of a ticket".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genLabels.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(labels)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo  = new DoobieLabelRepository[IO](tx)
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _             <- ticket.submitter.traverse(createTicketsSubmitter)
+                                _             <- ticketRepo.createTicket(projectId)(ticket)
+                                _             <- labels.traverse(label => labelRepo.createLabel(projectId)(label))
+                                createdLabels <- labelRepo.allLabels(projectId).compile.toList
+                                _ <- createdLabels.traverse(cl => ticketRepo.addLabel(projectId)(ticket.number)(cl))
+                                foundLabels <- ticketRepo.loadLabels(projectId)(ticket.number).compile.toList
+                            } yield (createdLabels, foundLabels)
+                    }
+                } yield result
+                test.map { result =>
+                    val (createdLabels, foundLabels) = result
+                    assertEquals(foundLabels.sortBy(_.id), createdLabels.sortBy(_.id))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("loadMilestones must return all milestones of a ticket".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genMilestones.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(milestones)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val ticketRepo    = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    result <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _ <- ticket.submitter.traverse(createTicketsSubmitter)
+                                _ <- ticketRepo.createTicket(projectId)(ticket)
+                                _ <- milestones.traverse(milestone =>
+                                    milestoneRepo.createMilestone(projectId)(milestone)
+                                )
+                                createdMilestones <- milestoneRepo.allMilestones(projectId).compile.toList
+                                _ <- createdMilestones.traverse(cm =>
+                                    ticketRepo.addMilestone(projectId)(ticket.number)(cm)
+                                )
+                                foundMilestones <- ticketRepo.loadMilestones(projectId)(ticket.number).compile.toList
+                            } yield (createdMilestones, foundMilestones)
+                    }
+                } yield result
+                test.map { result =>
+                    val (createdMilestones, foundMilestones) = result
+                    assertEquals(foundMilestones.sortBy(_.title), createdMilestones.sortBy(_.title))
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("removeAssignee must remove the assignees from the ticket".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicketsUser.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(user)) =>
+                val assignee = Assignee(AssigneeId(user.uid.toUUID), AssigneeName(user.name.toString))
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsUser(user)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    _         <- ticket.submitter.traverse(createTicketsSubmitter)
+                    foundAssignees <- projectId match {
+                        case None => IO.pure(Nil)
+                        case Some(projectId) =>
+                            for {
+                                _              <- ticketRepo.createTicket(projectId)(ticket)
+                                _              <- ticketRepo.addAssignee(projectId)(ticket.number)(assignee)
+                                _              <- ticketRepo.removeAssignee(projectId)(ticket)(assignee)
+                                foundAssignees <- ticketRepo.loadAssignees(projectId)(ticket.number).compile.toList
+                            } yield foundAssignees
+                    }
+                } yield foundAssignees
+                test.map { foundAssignees =>
+                    assertEquals(foundAssignees, Nil)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("removeLabel must remove the label from the ticket".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genLabel.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(label)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val labelRepo  = new DoobieLabelRepository[IO](tx)
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    foundLabels <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _            <- ticket.submitter.traverse(createTicketsSubmitter)
+                                _            <- labelRepo.createLabel(projectId)(label)
+                                createdLabel <- labelRepo.findLabel(projectId)(label.name)
+                                _            <- ticketRepo.createTicket(projectId)(ticket)
+                                _ <- createdLabel.traverse(cl => ticketRepo.addLabel(projectId)(ticket.number)(cl))
+                                _ <- createdLabel.traverse(cl => ticketRepo.removeLabel(projectId)(ticket)(cl))
+                                foundLabels <- loadTicketLabelIds(projectId, ticket.number)
+                            } yield foundLabels
+                    }
+                } yield foundLabels
+                test.map { foundLabels =>
+                    assertEquals(foundLabels, Nil)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("removeMilestone must remove the milestone from the ticket".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genMilestone.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(milestone)) =>
+                val project  = generatedProject.copy(owner = owner)
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val milestoneRepo = new DoobieMilestoneRepository[IO](tx)
+                val ticketRepo    = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _         <- createProjectOwner(owner)
+                    _         <- createTicketsProject(project)
+                    projectId <- loadProjectId(owner.uid, project.name)
+                    foundMilestones <- projectId match {
+                        case None => fail("Project ID not found in database!")
+                        case Some(projectId) =>
+                            for {
+                                _                <- ticket.submitter.traverse(createTicketsSubmitter)
+                                _                <- milestoneRepo.createMilestone(projectId)(milestone)
+                                createdMilestone <- milestoneRepo.findMilestone(projectId)(milestone.title)
+                                _                <- ticketRepo.createTicket(projectId)(ticket)
+                                _ <- createdMilestone.traverse(ms =>
+                                    ticketRepo.addMilestone(projectId)(ticket.number)(ms)
+                                )
+                                _ <- createdMilestone.traverse(ms => ticketRepo.removeMilestone(projectId)(ticket)(ms))
+                                foundMilestones <- loadTicketMilestoneIds(projectId, ticket.number)
+                            } yield foundMilestones
+                    }
+                } yield foundMilestones
+                test.map { foundMilestones =>
+                    assertEquals(foundMilestones, Nil)
+                }
+            case _ => fail("Could not generate data samples!")
+        }
+    }
+
+    test("updateTicket must update the ticket in the database".tag(NeedsDatabase)) {
+        (genProjectOwner.sample, genProject.sample, genTicket.sample, genTicket.sample) match {
+            case (Some(owner), Some(generatedProject), Some(ticket), Some(anotherTicket)) =>
+                val project = generatedProject.copy(owner = owner)
+                val updatedTicket =
+                    ticket.copy(
+                        title = anotherTicket.title,
+                        content = anotherTicket.content,
+                        submitter = anotherTicket.submitter
+                    )
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val ticketRepo = new DoobieTicketRepository[IO](tx)
+                val test = for {
+                    _           <- createProjectOwner(owner)
+                    _           <- ticket.submitter.traverse(createTicketsSubmitter)
+                    _           <- updatedTicket.submitter.traverse(createTicketsSubmitter)
+                    _           <- createTicketsProject(project)
+                    projectId   <- loadProjectId(owner.uid, project.name)
+                    _           <- projectId.traverse(projectId => ticketRepo.createTicket(projectId)(ticket))
+                    _           <- projectId.traverse(projectId => ticketRepo.updateTicket(projectId)(updatedTicket))
+                    foundTicket <- projectId.traverse(projectId => ticketRepo.findTicket(projectId)(ticket.number))
+                } yield foundTicket.getOrElse(None)
+                test.map { foundTicket =>
+                    foundTicket match {
+                        case None => fail("Created ticket not found!")
+                        case Some(foundTicket) =>
+                            assertEquals(
+                                foundTicket,
+                                updatedTicket.copy(createdAt = foundTicket.createdAt, updatedAt = foundTicket.updatedAt)
+                            )
+                    }
+                }
+            case _ => fail("Could not generate data samples!")
         }
-      case _ => fail("Could not generate data samples!")
     }
-  }
 
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketServiceApiTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketServiceApiTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketServiceApiTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/DoobieTicketServiceApiTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -23,85 +23,85 @@
 import doobie.*
 
 final class DoobieTicketServiceApiTest extends BaseSpec {
-  test("createOrUpdateUser must create new users".tag(NeedsDatabase)) {
-    genTicketsUser.sample match {
-      case Some(user) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val api = new DoobieTicketServiceApi[IO](tx)
-        val test = for {
-          written   <- api.createOrUpdateUser(user)
-          foundUser <- loadTicketsUser(user.uid)
-        } yield (written, foundUser)
-        test.map { result =>
-          val (written, foundUser) = result
-          assert(written > 0, "No rows written to database!")
-          assertEquals(foundUser, Some(user))
-        }
+    test("createOrUpdateUser must create new users".tag(NeedsDatabase)) {
+        genTicketsUser.sample match {
+            case Some(user) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val api = new DoobieTicketServiceApi[IO](tx)
+                val test = for {
+                    written   <- api.createOrUpdateUser(user)
+                    foundUser <- loadTicketsUser(user.uid)
+                } yield (written, foundUser)
+                test.map { result =>
+                    val (written, foundUser) = result
+                    assert(written > 0, "No rows written to database!")
+                    assertEquals(foundUser, Some(user))
+                }
 
-      case _ => fail("Could not generate data samples!")
+            case _ => fail("Could not generate data samples!")
+        }
     }
-  }
 
-  test("createOrUpdateUser must update existing users".tag(NeedsDatabase)) {
-    (genTicketsUser.sample, genTicketsUser.sample) match {
-      case (Some(user), Some(anotherUser)) =>
-        val updatedUser = anotherUser.copy(uid = user.uid)
-        val dbConfig    = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val api = new DoobieTicketServiceApi[IO](tx)
-        val test = for {
-          created   <- api.createOrUpdateUser(user)
-          updated   <- api.createOrUpdateUser(updatedUser)
-          foundUser <- loadTicketsUser(user.uid)
-        } yield (created, updated, foundUser)
-        test.map { result =>
-          val (created, updated, foundUser) = result
-          assert(created > 0, "No rows written to database!")
-          assert(updated > 0, "No rows updated in database!")
-          assertEquals(foundUser, Some(updatedUser))
-        }
+    test("createOrUpdateUser must update existing users".tag(NeedsDatabase)) {
+        (genTicketsUser.sample, genTicketsUser.sample) match {
+            case (Some(user), Some(anotherUser)) =>
+                val updatedUser = anotherUser.copy(uid = user.uid)
+                val dbConfig    = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val api = new DoobieTicketServiceApi[IO](tx)
+                val test = for {
+                    created   <- api.createOrUpdateUser(user)
+                    updated   <- api.createOrUpdateUser(updatedUser)
+                    foundUser <- loadTicketsUser(user.uid)
+                } yield (created, updated, foundUser)
+                test.map { result =>
+                    val (created, updated, foundUser) = result
+                    assert(created > 0, "No rows written to database!")
+                    assert(updated > 0, "No rows updated in database!")
+                    assertEquals(foundUser, Some(updatedUser))
+                }
 
-      case _ => fail("Could not generate data samples!")
+            case _ => fail("Could not generate data samples!")
+        }
     }
-  }
 
-  test("deleteUser must delete existing users".tag(NeedsDatabase)) {
-    genTicketsUser.sample match {
-      case Some(user) =>
-        val dbConfig = configuration.database
-        val tx = Transactor.fromDriverManager[IO](
-          driver = dbConfig.driver,
-          url = dbConfig.url,
-          user = dbConfig.user,
-          password = dbConfig.pass,
-          logHandler = None
-        )
-        val api = new DoobieTicketServiceApi[IO](tx)
-        val test = for {
-          _         <- api.createOrUpdateUser(user)
-          deleted   <- api.deleteUser(user.uid)
-          foundUser <- loadTicketsUser(user.uid)
-        } yield (deleted, foundUser)
-        test.map { result =>
-          val (deleted, foundUser) = result
-          assert(deleted > 0, "No rows deleted from database!")
-          assert(foundUser.isEmpty, "User not deleted from database!")
-        }
+    test("deleteUser must delete existing users".tag(NeedsDatabase)) {
+        genTicketsUser.sample match {
+            case Some(user) =>
+                val dbConfig = configuration.database
+                val tx = Transactor.fromDriverManager[IO](
+                    driver = dbConfig.driver,
+                    url = dbConfig.url,
+                    user = dbConfig.user,
+                    password = dbConfig.pass,
+                    logHandler = None
+                )
+                val api = new DoobieTicketServiceApi[IO](tx)
+                val test = for {
+                    _         <- api.createOrUpdateUser(user)
+                    deleted   <- api.deleteUser(user.uid)
+                    foundUser <- loadTicketsUser(user.uid)
+                } yield (deleted, foundUser)
+                test.map { result =>
+                    val (deleted, foundUser) = result
+                    assert(deleted > 0, "No rows deleted from database!")
+                    assert(foundUser.isEmpty, "User not deleted from database!")
+                }
 
-      case _ => fail("Could not generate data samples!")
+            case _ => fail("Could not generate data samples!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/Generators.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/Generators.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/Generators.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/Generators.scala	2025-01-13 17:13:25.068470995 +0000
@@ -33,255 +33,258 @@
 import scala.jdk.CollectionConverters.*
 
 object Generators {
-  val MinimumYear: Int = -4713  // Lowest year supported by PostgreSQL
-  val MaximumYear: Int = 294276 // Highest year supported by PostgreSQL
+    val MinimumYear: Int = -4713  // Lowest year supported by PostgreSQL
+    val MaximumYear: Int = 294276 // Highest year supported by PostgreSQL
 
-  /** Prepend a zero to a single character hexadecimal code.
-    *
-    * @param hexCode
-    *   A string supposed to contain a hexadecimal code between 0 and ff.
-    * @return
-    *   Either the given code prepended with a leading zero if it had only a single character or the originally given
-    *   code otherwise.
-    */
-  private def hexPadding(hexCode: String): String =
-    if (hexCode.length < 2)
-      "0" + hexCode
-    else
-      hexCode
-
-  val genLocalDate: Gen[LocalDate] =
-    for {
-      year  <- Gen.choose(MinimumYear, MaximumYear)
-      month <- Gen.choose(1, 12)
-      day   <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
-    } yield LocalDate.of(year, month, day)
-
-  given Arbitrary[LocalDate] = Arbitrary(genLocalDate)
-
-  val genOffsetDateTime: Gen[OffsetDateTime] =
-    for {
-      year       <- Gen.choose(MinimumYear, MaximumYear)
-      month      <- Gen.choose(1, 12)
-      day        <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
-      hour       <- Gen.choose(0, 23)
-      minute     <- Gen.choose(0, 59)
-      second     <- Gen.choose(0, 59)
-      nanosecond <- Gen.const(0) // Avoid issues with loosing information during saving and loading.
-      offset <- Gen.oneOf(
-        ZoneId.getAvailableZoneIds.asScala.toList.map(ZoneId.of).map(ZonedDateTime.now).map(_.getOffset)
-      )
-    } yield OffsetDateTime.of(year, month, day, hour, minute, second, nanosecond, offset)
-
-  given Arbitrary[OffsetDateTime] = Arbitrary(genOffsetDateTime)
-
-  val genLocale: Gen[Locale]             = Gen.oneOf(Locale.getAvailableLocales.toList)
-  val genLanguageCode: Gen[LanguageCode] = genLocale.map(_.getISO3Language).filter(_.nonEmpty).map(LanguageCode.apply)
-
-  val genProjectOwnerId: Gen[ProjectOwnerId] = Gen.delay(ProjectOwnerId.randomProjectOwnerId)
-
-  val genSubmitterId: Gen[SubmitterId] = Gen.delay(SubmitterId.randomSubmitterId)
-
-  val genUUID: Gen[UUID] = Gen.delay(UUID.randomUUID)
-
-  val genUserId: Gen[UserId] = Gen.delay(UserId.randomUserId)
-
-  val genUsername: Gen[Username] = for {
-    length <- Gen.choose(2, 30)
-    prefix <- Gen.alphaChar
-    chars <- Gen
-      .nonEmptyListOf(Gen.alphaNumChar)
-      .map(_.take(length).mkString.toLowerCase(Locale.ROOT))
-  } yield Username(prefix.toString.toLowerCase(Locale.ROOT) + chars)
-
-  val genSubmitter: Gen[Submitter] = for {
-    id   <- genSubmitterId
-    name <- genUsername.map(name => SubmitterName(name.toString))
-  } yield Submitter(id, name)
-
-  val genEmailAddress: Gen[EmailAddress] =
-    for {
-      length <- Gen.choose(4, 64)
-      chars  <- Gen.nonEmptyListOf(Gen.alphaNumChar)
-      email = chars.take(length).mkString
-    } yield EmailAddress(email + "@example.com")
-
-  val genTicketContent: Gen[Option[TicketContent]] = Gen.alphaStr.map(TicketContent.from)
-
-  val genTicketStatus: Gen[TicketStatus]           = Gen.oneOf(TicketStatus.values.toList)
-  val genTicketStatusList: Gen[List[TicketStatus]] = Gen.nonEmptyListOf(genTicketStatus).map(_.distinct)
-
-  val genTicketResolution: Gen[TicketResolution]        = Gen.oneOf(TicketResolution.values.toList)
-  val genTicketResolutions: Gen[List[TicketResolution]] = Gen.nonEmptyListOf(genTicketResolution).map(_.distinct)
-
-  val genTicketNumber: Gen[TicketNumber]        = Gen.choose(0, Int.MaxValue).map(TicketNumber.apply)
-  val genTicketNumbers: Gen[List[TicketNumber]] = Gen.nonEmptyListOf(genTicketNumber).map(_.distinct)
-
-  val genTicketTitle: Gen[TicketTitle] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(TicketTitle.MaxLength).mkString).map(TicketTitle.apply)
-
-  val genTicketsUser: Gen[TicketsUser] = for {
-    uid      <- genUserId
-    name     <- genUsername
-    email    <- genEmailAddress
-    language <- Gen.option(genLanguageCode)
-  } yield TicketsUser(uid, name, email, language)
-
-  val genTicketsUsers: Gen[List[TicketsUser]] = Gen.nonEmptyListOf(genTicketsUser)
-
-  val genTicket: Gen[Ticket] = for {
-    number     <- genTicketNumber
-    title      <- genTicketTitle
-    content    <- genTicketContent
-    status     <- genTicketStatus
-    resolution <- Gen.option(genTicketResolution)
-    submitter  <- Gen.option(genSubmitter)
-    createdAt  <- genOffsetDateTime
-    updatedAt  <- genOffsetDateTime
-  } yield Ticket(
-    number,
-    title,
-    content,
-    status,
-    resolution,
-    submitter,
-    createdAt,
-    updatedAt
-  )
-
-  val genTickets: Gen[List[Ticket]] =
-    Gen.nonEmptyListOf(genTicket).map(_.zipWithIndex.map(tuple => tuple._1.copy(number = TicketNumber(tuple._2 + 1))))
-
-  val genTicketFilter: Gen[TicketFilter] =
-    for {
-      number     <- Gen.listOf(genTicketNumber)
-      status     <- Gen.listOf(genTicketStatus)
-      resolution <- Gen.listOf(genTicketResolution)
-      submitter  <- Gen.listOf(genSubmitter)
-    } yield TicketFilter(number, status, resolution, submitter.map(_.name).distinct)
-
-  val genProjectOwnerName: Gen[ProjectOwnerName] = for {
-    length <- Gen.choose(2, 30)
-    prefix <- Gen.alphaChar
-    chars <- Gen
-      .nonEmptyListOf(Gen.alphaNumChar)
-      .map(_.take(length).mkString.toLowerCase(Locale.ROOT))
-  } yield ProjectOwnerName(prefix.toString.toLowerCase(Locale.ROOT) + chars)
-
-  val genProjectOwner: Gen[ProjectOwner] = for {
-    id    <- genProjectOwnerId
-    name  <- genProjectOwnerName
-    email <- genEmailAddress
-  } yield ProjectOwner(uid = id, name = name, email = email)
-
-  given Arbitrary[ProjectOwner] = Arbitrary(genProjectOwner)
-
-  val genProjectOwners: Gen[List[ProjectOwner]] = Gen
-    .nonEmptyListOf(genProjectOwner)
-    .map(_.foldLeft(List.empty[ProjectOwner]) { (acc, a) =>
-      if (acc.exists(_.name === a.name))
-        acc
-      else
-        a :: acc
-    }) // Ensure distinct user names.
-
-  val genLabelName: Gen[LabelName] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(LabelName.MaxLength).mkString).map(LabelName.apply)
-
-  val genLabelDescription: Gen[LabelDescription] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(LabelDescription.MaxLength).mkString).map(LabelDescription.apply)
-
-  val genColourCode: Gen[ColourCode] = for {
-    red   <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding)
-    green <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding)
-    blue  <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding)
-    hexString = s"#$red$green$blue"
-  } yield ColourCode(hexString)
-
-  val genLabel: Gen[Label] = for {
-    id          <- Gen.option(Gen.choose(0L, Long.MaxValue).map(LabelId.apply))
-    name        <- genLabelName
-    description <- Gen.option(genLabelDescription)
-    colour      <- genColourCode
-  } yield Label(id, name, description, colour)
-
-  val genLabels: Gen[List[Label]] = Gen.nonEmptyListOf(genLabel).map(_.distinct)
-
-  val genMilestoneTitle: Gen[MilestoneTitle] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(MilestoneTitle.MaxLength).mkString).map(MilestoneTitle.apply)
-
-  val genMilestoneDescription: Gen[MilestoneDescription] =
-    Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString).map(MilestoneDescription.apply)
-
-  val genMilestone: Gen[Milestone] =
-    for {
-      id     <- Gen.option(Gen.choose(0L, Long.MaxValue).map(MilestoneId.apply))
-      title  <- genMilestoneTitle
-      due    <- Gen.option(genLocalDate)
-      descr  <- Gen.option(genMilestoneDescription)
-      closed <- Gen.oneOf(List(false, true))
-    } yield Milestone(id, title, descr, due, closed)
-
-  val genMilestones: Gen[List[Milestone]] = Gen.nonEmptyListOf(genMilestone).map(_.distinct)
-
-  val genProjectName: Gen[ProjectName] = Gen
-    .nonEmptyListOf(
-      Gen.oneOf(
-        List(
-          "a",
-          "b",
-          "c",
-          "d",
-          "e",
-          "f",
-          "g",
-          "h",
-          "i",
-          "j",
-          "k",
-          "l",
-          "m",
-          "n",
-          "o",
-          "p",
-          "q",
-          "r",
-          "s",
-          "t",
-          "u",
-          "v",
-          "w",
-          "x",
-          "y",
-          "z",
-          "0",
-          "1",
-          "2",
-          "3",
-          "4",
-          "5",
-          "6",
-          "7",
-          "8",
-          "9",
-          "-",
-          "_"
-        )
-      )
+    /** Prepend a zero to a single character hexadecimal code.
+      *
+      * @param hexCode
+      *   A string supposed to contain a hexadecimal code between 0 and ff.
+      * @return
+      *   Either the given code prepended with a leading zero if it had only a single character or the originally given
+      *   code otherwise.
+      */
+    private def hexPadding(hexCode: String): String =
+        if (hexCode.length < 2)
+            "0" + hexCode
+        else
+            hexCode
+
+    val genLocalDate: Gen[LocalDate] =
+        for {
+            year  <- Gen.choose(MinimumYear, MaximumYear)
+            month <- Gen.choose(1, 12)
+            day   <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
+        } yield LocalDate.of(year, month, day)
+
+    given Arbitrary[LocalDate] = Arbitrary(genLocalDate)
+
+    val genOffsetDateTime: Gen[OffsetDateTime] =
+        for {
+            year       <- Gen.choose(MinimumYear, MaximumYear)
+            month      <- Gen.choose(1, 12)
+            day        <- Gen.choose(1, Month.of(month).length(Year.of(year).isLeap))
+            hour       <- Gen.choose(0, 23)
+            minute     <- Gen.choose(0, 59)
+            second     <- Gen.choose(0, 59)
+            nanosecond <- Gen.const(0) // Avoid issues with loosing information during saving and loading.
+            offset <- Gen.oneOf(
+                ZoneId.getAvailableZoneIds.asScala.toList.map(ZoneId.of).map(ZonedDateTime.now).map(_.getOffset)
+            )
+        } yield OffsetDateTime.of(year, month, day, hour, minute, second, nanosecond, offset)
+
+    given Arbitrary[OffsetDateTime] = Arbitrary(genOffsetDateTime)
+
+    val genLocale: Gen[Locale]             = Gen.oneOf(Locale.getAvailableLocales.toList)
+    val genLanguageCode: Gen[LanguageCode] = genLocale.map(_.getISO3Language).filter(_.nonEmpty).map(LanguageCode.apply)
+
+    val genProjectOwnerId: Gen[ProjectOwnerId] = Gen.delay(ProjectOwnerId.randomProjectOwnerId)
+
+    val genSubmitterId: Gen[SubmitterId] = Gen.delay(SubmitterId.randomSubmitterId)
+
+    val genUUID: Gen[UUID] = Gen.delay(UUID.randomUUID)
+
+    val genUserId: Gen[UserId] = Gen.delay(UserId.randomUserId)
+
+    val genUsername: Gen[Username] = for {
+        length <- Gen.choose(2, 30)
+        prefix <- Gen.alphaChar
+        chars <- Gen
+            .nonEmptyListOf(Gen.alphaNumChar)
+            .map(_.take(length).mkString.toLowerCase(Locale.ROOT))
+    } yield Username(prefix.toString.toLowerCase(Locale.ROOT) + chars)
+
+    val genSubmitter: Gen[Submitter] = for {
+        id   <- genSubmitterId
+        name <- genUsername.map(name => SubmitterName(name.toString))
+    } yield Submitter(id, name)
+
+    val genEmailAddress: Gen[EmailAddress] =
+        for {
+            length <- Gen.choose(4, 64)
+            chars  <- Gen.nonEmptyListOf(Gen.alphaNumChar)
+            email = chars.take(length).mkString
+        } yield EmailAddress(email + "@example.com")
+
+    val genTicketContent: Gen[Option[TicketContent]] = Gen.alphaStr.map(TicketContent.from)
+
+    val genTicketStatus: Gen[TicketStatus]           = Gen.oneOf(TicketStatus.values.toList)
+    val genTicketStatusList: Gen[List[TicketStatus]] = Gen.nonEmptyListOf(genTicketStatus).map(_.distinct)
+
+    val genTicketResolution: Gen[TicketResolution]        = Gen.oneOf(TicketResolution.values.toList)
+    val genTicketResolutions: Gen[List[TicketResolution]] = Gen.nonEmptyListOf(genTicketResolution).map(_.distinct)
+
+    val genTicketNumber: Gen[TicketNumber]        = Gen.choose(0, Int.MaxValue).map(TicketNumber.apply)
+    val genTicketNumbers: Gen[List[TicketNumber]] = Gen.nonEmptyListOf(genTicketNumber).map(_.distinct)
+
+    val genTicketTitle: Gen[TicketTitle] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(TicketTitle.MaxLength).mkString).map(TicketTitle.apply)
+
+    val genTicketsUser: Gen[TicketsUser] = for {
+        uid      <- genUserId
+        name     <- genUsername
+        email    <- genEmailAddress
+        language <- Gen.option(genLanguageCode)
+    } yield TicketsUser(uid, name, email, language)
+
+    val genTicketsUsers: Gen[List[TicketsUser]] = Gen.nonEmptyListOf(genTicketsUser)
+
+    val genTicket: Gen[Ticket] = for {
+        number     <- genTicketNumber
+        title      <- genTicketTitle
+        content    <- genTicketContent
+        status     <- genTicketStatus
+        resolution <- Gen.option(genTicketResolution)
+        submitter  <- Gen.option(genSubmitter)
+        createdAt  <- genOffsetDateTime
+        updatedAt  <- genOffsetDateTime
+    } yield Ticket(
+        number,
+        title,
+        content,
+        status,
+        resolution,
+        submitter,
+        createdAt,
+        updatedAt
     )
-    .map(cs => ProjectName(cs.take(64).mkString))
 
-  val genProjectDescription: Gen[Option[ProjectDescription]] =
-    Gen.alphaNumStr.map(_.take(ProjectDescription.MaximumLength)).map(ProjectDescription.from)
+    val genTickets: Gen[List[Ticket]] =
+        Gen.nonEmptyListOf(genTicket)
+            .map(_.zipWithIndex.map(tuple => tuple._1.copy(number = TicketNumber(tuple._2 + 1))))
+
+    val genTicketFilter: Gen[TicketFilter] =
+        for {
+            number     <- Gen.listOf(genTicketNumber)
+            status     <- Gen.listOf(genTicketStatus)
+            resolution <- Gen.listOf(genTicketResolution)
+            submitter  <- Gen.listOf(genSubmitter)
+        } yield TicketFilter(number, status, resolution, submitter.map(_.name).distinct)
+
+    val genProjectOwnerName: Gen[ProjectOwnerName] = for {
+        length <- Gen.choose(2, 30)
+        prefix <- Gen.alphaChar
+        chars <- Gen
+            .nonEmptyListOf(Gen.alphaNumChar)
+            .map(_.take(length).mkString.toLowerCase(Locale.ROOT))
+    } yield ProjectOwnerName(prefix.toString.toLowerCase(Locale.ROOT) + chars)
+
+    val genProjectOwner: Gen[ProjectOwner] = for {
+        id    <- genProjectOwnerId
+        name  <- genProjectOwnerName
+        email <- genEmailAddress
+    } yield ProjectOwner(uid = id, name = name, email = email)
+
+    given Arbitrary[ProjectOwner] = Arbitrary(genProjectOwner)
+
+    val genProjectOwners: Gen[List[ProjectOwner]] = Gen
+        .nonEmptyListOf(genProjectOwner)
+        .map(_.foldLeft(List.empty[ProjectOwner]) { (acc, a) =>
+            if (acc.exists(_.name === a.name))
+                acc
+            else
+                a :: acc
+        }) // Ensure distinct user names.
+
+    val genLabelName: Gen[LabelName] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(LabelName.MaxLength).mkString).map(LabelName.apply)
+
+    val genLabelDescription: Gen[LabelDescription] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar)
+            .map(_.take(LabelDescription.MaxLength).mkString)
+            .map(LabelDescription.apply)
+
+    val genColourCode: Gen[ColourCode] = for {
+        red   <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding)
+        green <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding)
+        blue  <- Gen.choose(0, 255).map(_.toHexString).map(hexPadding)
+        hexString = s"#$red$green$blue"
+    } yield ColourCode(hexString)
+
+    val genLabel: Gen[Label] = for {
+        id          <- Gen.option(Gen.choose(0L, Long.MaxValue).map(LabelId.apply))
+        name        <- genLabelName
+        description <- Gen.option(genLabelDescription)
+        colour      <- genColourCode
+    } yield Label(id, name, description, colour)
+
+    val genLabels: Gen[List[Label]] = Gen.nonEmptyListOf(genLabel).map(_.distinct)
+
+    val genMilestoneTitle: Gen[MilestoneTitle] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.take(MilestoneTitle.MaxLength).mkString).map(MilestoneTitle.apply)
+
+    val genMilestoneDescription: Gen[MilestoneDescription] =
+        Gen.nonEmptyListOf(Gen.alphaNumChar).map(_.mkString).map(MilestoneDescription.apply)
+
+    val genMilestone: Gen[Milestone] =
+        for {
+            id     <- Gen.option(Gen.choose(0L, Long.MaxValue).map(MilestoneId.apply))
+            title  <- genMilestoneTitle
+            due    <- Gen.option(genLocalDate)
+            descr  <- Gen.option(genMilestoneDescription)
+            closed <- Gen.oneOf(List(false, true))
+        } yield Milestone(id, title, descr, due, closed)
+
+    val genMilestones: Gen[List[Milestone]] = Gen.nonEmptyListOf(genMilestone).map(_.distinct)
+
+    val genProjectName: Gen[ProjectName] = Gen
+        .nonEmptyListOf(
+            Gen.oneOf(
+                List(
+                    "a",
+                    "b",
+                    "c",
+                    "d",
+                    "e",
+                    "f",
+                    "g",
+                    "h",
+                    "i",
+                    "j",
+                    "k",
+                    "l",
+                    "m",
+                    "n",
+                    "o",
+                    "p",
+                    "q",
+                    "r",
+                    "s",
+                    "t",
+                    "u",
+                    "v",
+                    "w",
+                    "x",
+                    "y",
+                    "z",
+                    "0",
+                    "1",
+                    "2",
+                    "3",
+                    "4",
+                    "5",
+                    "6",
+                    "7",
+                    "8",
+                    "9",
+                    "-",
+                    "_"
+                )
+            )
+        )
+        .map(cs => ProjectName(cs.take(64).mkString))
+
+    val genProjectDescription: Gen[Option[ProjectDescription]] =
+        Gen.alphaNumStr.map(_.take(ProjectDescription.MaximumLength)).map(ProjectDescription.from)
 
-  val genProject: Gen[Project] =
-    for {
-      name        <- genProjectName
-      description <- genProjectDescription
-      owner       <- genProjectOwner
-      isPrivate   <- Gen.oneOf(List(false, true))
-    } yield Project(owner, name, description, isPrivate)
+    val genProject: Gen[Project] =
+        for {
+            name        <- genProjectName
+            description <- genProjectDescription
+            owner       <- genProjectOwner
+            isPrivate   <- Gen.oneOf(List(false, true))
+        } yield Project(owner, name, description, isPrivate)
 
-  val genProjects: Gen[List[Project]] = Gen.nonEmptyListOf(genProject)
+    val genProjects: Gen[List[Project]] = Gen.nonEmptyListOf(genProject)
 
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelDescriptionTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,23 +25,23 @@
 import org.scalacheck.*
 
 final class LabelDescriptionTest extends ScalaCheckSuite {
-  given Arbitrary[LabelDescription] = Arbitrary(genLabelDescription)
+    given Arbitrary[LabelDescription] = Arbitrary(genLabelDescription)
 
-  test("LabelDescription.from must fail on empty input") {
-    assertEquals(LabelDescription.from(""), None)
-  }
+    test("LabelDescription.from must fail on empty input") {
+        assertEquals(LabelDescription.from(""), None)
+    }
 
-  property("LabelDescription.from must fail on too long input") {
-    forAll { (input: String) =>
-      if (input.length > LabelDescription.MaxLength)
-        assertEquals(LabelDescription.from(input), None)
+    property("LabelDescription.from must fail on too long input") {
+        forAll { (input: String) =>
+            if (input.length > LabelDescription.MaxLength)
+                assertEquals(LabelDescription.from(input), None)
+        }
     }
-  }
 
-  property("LabelDescription.from must succeed on valid input") {
-    forAll { (label: LabelDescription) =>
-      val input = label.toString
-      assertEquals(LabelDescription.from(input), Option(label))
+    property("LabelDescription.from must succeed on valid input") {
+        forAll { (label: LabelDescription) =>
+            val input = label.toString
+            assertEquals(LabelDescription.from(input), Option(label))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelNameTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelNameTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelNameTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelNameTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,23 +25,23 @@
 import org.scalacheck.*
 
 final class LabelNameTest extends ScalaCheckSuite {
-  given Arbitrary[LabelName] = Arbitrary(genLabelName)
+    given Arbitrary[LabelName] = Arbitrary(genLabelName)
 
-  test("LabelName.from must fail on empty input") {
-    assertEquals(LabelName.from(""), None)
-  }
+    test("LabelName.from must fail on empty input") {
+        assertEquals(LabelName.from(""), None)
+    }
 
-  property("LabelName.from must fail on too long input") {
-    forAll { (input: String) =>
-      if (input.length > LabelName.MaxLength)
-        assertEquals(LabelName.from(input), None)
+    property("LabelName.from must fail on too long input") {
+        forAll { (input: String) =>
+            if (input.length > LabelName.MaxLength)
+                assertEquals(LabelName.from(input), None)
+        }
     }
-  }
 
-  property("LabelName.from must succeed on valid input") {
-    forAll { (label: LabelName) =>
-      val input = label.toString
-      assertEquals(LabelName.from(input), Option(label))
+    property("LabelName.from must succeed on valid input") {
+        forAll { (label: LabelName) =>
+            val input = label.toString
+            assertEquals(LabelName.from(input), Option(label))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/LabelTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -26,11 +26,11 @@
 import org.scalacheck.*
 
 final class LabelTest extends ScalaCheckSuite {
-  given Arbitrary[Label] = Arbitrary(genLabel)
+    given Arbitrary[Label] = Arbitrary(genLabel)
 
-  property("Eq must hold") {
-    forAll { (label: Label) =>
-      assert(label === label, "Identical labels must be considered equal!")
+    property("Eq must hold") {
+        forAll { (label: Label) =>
+            assert(label === label, "Identical labels must be considered equal!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneDescriptionTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,16 +25,16 @@
 import org.scalacheck.*
 
 final class MilestoneDescriptionTest extends ScalaCheckSuite {
-  given Arbitrary[MilestoneDescription] = Arbitrary(genMilestoneDescription)
+    given Arbitrary[MilestoneDescription] = Arbitrary(genMilestoneDescription)
 
-  test("MilestoneDescription.from must fail on empty input") {
-    assertEquals(MilestoneDescription.from(""), None)
-  }
+    test("MilestoneDescription.from must fail on empty input") {
+        assertEquals(MilestoneDescription.from(""), None)
+    }
 
-  property("MilestoneDescription.from must succeed on valid input") {
-    forAll { (label: MilestoneDescription) =>
-      val input = label.toString
-      assertEquals(MilestoneDescription.from(input), Option(label))
+    property("MilestoneDescription.from must succeed on valid input") {
+        forAll { (label: MilestoneDescription) =>
+            val input = label.toString
+            assertEquals(MilestoneDescription.from(input), Option(label))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -26,11 +26,11 @@
 import org.scalacheck.*
 
 final class MilestoneTest extends ScalaCheckSuite {
-  given Arbitrary[Milestone] = Arbitrary(genMilestone)
+    given Arbitrary[Milestone] = Arbitrary(genMilestone)
 
-  property("Eq must hold") {
-    forAll { (label: Milestone) =>
-      assert(label === label, "Identical milestones must be considered equal!")
+    property("Eq must hold") {
+        forAll { (label: Milestone) =>
+            assert(label === label, "Identical milestones must be considered equal!")
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/MilestoneTitleTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,23 +25,23 @@
 import org.scalacheck.*
 
 final class MilestoneTitleTest extends ScalaCheckSuite {
-  given Arbitrary[MilestoneTitle] = Arbitrary(genMilestoneTitle)
+    given Arbitrary[MilestoneTitle] = Arbitrary(genMilestoneTitle)
 
-  test("MilestoneTitle.from must fail on empty input") {
-    assertEquals(MilestoneTitle.from(""), None)
-  }
+    test("MilestoneTitle.from must fail on empty input") {
+        assertEquals(MilestoneTitle.from(""), None)
+    }
 
-  property("MilestoneTitle.from must fail on too long input") {
-    forAll { (input: String) =>
-      if (input.length > MilestoneTitle.MaxLength)
-        assertEquals(MilestoneTitle.from(input), None)
+    property("MilestoneTitle.from must fail on too long input") {
+        forAll { (input: String) =>
+            if (input.length > MilestoneTitle.MaxLength)
+                assertEquals(MilestoneTitle.from(input), None)
+        }
     }
-  }
 
-  property("MilestoneTitle.from must succeed on valid input") {
-    forAll { (label: MilestoneTitle) =>
-      val input = label.toString
-      assertEquals(MilestoneTitle.from(input), Option(label))
+    property("MilestoneTitle.from must succeed on valid input") {
+        forAll { (label: MilestoneTitle) =>
+            val input = label.toString
+            assertEquals(MilestoneTitle.from(input), Option(label))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketContentTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketContentTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketContentTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketContentTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -24,13 +24,13 @@
 
 final class TicketContentTest extends ScalaCheckSuite {
 
-  property("TicketContent.from must only accept valid input") {
-    forAll { (input: String) =>
-      if (input.nonEmpty)
-        assertEquals(TicketContent.from(input), Some(TicketContent(input)))
-      else
-        assertEquals(TicketContent.from(input), None)
+    property("TicketContent.from must only accept valid input") {
+        forAll { (input: String) =>
+            if (input.nonEmpty)
+                assertEquals(TicketContent.from(input), Some(TicketContent(input)))
+            else
+                assertEquals(TicketContent.from(input), None)
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketFilterTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketFilterTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketFilterTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketFilterTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,99 +25,112 @@
 import org.scalacheck.*
 
 final class TicketFilterTest extends ScalaCheckSuite {
-  given Arbitrary[Submitter]        = Arbitrary(genSubmitter)
-  given Arbitrary[TicketFilter]     = Arbitrary(genTicketFilter)
-  given Arbitrary[TicketNumber]     = Arbitrary(genTicketNumber)
-  given Arbitrary[TicketResolution] = Arbitrary(genTicketResolution)
-  given Arbitrary[TicketStatus]     = Arbitrary(genTicketStatus)
-
-  property("fromQueryParameter must produce empty filters for invalid input") {
-    forAll { (randomInput: String) =>
-      assertEquals(
-        TicketFilter.fromQueryParameter(randomInput),
-        TicketFilter(number = Nil, status = Nil, resolution = Nil, submitter = Nil)
-      )
-    }
-  }
-
-  property("fromQueryParameter must work for numbers only") {
-    forAll { (numbers: List[TicketNumber]) =>
-      val filter = TicketFilter(number = numbers, status = Nil, resolution = Nil, submitter = Nil)
-      assertEquals(TicketFilter.fromQueryParameter(s"numbers: ${numbers.map(_.toString).mkString(",")}"), filter)
-    }
-  }
-
-  property("fromQueryParameter must work for status only") {
-    forAll { (status: List[TicketStatus]) =>
-      val filter = TicketFilter(number = Nil, status = status, resolution = Nil, submitter = Nil)
-      assertEquals(TicketFilter.fromQueryParameter(s"status: ${status.map(_.toString).mkString(",")}"), filter)
-    }
-  }
-
-  property("fromQueryParameter must work for resolution only") {
-    forAll { (resolution: List[TicketResolution]) =>
-      val filter = TicketFilter(number = Nil, status = Nil, resolution = resolution, submitter = Nil)
-      assertEquals(TicketFilter.fromQueryParameter(s"resolution: ${resolution.map(_.toString).mkString(",")}"), filter)
-    }
-  }
-
-  property("fromQueryParameter must work for submitter only") {
-    forAll { (submitters: List[Submitter]) =>
-      if (submitters.nonEmpty) {
-        val filter = TicketFilter(number = Nil, status = Nil, resolution = Nil, submitter = submitters.map(_.name))
-        assertEquals(
-          TicketFilter.fromQueryParameter(s"by: ${submitters.map(_.name.toString).mkString(",")}"),
-          filter
-        )
-      }
-    }
-  }
-
-  property("toQueryParameter must include numbers") {
-    forAll { (numbers: List[TicketNumber]) =>
-      if (numbers.nonEmpty) {
-        val filter = TicketFilter(number = numbers, status = Nil, resolution = Nil, submitter = Nil)
-        assert(TicketFilter.toQueryParameter(filter).contains(s"numbers: ${numbers.map(_.toString).mkString(",")}"))
-      }
-    }
-  }
-
-  property("toQueryParameter must include status") {
-    forAll { (status: List[TicketStatus]) =>
-      if (status.nonEmpty) {
-        val filter = TicketFilter(number = Nil, status = status, resolution = Nil, submitter = Nil)
-        assert(TicketFilter.toQueryParameter(filter).contains(s"status: ${status.map(_.toString).mkString(",")}"))
-      }
-    }
-  }
-
-  property("toQueryParameter must include resolution") {
-    forAll { (resolution: List[TicketResolution]) =>
-      if (resolution.nonEmpty) {
-        val filter = TicketFilter(number = Nil, status = Nil, resolution = resolution, submitter = Nil)
-        assert(
-          TicketFilter.toQueryParameter(filter).contains(s"resolution: ${resolution.map(_.toString).mkString(",")}"),
-          TicketFilter.toQueryParameter(filter)
-        )
-      }
-    }
-  }
-
-  property("toQueryParameter must include submitter") {
-    forAll { (submitters: List[Submitter]) =>
-      if (submitters.nonEmpty) {
-        val filter = TicketFilter(number = Nil, status = Nil, resolution = Nil, submitter = submitters.map(_.name))
-        assert(
-          TicketFilter.toQueryParameter(filter).contains(s"by: ${submitters.map(_.name.toString).mkString(",")}"),
-          TicketFilter.toQueryParameter(filter)
-        )
-      }
-    }
-  }
-
-  property("toQueryParameter must be the dual of fromQueryParameter") {
-    forAll { (filter: TicketFilter) =>
-      assertEquals(TicketFilter.fromQueryParameter(filter.toQueryParameter), filter)
+    given Arbitrary[Submitter]        = Arbitrary(genSubmitter)
+    given Arbitrary[TicketFilter]     = Arbitrary(genTicketFilter)
+    given Arbitrary[TicketNumber]     = Arbitrary(genTicketNumber)
+    given Arbitrary[TicketResolution] = Arbitrary(genTicketResolution)
+    given Arbitrary[TicketStatus]     = Arbitrary(genTicketStatus)
+
+    property("fromQueryParameter must produce empty filters for invalid input") {
+        forAll { (randomInput: String) =>
+            assertEquals(
+                TicketFilter.fromQueryParameter(randomInput),
+                TicketFilter(number = Nil, status = Nil, resolution = Nil, submitter = Nil)
+            )
+        }
+    }
+
+    property("fromQueryParameter must work for numbers only") {
+        forAll { (numbers: List[TicketNumber]) =>
+            val filter = TicketFilter(number = numbers, status = Nil, resolution = Nil, submitter = Nil)
+            assertEquals(TicketFilter.fromQueryParameter(s"numbers: ${numbers.map(_.toString).mkString(",")}"), filter)
+        }
+    }
+
+    property("fromQueryParameter must work for status only") {
+        forAll { (status: List[TicketStatus]) =>
+            val filter = TicketFilter(number = Nil, status = status, resolution = Nil, submitter = Nil)
+            assertEquals(TicketFilter.fromQueryParameter(s"status: ${status.map(_.toString).mkString(",")}"), filter)
+        }
+    }
+
+    property("fromQueryParameter must work for resolution only") {
+        forAll { (resolution: List[TicketResolution]) =>
+            val filter = TicketFilter(number = Nil, status = Nil, resolution = resolution, submitter = Nil)
+            assertEquals(
+                TicketFilter.fromQueryParameter(s"resolution: ${resolution.map(_.toString).mkString(",")}"),
+                filter
+            )
+        }
+    }
+
+    property("fromQueryParameter must work for submitter only") {
+        forAll { (submitters: List[Submitter]) =>
+            if (submitters.nonEmpty) {
+                val filter =
+                    TicketFilter(number = Nil, status = Nil, resolution = Nil, submitter = submitters.map(_.name))
+                assertEquals(
+                    TicketFilter.fromQueryParameter(s"by: ${submitters.map(_.name.toString).mkString(",")}"),
+                    filter
+                )
+            }
+        }
+    }
+
+    property("toQueryParameter must include numbers") {
+        forAll { (numbers: List[TicketNumber]) =>
+            if (numbers.nonEmpty) {
+                val filter = TicketFilter(number = numbers, status = Nil, resolution = Nil, submitter = Nil)
+                assert(
+                    TicketFilter.toQueryParameter(filter).contains(s"numbers: ${numbers.map(_.toString).mkString(",")}")
+                )
+            }
+        }
+    }
+
+    property("toQueryParameter must include status") {
+        forAll { (status: List[TicketStatus]) =>
+            if (status.nonEmpty) {
+                val filter = TicketFilter(number = Nil, status = status, resolution = Nil, submitter = Nil)
+                assert(
+                    TicketFilter.toQueryParameter(filter).contains(s"status: ${status.map(_.toString).mkString(",")}")
+                )
+            }
+        }
+    }
+
+    property("toQueryParameter must include resolution") {
+        forAll { (resolution: List[TicketResolution]) =>
+            if (resolution.nonEmpty) {
+                val filter = TicketFilter(number = Nil, status = Nil, resolution = resolution, submitter = Nil)
+                assert(
+                    TicketFilter
+                        .toQueryParameter(filter)
+                        .contains(s"resolution: ${resolution.map(_.toString).mkString(",")}"),
+                    TicketFilter.toQueryParameter(filter)
+                )
+            }
+        }
+    }
+
+    property("toQueryParameter must include submitter") {
+        forAll { (submitters: List[Submitter]) =>
+            if (submitters.nonEmpty) {
+                val filter =
+                    TicketFilter(number = Nil, status = Nil, resolution = Nil, submitter = submitters.map(_.name))
+                assert(
+                    TicketFilter
+                        .toQueryParameter(filter)
+                        .contains(s"by: ${submitters.map(_.name.toString).mkString(",")}"),
+                    TicketFilter.toQueryParameter(filter)
+                )
+            }
+        }
+    }
+
+    property("toQueryParameter must be the dual of fromQueryParameter") {
+        forAll { (filter: TicketFilter) =>
+            assertEquals(TicketFilter.fromQueryParameter(filter.toQueryParameter), filter)
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketNumberTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketNumberTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketNumberTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketNumberTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -24,13 +24,13 @@
 
 final class TicketNumberTest extends ScalaCheckSuite {
 
-  property("TicketNumber.from must only accept valid input") {
-    forAll { (integer: Int) =>
-      if (integer > 0)
-        assertEquals(TicketNumber.from(integer), Some(TicketNumber(integer)))
-      else
-        assertEquals(TicketNumber.from(integer), None)
+    property("TicketNumber.from must only accept valid input") {
+        forAll { (integer: Int) =>
+            if (integer > 0)
+                assertEquals(TicketNumber.from(integer), Some(TicketNumber(integer)))
+            else
+                assertEquals(TicketNumber.from(integer), None)
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketResolutionTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketResolutionTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketResolutionTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketResolutionTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,17 +25,17 @@
 import org.scalacheck.*
 
 final class TicketResolutionTest extends ScalaCheckSuite {
-  given Arbitrary[TicketResolution] = Arbitrary(genTicketResolution)
+    given Arbitrary[TicketResolution] = Arbitrary(genTicketResolution)
 
-  property("valueOf must work for all known instances") {
-    forAll { (status: TicketResolution) =>
-      assertEquals(TicketResolution.valueOf(status.toString), status)
+    property("valueOf must work for all known instances") {
+        forAll { (status: TicketResolution) =>
+            assertEquals(TicketResolution.valueOf(status.toString), status)
+        }
     }
-  }
 
-  property("fromString must work for all known instances") {
-    forAll { (status: TicketResolution) =>
-      assertEquals(TicketResolution.fromString(status.toString), Option(status))
+    property("fromString must work for all known instances") {
+        forAll { (status: TicketResolution) =>
+            assertEquals(TicketResolution.fromString(status.toString), Option(status))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketStatusTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketStatusTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketStatusTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketStatusTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -25,17 +25,17 @@
 import org.scalacheck.*
 
 final class TicketStatusTest extends ScalaCheckSuite {
-  given Arbitrary[TicketStatus] = Arbitrary(genTicketStatus)
+    given Arbitrary[TicketStatus] = Arbitrary(genTicketStatus)
 
-  property("valueOf must work for all known instances") {
-    forAll { (status: TicketStatus) =>
-      assertEquals(TicketStatus.valueOf(status.toString), status)
+    property("valueOf must work for all known instances") {
+        forAll { (status: TicketStatus) =>
+            assertEquals(TicketStatus.valueOf(status.toString), status)
+        }
     }
-  }
 
-  property("fromString must work for all known instances") {
-    forAll { (status: TicketStatus) =>
-      assertEquals(TicketStatus.fromString(status.toString), Option(status))
+    property("fromString must work for all known instances") {
+        forAll { (status: TicketStatus) =>
+            assertEquals(TicketStatus.fromString(status.toString), Option(status))
+        }
     }
-  }
 }
diff -rN -u old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketTitleTest.scala new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketTitleTest.scala
--- old-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketTitleTest.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/TicketTitleTest.scala	2025-01-13 17:13:25.068470995 +0000
@@ -24,13 +24,13 @@
 
 final class TicketTitleTest extends ScalaCheckSuite {
 
-  property("TicketTitle.from must only accept valid input") {
-    forAll { (input: String) =>
-      if (input.nonEmpty && input.length <= TicketTitle.MaxLength)
-        assertEquals(TicketTitle.from(input), Some(TicketTitle(input)))
-      else
-        assertEquals(TicketTitle.from(input), None)
+    property("TicketTitle.from must only accept valid input") {
+        forAll { (input: String) =>
+            if (input.nonEmpty && input.length <= TicketTitle.MaxLength)
+                assertEquals(TicketTitle.from(input), Some(TicketTitle(input)))
+            else
+                assertEquals(TicketTitle.from(input), None)
+        }
     }
-  }
 
 }
diff -rN -u old-smederee/.scalafmt.conf new-smederee/.scalafmt.conf
--- old-smederee/.scalafmt.conf	2025-01-13 17:13:25.024470933 +0000
+++ new-smederee/.scalafmt.conf	2025-01-13 17:13:25.048470967 +0000
@@ -3,12 +3,32 @@
 style          = "defaultWithAlign"
 # Other options...
 danglingParentheses.preset          = true
+indent.main                         = 4
+indent.significant                  = 4
+indent.callSite                     = 4
+indent.ctrlSite                     = 4
+indent.defnSite                     = 4
+indent.ctorSite                     = 4
+indent.matchSite                    = 4
+indent.caseSite                     = 4
+indent.extendSite                   = 4
+indent.withSiteRelativeToExtends    = 4
+indent.commaSiteRelativeToExtends   = 4
+indent.extraBeforeOpenParenDefnSite = 0
+indent.relativeToLhsLastLine        = [match, infix]
+indent.fewerBraces                  = "never"
 maxColumn                           = 120
+newlines.beforeOpenParenDefnSite    = null
 newlines.forceBeforeMultilineAssign = def
-project.excludeFilters              = [".*\\.sbt"]
+project.includePaths                = [
+    "glob:**/build.sbt",
+    "glob:**/project/plugins.sbt",
+    "glob:**/project/*.scala",
+    "glob:**/src/**.scala"
+]
 rewrite.rules                       = [RedundantBraces, RedundantParens]
 rewriteTokens                       = {
-  "⇒" = "=>"
-  "←" = "<-"
-  "→" = "->"
+    "⇒" = "=>"
+    "←" = "<-"
+    "→" = "->"
 }
diff -rN -u old-smederee/twirl/src/main/scala/org/http4s/twirl/TwirlInstances.scala new-smederee/twirl/src/main/scala/org/http4s/twirl/TwirlInstances.scala
--- old-smederee/twirl/src/main/scala/org/http4s/twirl/TwirlInstances.scala	2025-01-13 17:13:25.044470961 +0000
+++ new-smederee/twirl/src/main/scala/org/http4s/twirl/TwirlInstances.scala	2025-01-13 17:13:25.068470995 +0000
@@ -24,26 +24,26 @@
 
 @SuppressWarnings(Array("scalafix:DisableSyntax.defaultArgs"))
 object TwirlInstances {
-  implicit def htmlContentEncoder(implicit charset: Charset = `UTF-8`): EntityEncoder.Pure[Html] =
-    contentEncoder(MediaType.text.html)
+    implicit def htmlContentEncoder(implicit charset: Charset = `UTF-8`): EntityEncoder.Pure[Html] =
+        contentEncoder(MediaType.text.html)
 
-  /** Note: Twirl uses a media type of `text/javascript`. This is obsolete, so we instead return
-    * `application/javascript`.
-    */
-  implicit def jsContentEncoder(implicit
-      charset: Charset = `UTF-8`
-  ): EntityEncoder.Pure[JavaScript] = contentEncoder(MediaType.application.javascript)
+    /** Note: Twirl uses a media type of `text/javascript`. This is obsolete, so we instead return
+      * `application/javascript`.
+      */
+    implicit def jsContentEncoder(implicit
+        charset: Charset = `UTF-8`
+    ): EntityEncoder.Pure[JavaScript] = contentEncoder(MediaType.application.javascript)
 
-  implicit def xmlContentEncoder(implicit charset: Charset = `UTF-8`): EntityEncoder.Pure[Xml] =
-    contentEncoder(MediaType.application.xml)
+    implicit def xmlContentEncoder(implicit charset: Charset = `UTF-8`): EntityEncoder.Pure[Xml] =
+        contentEncoder(MediaType.application.xml)
 
-  implicit def txtContentEncoder(implicit charset: Charset = `UTF-8`): EntityEncoder.Pure[Txt] =
-    contentEncoder(MediaType.text.plain)
+    implicit def txtContentEncoder(implicit charset: Charset = `UTF-8`): EntityEncoder.Pure[Txt] =
+        contentEncoder(MediaType.text.plain)
 
-  private def contentEncoder[C <: Content](
-      mediaType: MediaType
-  )(implicit charset: Charset): EntityEncoder.Pure[C] =
-    EntityEncoder.stringEncoder
-      .contramap[C](content => content.body)
-      .withContentType(`Content-Type`(mediaType, charset))
+    private def contentEncoder[C <: Content](
+        mediaType: MediaType
+    )(implicit charset: Charset): EntityEncoder.Pure[C] =
+        EntityEncoder.stringEncoder
+            .contramap[C](content => content.body)
+            .withContentType(`Content-Type`(mediaType, charset))
 }