~jan0sch/smederee
Showing details for patch 624ff2e48d10dc4e21d997f6ccb4cad7f673245d.
diff -rN -u old-smederee/modules/hub/src/it/scala/de/smederee/hub/Generators.scala new-smederee/modules/hub/src/it/scala/de/smederee/hub/Generators.scala --- old-smederee/modules/hub/src/it/scala/de/smederee/hub/Generators.scala 2025-01-16 09:48:54.188767002 +0000 +++ new-smederee/modules/hub/src/it/scala/de/smederee/hub/Generators.scala 2025-01-16 09:48:54.196767011 +0000 @@ -189,12 +189,13 @@ 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) - vcsType <- genValidVcsType - } yield VcsRepository(name, owner, isPrivate, description, vcsType, None) + 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/main/resources/db/migration/hub/V5__add_ticket_tracker.sql new-smederee/modules/hub/src/main/resources/db/migration/hub/V5__add_ticket_tracker.sql --- old-smederee/modules/hub/src/main/resources/db/migration/hub/V5__add_ticket_tracker.sql 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/resources/db/migration/hub/V5__add_ticket_tracker.sql 2025-01-16 09:48:54.196767011 +0000 @@ -0,0 +1,4 @@ +ALTER TABLE "hub"."repositories" + ADD COLUMN "tickets_enabled" BOOLEAN NOT NULL DEFAULT FALSE; + +COMMENT ON COLUMN "hub"."repositories"."tickets_enabled" IS 'A flag indicating if ticket tracking support via the ticket service is enabled for this repository.'; diff -rN -u old-smederee/modules/hub/src/main/resources/messages.properties new-smederee/modules/hub/src/main/resources/messages.properties --- old-smederee/modules/hub/src/main/resources/messages.properties 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/resources/messages.properties 2025-01-16 09:48:54.196767011 +0000 @@ -34,6 +34,8 @@ form.create-repo.description=Description form.create-repo.description.placeholder= form.create-repo.description.help=An optional short description of you repo / project. +form.create-repo.tickets-enabled=Ticket tracking +form.create-repo.tickets-enabled.help=Enable ticket tracking support to be able to use tickets with labels and milestones for organising the development process if needed. form.create-repo.website=Website form.create-repo.website.placeholder=https://example.com form.create-repo.website.help=An optional URI pointing to the website of your project. @@ -207,6 +209,7 @@ repository.menu.labels=Labels repository.menu.milestones=Milestones repository.menu.overview=Overview +repository.menu.tickets=Tickets repository.menu.website=Website repository.menu.website.tooltip=Click here to open the project website ({0}) in a new tab or window. diff -rN -u old-smederee/modules/hub/src/main/resources/reference.conf new-smederee/modules/hub/src/main/resources/reference.conf --- old-smederee/modules/hub/src/main/resources/reference.conf 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/resources/reference.conf 2025-01-16 09:48:54.196767011 +0000 @@ -164,5 +164,13 @@ signup { enabled = true } + + # Configuration regarding the integration with the ticket service. + ticket-integration { + enabled = false + # The base URI used to build links to the ticket service. + base-uri = "http://localhost:8081" + base-uri = ${?SMEDEREE_TICKET_BASE_URI} + } } } 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/config/SmedereeHubConfig.scala 2025-01-16 09:48:54.200767016 +0000 @@ -197,6 +197,8 @@ * The configuration for the signup / registration feature. * @param ssh * Settings for the embedded SSH server component. + * @param tickets + * Configuration regarding the integration with the hub service. */ final case class ServiceConfig( host: Host, @@ -210,7 +212,8 @@ email: EmailMiddlewareConfiguration, external: ExternalUrlConfiguration, signup: SignupConfiguration, - ssh: SshServerConfiguration + ssh: SshServerConfiguration, + tickets: TicketIntegrationConfiguration ) object ServiceConfig { @@ -237,7 +240,7 @@ ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply) given ConfigReader[ServiceConfig] = - ConfigReader.forProduct12( + ConfigReader.forProduct13( "host", "port", "csrf-key-file", @@ -249,7 +252,8 @@ "email", "external", "signup", - "ssh" + "ssh", + "ticket-integration" )(ServiceConfig.apply) } @@ -370,3 +374,18 @@ given ConfigReader[SignupConfiguration] = ConfigReader.forProduct1("enabled")(SignupConfiguration.apply) } + +/** Configuration regarding the integration with the ticket service. + * + * @param baseUri + * The base URI used to build links to the ticket service. + * @param enabled + * A flag indicating if the ticket service integration is enabled. + */ +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) +} 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/DoobieVcsMetadataRepository.scala 2025-01-16 09:48:54.200767016 +0000 @@ -50,6 +50,7 @@ "accounts".email AS owner_email, "repos".is_private AS is_private, "repos".description AS description, + "repos".tickets_enabled AS tickets_enabled, "repos".vcs_type AS vcs_type, "repos".website AS website FROM "hub"."repositories" AS "repos" @@ -57,15 +58,41 @@ ON "repos".owner = "accounts".uid""" override def createFork(source: VcsRepositoryId, target: VcsRepositoryId): F[Int] = - sql"""INSERT INTO "hub"."forks" (original_repo, forked_repo) VALUES ($source, $target)""".update.run.transact(tx) + sql"""INSERT INTO "hub"."forks" ( + original_repo, + forked_repo + ) VALUES ( + $source, + $target + )""".update.run.transact(tx) override def createVcsRepository(repository: VcsRepository): F[Int] = - sql"""INSERT INTO "hub"."repositories" (name, owner, is_private, description, vcs_type, website, created_at, updated_at) VALUES (${repository.name}, ${repository.owner.uid}, ${repository.isPrivate}, ${repository.description}, ${repository.vcsType}, ${repository.website}, NOW(), NOW())""".update.run - .transact(tx) + sql"""INSERT INTO "hub"."repositories" ( + name, + owner, + is_private, + description, + tickets_enabled, + vcs_type, + website, + created_at, + updated_at + ) VALUES ( + ${repository.name}, + ${repository.owner.uid}, + ${repository.isPrivate}, + ${repository.description}, + ${repository.ticketsEnabled}, + ${repository.vcsType}, + ${repository.website}, + NOW(), + NOW() + )""".update.run.transact(tx) 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) + sql"""DELETE FROM "hub"."repositories" + WHERE owner = ${repository.owner.uid} + AND name = ${repository.name}""".update.run.transact(tx) override def findVcsRepository( owner: VcsRepositoryOwner, @@ -145,10 +172,13 @@ } override def updateVcsRepository(repository: VcsRepository): F[Int] = - sql"""UPDATE "hub"."repositories" SET is_private = ${repository.isPrivate}, - description = ${repository.description}, - website = ${repository.website}, - updated_at = NOW() - WHERE owner = ${repository.owner.uid} AND name = ${repository.name}""".update.run.transact(tx) + sql"""UPDATE "hub"."repositories" + SET is_private = ${repository.isPrivate}, + description = ${repository.description}, + tickets_enabled = ${repository.ticketsEnabled}, + website = ${repository.website}, + updated_at = NOW() + WHERE owner = ${repository.owner.uid} + AND name = ${repository.name}""".update.run.transact(tx) } 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/EditVcsRepositoryForm.scala 2025-01-16 09:48:54.200767016 +0000 @@ -33,6 +33,8 @@ * permissions. * @param description * An optional short text description of the repository. + * @param ticketsEnabled + * A flag indicating if ticket tracking support via the ticket service is enabled for this repository. * @param website * An optional uri pointing to a website related to the repository / project. */ @@ -40,14 +42,16 @@ name: VcsRepositoryName, isPrivate: Boolean, description: Option[VcsRepositoryDescription], + ticketsEnabled: Boolean, website: Option[Uri] ) object EditVcsRepositoryForm extends FormValidator[EditVcsRepositoryForm] { - val fieldDescription: FormField = FormField("description") - val fieldIsPrivate: FormField = FormField("is_private") - val fieldName: FormField = FormField("name") - 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. * @@ -57,7 +61,7 @@ * 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.website) + EditVcsRepositoryForm(repo.name, repo.isPrivate, repo.description, repo.ticketsEnabled, repo.website) override def validate(data: Map[String, String]): ValidatedNec[FormErrors, EditVcsRepositoryForm] = { val name = data @@ -79,6 +83,8 @@ .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 => @@ -96,8 +102,9 @@ } } .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList))) - (name, privateFlag, description, website).mapN { case (validName, isPrivate, validDescription, validWebsite) => - EditVcsRepositoryForm(validName, isPrivate, validDescription, validWebsite) + (name, privateFlag, description, ticketsEnabledFlag, website).mapN { + case (validName, isPrivate, validDescription, ticketsEnabled, validWebsite) => + EditVcsRepositoryForm(validName, isPrivate, validDescription, ticketsEnabled, validWebsite) } } @@ -115,9 +122,15 @@ "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.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) 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/NewVcsRepositoryForm.scala 2025-01-16 09:48:54.200767016 +0000 @@ -33,6 +33,8 @@ * permissions. * @param description * An optional short text description of the repository. + * @param ticketsEnabled + * A flag indicating if ticket tracking support via the ticket service is enabled for this repository. * @param website * An optional uri pointing to a website related to the repository / project. */ @@ -40,14 +42,16 @@ name: VcsRepositoryName, isPrivate: Boolean, description: Option[VcsRepositoryDescription], + ticketsEnabled: Boolean, website: Option[Uri] ) object NewVcsRepositoryForm extends FormValidator[NewVcsRepositoryForm] { - val fieldDescription: FormField = FormField("description") - val fieldIsPrivate: FormField = FormField("is_private") - val fieldName: FormField = FormField("name") - 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 @@ -69,6 +73,8 @@ .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 => @@ -86,8 +92,9 @@ } } .leftMap(es => NonEmptyChain.of(Map(fieldWebsite -> es.toList))) - (name, privateFlag, description, website).mapN { case (validName, isPrivate, validDescription, validWebsite) => - NewVcsRepositoryForm(validName, isPrivate, validDescription, validWebsite) + (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/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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepositoryRoutes.scala 2025-01-16 09:48:54.200767016 +0000 @@ -64,11 +64,12 @@ ) extends Http4sDsl[F] { private val log = LoggerFactory.getLogger(getClass) - private val MaximumFileSize = configuration.renderMaximumFileSize - private val createRepoPath = uri"/repo/create" - private val darcsConfig = configuration.darcs - private val linkConfig = configuration.external - private val sshConfig = configuration.ssh + 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()) @@ -337,6 +338,7 @@ views.html.showRepositoryBranches(baseUri, lang = language)( actionBaseUri, csrf, + linkToTicketService, s"Smederee/~$repositoryOwnerName/$repositoryName".some, user )(repo, branches) @@ -427,6 +429,7 @@ views.html.showRepositoryOverview(baseUri, lang = language)( actionBaseUri, csrf, + linkToTicketService, s"Smederee/~$repositoryOwnerName/$repositoryName".some, user )( @@ -570,7 +573,8 @@ views.html.showRepositoryFiles(baseUri, lang = language)( actionBaseUri, csrf, - Option(goBackUri), + goBackUri.some, + linkToTicketService, s"Smederee/~$repositoryOwnerName/$repositoryName".some, user )(fileContent, listing, repositoryBaseUri, repo, branches) @@ -666,7 +670,8 @@ views.html.showRepositoryHistory(baseUri, lang = language)( actionBaseUri, csrf, - Option(goBackUri), + goBackUri.some, + linkToTicketService, s"Smederee - History of ~$repositoryOwnerName/$repositoryName".some, user )(patches, next, repositoryBaseUri, repo, branches) @@ -735,7 +740,13 @@ case Some(repo) => Ok( views.html - .showRepositoryPatch(baseUri, lang = language)(actionBaseUri, csrf, patch.map(_.name.toString), user)( + .showRepositoryPatch(baseUri, lang = language)( + actionBaseUri, + csrf, + linkToTicketService, + patch.map(_.name.toString), + user + )( patch, cleanedHtmlPatchDetails, repo, @@ -990,14 +1001,14 @@ FormErrors.fromNec(es) ) ) - case Validated.Valid(newVcsRepository) => + case Validated.Valid(newVcsRepositoryForm) => for { directory <- Sync[F].delay( Paths.get(darcsConfig.repositoriesDirectory.toPath.toString, user.name.toString) ) repoInDb <- vcsMetadataRepo.findVcsRepository( user.toVcsRepositoryOwner, - newVcsRepository.name + newVcsRepositoryForm.name ) output <- repoInDb match { case None => @@ -1010,20 +1021,24 @@ val _ = Files.createDirectories(directory) } } - repoMetadata = VcsRepository( - newVcsRepository.name, + newRepo = VcsRepository( + newVcsRepositoryForm.name, user.toVcsRepositoryOwner, - newVcsRepository.isPrivate, - newVcsRepository.description, + newVcsRepositoryForm.isPrivate, + newVcsRepositoryForm.description, + newVcsRepositoryForm.ticketsEnabled, VcsType.Darcs, - newVcsRepository.website + newVcsRepositoryForm.website ) - output <- darcs.initialize(directory)(newVcsRepository.name.toString)(Chain.empty) + output <- darcs.initialize(directory)(newRepo.name.toString)(Chain.empty) _ <- if (output.exitValue === 0) - vcsMetadataRepo.createVcsRepository(repoMetadata) *> ticketsProjectRepo.createProject( - repoMetadata.convert - ) + 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 @@ -1041,7 +1056,7 @@ for { _ <- Sync[F].delay( log.error( - s"Error creating the repository ${newVcsRepository.name} in directory $directory: ${output.stderr.toList.mkString}" + s"Error creating the repository ${newVcsRepositoryForm.name} in directory $directory: ${output.stderr.toList.mkString}" ) ) resp <- InternalServerError( @@ -1171,15 +1186,36 @@ branches )(formData, FormErrors.fromNec(errors)) ) - case Validated.Valid(updatedVcsRepository) => - val repoMetadata = repo.copy( - isPrivate = updatedVcsRepository.isPrivate, - description = updatedVcsRepository.description, - website = updatedVcsRepository.website + 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(repoMetadata) - _ <- ticketsProjectRepo.updateProject(repoMetadata.convert) + _ <- 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 } 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/hub/VcsRepository.scala 2025-01-16 09:48:54.200767016 +0000 @@ -531,6 +531,8 @@ * permissions. * @param description * An optional short text description of the repository. + * @param ticketsEnabled + * A flag indicating if ticket tracking support via the ticket service is enabled for this repository. * @param vcsType * The type of the underlying DVCS that manages the repository. * @param website @@ -541,6 +543,7 @@ owner: VcsRepositoryOwner, isPrivate: Boolean, description: Option[VcsRepositoryDescription], + ticketsEnabled: Boolean, vcsType: VcsType, website: Option[Uri] ) 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/LabelRoutes.scala 2025-01-16 09:48:54.200767016 +0000 @@ -55,7 +55,8 @@ given CsrfProtectionConfiguration = configuration.csrfProtection - 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. * @@ -94,6 +95,7 @@ views.html.editLabels(lang = language)( projectBaseUri.addSegment("labels"), csrf, + linkToHubService, labels, projectBaseUri, "Manage your project labels.".some, @@ -179,6 +181,7 @@ views.html.editLabels(lang = language)( projectBaseUri.addSegment("labels"), csrf, + linkToHubService, labels.getOrElse(List.empty), projectBaseUri, "Manage your project labels.".some, @@ -200,6 +203,7 @@ views.html.editLabels(lang = language)( projectBaseUri.addSegment("labels"), csrf, + linkToHubService, labels.getOrElse(List.empty), projectBaseUri, "Manage your project labels.".some, @@ -347,6 +351,7 @@ views.html.editLabel(lang = language)( actionUri, csrf, + linkToHubService, label, projectBaseUri, s"Edit label ${label.name}".some, @@ -376,6 +381,7 @@ views.html.editLabel(lang = language)( actionUri, csrf, + linkToHubService, label, projectBaseUri, s"Edit label ${label.name}".some, @@ -433,7 +439,16 @@ formData <- Sync[F].delay(LabelForm.fromLabel(label)) resp <- Ok( views.html - .editLabel()(actionUri, csrf, label, projectBaseUri, s"Edit label ${label.name}".some, user, project)( + .editLabel()( + actionUri, + csrf, + linkToHubService, + label, + projectBaseUri, + s"Edit label ${label.name}".some, + user, + project + )( formData.toMap ) ) 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/MilestoneRoutes.scala 2025-01-16 09:48:54.200767016 +0000 @@ -53,7 +53,8 @@ given CsrfProtectionConfiguration = configuration.csrfProtection - val linkConfig = configuration.externalUrl + private val linkToHubService = configuration.hub.baseUri + private val linkConfig = configuration.externalUrl /** Logic for rendering a list of all milestones for a project and optionally management functionality. * @@ -91,6 +92,7 @@ views.html.editMilestones(lang = language)( projectBaseUri.addSegment("milestones"), csrf, + linkToHubService, milestones, projectBaseUri, "Manage your project milestones.".some, @@ -175,6 +177,7 @@ views.html.editMilestones(lang = language)( projectBaseUri.addSegment("milestones"), csrf, + linkToHubService, milestones.getOrElse(List.empty), projectBaseUri, "Manage your project milestones.".some, @@ -197,6 +200,7 @@ views.html.editMilestones(lang = language)( projectBaseUri.addSegment("milestones"), csrf, + linkToHubService, milestones.getOrElse(List.empty), projectBaseUri, "Manage your project milestones.".some, @@ -337,6 +341,7 @@ views.html.editMilestone(lang = language)( actionUri, csrf, + linkToHubService, milestone, projectBaseUri, s"Edit milestone ${milestone.title}".some, @@ -366,6 +371,7 @@ views.html.editMilestone(lang = language)( actionUri, csrf, + linkToHubService, milestone, projectBaseUri, s"Edit milestone ${milestone.title}".some, @@ -420,6 +426,7 @@ views.html.editMilestone(lang = language)( actionUri, csrf, + linkToHubService, milestone, projectBaseUri, s"Edit milestone ${milestone.title}".some, 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-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/scala/de/smederee/tickets/TicketRoutes.scala 2025-01-16 09:48:54.200767016 +0000 @@ -64,7 +64,8 @@ given CsrfProtectionConfiguration = configuration.csrfProtection - 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. @@ -147,6 +148,7 @@ views.html.showTicket(lang = language)( projectBaseUri.addSegment("tickets"), csrf, + linkToHubService, labels, milestones, ticket, @@ -201,6 +203,7 @@ views.html.showTickets(lang = language)( projectBaseUri.addSegment("tickets"), csrf, + linkToHubService, tickets, projectBaseUri, "Manage your project tickets.".some, @@ -248,6 +251,7 @@ views.html.createTicket(lang = language)( projectBaseUri.addSegment("tickets"), csrf, + linkToHubService, labels, milestones, projectBaseUri, @@ -333,6 +337,7 @@ views.html.createTicket(lang = language)( projectBaseUri.addSegment("tickets"), csrf, + linkToHubService, labels, milestones, projectBaseUri, @@ -413,6 +418,7 @@ views.html.createTicket(lang = language)( projectBaseUri.addSegment("tickets"), csrf, + linkToHubService, labels, milestones, projectBaseUri, @@ -467,6 +473,7 @@ views.html.editTicket(lang = language)( projectBaseUri.addSegment("tickets"), csrf, + linkToHubService, labels, milestones, projectBaseUri, diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/createRepository.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/createRepository.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/createRepository.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/createRepository.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -37,6 +37,12 @@ @renderFormErrors(fieldIsPrivate, formErrors) </div> <div class="pure-control-group"> + <label for="@{fieldTicketsEnabled}">@Messages("form.create-repo.tickets-enabled")</label> + <input id="@{fieldTicketsEnabled}" name="@{fieldTicketsEnabled}" type="checkbox" value="true" @if(formData.get(fieldTicketsEnabled).map(_ === "true").getOrElse(false)){ checked="" } else { }> + <span class="pure-form-message-inline" id="@{fieldTicketsEnabled}.help">@Messages("form.create-repo.tickets-enabled.help")</span> + @renderFormErrors(fieldTicketsEnabled, formErrors) + </div> + <div class="pure-control-group"> <label for="@{fieldDescription}">@Messages("form.create-repo.description")</label> <textarea class="pure-input-1-2" id="@{fieldDescription}" name="@{fieldDescription}" placeholder="@Messages("form.create-repo.description.placeholder")" maxlength="254" rows="3">@{formData.get(fieldDescription)}</textarea> <span class="pure-form-message" id="@{fieldDescription}.help">@Messages("form.create-repo.description.help")</span> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/editRepository.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -59,6 +59,12 @@ @renderFormErrors(fieldIsPrivate, formErrors) </div> <div class="pure-control-group"> + <label for="@{fieldTicketsEnabled}">@Messages("form.create-repo.tickets-enabled")</label> + <input id="@{fieldTicketsEnabled}" name="@{fieldTicketsEnabled}" type="checkbox" value="true" @if(formData.get(fieldTicketsEnabled).map(_ === "true").getOrElse(false)){ checked="" } else { }> + <span class="pure-form-message-inline" id="@{fieldTicketsEnabled}.help">@Messages("form.create-repo.tickets-enabled.help")</span> + @renderFormErrors(fieldTicketsEnabled, formErrors) + </div> + <div class="pure-control-group"> <label for="@{fieldDescription}">@Messages("form.edit-repo.description")</label> <textarea class="pure-input-1-2" id="@{fieldDescription}" name="@{fieldDescription}" maxlength="254" rows="3">@{formData.get(fieldDescription)}</textarea> <span class="pure-form-message" id="@{fieldDescription}.help">@Messages("form.edit-repo.description.help")</span> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryBranches.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -4,6 +4,7 @@ lang: LanguageCode = LanguageCode("en") )(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, + linkToTicketService: Option[Uri] = None, title: Option[String] = None, user: Option[Account] )(vcsRepository: VcsRepository, @@ -16,7 +17,7 @@ <div class="pure-u-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(actionBaseUri.addSegment("branches").some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri, linkToTicketService)(actionBaseUri.addSegment("branches").some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) <div class="repo-summary-description"> @Messages("repository.branches.summary", vcsRepositoryBranches.size) </div> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryFiles.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -7,6 +7,7 @@ )(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, + linkToTicketService: Option[Uri] = None, title: Option[String] = None, user: Option[Account] )(fileContent: List[String], @@ -22,7 +23,7 @@ <div class="pure-u-1-1 pure-u-md-1-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(repositoryBaseUri.addSegment("files").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri, linkToTicketService)(repositoryBaseUri.addSegment("files").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) <div class="repo-summary-description"> <code>@{actionBaseUri.path.toString.replaceFirst("/files", "")}</code> </div> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryHistory.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -5,6 +5,7 @@ )(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, goBackUri: Option[Uri] = None, + linkToTicketService: Option[Uri] = None, title: Option[String] = None, user: Option[Account] )(history: List[VcsRepositoryPatchMetadata], @@ -20,7 +21,7 @@ <div class="pure-u-1-1 pure-u-md-1-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(repositoryBaseUri.addSegment("history").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri, linkToTicketService)(repositoryBaseUri.addSegment("history").some, vcsRepositoryBranches.size, repositoryBaseUri, user, vcsRepository) <div class="repo-summary-description"> @if(history.isEmpty) { @Messages("repository.changes.description.empty") diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryMenu.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,6 +1,7 @@ @import de.smederee.hub._ -@(baseUri: Uri +@(baseUri: Uri, + linkToTicketService: Option[Uri] = None, )(activeUri: Option[Uri], branches: Int, repositoryBaseUri: Uri, @@ -15,6 +16,11 @@ @defining(repositoryBaseUri.addSegment("files")) { uri => <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("folder") @Messages("repository.menu.files")</a></li> } + @for(ticketUri <- linkToTicketService.filter(_ => vcsRepository.ticketsEnabled)) { + @defining(ticketUri.addPath(repositoryBaseUri.path.toString).addSegment("tickets")) { uri => + <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("crosshair") @Messages("repository.menu.tickets")</a></li> + } + } @defining(repositoryBaseUri.addSegment("history")) { uri => <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("list") @Messages("repository.menu.changes")</a></li> } diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryOverview.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -4,6 +4,7 @@ lang: LanguageCode = LanguageCode("en") )(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, + linkToTicketService: Option[Uri] = None, title: Option[String] = None, user: Option[Account] )(vcsRepository: VcsRepository, @@ -21,7 +22,7 @@ <div class="pure-u-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(actionBaseUri.some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri, linkToTicketService)(actionBaseUri.some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) <div class="repo-summary-description"> <strong>@Messages("repository.description.title")</strong> @vcsRepository.description </div> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/hub/views/showRepositoryPatch.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -4,6 +4,7 @@ lang: LanguageCode = LanguageCode("en") )(actionBaseUri: Uri, csrf: Option[CsrfToken] = None, + linkToTicketService: Option[Uri] = None, title: Option[String] = None, user: Option[Account] )(patch: Option[VcsRepositoryPatchMetadata], @@ -18,7 +19,7 @@ <div class="pure-u-1-1 pure-u-md-1-1"> <div class="l-box-left-right"> <h2><a href="@{baseUri.addSegment(s"~${vcsRepository.owner.name}")}">~@vcsRepository.owner.name</a>/@vcsRepository.name</h2> - @showRepositoryMenu(baseUri)(actionBaseUri.addSegment("history").some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) + @showRepositoryMenu(baseUri, linkToTicketService)(actionBaseUri.addSegment("history").some, vcsRepositoryBranches.size, actionBaseUri, user, vcsRepository) <div class="repo-summary-description"> @for(patch <- patch) { @Messages("repository.changes.patch.description", patch.hash.toString) diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/createTicket.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/createTicket.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/createTicket.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/createTicket.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets.TicketForm._ @import de.smederee.tickets._ @import de.smederee.tickets.forms._ @@ -10,6 +9,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, labels: List[Label] = Nil, milestones: List[Milestone] = Nil, projectBaseUri: Uri, @@ -42,8 +42,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(action.some, projectBaseUri, user, project) <div class="project-summary-description"> @Messages("project.tickets.view.title") </div> @@ -66,7 +66,7 @@ } </div> <div class="edit-tickets-form"> - <form action="@projectBaseUri.addSegment("tickets")" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("tickets")}" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> <fieldset class="pure-group"> <div class="pure-g"> <div class="pure-u-18-24 pure-u-md-18-24"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabel.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabel.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabel.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabel.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets.LabelForm._ @import de.smederee.tickets._ @import de.smederee.tickets.forms._ @@ -11,6 +10,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, label: Label, projectBaseUri: Uri, title: Option[String] = None, @@ -25,8 +25,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(projectBaseUri.addSegment("labels").some, projectBaseUri, user.some, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(projectBaseUri.addSegment("labels").some, projectBaseUri, user.some, project) <div class="project-summary-description"> @Messages("project.labels.edit.title") </div> @@ -50,7 +50,7 @@ } </div> <div class="edit-labels-form"> - <form action="@action" class="pure-form pure-form-aligned" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(action.path.toString)}" class="pure-form pure-form-aligned" method="POST" accept-charset="UTF-8"> <fieldset> <input type="hidden" id="@fieldId" name="@fieldId" readonly="" value="@label.id"> <div class="pure-control-group"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editLabels.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets.LabelForm._ @import de.smederee.tickets._ @import de.smederee.tickets.forms._ @@ -11,6 +10,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, labels: List[Label], projectBaseUri: Uri, title: Option[String] = None, @@ -25,8 +25,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(action.some, projectBaseUri, user, project) <div class="project-summary-description"> @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) { @Messages("project.labels.edit.title") @@ -54,7 +54,7 @@ } </div> <div class="edit-labels-form"> - <form action="@projectBaseUri.addSegment("labels")" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("labels")}" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> <fieldset> <div class="pure-control-group"> <label for="@{fieldName}">@Messages("form.label.name")</label> @@ -106,7 +106,7 @@ </div> <div class="pure-u-8-24 label-form" style="height: @{lineHeight}px; line-height: @{lineHeight}px; padding-left: 1em;"> @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) { - <form action="@projectBaseUri.addSegment("label").addSegment(label.name.toString).addSegment("delete")" class="pure-form" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("label").addSegment(label.name.toString).addSegment("delete")}" class="pure-form" method="POST" accept-charset="UTF-8"> <fieldset> <input type="hidden" id="@fieldId-@label.name" name="@fieldId" readonly="" value="@label.id"> <input type="hidden" id="@fieldName-@label.name" name="@fieldName" readonly="" value="@label.name"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestone.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets.MilestoneForm._ @import de.smederee.tickets._ @import de.smederee.tickets.forms._ @@ -11,6 +10,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, milestone: Milestone, projectBaseUri: Uri, title: Option[String] = None, @@ -25,8 +25,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(projectBaseUri.addSegment("milestones").some, projectBaseUri, user.some, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(projectBaseUri.addSegment("milestones").some, projectBaseUri, user.some, project) <div class="project-summary-description"> @Messages("project.milestones.edit.title") </div> @@ -50,7 +50,7 @@ } </div> <div class="edit-milestones-form"> - <form action="@action" class="pure-form pure-form-aligned" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(action.path.toString)}" class="pure-form pure-form-aligned" method="POST" accept-charset="UTF-8"> <fieldset> <input type="hidden" id="@fieldId" name="@fieldId" readonly="" value="@milestone.id"> <div class="pure-control-group"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html 2025-01-16 09:48:54.192767006 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editMilestones.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,6 +1,5 @@ @import java.time._ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets.MilestoneForm._ @import de.smederee.tickets._ @import de.smederee.tickets.forms._ @@ -13,6 +12,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, milestones: List[Milestone], projectBaseUri: Uri, title: Option[String] = None, @@ -27,8 +27,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(action.some, projectBaseUri, user, project) <div class="project-summary-description"> @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) { @Messages("project.milestones.edit.title") @@ -56,7 +56,7 @@ } </div> <div class="edit-milestones-form"> - <form action="@projectBaseUri.addSegment("milestones")" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("milestones")}" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> <fieldset> <div class="pure-control-group"> <label for="@{fieldTitle}">@Messages("form.milestone.title")</label> @@ -108,7 +108,7 @@ </div> <div class="pure-u-8-24 milestone-form" style="height: @{lineHeight}px; line-height: @{lineHeight}px; padding-left: 1em;"> @if(user.exists(account => ProjectOwnerId.fromUserId(account.uid) === project.owner.uid)) { - <form action="@projectBaseUri.addSegment("milestone").addSegment(milestone.title.toString).addSegment("delete")" class="pure-form" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("milestone").addSegment(milestone.title.toString).addSegment("delete")}" class="pure-form" method="POST" accept-charset="UTF-8"> <fieldset> <input type="hidden" id="@fieldId-@milestone.title" name="@fieldId" readonly="" value="@milestone.id"> <input type="hidden" id="@fieldTitle-@milestone.title" name="@fieldTitle" readonly="" value="@milestone.title"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editTicket.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editTicket.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editTicket.scala.html 2025-01-16 09:48:54.196767011 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/editTicket.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets.TicketForm._ @import de.smederee.tickets._ @import de.smederee.tickets.forms._ @@ -10,6 +9,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, labels: List[Label] = Nil, milestones: List[Milestone] = Nil, projectBaseUri: Uri, @@ -43,8 +43,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(action.some, projectBaseUri, user, project) <div class="project-summary-description"> @Messages("project.tickets.edit.title", ticketNumber) </div> @@ -67,7 +67,7 @@ } </div> <div class="edit-tickets-form"> - <form action="@projectBaseUri.addSegment("tickets").addSegment(ticketNumber.toString)" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> + <form action="@{linkToHubService.addPath(projectBaseUri.path.toString).addSegment("tickets").addSegment(ticketNumber.toString)}" class="pure-form pure-form-stacked" method="POST" accept-charset="UTF-8"> <fieldset class="pure-group"> <div class="pure-g"> <div class="pure-u-18-24 pure-u-md-18-24"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/main.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/main.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/main.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/main.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -0,0 +1,33 @@ +@import de.smederee.hub.Account + +@(baseUri: Uri = Uri(path = Uri.Path.Root), + lang: LanguageCode = LanguageCode("en"), + tags: MetaTags = MetaTags.empty +)(customFooters: Html = Html(""), + customHeaders: Html = Html("") +)(csrf: Option[CsrfToken], + title: Option[String], + user: Option[Account] +)(content: Html) +@defining(lang.toLocale) { implicit locale => +<!DOCTYPE html> +<html lang="@lang"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + @meta(tags) + <title>@title</title> + <link rel="stylesheet" href="@{baseUri.addPath("assets/purecss/3.0.0/pure-min.css")}" /> + <link rel="stylesheet" href="@{baseUri.addPath("assets/purecss/3.0.0/grids-responsive-min.css")}" /> + <link rel="stylesheet" href="@{baseUri.addPath("assets/css/main.css")}" /> + @customHeaders +</head> +<body> + <navbar class="header navbar" id="navbar-top">@navbar(baseUri, lang)(csrf, None, user)</navbar> + <main class="content-wrapper"> + @content + </main> + @customFooters +</body> +</html> +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/meta.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/meta.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/meta.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/meta.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -0,0 +1,3 @@ +@(tags: MetaTags) +@if(tags.description.nonEmpty){<meta name="description" content="@{tags.description}">} +@if(tags.keywords.nonEmpty){<meta name="keywords" content="@{tags.keywords.mkString}">} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/navbar.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/navbar.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/navbar.scala.html 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/navbar.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -0,0 +1,28 @@ +@import de.smederee.hub.Account + +@(baseUri: Uri, lang: LanguageCode)(csrf: Option[CsrfToken] = None, extraCss: Option[String] = None, user: Option[Account] = None) +@defining(lang.toLocale) { implicit locale => +<nav class="home-menu pure-menu pure-menu-horizontal pure-menu-scrollable @extraCss"> + <a class="logo pure-menu-heading" href="@baseUri">@Messages("global.navbar.top.logo")</a> + + <ul class="pure-menu-list"> + <li class="pure-menu-item"><a class="pure-menu-link" href="@{baseUri.addPath("projects")}">@Messages("global.navbar.top.repositories.all")</a></li> + @if(user.nonEmpty) { + @for(account <- user) { + <li class="pure-menu-item"><a class="pure-menu-link" href="@{baseUri.addPath(s"~${account.name}")}">@Messages("global.navbar.top.repositories.yours")</a></li> + } + <li class="pure-menu-item"><a class="pure-menu-link" href="@{baseUri.addPath("repo/create")}">+ @Messages("global.navbar.top.repository.new")</a></li> + <li class="pure-menu-item"><a class="pure-menu-link" href="@{baseUri.addPath("user/settings")}">@Messages("global.navbar.top.settings")</a></li> + <li class="pure-menu-item"> + <form action="@{baseUri.addPath("logout")}" method="POST" accept-charset="UTF-8" class="pure-form"> + @csrfToken(csrf) + <button class="pure-button" type="submit">@Messages("global.logout")</button> + </form> + </li> + } else { + <li class="pure-menu-item"><a class="pure-menu-link" href="@{baseUri.addPath("login")}">@Messages("global.login")</a></li> + <li class="pure-menu-item"><a class="pure-menu-link" href="@{baseUri.addPath("signup")}">@Messages("global.signup")</a></li> + } + </ul> +</nav> +} diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showProjectMenu.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showProjectMenu.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showProjectMenu.scala.html 2025-01-16 09:48:54.196767011 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showProjectMenu.scala.html 2025-01-16 09:48:54.200767016 +0000 @@ -1,7 +1,8 @@ -@import de.smederee.hub._ +@import de.smederee.hub.Account @import de.smederee.tickets.{ Project, ProjectOwnerId } -@(baseUri: Uri +@(baseUri: Uri, + linkToHubService: Uri )(activeUri: Option[Uri], projectBaseUri: Uri, user: Option[Account] = None, @@ -9,8 +10,8 @@ )(implicit locale: java.util.Locale) <nav class="pure-menu pure-menu-horizontal"> <ul class="pure-menu-list"> - @defining(projectBaseUri) { uri => - <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@{Uri.fromString(s"${uri.scheme.map(_.value).getOrElse("http")}://smeder.ee/${uri.path.toString}${uri.query.toString}").toOption}">@icon(baseUri)("eye") @Messages("project.menu.overview")</a></li> + @defining(linkToHubService.addPath(projectBaseUri.path.toString)) { uri => + <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("eye") @Messages("project.menu.overview")</a></li> } @defining(projectBaseUri.addSegment("tickets")) { uri => <li class="pure-menu-item@if(activeUri.exists(_ === uri)){ pure-menu-active}else{}"><a class="pure-menu-link" href="@uri">@icon(baseUri)("crosshair") @Messages("project.menu.tickets")</a></li> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html 2025-01-16 09:48:54.196767011 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTicket.scala.html 2025-01-16 09:48:54.204767020 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets._ @import de.smederee.tickets.views.html.format._ @@ -7,6 +6,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, labels: List[Label], milestones: List[Milestone], ticket: Ticket, @@ -22,9 +22,9 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) - <div class="project-summary-description">@ticket.number created by @formatTicketSubmitter(baseUri)(ticket) at @formatDateTime(ticket.createdAt)</div> + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(action.some, projectBaseUri, user, project) + <div class="project-summary-description">@ticket.number created by @formatTicketSubmitter(linkToHubService)(ticket) at @formatDateTime(ticket.createdAt)</div> </div> </div> </div> @@ -48,7 +48,7 @@ <div class="pure-g ticket-sidebar"> <div class="pure-u-1-5">Status</div><div class="pure-u-4-5">@formatTicketStatus(ticket)</div> <div class="pure-u-1-5">Assigned</div><div class="pure-u-4-5"></div> - <div class="pure-u-1-5">Reported</div><div class="pure-u-4-5">@formatTicketSubmitter(baseUri)(ticket) at @formatDateTime(ticket.createdAt)</div> + <div class="pure-u-1-5">Reported</div><div class="pure-u-4-5">@formatTicketSubmitter(linkToHubService)(ticket) at @formatDateTime(ticket.createdAt)</div> @if(milestones.nonEmpty) { <div class="pure-u-1-5">Milestones</div> <div class="pure-u-4-5"> diff -rN -u old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTickets.scala.html new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTickets.scala.html --- old-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTickets.scala.html 2025-01-16 09:48:54.196767011 +0000 +++ new-smederee/modules/hub/src/main/twirl/de/smederee/tickets/views/showTickets.scala.html 2025-01-16 09:48:54.204767020 +0000 @@ -1,5 +1,4 @@ @import de.smederee.hub.Account -@import de.smederee.hub.views.html.main @import de.smederee.tickets._ @import de.smederee.tickets.views.html.format.formatDateTime @@ -7,6 +6,7 @@ lang: LanguageCode = LanguageCode("en") )(action: Uri, csrf: Option[CsrfToken] = None, + linkToHubService: Uri, tickets: List[Ticket], projectBaseUri: Uri, title: Option[String] = None, @@ -19,8 +19,8 @@ <div class="pure-g"> <div class="pure-u-1"> <div class="l-box-left-right"> - <h2><a href="@{baseUri.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> - @showProjectMenu(baseUri)(action.some, projectBaseUri, user, project) + <h2><a href="@{linkToHubService.addSegment(s"~${project.owner.name}")}">~@project.owner.name</a>/@project.name</h2> + @showProjectMenu(baseUri, linkToHubService)(action.some, projectBaseUri, user, project) <div class="project-summary-description"> @Messages("project.tickets.view.title") </div> @@ -66,7 +66,7 @@ <div class="pure-u-8-24"></div> <div class="pure-u-3-24">@ticket.status</div> <div class="pure-u-3-24">@ticket.resolution</div> - <div class="pure-u-3-24">@for(submitter <- ticket.submitter){<a href="@{baseUri.addSegment(s"~${submitter.name}")}">@submitter.name</a>}</div> + <div class="pure-u-3-24">@for(submitter <- ticket.submitter){<a href="@{linkToHubService.addSegment(s"~${submitter.name}")}">@submitter.name</a>}</div> <div class="pure-u-6-24">@formatDateTime(ticket.updatedAt)</div> </div> } 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-16 09:48:54.196767011 +0000 +++ new-smederee/modules/hub/src/test/scala/de/smederee/hub/Generators.scala 2025-01-16 09:48:54.204767020 +0000 @@ -189,12 +189,13 @@ 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) - vcsType <- genValidVcsType - } yield VcsRepository(name, owner, isPrivate, description, vcsType, None) + 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/tickets/src/main/resources/reference.conf new-smederee/modules/tickets/src/main/resources/reference.conf --- old-smederee/modules/tickets/src/main/resources/reference.conf 2025-01-16 09:48:54.196767011 +0000 +++ new-smederee/modules/tickets/src/main/resources/reference.conf 2025-01-16 09:48:54.204767020 +0000 @@ -5,103 +5,110 @@ tickets { # Authentication / login settings authentication { - enabled = true + enabled = true - # The name used for the authentication cookie. - cookie-name = "sloetel" + # The name used for the authentication cookie. + cookie-name = "sloetel" - # The secret used for the cookie encryption and validation. - # Using the default should produce a warning message on startup. - cookie-secret = "CHANGEME" - - # Determines after how many failed login attempts an account gets locked. - lock-after = 5 - - # Timeouts for the authentication session. - timeouts { - # The maximum allowed age an authentication session. This setting will - # affect the invalidation of a session on the server side. - # This timeout MUST be triggered regardless of session activity. - absolute-timeout = 3 days - - # This timeout defines how long after the last activity a session will - # remain valid. - idle-timeout = 30 minutes - - # The time after which a session will be renewed (a new session ID will be - # generated). - renewal-timeout = 20 minutes - } + # The secret used for the cookie encryption and validation. + # Using the default should produce a warning message on startup. + cookie-secret = "CHANGEME" + + # Determines after how many failed login attempts an account gets locked. + lock-after = 5 + + # Timeouts for the authentication session. + timeouts { + # The maximum allowed age an authentication session. This setting will + # affect the invalidation of a session on the server side. + # This timeout MUST be triggered regardless of session activity. + absolute-timeout = 3 days + + # This timeout defines how long after the last activity a session will + # remain valid. + idle-timeout = 30 minutes + + # The time after which a session will be renewed (a new session ID will be + # generated). + renewal-timeout = 20 minutes + } } # Configuration of the CSRF protection middleware. csrf-protection { - # The official hostname of the service which will be used for the CSRF - # protection. - host = ${tickets.service.host} - - # The port number which defaults to the port the service is listening on. - # If the service is running behind a reverse proxy on a standard port e.g. - # 80 or 443 (http or https) then you MUST set this either to `port = null` - # or comment it out! - port = ${tickets.service.port} - - # The URL scheme which is used for links and will also determine if cookies - # will have the secure flag enabled. - # Valid options are: - # - http - # - https - scheme = "http" + # The official hostname of the service which will be used for the CSRF + # protection. + host = ${tickets.service.host} + + # The port number which defaults to the port the service is listening on. + # If the service is running behind a reverse proxy on a standard port e.g. + # 80 or 443 (http or https) then you MUST set this either to `port = null` + # or comment it out! + port = ${tickets.service.port} + + # The URL scheme which is used for links and will also determine if cookies + # will have the secure flag enabled. + # Valid options are: + # - http + # - https + scheme = "http" } # Configuration of the database. # Defaults are given except for password and can also be overridden via # environment variables. database { - # The class name of the JDBC driver to be used. - driver = "org.postgresql.Driver" - driver = ${?SMEDEREE_TICKETS_DB_DRIVER} - # The JDBC connection URL **without** username and password. - url = "jdbc:postgresql://localhost:5432/smederee" - url = ${?SMEDEREE_TICKETS_DB_URL} - # The username (login) needed to authenticate against the database. - user = "smederee_tickets" - user = ${?SMEDEREE_TICKETS_DB_USER} - # The password needed to authenticate against the database. - pass = ${?SMEDEREE_TICKETS_DB_PASS} + # The class name of the JDBC driver to be used. + driver = "org.postgresql.Driver" + driver = ${?SMEDEREE_TICKETS_DB_DRIVER} + # The JDBC connection URL **without** username and password. + url = "jdbc:postgresql://localhost:5432/smederee" + url = ${?SMEDEREE_TICKETS_DB_URL} + # The username (login) needed to authenticate against the database. + user = "smederee_tickets" + user = ${?SMEDEREE_TICKETS_DB_USER} + # The password needed to authenticate against the database. + pass = ${?SMEDEREE_TICKETS_DB_PASS} } # Settings affecting how the service will communicate several information to # the "outside world" e.g. if it runs behind a reverse proxy. external-url { - # The official hostname of the service which will be used for the generation - # of links. - host = ${tickets.service.host} - - # A possible path prefix that will be prepended to any paths used in link - # generation. If no path prefix is used then you MUST either comment it out - # or set it to `path = null`! - #path = null - - # The port number which defaults to the port the service is listening on. - # If the service is running behind a reverse proxy on a standard port e.g. - # 80 or 443 (http or https) then you MUST set this either to `port = null` - # or comment it out! - port = ${tickets.service.port} - - # The URL scheme which is used for links and will also determine if cookies - # will have the secure flag enabled. - # Valid options are: - # - http - # - https - scheme = "http" + # The official hostname of the service which will be used for the generation + # of links. + host = ${tickets.service.host} + + # A possible path prefix that will be prepended to any paths used in link + # generation. If no path prefix is used then you MUST either comment it out + # or set it to `path = null`! + #path = null + + # The port number which defaults to the port the service is listening on. + # If the service is running behind a reverse proxy on a standard port e.g. + # 80 or 443 (http or https) then you MUST set this either to `port = null` + # or comment it out! + port = ${tickets.service.port} + + # The URL scheme which is used for links and will also determine if cookies + # will have the secure flag enabled. + # Valid options are: + # - http + # - https + scheme = "http" + } + + # Configuration regarding the integration with the hub service. + hub-integration { + # The base URI used to build links to the hub service. + base-uri = "http://localhost:8080" + base-uri = ${?SMEDEREE_HUB_BASE_URI} } # Generic service configuration. service { - # The hostname on which the service shall listen for requests. - host = "localhost" - # The TCP port number on which the service shall listen for requests. - port = 8081 + # The hostname on which the service shall listen for requests. + host = "localhost" + # The TCP port number on which the service shall listen for requests. + port = 8081 } } 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-16 09:48:54.196767011 +0000 +++ new-smederee/modules/tickets/src/main/scala/de/smederee/tickets/config/SmedereeTicketsConfiguration.scala 2025-01-16 09:48:54.204767020 +0000 @@ -45,6 +45,34 @@ ConfigReader.forProduct3("host", "port", "scheme")(CsrfProtectionConfiguration.apply) } +/** Configuration regarding the integration with the hub service. + * + * @param baseUri + * The base URI used to build links to the hub service. + */ +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) +} + +/** Generic service configuration determining how the service will be run. + * + * @param host + * The hostname on which the service shall listen for requests. + * @param port + * The TCP port number on which the service shall listen for requests. + */ +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) +} + /** Wrapper class for the confiuration of the Smederee tickets module. * * @param csrfProtection @@ -54,11 +82,17 @@ * @param externalUrl * Configuration regarding support for generating "external urls" which is usually needed if the service runs behind * a reverse proxy. + * @param hub + * Configuration regarding the integration with the hub service. + * @param service + * Generic service configuration determining how the service will be run. */ final case class SmedereeTicketsConfiguration( csrfProtection: CsrfProtectionConfiguration, database: DatabaseConfig, - externalUrl: ExternalUrlConfiguration + externalUrl: ExternalUrlConfiguration, + hub: HubIntegrationConfiguration, + service: ServiceConfiguration ) object SmedereeTicketsConfiguration { @@ -73,5 +107,7 @@ ConfigReader.forProduct4("host", "path", "port", "scheme")(ExternalUrlConfiguration.apply) given ConfigReader[SmedereeTicketsConfiguration] = - ConfigReader.forProduct3("csrf-protection", "database", "external-url")(SmedereeTicketsConfiguration.apply) + ConfigReader.forProduct5("csrf-protection", "database", "external-url", "hub-integration", "service")( + SmedereeTicketsConfiguration.apply + ) } 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 1970-01-01 00:00:00.000000000 +0000 +++ new-smederee/modules/tickets/src/test/scala/de/smederee/tickets/config/SmedereeTicketsConfigurationTest.scala 2025-01-16 09:48:54.204767020 +0000 @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 Contributors as noted in the AUTHORS.md file + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +package de.smederee.tickets.config + +import cats.syntax.all._ +import com.typesafe.config._ +import org.http4s.Uri +import org.http4s.implicits._ +import pureconfig._ + +import munit._ + +final class SmedereeTicketsConfigurationTest 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"${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 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) + } + } +}