mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-04 18:39:33 +00:00
Create a place for admin routes
And move re-creation of fulltext index in this place.
This commit is contained in:
parent
f8735cbcdb
commit
306f064ad9
@ -111,6 +111,18 @@ docspell.server {
|
||||
}
|
||||
}
|
||||
|
||||
# This is a special endpoint that allows some basic administration.
|
||||
#
|
||||
# It is intended to be used by admins only, that is users who
|
||||
# installed the app and have access to the system. Normal users
|
||||
# should not have access and therefore a secret must be provided in
|
||||
# order to access it.
|
||||
admin-endpoint {
|
||||
# The secret. If empty, the endpoint is disabled.
|
||||
secret = ""
|
||||
|
||||
}
|
||||
|
||||
# Configuration of the full-text search engine.
|
||||
full-text-search {
|
||||
# The full-text search feature can be disabled. It requires an
|
||||
|
@ -18,13 +18,16 @@ case class Config(
|
||||
integrationEndpoint: Config.IntegrationEndpoint,
|
||||
maxItemPageSize: Int,
|
||||
maxNoteLength: Int,
|
||||
fullTextSearch: Config.FullTextSearch
|
||||
fullTextSearch: Config.FullTextSearch,
|
||||
adminEndpoint: Config.AdminEndpoint
|
||||
)
|
||||
|
||||
object Config {
|
||||
|
||||
case class Bind(address: String, port: Int)
|
||||
|
||||
case class AdminEndpoint(secret: String)
|
||||
|
||||
case class IntegrationEndpoint(
|
||||
enabled: Boolean,
|
||||
priority: Priority,
|
||||
|
@ -35,6 +35,9 @@ object RestServer {
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||
securedRoutes(cfg, pools, restApp, token)
|
||||
},
|
||||
"/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) {
|
||||
adminRoutes(cfg, restApp)
|
||||
},
|
||||
"/api/doc" -> templates.doc,
|
||||
"/app/assets" -> WebjarRoutes.appRoutes[F](pools.blocker),
|
||||
"/app" -> templates.app,
|
||||
@ -95,8 +98,12 @@ object RestServer {
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
||||
"checkfile" -> CheckFileRoutes.open(restApp.backend),
|
||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg),
|
||||
"fts" -> FullTextIndexRoutes.open(cfg, restApp.backend)
|
||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg)
|
||||
)
|
||||
|
||||
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
Router(
|
||||
"fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend)
|
||||
)
|
||||
|
||||
def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = {
|
||||
|
@ -1,6 +1,8 @@
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.data.OptionT
|
||||
import cats.effect.Sync
|
||||
import fs2.text.utf8Encode
|
||||
import fs2.{Pure, Stream}
|
||||
|
||||
@ -36,4 +38,7 @@ object Responses {
|
||||
)
|
||||
)
|
||||
|
||||
def notFoundRoute[F[_]: Sync]: HttpRoutes[F] =
|
||||
HttpRoutes(_ => OptionT.pure(Response.notFound[F]))
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data.{Kleisli, OptionT}
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.http4s.Responses
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server._
|
||||
import org.http4s.util.CaseInsensitiveString
|
||||
|
||||
object AdminRoutes {
|
||||
private val adminHeader = CaseInsensitiveString("Docspell-Admin-Secret")
|
||||
|
||||
def apply[F[_]: Effect](cfg: Config.AdminEndpoint)(
|
||||
f: HttpRoutes[F]
|
||||
): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = checkSecret[F](cfg)
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.context)))
|
||||
|
||||
val middleware: AuthMiddleware[F, Unit] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
if (cfg.secret.isEmpty) Responses.notFoundRoute[F]
|
||||
else middleware(AuthedRoutes(authReq => f.run(authReq.req)))
|
||||
}
|
||||
|
||||
private def checkSecret[F[_]: Effect](
|
||||
cfg: Config.AdminEndpoint
|
||||
): Kleisli[F, Request[F], Either[String, Unit]] =
|
||||
Kleisli(req =>
|
||||
extractSecret[F](req)
|
||||
.filter(compareSecret(cfg.secret))
|
||||
.toRight("Secret invalid")
|
||||
.map(_ => ())
|
||||
.pure[F]
|
||||
)
|
||||
|
||||
private def extractSecret[F[_]](req: Request[F]): Option[String] =
|
||||
req.headers.get(adminHeader).map(_.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 })
|
||||
}
|
@ -1,14 +1,13 @@
|
||||
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.common._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.Responses
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
@ -34,23 +33,20 @@ object FullTextIndexRoutes {
|
||||
}
|
||||
}
|
||||
|
||||
def open[F[_]: Effect](cfg: Config, backend: BackendApp[F]): HttpRoutes[F] =
|
||||
def admin[F[_]: Effect](cfg: Config, backend: BackendApp[F]): HttpRoutes[F] =
|
||||
if (!cfg.fullTextSearch.enabled) notFound[F]
|
||||
else {
|
||||
val dsl = Http4sDsl[F]
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of { case POST -> Root / "reIndexAll" / Ident(id) =>
|
||||
HttpRoutes.of { case POST -> Root / "reIndexAll" =>
|
||||
for {
|
||||
res <-
|
||||
if (id.nonEmpty && id == cfg.fullTextSearch.recreateKey)
|
||||
backend.fulltext.reindexAll.attempt
|
||||
else Left(new Exception("The provided key is invalid.")).pure[F]
|
||||
res <- backend.fulltext.reindexAll.attempt
|
||||
resp <- Ok(Conversions.basicResult(res, "Full-text index will be re-created."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
private def notFound[F[_]: Effect]: HttpRoutes[F] =
|
||||
HttpRoutes(_ => OptionT.pure(Response.notFound[F]))
|
||||
Responses.notFoundRoute[F]
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user