From a2865561160af9aaed469083e2f2ff2783c3986c Mon Sep 17 00:00:00 2001 From: eikek Date: Mon, 4 Oct 2021 09:46:08 +0200 Subject: [PATCH] Initial impl of search route --- .../scala/docspell/backend/ops/OShare.scala | 31 ++++++++++--- .../src/main/resources/docspell-openapi.yml | 33 ++++++++++++++ .../restserver/routes/ShareRoutes.scala | 10 ++--- .../restserver/routes/ShareSearchRoutes.scala | 43 +++++++++++++++++-- 4 files changed, 102 insertions(+), 15 deletions(-) diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala index 005938a0..d0b82171 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OShare.scala @@ -12,7 +12,7 @@ import cats.implicits._ import docspell.backend.PasswordCrypt import docspell.backend.auth.ShareToken -import docspell.backend.ops.OShare.VerifyResult +import docspell.backend.ops.OShare.{ShareQuery, VerifyResult} import docspell.common._ import docspell.query.ItemQuery import docspell.store.Store @@ -36,26 +36,37 @@ trait OShare[F[_]] { removePassword: Boolean ): F[OShare.ChangeResult] + // --- + + /** Verifies the given id and password and returns a authorization token on success. */ def verify(key: ByteVector)(id: Ident, password: Option[Password]): F[VerifyResult] + + /** Verifies the authorization token. */ def verifyToken(key: ByteVector)(token: String): F[VerifyResult] + + def findShareQuery(id: Ident): OptionT[F, ShareQuery] } object OShare { + final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery) sealed trait VerifyResult { def toEither: Either[String, ShareToken] = this match { - case VerifyResult.Success(token) => Right(token) - case _ => Left("Authentication failed.") + case VerifyResult.Success(token, _) => + Right(token) + case _ => Left("Authentication failed.") } } object VerifyResult { - case class Success(token: ShareToken) extends VerifyResult + case class Success(token: ShareToken, shareName: Option[String]) extends VerifyResult case object NotFound extends VerifyResult case object PasswordMismatch extends VerifyResult case object InvalidToken extends VerifyResult - def success(token: ShareToken): VerifyResult = Success(token) + def success(token: ShareToken): VerifyResult = Success(token, None) + def success(token: ShareToken, name: Option[String]): VerifyResult = + Success(token, name) def notFound: VerifyResult = NotFound def passwordMismatch: VerifyResult = PasswordMismatch def invalidToken: VerifyResult = InvalidToken @@ -158,8 +169,8 @@ object OShare { val token = ShareToken.create(id, shareKey) pwCheck match { - case Some(true) => token.map(VerifyResult.success) - case None => token.map(VerifyResult.success) + case Some(true) => token.map(t => VerifyResult.success(t, share.name)) + case None => token.map(t => VerifyResult.success(t, share.name)) case Some(false) => VerifyResult.passwordMismatch.pure[F] } } @@ -186,5 +197,11 @@ object OShare { logger.debug(s"Invalid session token: $err") *> VerifyResult.invalidToken.pure[F] } + + def findShareQuery(id: Ident): OptionT[F, ShareQuery] = + RShare + .findCurrentActive(id) + .mapK(store.transform) + .map(share => ShareQuery(share.id, share.cid, share.query)) } } diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index f1e5176b..2f563116 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -1558,6 +1558,30 @@ paths: schema: $ref: "#/components/schemas/BasicResult" + /share/search: + post: + operationId: "share-search" + tags: [Share] + summary: Performs a search in a share. + description: | + Allows to run a search query in the shared documents. The + input data structure is the same as with a standard query. The + `searchMode` parameter is ignored here. + security: + - shareTokenHeader: [] + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/ItemQuery" + responses: + 200: + description: Ok + content: + application/json: + schema: + $ref: "#/components/schemas/ItemLightList" + /admin/user/resetPassword: post: operationId: "admin-user-reset-password" @@ -4248,6 +4272,11 @@ components: type: boolean message: type: string + name: + type: string + description: | + The name of the share if it exists. Only valid to use when + `success` is `true`. ShareData: description: | @@ -6475,6 +6504,10 @@ components: type: apiKey in: header name: Docspell-Admin-Secret + shareTokenHeader: + type: apiKey + in: header + name: Docspell-Share-Auth parameters: id: name: id diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala index 1e5947a6..5e9b13b5 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareRoutes.scala @@ -81,16 +81,16 @@ object ShareRoutes { res <- backend.share .verify(cfg.auth.serverSecret)(secret.shareId, secret.password) resp <- res match { - case VerifyResult.Success(token) => + case VerifyResult.Success(token, name) => val cd = ShareCookieData(token) - Ok(ShareVerifyResult(true, token.asString, false, "Success")) + Ok(ShareVerifyResult(true, token.asString, false, "Success", name)) .map(cd.addCookie(ClientRequestInfo.getBaseUrl(cfg, req))) case VerifyResult.PasswordMismatch => - Ok(ShareVerifyResult(false, "", true, "Failed")) + Ok(ShareVerifyResult(false, "", true, "Failed", None)) case VerifyResult.NotFound => - Ok(ShareVerifyResult(false, "", false, "Failed")) + Ok(ShareVerifyResult(false, "", false, "Failed", None)) case VerifyResult.InvalidToken => - Ok(ShareVerifyResult(false, "", false, "Failed")) + Ok(ShareVerifyResult(false, "", false, "Failed", None)) } } yield resp } diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala index 720b5d2f..a0ffa3a9 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ShareSearchRoutes.scala @@ -7,13 +7,20 @@ package docspell.restserver.routes import cats.effect._ +import cats.implicits._ import docspell.backend.BackendApp import docspell.backend.auth.ShareToken -import docspell.common.Logger +import docspell.backend.ops.OSimpleSearch +import docspell.common._ +import docspell.restapi.model.ItemQuery import docspell.restserver.Config +import docspell.store.qb.Batch +import docspell.store.queries.Query import org.http4s.HttpRoutes +import org.http4s.circe.CirceEntityDecoder._ +import org.http4s.dsl.Http4sDsl object ShareSearchRoutes { @@ -23,7 +30,37 @@ object ShareSearchRoutes { token: ShareToken ): HttpRoutes[F] = { val logger = Logger.log4s[F](org.log4s.getLogger) - logger.trace(s"$backend $cfg $token") - ??? + + val dsl = new Http4sDsl[F] {} + import dsl._ + + HttpRoutes.of { case req @ POST -> Root => + backend.share + .findShareQuery(token.id) + .semiflatMap { share => + for { + userQuery <- req.as[ItemQuery] + batch = Batch( + userQuery.offset.getOrElse(0), + userQuery.limit.getOrElse(cfg.maxItemPageSize) + ).restrictLimitTo( + cfg.maxItemPageSize + ) + itemQuery = ItemQueryString(userQuery.query) + settings = OSimpleSearch.Settings( + batch, + cfg.fullTextSearch.enabled, + userQuery.withDetails.getOrElse(false), + cfg.maxNoteLength, + searchMode = SearchMode.Normal + ) + account = AccountId(share.cid, Ident.unsafe("")) + fixQuery = Query.Fix(account, Some(share.query.expr), None) + _ <- logger.debug(s"Searching in share ${share.id.id}: ${userQuery.query}") + resp <- ItemRoutes.searchItems(backend, dsl)(settings, fixQuery, itemQuery) + } yield resp + } + .getOrElseF(NotFound()) + } } }