mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-02 21:42:52 +00:00
Authorize share access
This commit is contained in:
parent
97922340d9
commit
f4596db63d
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.backend.auth
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.Common
|
||||
import docspell.common.{Ident, Timestamp}
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
/** Can be used as an authenticator to access data behind a share. */
|
||||
final case class ShareToken(created: Timestamp, id: Ident, salt: String, sig: String) {
|
||||
def asString = s"${created.toMillis}-${TokenUtil.b64enc(id.id)}-$salt-$sig"
|
||||
|
||||
def sigValid(key: ByteVector): Boolean = {
|
||||
val newSig = TokenUtil.sign(this, key)
|
||||
TokenUtil.constTimeEq(sig, newSig)
|
||||
}
|
||||
def sigInvalid(key: ByteVector): Boolean =
|
||||
!sigValid(key)
|
||||
}
|
||||
|
||||
object ShareToken {
|
||||
|
||||
def fromString(s: String): Either[String, ShareToken] =
|
||||
s.split("-", 4) match {
|
||||
case Array(ms, id, salt, sig) =>
|
||||
for {
|
||||
created <- ms.toLongOption.toRight("Invalid timestamp")
|
||||
idStr <- TokenUtil.b64dec(id).toRight("Cannot read authenticator data")
|
||||
shareId <- Ident.fromString(idStr)
|
||||
} yield ShareToken(Timestamp.ofMillis(created), shareId, salt, sig)
|
||||
|
||||
case _ =>
|
||||
Left("Invalid authenticator")
|
||||
}
|
||||
|
||||
def create[F[_]: Sync](shareId: Ident, key: ByteVector): F[ShareToken] =
|
||||
for {
|
||||
now <- Timestamp.current[F]
|
||||
salt <- Common.genSaltString[F]
|
||||
cd = ShareToken(now, shareId, salt, "")
|
||||
sig = TokenUtil.sign(cd, key)
|
||||
} yield cd.copy(sig = sig)
|
||||
|
||||
}
|
@ -18,17 +18,24 @@ private[auth] object TokenUtil {
|
||||
|
||||
def sign(cd: RememberToken, key: ByteVector): String = {
|
||||
val raw = cd.nowMillis.toString + cd.rememberId.id + cd.salt
|
||||
val mac = Mac.getInstance("HmacSHA1")
|
||||
mac.init(new SecretKeySpec(key.toArray, "HmacSHA1"))
|
||||
ByteVector.view(mac.doFinal(raw.getBytes(utf8))).toBase64
|
||||
signRaw(raw, key)
|
||||
}
|
||||
|
||||
def sign(cd: AuthToken, key: ByteVector): String = {
|
||||
val raw =
|
||||
cd.nowMillis.toString + cd.account.asString + cd.requireSecondFactor + cd.salt
|
||||
signRaw(raw, key)
|
||||
}
|
||||
|
||||
def sign(sd: ShareToken, key: ByteVector): String = {
|
||||
val raw = s"${sd.created.toMillis}${sd.id.id}${sd.salt}"
|
||||
signRaw(raw, key)
|
||||
}
|
||||
|
||||
private def signRaw(data: String, key: ByteVector): String = {
|
||||
val mac = Mac.getInstance("HmacSHA1")
|
||||
mac.init(new SecretKeySpec(key.toArray, "HmacSHA1"))
|
||||
ByteVector.view(mac.doFinal(raw.getBytes(utf8))).toBase64
|
||||
ByteVector.view(mac.doFinal(data.getBytes(utf8))).toBase64
|
||||
}
|
||||
|
||||
def b64enc(s: String): String =
|
||||
|
@ -11,11 +11,15 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.PasswordCrypt
|
||||
import docspell.backend.auth.ShareToken
|
||||
import docspell.backend.ops.OShare.VerifyResult
|
||||
import docspell.common._
|
||||
import docspell.query.ItemQuery
|
||||
import docspell.store.Store
|
||||
import docspell.store.records.RShare
|
||||
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
trait OShare[F[_]] {
|
||||
|
||||
def findAll(collective: Ident): F[List[RShare]]
|
||||
@ -31,10 +35,32 @@ trait OShare[F[_]] {
|
||||
share: OShare.NewShare,
|
||||
removePassword: Boolean
|
||||
): F[OShare.ChangeResult]
|
||||
|
||||
def verify(key: ByteVector)(id: Ident, password: Option[Password]): F[VerifyResult]
|
||||
def verifyToken(key: ByteVector)(token: String): F[VerifyResult]
|
||||
}
|
||||
|
||||
object OShare {
|
||||
|
||||
sealed trait VerifyResult {
|
||||
def toEither: Either[String, ShareToken] =
|
||||
this match {
|
||||
case VerifyResult.Success(token) => Right(token)
|
||||
case _ => Left("Authentication failed.")
|
||||
}
|
||||
}
|
||||
object VerifyResult {
|
||||
case class Success(token: ShareToken) 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 notFound: VerifyResult = NotFound
|
||||
def passwordMismatch: VerifyResult = PasswordMismatch
|
||||
def invalidToken: VerifyResult = InvalidToken
|
||||
}
|
||||
|
||||
final case class NewShare(
|
||||
cid: Ident,
|
||||
name: Option[String],
|
||||
@ -55,6 +81,8 @@ object OShare {
|
||||
|
||||
def apply[F[_]: Async](store: Store[F]): OShare[F] =
|
||||
new OShare[F] {
|
||||
private[this] val logger = Logger.log4s[F](org.log4s.getLogger)
|
||||
|
||||
def findAll(collective: Ident): F[List[RShare]] =
|
||||
store.transact(RShare.findAllByCollective(collective))
|
||||
|
||||
@ -112,5 +140,51 @@ object OShare {
|
||||
|
||||
def findOne(id: Ident, collective: Ident): OptionT[F, RShare] =
|
||||
RShare.findOne(id, collective).mapK(store.transform)
|
||||
|
||||
def verify(
|
||||
key: ByteVector
|
||||
)(id: Ident, password: Option[Password]): F[VerifyResult] =
|
||||
RShare
|
||||
.findCurrentActive(id)
|
||||
.mapK(store.transform)
|
||||
.semiflatMap { share =>
|
||||
val pwCheck =
|
||||
share.password.map(encPw => password.exists(PasswordCrypt.check(_, encPw)))
|
||||
|
||||
// add the password (if existing) to the server secret key; this way the token
|
||||
// invalidates when the user changes the password
|
||||
val shareKey =
|
||||
share.password.map(pw => key ++ pw.asByteVector).getOrElse(key)
|
||||
|
||||
val token = ShareToken.create(id, shareKey)
|
||||
pwCheck match {
|
||||
case Some(true) => token.map(VerifyResult.success)
|
||||
case None => token.map(VerifyResult.success)
|
||||
case Some(false) => VerifyResult.passwordMismatch.pure[F]
|
||||
}
|
||||
}
|
||||
.getOrElse(VerifyResult.notFound)
|
||||
|
||||
def verifyToken(key: ByteVector)(token: String): F[VerifyResult] =
|
||||
ShareToken.fromString(token) match {
|
||||
case Right(st) =>
|
||||
RShare
|
||||
.findActivePassword(st.id)
|
||||
.mapK(store.transform)
|
||||
.semiflatMap { password =>
|
||||
val shareKey =
|
||||
password.map(pw => key ++ pw.asByteVector).getOrElse(key)
|
||||
if (st.sigValid(shareKey)) VerifyResult.success(st).pure[F]
|
||||
else
|
||||
logger.info(
|
||||
s"Signature failure for share: ${st.id.id}"
|
||||
) *> VerifyResult.invalidToken.pure[F]
|
||||
}
|
||||
.getOrElse(VerifyResult.notFound)
|
||||
|
||||
case Left(err) =>
|
||||
logger.debug(s"Invalid session token: $err") *>
|
||||
VerifyResult.invalidToken.pure[F]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,18 +6,29 @@
|
||||
|
||||
package docspell.common
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
|
||||
import io.circe.{Decoder, Encoder}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
final class Password(val pass: String) extends AnyVal {
|
||||
|
||||
def isEmpty: Boolean = pass.isEmpty
|
||||
def nonEmpty: Boolean = pass.nonEmpty
|
||||
def length: Int = pass.length
|
||||
|
||||
def asByteVector: ByteVector =
|
||||
ByteVector.view(pass.getBytes(StandardCharsets.UTF_8))
|
||||
|
||||
override def toString: String =
|
||||
if (pass.isEmpty) "<empty>" else "***"
|
||||
|
||||
def compare(other: Password): Boolean =
|
||||
this.pass.zip(other.pass).forall { case (a, b) => a == b } &&
|
||||
this.nonEmpty && this.length == other.length
|
||||
}
|
||||
|
||||
object Password {
|
||||
|
@ -70,6 +70,9 @@ object Timestamp {
|
||||
def atUtc(ldt: LocalDateTime): Timestamp =
|
||||
from(ldt.atZone(UTC))
|
||||
|
||||
def ofMillis(ms: Long): Timestamp =
|
||||
Timestamp(Instant.ofEpochMilli(ms))
|
||||
|
||||
def daysBetween(ts0: Timestamp, ts1: Timestamp): Long =
|
||||
ChronoUnit.DAYS.between(ts0.toUtcDate, ts1.toUtcDate)
|
||||
|
||||
|
@ -538,6 +538,37 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/InviteResult"
|
||||
|
||||
/open/share/verify:
|
||||
post:
|
||||
operationId: "open-share-verify"
|
||||
tags: [ Share ]
|
||||
summary: Verify a secret for a share
|
||||
description: |
|
||||
Given the share id and optionally a password, it verifies the
|
||||
correctness of the given data. As a result, a token is
|
||||
returned that must be used with all `share/*` routes. If the
|
||||
password is missing, but required, the response indicates
|
||||
this. Then the requests needs to be replayed with the correct
|
||||
password to retrieve the token.
|
||||
|
||||
The token is also added as a session cookie to the response.
|
||||
|
||||
The token is used to avoid passing the user define password
|
||||
with every request.
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareSecret"
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ShareVerifyResult"
|
||||
|
||||
/sec/auth/session:
|
||||
post:
|
||||
operationId: "sec-auth-session"
|
||||
@ -4186,6 +4217,38 @@ paths:
|
||||
|
||||
components:
|
||||
schemas:
|
||||
ShareSecret:
|
||||
description: |
|
||||
The secret (the share id + optional password) to access a
|
||||
share.
|
||||
required:
|
||||
- shareId
|
||||
properties:
|
||||
shareId:
|
||||
type: string
|
||||
format: ident
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
|
||||
ShareVerifyResult:
|
||||
description: |
|
||||
The data returned when verifying a `ShareSecret`.
|
||||
required:
|
||||
- success
|
||||
- token
|
||||
- passwordRequired
|
||||
- message
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
token:
|
||||
type: string
|
||||
passwordRequired:
|
||||
type: boolean
|
||||
message:
|
||||
type: string
|
||||
|
||||
ShareData:
|
||||
description: |
|
||||
Editable data for a share.
|
||||
|
@ -10,7 +10,7 @@ import cats.effect._
|
||||
import cats.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.auth.{AuthToken, ShareToken}
|
||||
import docspell.common._
|
||||
import docspell.oidc.CodeFlowRoutes
|
||||
import docspell.restserver.auth.OpenId
|
||||
@ -44,9 +44,12 @@ object RestServer {
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||
securedRoutes(cfg, restApp, token)
|
||||
},
|
||||
"/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) {
|
||||
"/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),
|
||||
@ -120,7 +123,8 @@ object RestServer {
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
||||
"checkfile" -> CheckFileRoutes.open(restApp.backend),
|
||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg)
|
||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg),
|
||||
"share" -> ShareRoutes.verify(restApp.backend, cfg)
|
||||
)
|
||||
|
||||
def adminRoutes[F[_]: Async](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
@ -132,6 +136,15 @@ object RestServer {
|
||||
"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)
|
||||
)
|
||||
|
||||
def redirectTo[F[_]: Async](path: String): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.auth
|
||||
|
||||
import docspell.backend.auth.ShareToken
|
||||
import docspell.common._
|
||||
|
||||
import org.http4s._
|
||||
import org.typelevel.ci.CIString
|
||||
|
||||
final case class ShareCookieData(token: ShareToken) {
|
||||
def asString: String = token.asString
|
||||
|
||||
def asCookie(baseUrl: LenientUri): ResponseCookie = {
|
||||
val sec = baseUrl.scheme.exists(_.endsWith("s"))
|
||||
val path = baseUrl.path / "api" / "v1"
|
||||
ResponseCookie(
|
||||
name = ShareCookieData.cookieName,
|
||||
content = asString,
|
||||
domain = None,
|
||||
path = Some(path.asString),
|
||||
httpOnly = true,
|
||||
secure = sec,
|
||||
maxAge = None,
|
||||
expires = None
|
||||
)
|
||||
}
|
||||
|
||||
def addCookie[F[_]](baseUrl: LenientUri)(
|
||||
resp: Response[F]
|
||||
): Response[F] =
|
||||
resp.addCookie(asCookie(baseUrl))
|
||||
}
|
||||
|
||||
object ShareCookieData {
|
||||
val cookieName = "docspell_share"
|
||||
val headerName = "Docspell-Share-Auth"
|
||||
|
||||
def fromCookie[F[_]](req: Request[F]): Option[String] =
|
||||
for {
|
||||
header <- req.headers.get[headers.Cookie]
|
||||
cookie <- header.values.toList.find(_.name == cookieName)
|
||||
} yield cookie.content
|
||||
|
||||
def fromHeader[F[_]](req: Request[F]): Option[String] =
|
||||
req.headers
|
||||
.get(CIString(headerName))
|
||||
.map(_.head.value)
|
||||
|
||||
def fromRequest[F[_]](req: Request[F]): Option[String] =
|
||||
fromCookie(req).orElse(fromHeader(req))
|
||||
|
||||
def delete(baseUrl: LenientUri): ResponseCookie =
|
||||
ResponseCookie(
|
||||
name = cookieName,
|
||||
content = "",
|
||||
domain = None,
|
||||
path = Some(baseUrl.path / "api" / "v1").map(_.asString),
|
||||
httpOnly = true,
|
||||
secure = baseUrl.scheme.exists(_.endsWith("s")),
|
||||
maxAge = None,
|
||||
expires = None
|
||||
)
|
||||
|
||||
}
|
@ -10,6 +10,7 @@ import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.Password
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.http4s.Responses
|
||||
|
||||
@ -19,7 +20,7 @@ import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server._
|
||||
import org.typelevel.ci.CIString
|
||||
|
||||
object AdminRoutes {
|
||||
object AdminAuth {
|
||||
private val adminHeader = CIString("Docspell-Admin-Secret")
|
||||
|
||||
def apply[F[_]: Async](cfg: Config.AdminEndpoint)(
|
||||
@ -55,6 +56,5 @@ object AdminRoutes {
|
||||
req.headers.get(adminHeader).map(_.head.value)
|
||||
|
||||
private def compareSecret(s1: String)(s2: String): Boolean =
|
||||
s1.length > 0 && s1.length == s2.length &&
|
||||
s1.zip(s2).forall { case (a, b) => a == b }
|
||||
Password(s1).compare(Password(s2))
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.auth.{Login, ShareToken}
|
||||
import docspell.backend.ops.OShare
|
||||
import docspell.backend.ops.OShare.VerifyResult
|
||||
import docspell.restserver.auth.ShareCookieData
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server._
|
||||
|
||||
object ShareAuth {
|
||||
|
||||
def authenticateRequest[F[_]: Async](
|
||||
validate: String => F[VerifyResult]
|
||||
)(req: Request[F]): F[OShare.VerifyResult] =
|
||||
ShareCookieData.fromRequest(req) match {
|
||||
case Some(tokenStr) =>
|
||||
validate(tokenStr)
|
||||
case None =>
|
||||
VerifyResult.notFound.pure[F]
|
||||
}
|
||||
|
||||
private def getToken[F[_]: Async](
|
||||
auth: String => F[VerifyResult]
|
||||
): Kleisli[F, Request[F], Either[String, ShareToken]] =
|
||||
Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
|
||||
|
||||
def of[F[_]: Async](S: OShare[F], cfg: Login.Config)(
|
||||
pf: PartialFunction[AuthedRequest[F, ShareToken], F[Response[F]]]
|
||||
): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = getToken[F](S.verifyToken(cfg.serverSecret))
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
|
||||
|
||||
val middleware: AuthMiddleware[F, ShareToken] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes.of(pf))
|
||||
}
|
||||
|
||||
def apply[F[_]: Async](S: OShare[F], cfg: Login.Config)(
|
||||
f: ShareToken => HttpRoutes[F]
|
||||
): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = getToken[F](S.verifyToken(cfg.serverSecret))
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
|
||||
|
||||
val middleware: AuthMiddleware[F, ShareToken] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes(authReq => f(authReq.context).run(authReq.req)))
|
||||
}
|
||||
}
|
@ -9,13 +9,18 @@ package docspell.restserver.routes
|
||||
import cats.data.OptionT
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OShare
|
||||
import docspell.backend.ops.OShare.VerifyResult
|
||||
import docspell.common.{Ident, Timestamp}
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.auth.ShareCookieData
|
||||
import docspell.restserver.http4s.{ClientRequestInfo, ResponseGenerator}
|
||||
import docspell.store.records.RShare
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
@ -66,6 +71,31 @@ object ShareRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
def verify[F[_]: Async](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of { case req @ POST -> Root / "verify" =>
|
||||
for {
|
||||
secret <- req.as[ShareSecret]
|
||||
res <- backend.share
|
||||
.verify(cfg.auth.serverSecret)(secret.shareId, secret.password)
|
||||
resp <- res match {
|
||||
case VerifyResult.Success(token) =>
|
||||
val cd = ShareCookieData(token)
|
||||
Ok(ShareVerifyResult(true, token.asString, false, "Success"))
|
||||
.map(cd.addCookie(ClientRequestInfo.getBaseUrl(cfg, req)))
|
||||
case VerifyResult.PasswordMismatch =>
|
||||
Ok(ShareVerifyResult(false, "", true, "Failed"))
|
||||
case VerifyResult.NotFound =>
|
||||
Ok(ShareVerifyResult(false, "", false, "Failed"))
|
||||
case VerifyResult.InvalidToken =>
|
||||
Ok(ShareVerifyResult(false, "", false, "Failed"))
|
||||
}
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def mkNewShare(data: ShareData, user: AuthToken): OShare.NewShare =
|
||||
OShare.NewShare(
|
||||
user.account.collective,
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2020 Eike K. & Contributors
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.ShareToken
|
||||
import docspell.common.Logger
|
||||
import docspell.restserver.Config
|
||||
|
||||
import org.http4s.HttpRoutes
|
||||
|
||||
object ShareSearchRoutes {
|
||||
|
||||
def apply[F[_]: Async](
|
||||
backend: BackendApp[F],
|
||||
cfg: Config,
|
||||
token: ShareToken
|
||||
): HttpRoutes[F] = {
|
||||
val logger = Logger.log4s[F](org.log4s.getLogger)
|
||||
logger.trace(s"$backend $cfg $token")
|
||||
???
|
||||
}
|
||||
}
|
@ -101,6 +101,28 @@ object RShare {
|
||||
.option
|
||||
)
|
||||
|
||||
private def activeCondition(t: Table, id: Ident, current: Timestamp): Condition =
|
||||
t.id === id && t.enabled === true && t.publishedUntil > current
|
||||
|
||||
def findActive(id: Ident, current: Timestamp): OptionT[ConnectionIO, RShare] =
|
||||
OptionT(
|
||||
Select(
|
||||
select(T.all),
|
||||
from(T),
|
||||
activeCondition(T, id, current)
|
||||
).build.query[RShare].option
|
||||
)
|
||||
|
||||
def findCurrentActive(id: Ident): OptionT[ConnectionIO, RShare] =
|
||||
OptionT.liftF(Timestamp.current[ConnectionIO]).flatMap(now => findActive(id, now))
|
||||
|
||||
def findActivePassword(id: Ident): OptionT[ConnectionIO, Option[Password]] =
|
||||
OptionT(Timestamp.current[ConnectionIO].flatMap { now =>
|
||||
Select(select(T.password), from(T), activeCondition(T, id, now)).build
|
||||
.query[Option[Password]]
|
||||
.option
|
||||
})
|
||||
|
||||
def findAllByCollective(cid: Ident): ConnectionIO[List[RShare]] =
|
||||
Select(select(T.all), from(T), T.cid === cid)
|
||||
.orderBy(T.publishedAt.desc)
|
||||
|
Loading…
x
Reference in New Issue
Block a user