diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestApp.scala b/modules/restserver/src/main/scala/docspell/restserver/RestApp.scala index 44c80eaa..9ad2f34e 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestApp.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestApp.scala @@ -7,8 +7,9 @@ package docspell.restserver import fs2.Stream - import docspell.backend.BackendApp +import org.http4s.HttpRoutes +import org.http4s.server.websocket.WebSocketBuilder2 trait RestApp[F[_]] { @@ -25,4 +26,7 @@ trait RestApp[F[_]] { * via websocket. */ def subscriptions: Stream[F, Nothing] + + /** Http4s endpoint definitions. */ + def routes(wsb: WebSocketBuilder2[F]): HttpRoutes[F] } diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala index ce14ecd3..ea44065d 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestAppImpl.scala @@ -9,22 +9,30 @@ package docspell.restserver import cats.effect._ import fs2.Stream import fs2.concurrent.Topic - import docspell.backend.BackendApp +import docspell.backend.auth.{AuthToken, ShareToken} import docspell.ftsclient.FtsClient import docspell.ftssolr.SolrFtsClient import docspell.notification.api.NotificationModule import docspell.notification.impl.NotificationModuleImpl +import docspell.oidc.CodeFlowRoutes import docspell.pubsub.api.{PubSub, PubSubT} -import docspell.restserver.ws.OutputEvent +import docspell.restserver.auth.OpenId +import docspell.restserver.http4s.EnvMiddleware +import docspell.restserver.routes._ +import docspell.restserver.webapp.{TemplateRoutes, Templates, WebjarRoutes} +import docspell.restserver.ws.{OutputEvent, WebSocketRoutes} import docspell.store.Store - import emil.javamail.JavaMailEmil +import org.http4s.HttpRoutes import org.http4s.client.Client +import org.http4s.server.Router +import org.http4s.server.websocket.WebSocketBuilder2 final class RestAppImpl[F[_]: Async]( val config: Config, val backend: BackendApp[F], + httpClient: Client[F], notificationMod: NotificationModule[F], wsTopic: Topic[F, OutputEvent], pubSub: PubSubT[F] @@ -35,6 +43,107 @@ final class RestAppImpl[F[_]: Async]( def subscriptions: Stream[F, Nothing] = Subscriptions[F](wsTopic, pubSub) + + def routes(wsb: WebSocketBuilder2[F]): HttpRoutes[F] = + createHttpApp(wsb) + + val templates = TemplateRoutes[F](config, Templates[F]) + + def createHttpApp( + wsB: WebSocketBuilder2[F] + ) = + Router( + "/api/info" -> InfoRoutes(), + "/api/v1/open/" -> openRoutes(httpClient), + "/api/v1/sec/" -> Authenticate(backend.login, config.auth) { token => + securedRoutes(wsB, token) + }, + "/api/v1/admin" -> AdminAuth(config.adminEndpoint) { + adminRoutes + }, + "/api/v1/share" -> ShareAuth(backend.share, config.auth) { token => + shareRoutes(token) + }, + "/api/doc" -> templates.doc, + "/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F]), + "/app" -> EnvMiddleware(templates.app), + "/sw.js" -> EnvMiddleware(templates.serviceWorker) + ) + + def adminRoutes: HttpRoutes[F] = + Router( + "fts" -> FullTextIndexRoutes.admin(config, backend), + "user/otp" -> TotpRoutes.admin(backend), + "user" -> UserRoutes.admin(backend), + "info" -> InfoRoutes.admin(config), + "attachments" -> AttachmentRoutes.admin(backend) + ) + + def shareRoutes( + token: ShareToken + ): HttpRoutes[F] = + Router( + "search" -> ShareSearchRoutes(backend, config, token), + "attachment" -> ShareAttachmentRoutes(backend, token), + "item" -> ShareItemRoutes(backend, token), + "clientSettings" -> ClientSettingsRoutes.share(backend, token) + ) + def openRoutes( + client: Client[F] + ): HttpRoutes[F] = + Router( + "auth/openid" -> CodeFlowRoutes( + config.openIdEnabled, + OpenId.handle[F](backend, config), + OpenId.codeFlowConfig(config), + client + ), + "auth" -> LoginRoutes.login(backend.login, config), + "signup" -> RegisterRoutes(backend, config), + "upload" -> UploadRoutes.open(backend, config), + "checkfile" -> CheckFileRoutes.open(backend), + "integration" -> IntegrationEndpointRoutes.open(backend, config), + "share" -> ShareRoutes.verify(backend, config) + ) + + def securedRoutes( + wsB: WebSocketBuilder2[F], + token: AuthToken + ): HttpRoutes[F] = + Router( + "ws" -> WebSocketRoutes(token, backend, wsTopic, wsB), + "auth" -> LoginRoutes.session(backend.login, config, token), + "tag" -> TagRoutes(backend, token), + "equipment" -> EquipmentRoutes(backend, token), + "organization" -> OrganizationRoutes(backend, token), + "person" -> PersonRoutes(backend, token), + "source" -> SourceRoutes(backend, token), + "user/otp" -> TotpRoutes(backend, config, token), + "user" -> UserRoutes(backend, token), + "collective" -> CollectiveRoutes(backend, token), + "queue" -> JobQueueRoutes(backend, token), + "item" -> ItemRoutes(config, backend, token), + "items" -> ItemMultiRoutes(config, backend, token), + "attachment" -> AttachmentRoutes(backend, token), + "attachments" -> AttachmentMultiRoutes(backend, token), + "upload" -> UploadRoutes.secured(backend, config, token), + "checkfile" -> CheckFileRoutes.secured(backend, token), + "email/send" -> MailSendRoutes(backend, token), + "email/settings" -> MailSettingsRoutes(backend, token), + "email/sent" -> SentMailRoutes(backend, token), + "share" -> ShareRoutes.manage(backend, token), + "usertask/notifydueitems" -> NotifyDueItemsRoutes(config, backend, token), + "usertask/scanmailbox" -> ScanMailboxRoutes(backend, token), + "usertask/periodicquery" -> PeriodicQueryRoutes(config, backend, token), + "calevent/check" -> CalEventCheckRoutes(), + "fts" -> FullTextIndexRoutes.secured(config, backend, token), + "folder" -> FolderRoutes(backend, token), + "customfield" -> CustomFieldRoutes(backend, token), + "clientSettings" -> ClientSettingsRoutes(backend, token), + "notification" -> NotificationRoutes(config, backend, token), + "querybookmark" -> BookmarkRoutes(backend, token) + ) + } object RestAppImpl { @@ -58,7 +167,14 @@ object RestAppImpl { backend <- BackendApp .create[F](store, javaEmil, ftsClient, pubSubT, notificationMod) - app = new RestAppImpl[F](cfg, backend, notificationMod, wsTopic, pubSubT) + app = new RestAppImpl[F]( + cfg, + backend, + httpClient, + notificationMod, + wsTopic, + pubSubT + ) } yield app } diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 891d177e..0b58a584 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -7,30 +7,21 @@ package docspell.restserver import scala.concurrent.duration._ - import cats.effect._ import cats.implicits._ import fs2.Stream import fs2.concurrent.Topic - -import docspell.backend.auth.{AuthToken, ShareToken} import docspell.backend.msg.Topics import docspell.common._ -import docspell.oidc.CodeFlowRoutes import docspell.pubsub.naive.NaivePubSub -import docspell.restserver.auth.OpenId -import docspell.restserver.http4s.{EnvMiddleware, InternalHeader} -import docspell.restserver.routes._ -import docspell.restserver.webapp._ +import docspell.restserver.http4s.InternalHeader import docspell.restserver.ws.OutputEvent.KeepAlive -import docspell.restserver.ws.{OutputEvent, WebSocketRoutes} +import docspell.restserver.ws.OutputEvent import docspell.store.Store import docspell.store.records.RInternalSetting - import org.http4s._ import org.http4s.blaze.client.BlazeClientBuilder import org.http4s.blaze.server.BlazeServerBuilder -import org.http4s.client.Client import org.http4s.dsl.Http4sDsl import org.http4s.headers.Location import org.http4s.implicits._ @@ -51,7 +42,7 @@ object RestServer { server = Stream .resource(createApp(cfg, pools, wsTopic)) - .flatMap { case (restApp, pubSub, httpClient, setting) => + .flatMap { case (restApp, pubSub, setting) => Stream( restApp.subscriptions, restApp.eventConsume(2), @@ -59,7 +50,7 @@ object RestServer { .bindHttp(cfg.bind.port, cfg.bind.address) .withoutBanner .withHttpWebSocketApp( - createHttpApp(cfg, setting, httpClient, pubSub, restApp, wsTopic) + createHttpApp(setting, pubSub, restApp) ) .serve .drain @@ -76,7 +67,7 @@ object RestServer { wsTopic: Topic[F, OutputEvent] ): Resource[ F, - (RestApp[F], NaivePubSub[F], Client[F], RInternalSetting) + (RestApp[F], NaivePubSub[F], RInternalSetting) ] = for { httpClient <- BlazeClientBuilder[F].resource @@ -92,41 +83,22 @@ object RestServer { httpClient )(Topics.all.map(_.topic)) restApp <- RestAppImpl.create[F](cfg, store, httpClient, pubSub, wsTopic) - } yield (restApp, pubSub, httpClient, setting) + } yield (restApp, pubSub, setting) def createHttpApp[F[_]: Async]( - cfg: Config, internSettings: RInternalSetting, - httpClient: Client[F], pubSub: NaivePubSub[F], - restApp: RestApp[F], - topic: Topic[F, OutputEvent] + restApp: RestApp[F] )( wsB: WebSocketBuilder2[F] ) = { - val templates = TemplateRoutes[F](cfg, Templates[F]) - val httpApp = Router( + val internal = Router( + "/" -> redirectTo("/app"), "/internal" -> InternalHeader(internSettings.internalRouteKey) { internalRoutes(pubSub) - }, - "/api/info" -> routes.InfoRoutes(), - "/api/v1/open/" -> openRoutes(cfg, httpClient, restApp), - "/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token => - securedRoutes(cfg, restApp, wsB, topic, token) - }, - "/api/v1/admin" -> AdminAuth(cfg.adminEndpoint) { - adminRoutes(cfg, restApp) - }, - "/api/v1/share" -> ShareAuth(restApp.backend.share, cfg.auth) { token => - shareRoutes(cfg, restApp, token) - }, - "/api/doc" -> templates.doc, - "/app/assets" -> EnvMiddleware(WebjarRoutes.appRoutes[F]), - "/app" -> EnvMiddleware(templates.app), - "/sw.js" -> EnvMiddleware(templates.serviceWorker), - "/" -> redirectTo("/app") - ).orNotFound - + } + ) + val httpApp = (internal <+> restApp.routes(wsB)).orNotFound Logger.httpApp(logHeaders = false, logBody = false)(httpApp) } @@ -135,88 +107,6 @@ object RestServer { "pubsub" -> pubSub.receiveRoute ) - def securedRoutes[F[_]: Async]( - cfg: Config, - restApp: RestApp[F], - wsB: WebSocketBuilder2[F], - topic: Topic[F, OutputEvent], - token: AuthToken - ): HttpRoutes[F] = - Router( - "ws" -> WebSocketRoutes(token, restApp.backend, topic, wsB), - "auth" -> LoginRoutes.session(restApp.backend.login, cfg, token), - "tag" -> TagRoutes(restApp.backend, token), - "equipment" -> EquipmentRoutes(restApp.backend, token), - "organization" -> OrganizationRoutes(restApp.backend, token), - "person" -> PersonRoutes(restApp.backend, token), - "source" -> SourceRoutes(restApp.backend, token), - "user/otp" -> TotpRoutes(restApp.backend, cfg, token), - "user" -> UserRoutes(restApp.backend, token), - "collective" -> CollectiveRoutes(restApp.backend, token), - "queue" -> JobQueueRoutes(restApp.backend, token), - "item" -> ItemRoutes(cfg, restApp.backend, token), - "items" -> ItemMultiRoutes(cfg, restApp.backend, token), - "attachment" -> AttachmentRoutes(restApp.backend, token), - "attachments" -> AttachmentMultiRoutes(restApp.backend, token), - "upload" -> UploadRoutes.secured(restApp.backend, cfg, token), - "checkfile" -> CheckFileRoutes.secured(restApp.backend, token), - "email/send" -> MailSendRoutes(restApp.backend, token), - "email/settings" -> MailSettingsRoutes(restApp.backend, token), - "email/sent" -> SentMailRoutes(restApp.backend, token), - "share" -> ShareRoutes.manage(restApp.backend, token), - "usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token), - "usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token), - "usertask/periodicquery" -> PeriodicQueryRoutes(cfg, restApp.backend, token), - "calevent/check" -> CalEventCheckRoutes(), - "fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token), - "folder" -> FolderRoutes(restApp.backend, token), - "customfield" -> CustomFieldRoutes(restApp.backend, token), - "clientSettings" -> ClientSettingsRoutes(restApp.backend, token), - "notification" -> NotificationRoutes(cfg, restApp.backend, token), - "querybookmark" -> BookmarkRoutes(restApp.backend, token) - ) - - def openRoutes[F[_]: Async]( - cfg: Config, - client: Client[F], - restApp: RestApp[F] - ): HttpRoutes[F] = - Router( - "auth/openid" -> CodeFlowRoutes( - cfg.openIdEnabled, - OpenId.handle[F](restApp.backend, cfg), - OpenId.codeFlowConfig(cfg), - client - ), - "auth" -> LoginRoutes.login(restApp.backend.login, cfg), - "signup" -> RegisterRoutes(restApp.backend, cfg), - "upload" -> UploadRoutes.open(restApp.backend, cfg), - "checkfile" -> CheckFileRoutes.open(restApp.backend), - "integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg), - "share" -> ShareRoutes.verify(restApp.backend, cfg) - ) - - def adminRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] = - Router( - "fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend), - "user/otp" -> TotpRoutes.admin(restApp.backend), - "user" -> UserRoutes.admin(restApp.backend), - "info" -> InfoRoutes.admin(cfg), - "attachments" -> AttachmentRoutes.admin(restApp.backend) - ) - - def shareRoutes[F[_]: Async]( - cfg: Config, - restApp: RestApp[F], - token: ShareToken - ): HttpRoutes[F] = - Router( - "search" -> ShareSearchRoutes(restApp.backend, cfg, token), - "attachment" -> ShareAttachmentRoutes(restApp.backend, token), - "item" -> ShareItemRoutes(restApp.backend, token), - "clientSettings" -> ClientSettingsRoutes.share(restApp.backend, token) - ) - def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = { val dsl = new Http4sDsl[F] {} import dsl._