mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-06 15:15:58 +00:00
Add routes for storing/retrieving client settings
This commit is contained in:
parent
8c127cde5f
commit
25788a0b23
@ -38,6 +38,7 @@ trait BackendApp[F[_]] {
|
||||
def folder: OFolder[F]
|
||||
def customFields: OCustomFields[F]
|
||||
def simpleSearch: OSimpleSearch[F]
|
||||
def clientSettings: OClientSettings[F]
|
||||
}
|
||||
|
||||
object BackendApp {
|
||||
@ -73,6 +74,7 @@ object BackendApp {
|
||||
folderImpl <- OFolder(store)
|
||||
customFieldsImpl <- OCustomFields(store)
|
||||
simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl)
|
||||
clientSettingsImpl <- OClientSettings(store)
|
||||
} yield new BackendApp[F] {
|
||||
val login = loginImpl
|
||||
val signup = signupImpl
|
||||
@ -93,6 +95,7 @@ object BackendApp {
|
||||
val folder = folderImpl
|
||||
val customFields = customFieldsImpl
|
||||
val simpleSearch = simpleSearchImpl
|
||||
val clientSettings = clientSettingsImpl
|
||||
}
|
||||
|
||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
||||
|
@ -0,0 +1,78 @@
|
||||
package docspell.backend.ops
|
||||
|
||||
import cats.data.OptionT
|
||||
import cats.effect.{Effect, Resource}
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common.AccountId
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.store.Store
|
||||
import docspell.store.records.RClientSettings
|
||||
import docspell.store.records.RUser
|
||||
|
||||
import io.circe.Json
|
||||
import org.log4s._
|
||||
|
||||
trait OClientSettings[F[_]] {
|
||||
|
||||
def delete(clientId: Ident, account: AccountId): F[Boolean]
|
||||
def save(clientId: Ident, account: AccountId, data: Json): F[Unit]
|
||||
def load(clientId: Ident, account: AccountId): F[Option[RClientSettings]]
|
||||
|
||||
}
|
||||
|
||||
object OClientSettings {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
def apply[F[_]: Effect](store: Store[F]): Resource[F, OClientSettings[F]] =
|
||||
Resource.pure[F, OClientSettings[F]](new OClientSettings[F] {
|
||||
|
||||
private def getUserId(account: AccountId): OptionT[F, Ident] =
|
||||
OptionT(store.transact(RUser.findByAccount(account))).map(_.uid)
|
||||
|
||||
def delete(clientId: Ident, account: AccountId): F[Boolean] =
|
||||
(for {
|
||||
_ <- OptionT.liftF(
|
||||
logger.fdebug(
|
||||
s"Deleting client settings for client ${clientId.id} and account $account"
|
||||
)
|
||||
)
|
||||
userId <- getUserId(account)
|
||||
n <- OptionT.liftF(
|
||||
store.transact(
|
||||
RClientSettings.delete(clientId, userId)
|
||||
)
|
||||
)
|
||||
} yield n > 0).getOrElse(false)
|
||||
|
||||
def save(clientId: Ident, account: AccountId, data: Json): F[Unit] =
|
||||
(for {
|
||||
_ <- OptionT.liftF(
|
||||
logger.fdebug(
|
||||
s"Storing client settings for client ${clientId.id} and account $account"
|
||||
)
|
||||
)
|
||||
userId <- getUserId(account)
|
||||
n <- OptionT.liftF(
|
||||
store.transact(RClientSettings.upsert(clientId, userId, data))
|
||||
)
|
||||
_ <- OptionT.liftF(
|
||||
if (n <= 0) Effect[F].raiseError(new Exception("No rows updated!"))
|
||||
else ().pure[F]
|
||||
)
|
||||
} yield ()).getOrElse(())
|
||||
|
||||
def load(clientId: Ident, account: AccountId): F[Option[RClientSettings]] =
|
||||
(for {
|
||||
_ <- OptionT.liftF(
|
||||
logger.fdebug(
|
||||
s"Loading client settings for client ${clientId.id} and account $account"
|
||||
)
|
||||
)
|
||||
userId <- getUserId(account)
|
||||
data <- OptionT(store.transact(RClientSettings.find(clientId, userId)))
|
||||
} yield data).value
|
||||
|
||||
})
|
||||
}
|
@ -1185,6 +1185,68 @@ paths:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/sec/clientSettings/{clientId}:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/clientId"
|
||||
get:
|
||||
tags: [ Client Settings ]
|
||||
summary: Return the current user settings
|
||||
description: |
|
||||
Returns the settings for the current user. The `clientId` is
|
||||
an identifier to a client application. It returns a JSON
|
||||
structure. The server doesn't care about the actual data,
|
||||
since it is meant to be interpreted by clients.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
put:
|
||||
tags: [ Client Settings ]
|
||||
summary: Update current user settings
|
||||
description: |
|
||||
Updates (replaces or creates) the current user's settings with
|
||||
the given data. The `clientId` is an identifier to a client
|
||||
application. The request body is expected to be JSON, the
|
||||
structure is not important to the server.
|
||||
|
||||
The data is stored for the current user and given `clientId`.
|
||||
|
||||
The data is only saved without being checked in any way
|
||||
(besides being valid JSON). It is returned "as is" to the
|
||||
client in the corresponding GET endpoint.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema: {}
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
delete:
|
||||
tags: [ Client Settings ]
|
||||
summary: Clears the current user settings
|
||||
description: |
|
||||
Removes all stored user settings for the client identified by
|
||||
`clientId`.
|
||||
security:
|
||||
- authTokenHeader: []
|
||||
responses:
|
||||
200:
|
||||
description: Ok
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BasicResult"
|
||||
|
||||
/admin/user/resetPassword:
|
||||
post:
|
||||
tags: [ Collective, Admin ]
|
||||
@ -5571,3 +5633,11 @@ components:
|
||||
required: false
|
||||
schema:
|
||||
type: boolean
|
||||
clientId:
|
||||
name: clientId
|
||||
in: path
|
||||
required: true
|
||||
description: |
|
||||
some identifier for a client application
|
||||
schema:
|
||||
type: string
|
||||
|
@ -91,7 +91,8 @@ object RestServer {
|
||||
"calevent/check" -> CalEventCheckRoutes(),
|
||||
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
|
||||
"folder" -> FolderRoutes(restApp.backend, token),
|
||||
"customfield" -> CustomFieldRoutes(restApp.backend, token)
|
||||
"customfield" -> CustomFieldRoutes(restApp.backend, token),
|
||||
"clientSettings" -> ClientSettingsRoutes(restApp.backend, token)
|
||||
)
|
||||
|
||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
|
@ -0,0 +1,52 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common._
|
||||
import docspell.restapi.model._
|
||||
|
||||
import io.circe.Json
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object ClientSettingsRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ PUT -> Root / Ident(clientId) =>
|
||||
for {
|
||||
data <- req.as[Json]
|
||||
_ <- backend.clientSettings.save(clientId, user.account, data)
|
||||
res <- Ok(BasicResult(true, "Settings stored"))
|
||||
} yield res
|
||||
|
||||
case GET -> Root / Ident(clientId) =>
|
||||
for {
|
||||
data <- backend.clientSettings.load(clientId, user.account)
|
||||
res <- data match {
|
||||
case Some(d) => Ok(d.settingsData)
|
||||
case None => NotFound()
|
||||
}
|
||||
} yield res
|
||||
|
||||
case DELETE -> Root / Ident(clientId) =>
|
||||
for {
|
||||
flag <- backend.clientSettings.delete(clientId, user.account)
|
||||
res <- Ok(
|
||||
BasicResult(
|
||||
flag,
|
||||
if (flag) "Settings deleted" else "Deleting settings failed"
|
||||
)
|
||||
)
|
||||
} yield res
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
CREATE TABLE "client_settings" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"client_id" varchar(254) not null,
|
||||
"user_id" varchar(254) not null,
|
||||
"settings_data" text not null,
|
||||
"created" timestamp not null,
|
||||
"updated" timestamp not null,
|
||||
foreign key ("user_id") references "user_"("uid") on delete cascade,
|
||||
unique ("client_id", "user_id")
|
||||
);
|
@ -0,0 +1,10 @@
|
||||
CREATE TABLE `client_settings` (
|
||||
`id` varchar(254) not null primary key,
|
||||
`client_id` varchar(254) not null,
|
||||
`user_id` varchar(254) not null,
|
||||
`settings_data` longtext not null,
|
||||
`created` timestamp not null,
|
||||
`updated` timestamp not null,
|
||||
foreign key (`user_id`) references `user_`(`uid`) on delete cascade,
|
||||
unique (`client_id`, `user_id`)
|
||||
);
|
@ -0,0 +1,10 @@
|
||||
CREATE TABLE "client_settings" (
|
||||
"id" varchar(254) not null primary key,
|
||||
"client_id" varchar(254) not null,
|
||||
"user_id" varchar(254) not null,
|
||||
"settings_data" text not null,
|
||||
"created" timestamp not null,
|
||||
"updated" timestamp not null,
|
||||
foreign key ("user_id") references "user_"("uid") on delete cascade,
|
||||
unique ("client_id", "user_id")
|
||||
);
|
@ -11,6 +11,7 @@ import doobie._
|
||||
import doobie.implicits.legacy.instant._
|
||||
import doobie.util.log.Success
|
||||
import emil.doobie.EmilDoobieMeta
|
||||
import io.circe.Json
|
||||
import io.circe.{Decoder, Encoder}
|
||||
|
||||
trait DoobieMeta extends EmilDoobieMeta {
|
||||
@ -112,10 +113,18 @@ trait DoobieMeta extends EmilDoobieMeta {
|
||||
|
||||
implicit val metaOrgUse: Meta[OrgUse] =
|
||||
Meta[String].timap(OrgUse.unsafeFromString)(_.name)
|
||||
|
||||
implicit val metaJsonString: Meta[Json] =
|
||||
Meta[String].timap(DoobieMeta.parseJsonUnsafe)(_.noSpaces)
|
||||
}
|
||||
|
||||
object DoobieMeta extends DoobieMeta {
|
||||
import org.log4s._
|
||||
private val logger = getLogger
|
||||
|
||||
private def parseJsonUnsafe(str: String): Json =
|
||||
io.circe.parser
|
||||
.parse(str)
|
||||
.fold(throw _, identity)
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,79 @@
|
||||
package docspell.store.records
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.implicits._
|
||||
|
||||
import docspell.common._
|
||||
import docspell.store.qb.DSL._
|
||||
import docspell.store.qb._
|
||||
|
||||
import doobie._
|
||||
import doobie.implicits._
|
||||
import io.circe.Json
|
||||
|
||||
case class RClientSettings(
|
||||
id: Ident,
|
||||
clientId: Ident,
|
||||
userId: Ident,
|
||||
settingsData: Json,
|
||||
updated: Timestamp,
|
||||
created: Timestamp
|
||||
) {}
|
||||
|
||||
object RClientSettings {
|
||||
|
||||
final case class Table(alias: Option[String]) extends TableDef {
|
||||
val tableName = "client_settings"
|
||||
|
||||
val id = Column[Ident]("id", this)
|
||||
val clientId = Column[Ident]("client_id", this)
|
||||
val userId = Column[Ident]("user_id", this)
|
||||
val settingsData = Column[Json]("settings_data", this)
|
||||
val updated = Column[Timestamp]("updated", this)
|
||||
val created = Column[Timestamp]("created", this)
|
||||
val all =
|
||||
NonEmptyList.of[Column[_]](id, clientId, userId, settingsData, updated, created)
|
||||
}
|
||||
|
||||
def as(alias: String): Table = Table(Some(alias))
|
||||
val T = Table(None)
|
||||
|
||||
def insert(v: RClientSettings): ConnectionIO[Int] = {
|
||||
val t = Table(None)
|
||||
DML.insert(
|
||||
t,
|
||||
t.all,
|
||||
fr"${v.id},${v.clientId},${v.userId},${v.settingsData},${v.updated},${v.created}"
|
||||
)
|
||||
}
|
||||
|
||||
def updateSettings(
|
||||
clientId: Ident,
|
||||
userId: Ident,
|
||||
data: Json,
|
||||
updateTs: Timestamp
|
||||
): ConnectionIO[Int] =
|
||||
DML.update(
|
||||
T,
|
||||
T.clientId === clientId && T.userId === userId,
|
||||
DML.set(T.settingsData.setTo(data), T.updated.setTo(updateTs))
|
||||
)
|
||||
|
||||
def upsert(clientId: Ident, userId: Ident, data: Json): ConnectionIO[Int] =
|
||||
for {
|
||||
id <- Ident.randomId[ConnectionIO]
|
||||
now <- Timestamp.current[ConnectionIO]
|
||||
nup <- updateSettings(clientId, userId, data, now)
|
||||
nin <-
|
||||
if (nup <= 0) insert(RClientSettings(id, clientId, userId, data, now, now))
|
||||
else 0.pure[ConnectionIO]
|
||||
} yield nup + nin
|
||||
|
||||
def delete(clientId: Ident, userId: Ident): ConnectionIO[Int] =
|
||||
DML.delete(T, T.clientId === clientId && T.userId === userId)
|
||||
|
||||
def find(clientId: Ident, userId: Ident): ConnectionIO[Option[RClientSettings]] =
|
||||
run(select(T.all), from(T), T.clientId === clientId && T.userId === userId)
|
||||
.query[RClientSettings]
|
||||
.option
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user