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 folder: OFolder[F]
|
||||||
def customFields: OCustomFields[F]
|
def customFields: OCustomFields[F]
|
||||||
def simpleSearch: OSimpleSearch[F]
|
def simpleSearch: OSimpleSearch[F]
|
||||||
|
def clientSettings: OClientSettings[F]
|
||||||
}
|
}
|
||||||
|
|
||||||
object BackendApp {
|
object BackendApp {
|
||||||
@ -73,26 +74,28 @@ object BackendApp {
|
|||||||
folderImpl <- OFolder(store)
|
folderImpl <- OFolder(store)
|
||||||
customFieldsImpl <- OCustomFields(store)
|
customFieldsImpl <- OCustomFields(store)
|
||||||
simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl)
|
simpleSearchImpl = OSimpleSearch(fulltextImpl, itemSearchImpl)
|
||||||
|
clientSettingsImpl <- OClientSettings(store)
|
||||||
} yield new BackendApp[F] {
|
} yield new BackendApp[F] {
|
||||||
val login = loginImpl
|
val login = loginImpl
|
||||||
val signup = signupImpl
|
val signup = signupImpl
|
||||||
val collective = collImpl
|
val collective = collImpl
|
||||||
val source = sourceImpl
|
val source = sourceImpl
|
||||||
val tag = tagImpl
|
val tag = tagImpl
|
||||||
val equipment = equipImpl
|
val equipment = equipImpl
|
||||||
val organization = orgImpl
|
val organization = orgImpl
|
||||||
val upload = uploadImpl
|
val upload = uploadImpl
|
||||||
val node = nodeImpl
|
val node = nodeImpl
|
||||||
val job = jobImpl
|
val job = jobImpl
|
||||||
val item = itemImpl
|
val item = itemImpl
|
||||||
val itemSearch = itemSearchImpl
|
val itemSearch = itemSearchImpl
|
||||||
val fulltext = fulltextImpl
|
val fulltext = fulltextImpl
|
||||||
val mail = mailImpl
|
val mail = mailImpl
|
||||||
val joex = joexImpl
|
val joex = joexImpl
|
||||||
val userTask = userTaskImpl
|
val userTask = userTaskImpl
|
||||||
val folder = folderImpl
|
val folder = folderImpl
|
||||||
val customFields = customFieldsImpl
|
val customFields = customFieldsImpl
|
||||||
val simpleSearch = simpleSearchImpl
|
val simpleSearch = simpleSearchImpl
|
||||||
|
val clientSettings = clientSettingsImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
def apply[F[_]: ConcurrentEffect: ContextShift](
|
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:
|
schema:
|
||||||
$ref: "#/components/schemas/BasicResult"
|
$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:
|
/admin/user/resetPassword:
|
||||||
post:
|
post:
|
||||||
tags: [ Collective, Admin ]
|
tags: [ Collective, Admin ]
|
||||||
@ -5571,3 +5633,11 @@ components:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
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(),
|
"calevent/check" -> CalEventCheckRoutes(),
|
||||||
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
|
"fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token),
|
||||||
"folder" -> FolderRoutes(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] =
|
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.implicits.legacy.instant._
|
||||||
import doobie.util.log.Success
|
import doobie.util.log.Success
|
||||||
import emil.doobie.EmilDoobieMeta
|
import emil.doobie.EmilDoobieMeta
|
||||||
|
import io.circe.Json
|
||||||
import io.circe.{Decoder, Encoder}
|
import io.circe.{Decoder, Encoder}
|
||||||
|
|
||||||
trait DoobieMeta extends EmilDoobieMeta {
|
trait DoobieMeta extends EmilDoobieMeta {
|
||||||
@ -112,10 +113,18 @@ trait DoobieMeta extends EmilDoobieMeta {
|
|||||||
|
|
||||||
implicit val metaOrgUse: Meta[OrgUse] =
|
implicit val metaOrgUse: Meta[OrgUse] =
|
||||||
Meta[String].timap(OrgUse.unsafeFromString)(_.name)
|
Meta[String].timap(OrgUse.unsafeFromString)(_.name)
|
||||||
|
|
||||||
|
implicit val metaJsonString: Meta[Json] =
|
||||||
|
Meta[String].timap(DoobieMeta.parseJsonUnsafe)(_.noSpaces)
|
||||||
}
|
}
|
||||||
|
|
||||||
object DoobieMeta extends DoobieMeta {
|
object DoobieMeta extends DoobieMeta {
|
||||||
import org.log4s._
|
import org.log4s._
|
||||||
private val logger = getLogger
|
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