mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-06 07:05:59 +00:00
Add a reset-password admin route
This commit is contained in:
parent
2a172ce720
commit
668abf2140
@ -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 =
|
||||||
|
@ -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
|
||||||
|
@ -103,7 +103,8 @@ object RestServer {
|
|||||||
|
|
||||||
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
def adminRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||||
Router(
|
Router(
|
||||||
"fts" -> FullTextIndexRoutes.admin(cfg, restApp.backend)
|
"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")
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user