Initial impl of search route

This commit is contained in:
eikek 2021-10-04 09:46:08 +02:00
parent f4596db63d
commit a286556116
4 changed files with 102 additions and 15 deletions

View File

@ -12,7 +12,7 @@ import cats.implicits._
import docspell.backend.PasswordCrypt import docspell.backend.PasswordCrypt
import docspell.backend.auth.ShareToken import docspell.backend.auth.ShareToken
import docspell.backend.ops.OShare.VerifyResult import docspell.backend.ops.OShare.{ShareQuery, VerifyResult}
import docspell.common._ import docspell.common._
import docspell.query.ItemQuery import docspell.query.ItemQuery
import docspell.store.Store import docspell.store.Store
@ -36,26 +36,37 @@ trait OShare[F[_]] {
removePassword: Boolean removePassword: Boolean
): F[OShare.ChangeResult] ): 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] def verify(key: ByteVector)(id: Ident, password: Option[Password]): F[VerifyResult]
/** Verifies the authorization token. */
def verifyToken(key: ByteVector)(token: String): F[VerifyResult] def verifyToken(key: ByteVector)(token: String): F[VerifyResult]
def findShareQuery(id: Ident): OptionT[F, ShareQuery]
} }
object OShare { object OShare {
final case class ShareQuery(id: Ident, cid: Ident, query: ItemQuery)
sealed trait VerifyResult { sealed trait VerifyResult {
def toEither: Either[String, ShareToken] = def toEither: Either[String, ShareToken] =
this match { this match {
case VerifyResult.Success(token) => Right(token) case VerifyResult.Success(token, _) =>
case _ => Left("Authentication failed.") Right(token)
case _ => Left("Authentication failed.")
} }
} }
object VerifyResult { 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 NotFound extends VerifyResult
case object PasswordMismatch extends VerifyResult case object PasswordMismatch extends VerifyResult
case object InvalidToken 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 notFound: VerifyResult = NotFound
def passwordMismatch: VerifyResult = PasswordMismatch def passwordMismatch: VerifyResult = PasswordMismatch
def invalidToken: VerifyResult = InvalidToken def invalidToken: VerifyResult = InvalidToken
@ -158,8 +169,8 @@ object OShare {
val token = ShareToken.create(id, shareKey) val token = ShareToken.create(id, shareKey)
pwCheck match { pwCheck match {
case Some(true) => token.map(VerifyResult.success) case Some(true) => token.map(t => VerifyResult.success(t, share.name))
case None => token.map(VerifyResult.success) case None => token.map(t => VerifyResult.success(t, share.name))
case Some(false) => VerifyResult.passwordMismatch.pure[F] case Some(false) => VerifyResult.passwordMismatch.pure[F]
} }
} }
@ -186,5 +197,11 @@ object OShare {
logger.debug(s"Invalid session token: $err") *> logger.debug(s"Invalid session token: $err") *>
VerifyResult.invalidToken.pure[F] 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))
} }
} }

View File

@ -1558,6 +1558,30 @@ paths:
schema: schema:
$ref: "#/components/schemas/BasicResult" $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: /admin/user/resetPassword:
post: post:
operationId: "admin-user-reset-password" operationId: "admin-user-reset-password"
@ -4248,6 +4272,11 @@ components:
type: boolean type: boolean
message: message:
type: string type: string
name:
type: string
description: |
The name of the share if it exists. Only valid to use when
`success` is `true`.
ShareData: ShareData:
description: | description: |
@ -6475,6 +6504,10 @@ components:
type: apiKey type: apiKey
in: header in: header
name: Docspell-Admin-Secret name: Docspell-Admin-Secret
shareTokenHeader:
type: apiKey
in: header
name: Docspell-Share-Auth
parameters: parameters:
id: id:
name: id name: id

View File

@ -81,16 +81,16 @@ object ShareRoutes {
res <- backend.share res <- backend.share
.verify(cfg.auth.serverSecret)(secret.shareId, secret.password) .verify(cfg.auth.serverSecret)(secret.shareId, secret.password)
resp <- res match { resp <- res match {
case VerifyResult.Success(token) => case VerifyResult.Success(token, name) =>
val cd = ShareCookieData(token) 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))) .map(cd.addCookie(ClientRequestInfo.getBaseUrl(cfg, req)))
case VerifyResult.PasswordMismatch => case VerifyResult.PasswordMismatch =>
Ok(ShareVerifyResult(false, "", true, "Failed")) Ok(ShareVerifyResult(false, "", true, "Failed", None))
case VerifyResult.NotFound => case VerifyResult.NotFound =>
Ok(ShareVerifyResult(false, "", false, "Failed")) Ok(ShareVerifyResult(false, "", false, "Failed", None))
case VerifyResult.InvalidToken => case VerifyResult.InvalidToken =>
Ok(ShareVerifyResult(false, "", false, "Failed")) Ok(ShareVerifyResult(false, "", false, "Failed", None))
} }
} yield resp } yield resp
} }

View File

@ -7,13 +7,20 @@
package docspell.restserver.routes package docspell.restserver.routes
import cats.effect._ import cats.effect._
import cats.implicits._
import docspell.backend.BackendApp import docspell.backend.BackendApp
import docspell.backend.auth.ShareToken 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.restserver.Config
import docspell.store.qb.Batch
import docspell.store.queries.Query
import org.http4s.HttpRoutes import org.http4s.HttpRoutes
import org.http4s.circe.CirceEntityDecoder._
import org.http4s.dsl.Http4sDsl
object ShareSearchRoutes { object ShareSearchRoutes {
@ -23,7 +30,37 @@ object ShareSearchRoutes {
token: ShareToken token: ShareToken
): HttpRoutes[F] = { ): HttpRoutes[F] = {
val logger = Logger.log4s[F](org.log4s.getLogger) 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())
}
} }
} }