mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-04-05 10:59:33 +00:00
commit
6530566485
@ -130,6 +130,8 @@ val openapiScalaSettings = Seq(
|
|||||||
.addMapping(CustomMapping.forFormatType({
|
.addMapping(CustomMapping.forFormatType({
|
||||||
case "ident" =>
|
case "ident" =>
|
||||||
field => field.copy(typeDef = TypeDef("Ident", Imports("docspell.common.Ident")))
|
field => field.copy(typeDef = TypeDef("Ident", Imports("docspell.common.Ident")))
|
||||||
|
case "accountid" =>
|
||||||
|
field => field.copy(typeDef = TypeDef("AccountId", Imports("docspell.common.AccountId")))
|
||||||
case "collectivestate" =>
|
case "collectivestate" =>
|
||||||
field =>
|
field =>
|
||||||
field.copy(typeDef =
|
field.copy(typeDef =
|
||||||
|
@ -13,7 +13,6 @@ docspell.server {
|
|||||||
# Configuration of the full-text search engine.
|
# Configuration of the full-text search engine.
|
||||||
full-text-search {
|
full-text-search {
|
||||||
enabled = true
|
enabled = true
|
||||||
recreate-key = ""
|
|
||||||
solr = {
|
solr = {
|
||||||
url = "http://solr:8983/solr/docspell"
|
url = "http://solr:8983/solr/docspell"
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,8 @@ trait OCollective[F[_]] {
|
|||||||
newPass: Password
|
newPass: Password
|
||||||
): F[PassChangeResult]
|
): F[PassChangeResult]
|
||||||
|
|
||||||
|
def resetPassword(accountId: AccountId): F[PassResetResult]
|
||||||
|
|
||||||
def getContacts(
|
def getContacts(
|
||||||
collective: Ident,
|
collective: Ident,
|
||||||
query: Option[String],
|
query: Option[String],
|
||||||
@ -77,6 +79,15 @@ object OCollective {
|
|||||||
type Classifier = RClassifierSetting.Classifier
|
type Classifier = RClassifierSetting.Classifier
|
||||||
val Classifier = RClassifierSetting.Classifier
|
val Classifier = RClassifierSetting.Classifier
|
||||||
|
|
||||||
|
sealed trait PassResetResult
|
||||||
|
object PassResetResult {
|
||||||
|
case class Success(newPw: Password) extends PassResetResult
|
||||||
|
case object NotFound extends PassResetResult
|
||||||
|
|
||||||
|
def success(np: Password): PassResetResult = Success(np)
|
||||||
|
def notFound: PassResetResult = NotFound
|
||||||
|
}
|
||||||
|
|
||||||
sealed trait PassChangeResult
|
sealed trait PassChangeResult
|
||||||
object PassChangeResult {
|
object PassChangeResult {
|
||||||
case object UserNotFound extends PassChangeResult
|
case object UserNotFound extends PassChangeResult
|
||||||
@ -184,6 +195,17 @@ object OCollective {
|
|||||||
def tagCloud(collective: Ident): F[List[TagCount]] =
|
def tagCloud(collective: Ident): F[List[TagCount]] =
|
||||||
store.transact(QCollective.tagCloud(collective))
|
store.transact(QCollective.tagCloud(collective))
|
||||||
|
|
||||||
|
def resetPassword(accountId: AccountId): F[PassResetResult] =
|
||||||
|
for {
|
||||||
|
newPass <- Password.generate[F]
|
||||||
|
n <- store.transact(
|
||||||
|
RUser.updatePassword(accountId, PasswordCrypt.crypt(newPass))
|
||||||
|
)
|
||||||
|
res =
|
||||||
|
if (n <= 0) PassResetResult.notFound
|
||||||
|
else PassResetResult.success(newPass)
|
||||||
|
} yield res
|
||||||
|
|
||||||
def changePassword(
|
def changePassword(
|
||||||
accountId: AccountId,
|
accountId: AccountId,
|
||||||
current: Password,
|
current: Password,
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
package docspell.common
|
package docspell.common
|
||||||
|
|
||||||
|
import cats.effect.Sync
|
||||||
|
import cats.implicits._
|
||||||
|
|
||||||
import io.circe.{Decoder, Encoder}
|
import io.circe.{Decoder, Encoder}
|
||||||
|
|
||||||
final class Password(val pass: String) extends AnyVal {
|
final class Password(val pass: String) extends AnyVal {
|
||||||
@ -18,6 +21,12 @@ object Password {
|
|||||||
def apply(pass: String): Password =
|
def apply(pass: String): Password =
|
||||||
new Password(pass)
|
new Password(pass)
|
||||||
|
|
||||||
|
def generate[F[_]: Sync]: F[Password] =
|
||||||
|
for {
|
||||||
|
id <- Ident.randomId[F]
|
||||||
|
pass = id.id.take(11)
|
||||||
|
} yield Password(pass)
|
||||||
|
|
||||||
implicit val passwordEncoder: Encoder[Password] =
|
implicit val passwordEncoder: Encoder[Password] =
|
||||||
Encoder.encodeString.contramap(_.pass)
|
Encoder.encodeString.contramap(_.pass)
|
||||||
|
|
||||||
|
@ -151,9 +151,9 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
/open/fts/reIndexAll/{id}:
|
/admin/fts/reIndexAll:
|
||||||
post:
|
post:
|
||||||
tags: [Full-Text Index]
|
tags: [Full-Text Index, Admin]
|
||||||
summary: Re-creates the full-text index.
|
summary: Re-creates the full-text index.
|
||||||
description: |
|
description: |
|
||||||
Clears the full-text index and inserts all data from the
|
Clears the full-text index and inserts all data from the
|
||||||
@ -162,10 +162,10 @@ paths:
|
|||||||
by a job executor. Note that this affects all data of all
|
by a job executor. Note that this affects all data of all
|
||||||
collectives.
|
collectives.
|
||||||
|
|
||||||
The `id` is required and refers to the key given in the config
|
This is an admin route, so you need to provide the secret from
|
||||||
file to ensure that only admins can call this route.
|
the config file as header `Docspell-Admin-Secret`.
|
||||||
parameters:
|
security:
|
||||||
- $ref: "#/components/parameters/id"
|
- adminHeader: []
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Ok
|
description: Ok
|
||||||
@ -1184,6 +1184,31 @@ paths:
|
|||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$ref: "#/components/schemas/BasicResult"
|
||||||
|
|
||||||
|
/admin/user/resetPassword:
|
||||||
|
post:
|
||||||
|
tags: [ Collective, Admin ]
|
||||||
|
summary: Reset a user password.
|
||||||
|
description: |
|
||||||
|
Resets a user password to some random string which is returned
|
||||||
|
as the result. This is an admin route, so you need to specify
|
||||||
|
the secret from the config file via a http header
|
||||||
|
`Docspell-Admin-Secret`.
|
||||||
|
security:
|
||||||
|
- adminHeader: []
|
||||||
|
requestBody:
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ResetPassword"
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Ok
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/ResetPasswordResult"
|
||||||
|
|
||||||
/sec/source:
|
/sec/source:
|
||||||
get:
|
get:
|
||||||
tags: [ Source ]
|
tags: [ Source ]
|
||||||
@ -3416,6 +3441,32 @@ paths:
|
|||||||
|
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
ResetPassword:
|
||||||
|
description: |
|
||||||
|
The account to reset the password.
|
||||||
|
required:
|
||||||
|
- account
|
||||||
|
properties:
|
||||||
|
account:
|
||||||
|
type: string
|
||||||
|
format: accountid
|
||||||
|
|
||||||
|
ResetPasswordResult:
|
||||||
|
description: |
|
||||||
|
Contains the newly generated password or an error.
|
||||||
|
required:
|
||||||
|
- success
|
||||||
|
- newPassword
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: boolean
|
||||||
|
newPassword:
|
||||||
|
type: string
|
||||||
|
format: password
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
|
||||||
ItemsAndFieldValue:
|
ItemsAndFieldValue:
|
||||||
description: |
|
description: |
|
||||||
Holds a list of item ids and a custom field value.
|
Holds a list of item ids and a custom field value.
|
||||||
@ -5421,6 +5472,10 @@ components:
|
|||||||
type: apiKey
|
type: apiKey
|
||||||
in: header
|
in: header
|
||||||
name: X-Docspell-Auth
|
name: X-Docspell-Auth
|
||||||
|
adminHeader:
|
||||||
|
type: apiKey
|
||||||
|
in: header
|
||||||
|
name: Docspell-Admin-Secret
|
||||||
parameters:
|
parameters:
|
||||||
id:
|
id:
|
||||||
name: id
|
name: id
|
||||||
|
@ -111,6 +111,21 @@ 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.
|
||||||
|
#
|
||||||
|
# This is used for some endpoints, for example:
|
||||||
|
# - re-create complete fulltext index:
|
||||||
|
# curl -XPOST -H'Docspell-Admin-Secret: xyz' http://localhost:7880/api/v1/admin/fts/reIndexAll
|
||||||
|
admin-endpoint {
|
||||||
|
# The secret. If empty, the endpoint is disabled.
|
||||||
|
secret = ""
|
||||||
|
}
|
||||||
|
|
||||||
# Configuration of the full-text search engine.
|
# Configuration of the full-text search engine.
|
||||||
full-text-search {
|
full-text-search {
|
||||||
# The full-text search feature can be disabled. It requires an
|
# The full-text search feature can be disabled. It requires an
|
||||||
@ -120,14 +135,6 @@ docspell.server {
|
|||||||
# Currently the SOLR search platform is supported.
|
# Currently the SOLR search platform is supported.
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
# When re-creating the complete index via a REST call, this key
|
|
||||||
# is required. If left empty (the default), recreating the index
|
|
||||||
# is disabled.
|
|
||||||
#
|
|
||||||
# Example curl command:
|
|
||||||
# curl -XPOST http://localhost:7880/api/v1/open/fts/reIndexAll/test123
|
|
||||||
recreate-key = ""
|
|
||||||
|
|
||||||
# Configuration for the SOLR backend.
|
# Configuration for the SOLR backend.
|
||||||
solr = {
|
solr = {
|
||||||
# The URL to solr
|
# The URL to solr
|
||||||
|
@ -18,13 +18,16 @@ case class Config(
|
|||||||
integrationEndpoint: Config.IntegrationEndpoint,
|
integrationEndpoint: Config.IntegrationEndpoint,
|
||||||
maxItemPageSize: Int,
|
maxItemPageSize: Int,
|
||||||
maxNoteLength: Int,
|
maxNoteLength: Int,
|
||||||
fullTextSearch: Config.FullTextSearch
|
fullTextSearch: Config.FullTextSearch,
|
||||||
|
adminEndpoint: Config.AdminEndpoint
|
||||||
)
|
)
|
||||||
|
|
||||||
object Config {
|
object Config {
|
||||||
|
|
||||||
case class Bind(address: String, port: Int)
|
case class Bind(address: String, port: Int)
|
||||||
|
|
||||||
|
case class AdminEndpoint(secret: String)
|
||||||
|
|
||||||
case class IntegrationEndpoint(
|
case class IntegrationEndpoint(
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
priority: Priority,
|
priority: Priority,
|
||||||
@ -56,7 +59,7 @@ object Config {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case class FullTextSearch(enabled: Boolean, recreateKey: Ident, solr: SolrConfig)
|
case class FullTextSearch(enabled: Boolean, solr: SolrConfig)
|
||||||
|
|
||||||
object FullTextSearch {}
|
object FullTextSearch {}
|
||||||
|
|
||||||
|
@ -35,6 +35,9 @@ object RestServer {
|
|||||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) { token =>
|
||||||
securedRoutes(cfg, pools, restApp, token)
|
securedRoutes(cfg, pools, restApp, token)
|
||||||
},
|
},
|
||||||
|
"/api/v1/admin" -> AdminRoutes(cfg.adminEndpoint) {
|
||||||
|
adminRoutes(cfg, restApp)
|
||||||
|
},
|
||||||
"/api/doc" -> templates.doc,
|
"/api/doc" -> templates.doc,
|
||||||
"/app/assets" -> WebjarRoutes.appRoutes[F](pools.blocker),
|
"/app/assets" -> WebjarRoutes.appRoutes[F](pools.blocker),
|
||||||
"/app" -> templates.app,
|
"/app" -> templates.app,
|
||||||
@ -95,8 +98,13 @@ object RestServer {
|
|||||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||||
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
"upload" -> UploadRoutes.open(restApp.backend, cfg),
|
||||||
"checkfile" -> CheckFileRoutes.open(restApp.backend),
|
"checkfile" -> CheckFileRoutes.open(restApp.backend),
|
||||||
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg),
|
"integration" -> IntegrationEndpointRoutes.open(restApp.backend, cfg)
|
||||||
"fts" -> FullTextIndexRoutes.open(cfg, restApp.backend)
|
)
|
||||||
|
|
||||||
|
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||||
|
Router(
|
||||||
|
"fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend),
|
||||||
|
"user" -> UserRoutes.admin(restApp.backend)
|
||||||
)
|
)
|
||||||
|
|
||||||
def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = {
|
def redirectTo[F[_]: Effect](path: String): HttpRoutes[F] = {
|
||||||
|
@ -17,11 +17,6 @@ object QueryParam {
|
|||||||
implicit val queryStringDecoder: QueryParamDecoder[QueryString] =
|
implicit val queryStringDecoder: QueryParamDecoder[QueryString] =
|
||||||
QueryParamDecoder[String].map(s => QueryString(s.trim.toLowerCase))
|
QueryParamDecoder[String].map(s => QueryString(s.trim.toLowerCase))
|
||||||
|
|
||||||
// implicit val booleanDecoder: QueryParamDecoder[Boolean] =
|
|
||||||
// QueryParamDecoder.fromUnsafeCast(qp => Option(qp.value).exists(_.equalsIgnoreCase("true")))(
|
|
||||||
// "Boolean"
|
|
||||||
// )
|
|
||||||
|
|
||||||
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
object FullOpt extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
||||||
|
|
||||||
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
|
object OwningOpt extends OptionalQueryParamDecoderMatcher[Boolean]("owning")
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package docspell.restserver.http4s
|
package docspell.restserver.http4s
|
||||||
|
|
||||||
import cats.data.NonEmptyList
|
import cats.data.NonEmptyList
|
||||||
|
import cats.data.OptionT
|
||||||
|
import cats.effect.Sync
|
||||||
import fs2.text.utf8Encode
|
import fs2.text.utf8Encode
|
||||||
import fs2.{Pure, Stream}
|
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
|
package docspell.restserver.routes
|
||||||
|
|
||||||
import cats.data.OptionT
|
|
||||||
import cats.effect._
|
import cats.effect._
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
|
|
||||||
import docspell.backend.BackendApp
|
import docspell.backend.BackendApp
|
||||||
import docspell.backend.auth.AuthToken
|
import docspell.backend.auth.AuthToken
|
||||||
import docspell.common._
|
|
||||||
import docspell.restserver.Config
|
import docspell.restserver.Config
|
||||||
import docspell.restserver.conv.Conversions
|
import docspell.restserver.conv.Conversions
|
||||||
|
import docspell.restserver.http4s.Responses
|
||||||
|
|
||||||
import org.http4s._
|
import org.http4s._
|
||||||
import org.http4s.circe.CirceEntityEncoder._
|
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]
|
if (!cfg.fullTextSearch.enabled) notFound[F]
|
||||||
else {
|
else {
|
||||||
val dsl = Http4sDsl[F]
|
val dsl = Http4sDsl[F]
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of { case POST -> Root / "reIndexAll" / Ident(id) =>
|
HttpRoutes.of { case POST -> Root / "reIndexAll" =>
|
||||||
for {
|
for {
|
||||||
res <-
|
res <- backend.fulltext.reindexAll.attempt
|
||||||
if (id.nonEmpty && id == cfg.fullTextSearch.recreateKey)
|
|
||||||
backend.fulltext.reindexAll.attempt
|
|
||||||
else Left(new Exception("The provided key is invalid.")).pure[F]
|
|
||||||
resp <- Ok(Conversions.basicResult(res, "Full-text index will be re-created."))
|
resp <- Ok(Conversions.basicResult(res, "Full-text index will be re-created."))
|
||||||
} yield resp
|
} yield resp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def notFound[F[_]: Effect]: HttpRoutes[F] =
|
private def notFound[F[_]: Effect]: HttpRoutes[F] =
|
||||||
HttpRoutes(_ => OptionT.pure(Response.notFound[F]))
|
Responses.notFoundRoute[F]
|
||||||
}
|
}
|
||||||
|
@ -5,10 +5,10 @@ import cats.implicits._
|
|||||||
|
|
||||||
import docspell.backend.BackendApp
|
import docspell.backend.BackendApp
|
||||||
import docspell.backend.auth.AuthToken
|
import docspell.backend.auth.AuthToken
|
||||||
import docspell.common.Ident
|
import docspell.backend.ops.OCollective
|
||||||
|
import docspell.common._
|
||||||
import docspell.restapi.model._
|
import docspell.restapi.model._
|
||||||
import docspell.restserver.conv.Conversions._
|
import docspell.restserver.conv.Conversions._
|
||||||
import docspell.restserver.http4s.ResponseGenerator
|
|
||||||
|
|
||||||
import org.http4s.HttpRoutes
|
import org.http4s.HttpRoutes
|
||||||
import org.http4s.circe.CirceEntityDecoder._
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
@ -18,7 +18,7 @@ import org.http4s.dsl.Http4sDsl
|
|||||||
object UserRoutes {
|
object UserRoutes {
|
||||||
|
|
||||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
val dsl = new Http4sDsl[F] {}
|
||||||
import dsl._
|
import dsl._
|
||||||
|
|
||||||
HttpRoutes.of {
|
HttpRoutes.of {
|
||||||
@ -63,4 +63,25 @@ object UserRoutes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def admin[F[_]: Effect](backend: BackendApp[F]): HttpRoutes[F] = {
|
||||||
|
val dsl = new Http4sDsl[F] {}
|
||||||
|
import dsl._
|
||||||
|
|
||||||
|
HttpRoutes.of { case req @ POST -> Root / "resetPassword" =>
|
||||||
|
for {
|
||||||
|
input <- req.as[ResetPassword]
|
||||||
|
result <- backend.collective.resetPassword(input.account)
|
||||||
|
resp <- Ok(result match {
|
||||||
|
case OCollective.PassResetResult.Success(np) =>
|
||||||
|
ResetPasswordResult(true, np, "Password updated")
|
||||||
|
case OCollective.PassResetResult.NotFound =>
|
||||||
|
ResetPasswordResult(
|
||||||
|
false,
|
||||||
|
Password(""),
|
||||||
|
"Password update failed. User not found."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
} yield resp
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,9 @@ let
|
|||||||
header-value = "some-secret";
|
header-value = "some-secret";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
admin-endpoint = {
|
||||||
|
secret = "";
|
||||||
|
};
|
||||||
full-text-search = {
|
full-text-search = {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
solr = {
|
solr = {
|
||||||
@ -49,7 +52,6 @@ let
|
|||||||
def-type = "lucene";
|
def-type = "lucene";
|
||||||
q-op = "OR";
|
q-op = "OR";
|
||||||
};
|
};
|
||||||
recreate-key = "";
|
|
||||||
};
|
};
|
||||||
auth = {
|
auth = {
|
||||||
server-secret = "hex:caffee";
|
server-secret = "hex:caffee";
|
||||||
@ -343,6 +345,20 @@ in {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
admin-endpoint = mkOption {
|
||||||
|
type = types.submodule({
|
||||||
|
options = {
|
||||||
|
secret = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = defaults.admin-endpoint.secret;
|
||||||
|
description = "The secret used to call admin endpoints.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
default = defaults.admin-endpoint;
|
||||||
|
description = "An endpoint for administration tasks.";
|
||||||
|
};
|
||||||
|
|
||||||
full-text-search = mkOption {
|
full-text-search = mkOption {
|
||||||
type = types.submodule({
|
type = types.submodule({
|
||||||
options = {
|
options = {
|
||||||
@ -394,18 +410,6 @@ in {
|
|||||||
default = defaults.full-text-search.solr;
|
default = defaults.full-text-search.solr;
|
||||||
description = "Configuration for the SOLR backend.";
|
description = "Configuration for the SOLR backend.";
|
||||||
};
|
};
|
||||||
recreate-key = mkOption {
|
|
||||||
type = types.str;
|
|
||||||
default = defaults.full-text-search.recreate-key;
|
|
||||||
description = ''
|
|
||||||
When re-creating the complete index via a REST call, this key
|
|
||||||
is required. If left empty (the default), recreating the index
|
|
||||||
is disabled.
|
|
||||||
|
|
||||||
Example curl command:
|
|
||||||
curl -XPOST http://localhost:7880/api/v1/open/fts/reIndexAll/test123
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
default = defaults.full-text-search;
|
default = defaults.full-text-search;
|
||||||
|
@ -174,9 +174,9 @@ downloadAttachment() {
|
|||||||
rm -f "$attachOut"
|
rm -f "$attachOut"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
checksum1=$(curl --fail -s -I -H "X-Docspell-Auth: $auth_token" "$ATTACH_URL/$attachId/original" | \
|
checksum1=$(curl -s -I -H "X-Docspell-Auth: $auth_token" "$ATTACH_URL/$attachId/original" | \
|
||||||
grep 'ETag' | cut -d' ' -f2 | jq -r)
|
grep -i 'etag' | cut -d' ' -f2 | jq -r)
|
||||||
curl --fail -s -o "$attachOut" -H "X-Docspell-Auth: $auth_token" "$ATTACH_URL/$attachId/original"
|
curl -s -o "$attachOut" -H "X-Docspell-Auth: $auth_token" "$ATTACH_URL/$attachId/original"
|
||||||
checksum2=$(sha256sum "$attachOut" | cut -d' ' -f1 | xargs)
|
checksum2=$(sha256sum "$attachOut" | cut -d' ' -f1 | xargs)
|
||||||
if [ "$checksum1" == "$checksum2" ]; then
|
if [ "$checksum1" == "$checksum2" ]; then
|
||||||
errout " - Checksum ok."
|
errout " - Checksum ok."
|
||||||
|
46
tools/reset-password/reset-password.sh
Executable file
46
tools/reset-password/reset-password.sh
Executable file
@ -0,0 +1,46 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# A script to reset a password.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./reset-password.sh <baseurl> <admin-secret> <account>
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# ./reset-password.sh http://localhost:7880 test123 your/account
|
||||||
|
#
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "The docspell base-url is required as first argument."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
BASE_URL="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$2" ]; then
|
||||||
|
echo "The admin secret is required as second argument."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
SECRET="$2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$3" ]; then
|
||||||
|
echo "The user account is required as third argument."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
USER="$3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
RESET_URL="${BASE_URL}/api/v1/admin/user/resetPassword"
|
||||||
|
|
||||||
|
OUT=$(curl -s -XPOST \
|
||||||
|
-H "Docspell-Admin-Secret: $SECRET" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"account\": \"$USER\"}" \
|
||||||
|
"$RESET_URL")
|
||||||
|
|
||||||
|
|
||||||
|
if command -v jq > /dev/null; then
|
||||||
|
echo $OUT | jq
|
||||||
|
else
|
||||||
|
echo $OUT
|
||||||
|
fi
|
@ -16,11 +16,14 @@ workflow.
|
|||||||
The "raw" `openapi.yml` specification file can be found
|
The "raw" `openapi.yml` specification file can be found
|
||||||
[here](/openapi/docspell-openapi.yml).
|
[here](/openapi/docspell-openapi.yml).
|
||||||
|
|
||||||
The routes can be divided into protected and unprotected routes. The
|
The routes can be divided into protected, unprotected routes and admin
|
||||||
unprotected, or open routes are at `/open/*` while the protected
|
routes. The unprotected, or open routes are at `/open/*` while the
|
||||||
routes are at `/sec/*`. Open routes don't require authenticated access
|
protected routes are at `/sec/*` and admin routes are at `/admin/*`.
|
||||||
and can be used by any user. The protected routes require an
|
Open routes don't require authenticated access and can be used by any
|
||||||
authenticated user.
|
user. The protected routes require an authenticated user. The admin
|
||||||
|
routes require a special http header with a value from the config
|
||||||
|
file. They are disabled by default, you need to specify a secret in
|
||||||
|
order to enable admin routes.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
@ -38,6 +41,28 @@ a "normal" http header. If a cookie header is used, the cookie name
|
|||||||
must be `docspell_auth` and a custom header must be named
|
must be `docspell_auth` and a custom header must be named
|
||||||
`X-Docspell-Auth`.
|
`X-Docspell-Auth`.
|
||||||
|
|
||||||
|
The admin route (see below) `/admin/user/resetPassword` can be used to
|
||||||
|
reset a password of a user.
|
||||||
|
|
||||||
|
## Admin
|
||||||
|
|
||||||
|
There are some endpoints available for adminstration tasks, for
|
||||||
|
example re-creating the complete fulltext index or resetting a
|
||||||
|
password. These endpoints are not available to normal users, but to
|
||||||
|
admins only. Docspell has no special admin users, it simply uses a
|
||||||
|
secret defined in the configuration file. The person who installs
|
||||||
|
docspell is the admin and knows this secret (and may share it) and
|
||||||
|
requests must provide it as a http header `Docspell-Admin-Secret`.
|
||||||
|
|
||||||
|
Example: re-create the fulltext index (over all collectives):
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
$ curl -XPOST -H "Docspell-Admin-Secret: test123" http://localhost:7880/api/v1/admin/fts/reIndexAll
|
||||||
|
```
|
||||||
|
|
||||||
|
To enable these endpoints, you must provide a secret in the
|
||||||
|
[configuration](@/docs/configure/_index.md#admin-endpoint).
|
||||||
|
|
||||||
## Live Api
|
## Live Api
|
||||||
|
|
||||||
Besides the statically generated documentation at this site, the rest
|
Besides the statically generated documentation at this site, the rest
|
||||||
|
@ -73,6 +73,22 @@ H2
|
|||||||
url = "jdbc:h2:///path/to/a/file.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE"
|
url = "jdbc:h2:///path/to/a/file.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Admin Endpoint
|
||||||
|
|
||||||
|
The admin endpoint defines some [routes](@/docs/api/intro.md#admin)
|
||||||
|
for adminstration tasks. This is disabled by default and can be
|
||||||
|
enabled by providing a secret:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
...
|
||||||
|
admin-endpoint {
|
||||||
|
secret = "123"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This secret must be provided to all requests to a `/api/v1/admin/`
|
||||||
|
endpoint.
|
||||||
|
|
||||||
|
|
||||||
## Full-Text Search: SOLR
|
## Full-Text Search: SOLR
|
||||||
|
|
||||||
@ -105,27 +121,21 @@ documentation](https://lucene.apache.org/solr/guide/8_4/installing-solr.html).
|
|||||||
That will provide you with the connection url (the last part is the
|
That will provide you with the connection url (the last part is the
|
||||||
core name).
|
core name).
|
||||||
|
|
||||||
While the `full-text-search.solr` options are the same for joex and
|
The `full-text-search.solr` options are the same for joex and the
|
||||||
the restserver, there are some settings that differ. The restserver
|
restserver.
|
||||||
has this additional setting, that may be of interest:
|
|
||||||
|
There is an [admin route](@/docs/api/intro.md#admin) that allows to
|
||||||
|
re-create the entire index (for all collectives). This is possible via
|
||||||
|
a call:
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
full-text-search {
|
$ curl -XPOST -H "Docspell-Admin-Secret: test123" http://localhost:7880/api/v1/admin/fts/reIndexAll
|
||||||
recreate-key = "test123"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This key is required if you want docspell to drop and re-create the
|
Here the `test123` is the key defined with `admin-endpoint.secret`. If
|
||||||
entire index. This is possible via a REST call:
|
it is empty (the default), this call is disabled (all admin routes).
|
||||||
|
Otherwise, the POST request will submit a system task that is executed
|
||||||
``` bash
|
by a joex instance eventually.
|
||||||
$ curl -XPOST http://localhost:7880/api/v1/open/fts/reIndexAll/test123
|
|
||||||
```
|
|
||||||
|
|
||||||
Here the `test123` is the key defined with `recreate-key`. If it is
|
|
||||||
empty (the default), this REST call is disabled. Otherwise, the POST
|
|
||||||
request will submit a system task that is executed by a joex instance
|
|
||||||
eventually.
|
|
||||||
|
|
||||||
Using this endpoint, the index will be re-created. This is sometimes
|
Using this endpoint, the index will be re-created. This is sometimes
|
||||||
necessary, for example if you upgrade SOLR or delete the core to
|
necessary, for example if you upgrade SOLR or delete the core to
|
||||||
|
39
website/site/content/docs/tools/reset-password.md
Normal file
39
website/site/content/docs/tools/reset-password.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
+++
|
||||||
|
title = "Reset Password"
|
||||||
|
description = "Resets a user password."
|
||||||
|
weight = 70
|
||||||
|
+++
|
||||||
|
|
||||||
|
|
||||||
|
This script can be used to reset a user password. This can be done by
|
||||||
|
admins, who know the `admin-endpoint.secret` value in the
|
||||||
|
[configuration](@/docs/configure/_index.md#admin-endpoint) file.
|
||||||
|
|
||||||
|
The script is in `/tools/reset-password/reset-password.sh` and it is
|
||||||
|
only a wrapper around the admin endpoint `/admin/user/resetPassword`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
It's very simple:
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
reset-password.sh <base-url> <admin-secret> <account>
|
||||||
|
```
|
||||||
|
|
||||||
|
Three arguments are required to specify the docspell base url, the
|
||||||
|
admin secret and the account you want to reset the password.
|
||||||
|
|
||||||
|
After the password has been reset, the user can login using it and
|
||||||
|
change it again in the webapp.
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
``` json
|
||||||
|
❯ ./tools/reset-password/reset-password.sh http://localhost:7880 123 eike
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"newPassword": "HjtpG9BFo9y",
|
||||||
|
"message": "Password updated"
|
||||||
|
}
|
||||||
|
```
|
Loading…
x
Reference in New Issue
Block a user