mirror of
https://github.com/TheAnachronism/docspell.git
synced 2025-06-21 18:08:25 +00:00
Initial version.
Features: - Upload PDF files let them analyze - Manage meta data and items - See processing in webapp
This commit is contained in:
@ -1,3 +1,87 @@
|
||||
docspell.restserver {
|
||||
docspell.server {
|
||||
|
||||
# This is shown in the top right corner of the web application
|
||||
app-name = "Docspell"
|
||||
|
||||
# This is the id of this node. If you run more than one server, you
|
||||
# have to make sure to provide unique ids per node.
|
||||
app-id = "rest1"
|
||||
|
||||
# This is the base URL this application is deployed to. This is used
|
||||
# to create absolute URLs and to configure the cookie.
|
||||
base-url = "http://localhost:7880"
|
||||
|
||||
# Where the server binds to.
|
||||
bind {
|
||||
address = "localhost"
|
||||
port = 7880
|
||||
}
|
||||
|
||||
# Authentication.
|
||||
auth {
|
||||
|
||||
# The secret for this server that is used to sign the authenicator
|
||||
# tokens. If multiple servers are running, all must share the same
|
||||
# secret. You can use base64 or hex strings (prefix with b64: and
|
||||
# hex:, respectively)
|
||||
server-secret = "hex:caffee"
|
||||
|
||||
# How long an authentication token is valid. The web application
|
||||
# will get a new one periodically.
|
||||
session-valid = "5 minutes"
|
||||
}
|
||||
|
||||
# Configuration for the backend.
|
||||
backend {
|
||||
|
||||
# The database connection.
|
||||
#
|
||||
# By default a H2 file-based database is configured. You can
|
||||
# provide a postgresql or mariadb connection here. When using H2
|
||||
# use the PostgreSQL compatibility mode and AUTO_SERVER feature.
|
||||
jdbc {
|
||||
url = "jdbc:h2://"${java.io.tmpdir}"/docspell-demo.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;AUTO_SERVER=TRUE"
|
||||
user = "sa"
|
||||
password = ""
|
||||
}
|
||||
|
||||
# Configuration for registering new users.
|
||||
signup {
|
||||
|
||||
# The mode defines if new users can signup or not. It can have
|
||||
# three values:
|
||||
#
|
||||
# - open: every new user can sign up
|
||||
# - invite: new users can sign up only if they provide a correct
|
||||
# invitation key. Invitation keys can be generated by the
|
||||
# server.
|
||||
# - closed: signing up is disabled.
|
||||
mode = "open"
|
||||
|
||||
# If mode == 'invite', a password must be provided to generate
|
||||
# invitation keys. It must not be empty.
|
||||
new-invite-password = ""
|
||||
|
||||
# If mode == 'invite', this is the period an invitation token is
|
||||
# considered valid.
|
||||
invite-time = "3 days"
|
||||
}
|
||||
|
||||
files {
|
||||
# Defines the chunk size used to store bytes. This will affect
|
||||
# the memory footprint when uploading and downloading files. At
|
||||
# most this amount is loaded into RAM for down- and uploading.
|
||||
#
|
||||
# It also defines the chunk size used for the blobs inside the
|
||||
# database.
|
||||
chunk-size = 524288
|
||||
|
||||
# The file content types that are considered valid. Docspell
|
||||
# will only pass these files to processing. The processing code
|
||||
# itself has also checks for which files are supported and which
|
||||
# not. This affects the uploading part and is a first check to
|
||||
# avoid that 'bad' files get into the system.
|
||||
valid-mime-types = [ "application/pdf" ]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,33 @@
|
||||
package docspell.restserver
|
||||
|
||||
import docspell.backend.auth.Login
|
||||
import docspell.backend.signup.{Config => SignupConfig}
|
||||
import docspell.store.JdbcConfig
|
||||
import docspell.backend.{Config => BackendConfig}
|
||||
import docspell.common._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
case class Config(appName: String
|
||||
, baseUrl: String
|
||||
, appId: Ident
|
||||
, baseUrl: LenientUri
|
||||
, bind: Config.Bind
|
||||
, jdbc: JdbcConfig
|
||||
, backend: BackendConfig
|
||||
, auth: Login.Config
|
||||
)
|
||||
|
||||
object Config {
|
||||
|
||||
val postgres = JdbcConfig(LenientUri.unsafe("jdbc:postgresql://localhost:5432/docspelldev"), "dev", "dev")
|
||||
val h2 = JdbcConfig(LenientUri.unsafe("jdbc:h2:./target/docspelldev.db;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE"), "sa", "")
|
||||
|
||||
val default: Config =
|
||||
Config("Docspell", "http://localhost:7880", Config.Bind("localhost", 7880), JdbcConfig("", "", ""))
|
||||
|
||||
Config("Docspell"
|
||||
, Ident.unsafe("restserver1")
|
||||
, LenientUri.unsafe("http://localhost:7880")
|
||||
, Config.Bind("localhost", 7880)
|
||||
, BackendConfig(postgres
|
||||
, SignupConfig(SignupConfig.invite, Password("testpass"), Duration.hours(5 * 24))
|
||||
, BackendConfig.Files(512 * 1024, List(MimeType.pdf)))
|
||||
, Login.Config(ByteVector.fromValidHex("caffee"), Duration.minutes(2)))
|
||||
|
||||
case class Bind(address: String, port: Int)
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package docspell.restserver
|
||||
|
||||
import docspell.common.pureconfig.Implicits._
|
||||
import docspell.backend.signup.{Config => SignupConfig}
|
||||
import _root_.pureconfig._
|
||||
import _root_.pureconfig.generic.auto._
|
||||
|
||||
object ConfigFile {
|
||||
import Implicits._
|
||||
|
||||
def loadConfig: Config =
|
||||
ConfigSource.default.at("docspell.server").loadOrThrow[Config]
|
||||
|
||||
object Implicits {
|
||||
implicit val signupModeReader: ConfigReader[SignupConfig.Mode] =
|
||||
ConfigReader[String].emap(reason(SignupConfig.Mode.fromString))
|
||||
}
|
||||
}
|
@ -2,17 +2,24 @@ package docspell.restserver
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import java.util.concurrent.Executors
|
||||
import java.nio.file.{Files, Paths}
|
||||
|
||||
import docspell.common.{Banner, ThreadFactories}
|
||||
import org.log4s._
|
||||
|
||||
object Main extends IOApp {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(Executors.newCachedThreadPool)
|
||||
val blockingEc: ExecutionContext = ExecutionContext.fromExecutor(Executors.newCachedThreadPool(
|
||||
ThreadFactories.ofName("docspell-restserver-blocking")))
|
||||
val blocker = Blocker.liftExecutionContext(blockingEc)
|
||||
|
||||
val connectEC: ExecutionContext = ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(5,
|
||||
ThreadFactories.ofName("docspell-dbconnect")))
|
||||
|
||||
def run(args: List[String]) = {
|
||||
args match {
|
||||
case file :: Nil =>
|
||||
@ -33,7 +40,14 @@ object Main extends IOApp {
|
||||
}
|
||||
}
|
||||
|
||||
val cfg = Config.default
|
||||
RestServer.stream[IO](cfg, blocker).compile.drain.as(ExitCode.Success)
|
||||
val cfg = ConfigFile.loadConfig
|
||||
val banner = Banner("REST Server"
|
||||
, BuildInfo.version
|
||||
, BuildInfo.gitHeadCommit
|
||||
, cfg.backend.jdbc.url
|
||||
, Option(System.getProperty("config.file"))
|
||||
, cfg.appId, cfg.baseUrl)
|
||||
logger.info(s"\n${banner.render("***>")}")
|
||||
RestServer.stream[IO](cfg, connectEC, blockingEc, blocker).compile.drain.as(ExitCode.Success)
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,12 @@
|
||||
package docspell.restserver
|
||||
|
||||
import docspell.backend.BackendApp
|
||||
|
||||
trait RestApp[F[_]] {
|
||||
|
||||
def init: F[Unit]
|
||||
|
||||
def config: Config
|
||||
|
||||
def backend: BackendApp[F]
|
||||
}
|
||||
|
@ -1,16 +1,28 @@
|
||||
package docspell.restserver
|
||||
|
||||
import cats.implicits._
|
||||
import cats.effect._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.common.NodeType
|
||||
|
||||
final class RestAppImpl[F[_]: Sync](cfg: Config) extends RestApp[F] {
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
final class RestAppImpl[F[_]: Sync](val config: Config, val backend: BackendApp[F]) extends RestApp[F] {
|
||||
|
||||
def init: F[Unit] =
|
||||
Sync[F].pure(())
|
||||
backend.node.register(config.appId, NodeType.Restserver, config.baseUrl)
|
||||
|
||||
def shutdown: F[Unit] =
|
||||
backend.node.unregister(config.appId)
|
||||
}
|
||||
|
||||
object RestAppImpl {
|
||||
|
||||
def create[F[_]: Sync](cfg: Config): Resource[F, RestApp[F]] =
|
||||
Resource.liftF(Sync[F].pure(new RestAppImpl(cfg)))
|
||||
def create[F[_]: ConcurrentEffect: ContextShift](cfg: Config, connectEC: ExecutionContext, httpClientEc: ExecutionContext, blocker: Blocker): Resource[F, RestApp[F]] =
|
||||
for {
|
||||
backend <- BackendApp(cfg.backend, connectEC, httpClientEc, blocker)
|
||||
app = new RestAppImpl[F](cfg, backend)
|
||||
appR <- Resource.make(app.init.map(_ => app))(_.shutdown)
|
||||
} yield appR
|
||||
|
||||
}
|
||||
|
@ -1,42 +1,69 @@
|
||||
package docspell.restserver
|
||||
|
||||
import cats.effect._
|
||||
import docspell.backend.auth.AuthToken
|
||||
import org.http4s.server.blaze.BlazeServerBuilder
|
||||
import org.http4s.implicits._
|
||||
import fs2.Stream
|
||||
|
||||
import org.http4s.server.middleware.Logger
|
||||
import org.http4s.server.Router
|
||||
|
||||
import docspell.restserver.webapp._
|
||||
import docspell.restserver.routes._
|
||||
import org.http4s.HttpRoutes
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
|
||||
object RestServer {
|
||||
|
||||
def stream[F[_]: ConcurrentEffect](cfg: Config, blocker: Blocker)
|
||||
def stream[F[_]: ConcurrentEffect](cfg: Config, connectEC: ExecutionContext, httpClientEc: ExecutionContext, blocker: Blocker)
|
||||
(implicit T: Timer[F], CS: ContextShift[F]): Stream[F, Nothing] = {
|
||||
|
||||
val app = for {
|
||||
restApp <- RestAppImpl.create[F](cfg)
|
||||
_ <- Resource.liftF(restApp.init)
|
||||
restApp <- RestAppImpl.create[F](cfg, connectEC, httpClientEc, blocker)
|
||||
|
||||
httpApp = Router(
|
||||
"/api/info" -> InfoRoutes(cfg),
|
||||
"/api/info" -> routes.InfoRoutes(cfg),
|
||||
"/api/v1/open/" -> openRoutes(cfg, restApp),
|
||||
"/api/v1/sec/" -> Authenticate(restApp.backend.login, cfg.auth) {
|
||||
token => securedRoutes(cfg, restApp, token)
|
||||
},
|
||||
"/app/assets" -> WebjarRoutes.appRoutes[F](blocker, cfg),
|
||||
"/app" -> TemplateRoutes[F](blocker, cfg)
|
||||
).orNotFound
|
||||
|
||||
// With Middlewares in place
|
||||
finalHttpApp = Logger.httpApp(false, false)(httpApp)
|
||||
finalHttpApp = Logger.httpApp(logHeaders = false, logBody = false)(httpApp)
|
||||
|
||||
} yield finalHttpApp
|
||||
|
||||
|
||||
Stream.resource(app).flatMap(httpApp =>
|
||||
BlazeServerBuilder[F]
|
||||
.bindHttp(cfg.bind.port, cfg.bind.address)
|
||||
.withHttpApp(httpApp)
|
||||
.serve
|
||||
BlazeServerBuilder[F].
|
||||
bindHttp(cfg.bind.port, cfg.bind.address).
|
||||
withHttpApp(httpApp).
|
||||
withoutBanner.
|
||||
serve)
|
||||
}.drain
|
||||
|
||||
|
||||
def securedRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F], token: AuthToken): HttpRoutes[F] =
|
||||
Router(
|
||||
"auth" -> LoginRoutes.session(restApp.backend.login, cfg),
|
||||
"tag" -> TagRoutes(restApp.backend, cfg, token),
|
||||
"equipment" -> EquipmentRoutes(restApp.backend, cfg, token),
|
||||
"organization" -> OrganizationRoutes(restApp.backend, cfg, token),
|
||||
"person" -> PersonRoutes(restApp.backend, cfg, token),
|
||||
"source" -> SourceRoutes(restApp.backend, cfg, token),
|
||||
"user" -> UserRoutes(restApp.backend, cfg, token),
|
||||
"collective" -> CollectiveRoutes(restApp.backend, cfg, token),
|
||||
"queue" -> JobQueueRoutes(restApp.backend, cfg, token),
|
||||
"item" -> ItemRoutes(restApp.backend, cfg, token),
|
||||
"attachment" -> AttachmentRoutes(restApp.backend, cfg, token),
|
||||
"upload" -> UploadRoutes.secured(restApp.backend, cfg, token)
|
||||
)
|
||||
|
||||
}.drain
|
||||
def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] =
|
||||
Router(
|
||||
"auth" -> LoginRoutes.login(restApp.backend.login, cfg),
|
||||
"signup" -> RegisterRoutes(restApp.backend, cfg),
|
||||
"upload" -> UploadRoutes.open(restApp.backend, cfg)
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
package docspell.restserver.auth
|
||||
|
||||
import org.http4s._
|
||||
import org.http4s.util._
|
||||
import docspell.backend.auth._
|
||||
import docspell.common.AccountId
|
||||
import docspell.restserver.Config
|
||||
|
||||
case class CookieData(auth: AuthToken) {
|
||||
def accountId: AccountId = auth.account
|
||||
def asString: String = auth.asString
|
||||
|
||||
def asCookie(cfg: Config): ResponseCookie = {
|
||||
val domain = "" //cfg.baseUrl.hostAndPort
|
||||
val sec = false //cfg.baseUrl.protocol.exists(_.endsWith("s"))
|
||||
ResponseCookie(CookieData.cookieName, asString, domain = Some(domain), path = Some("/api/v1"), httpOnly = true, secure = sec)
|
||||
}
|
||||
}
|
||||
object CookieData {
|
||||
val cookieName = "docspell_auth"
|
||||
val headerName = "X-Docspell-Auth"
|
||||
|
||||
def authenticator[F[_]](r: Request[F]): Either[String, String] =
|
||||
fromCookie(r) orElse fromHeader(r)
|
||||
|
||||
def fromCookie[F[_]](req: Request[F]): Either[String, String] = {
|
||||
for {
|
||||
header <- headers.Cookie.from(req.headers).toRight("Cookie parsing error")
|
||||
cookie <- header.values.toList.find(_.name == cookieName).toRight("Couldn't find the authcookie")
|
||||
} yield cookie.content
|
||||
}
|
||||
|
||||
def fromHeader[F[_]](req: Request[F]): Either[String, String] = {
|
||||
req.headers.get(CaseInsensitiveString(headerName)).map(_.value).toRight("Couldn't find an authenticator")
|
||||
}
|
||||
}
|
@ -0,0 +1,337 @@
|
||||
package docspell.restserver.conv
|
||||
|
||||
import java.time.{LocalDate, ZoneId}
|
||||
|
||||
import fs2.Stream
|
||||
import cats.implicits._
|
||||
import cats.effect.{Effect, Sync}
|
||||
import docspell.common._
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.restapi.model._
|
||||
import docspell.store.records._
|
||||
import Conversions._
|
||||
import bitpeace.FileMeta
|
||||
import docspell.backend.ops.OCollective.{InsightData, PassChangeResult}
|
||||
import docspell.backend.ops.OJob.JobCancelResult
|
||||
import docspell.backend.ops.OUpload.{UploadData, UploadMeta, UploadResult}
|
||||
import docspell.backend.ops.{OItem, OJob, OOrganization, OUpload}
|
||||
import docspell.store.AddResult
|
||||
import org.http4s.multipart.Multipart
|
||||
import org.http4s.headers.`Content-Type`
|
||||
import org.log4s.Logger
|
||||
|
||||
trait Conversions {
|
||||
|
||||
// insights
|
||||
def mkItemInsights(d: InsightData): ItemInsights =
|
||||
ItemInsights(d.incoming, d.outgoing, d.bytes, TagCloud(d.tags.toList.map(p => NameCount(p._1, p._2))))
|
||||
|
||||
// attachment meta
|
||||
def mkAttachmentMeta(rm: RAttachmentMeta): AttachmentMeta =
|
||||
AttachmentMeta(rm.content.getOrElse("")
|
||||
, rm.nerlabels.map(nl => Label(nl.tag, nl.label, nl.startPosition, nl.endPosition))
|
||||
, mkItemProposals(rm.proposals))
|
||||
|
||||
|
||||
// item proposal
|
||||
def mkItemProposals(ml: MetaProposalList): ItemProposals = {
|
||||
def get(mpt: MetaProposalType) =
|
||||
ml.find(mpt).
|
||||
map(mp => mp.values.toList.map(_.ref).map(mkIdName)).
|
||||
getOrElse(Nil)
|
||||
def getDates(mpt: MetaProposalType): List[Timestamp] =
|
||||
ml.find(mpt).
|
||||
map(mp => mp.values.toList.
|
||||
map(cand => cand.ref.id.id).
|
||||
flatMap(str => Either.catchNonFatal(LocalDate.parse(str)).toOption).
|
||||
map(_.atTime(12, 0).atZone(ZoneId.of("GMT"))).
|
||||
map(zdt => Timestamp(zdt.toInstant))).
|
||||
getOrElse(Nil).
|
||||
distinct.
|
||||
take(5)
|
||||
|
||||
ItemProposals(
|
||||
corrOrg = get(MetaProposalType.CorrOrg),
|
||||
corrPerson = get(MetaProposalType.CorrPerson),
|
||||
concPerson = get(MetaProposalType.ConcPerson),
|
||||
concEquipment = get(MetaProposalType.ConcEquip),
|
||||
itemDate = getDates(MetaProposalType.DocDate),
|
||||
dueDate = getDates(MetaProposalType.DueDate)
|
||||
)
|
||||
}
|
||||
|
||||
// item detail
|
||||
def mkItemDetail(data: OItem.ItemData): ItemDetail =
|
||||
ItemDetail(data.item.id
|
||||
, data.item.direction
|
||||
, data.item.name
|
||||
, data.item.source
|
||||
, data.item.state
|
||||
, data.item.created
|
||||
, data.item.updated
|
||||
, data.item.itemDate
|
||||
, data.corrOrg.map(o => IdName(o.oid, o.name))
|
||||
, data.corrPerson.map(p => IdName(p.pid, p.name))
|
||||
, data.concPerson.map(p => IdName(p.pid, p.name))
|
||||
, data.concEquip.map(e => IdName(e.eid, e.name))
|
||||
, data.inReplyTo.map(mkIdName)
|
||||
, data.item.dueDate
|
||||
, data.item.notes
|
||||
, data.attachments.map((mkAttachment _).tupled).toList
|
||||
, data.tags.map(mkTag).toList)
|
||||
|
||||
def mkAttachment(ra: RAttachment, m: FileMeta): Attachment =
|
||||
Attachment(ra.id, ra.name, m.length, MimeType.unsafe(m.mimetype.asString))
|
||||
|
||||
// item list
|
||||
|
||||
def mkQuery(m: ItemSearch, coll: Ident): OItem.Query =
|
||||
OItem.Query(coll
|
||||
, m.name
|
||||
, if (m.inbox) Seq(ItemState.Created) else Seq(ItemState.Created, ItemState.Confirmed)
|
||||
, m.direction
|
||||
, m.corrPerson
|
||||
, m.corrOrg
|
||||
, m.concPerson
|
||||
, m.concEquip
|
||||
, m.tagsInclude.map(Ident.unsafe)
|
||||
, m.tagsExclude.map(Ident.unsafe)
|
||||
, m.dateFrom
|
||||
, m.dateUntil
|
||||
, m.dueDateFrom
|
||||
, m.dueDateUntil
|
||||
)
|
||||
|
||||
def mkItemList(v: Vector[OItem.ListItem]): ItemLightList = {
|
||||
val groups = v.groupBy(item => item.date.toDate.toString.substring(0, 7))
|
||||
|
||||
def mkGroup(g: (String, Vector[OItem.ListItem])): ItemLightGroup =
|
||||
ItemLightGroup(g._1, g._2.map(mkItemLight).toList)
|
||||
|
||||
val gs = groups.map(mkGroup _).toList.sortWith((g1, g2) => g1.name.compareTo(g2.name) >= 0)
|
||||
ItemLightList(gs)
|
||||
}
|
||||
|
||||
def mkItemLight(i: OItem.ListItem): ItemLight =
|
||||
ItemLight(i.id, i.name, i.state, i.date, i.dueDate, i.source, i.direction.name.some, i.corrOrg.map(mkIdName),
|
||||
i.corrPerson.map(mkIdName), i.concPerson.map(mkIdName), i.concEquip.map(mkIdName), i.fileCount)
|
||||
|
||||
// job
|
||||
def mkJobQueueState(state: OJob.CollectiveQueueState): JobQueueState = {
|
||||
def desc(f: JobDetail => Option[Timestamp])(j1: JobDetail, j2: JobDetail): Boolean = {
|
||||
val t1 = f(j1).getOrElse(Timestamp.Epoch)
|
||||
val t2 = f(j2).getOrElse(Timestamp.Epoch)
|
||||
t1.value.isAfter(t2.value)
|
||||
}
|
||||
def asc(f: JobDetail => Option[Timestamp])(j1: JobDetail, j2: JobDetail): Boolean = {
|
||||
val t1 = f(j1).getOrElse(Timestamp.Epoch)
|
||||
val t2 = f(j2).getOrElse(Timestamp.Epoch)
|
||||
t1.value.isBefore(t2.value)
|
||||
}
|
||||
JobQueueState(state.running.map(mkJobDetail).toList.sortWith(asc(_.started))
|
||||
, state.done.map(mkJobDetail).toList.sortWith(desc(_.finished))
|
||||
, state.queued.map(mkJobDetail).toList.sortWith(asc(_.submitted.some)))
|
||||
}
|
||||
|
||||
def mkJobDetail(jd: OJob.JobDetail): JobDetail =
|
||||
JobDetail(jd.job.id
|
||||
, jd.job.subject
|
||||
, jd.job.submitted
|
||||
, jd.job.priority
|
||||
, jd.job.state
|
||||
, jd.job.retries
|
||||
, jd.logs.map(mkJobLog).toList
|
||||
, jd.job.progress
|
||||
, jd.job.worker
|
||||
, jd.job.started
|
||||
, jd.job.finished)
|
||||
|
||||
def mkJobLog(jl: RJobLog): JobLogEvent =
|
||||
JobLogEvent(jl.created, jl.level, jl.message)
|
||||
|
||||
// upload
|
||||
def readMultipart[F[_]: Effect](mp: Multipart[F], logger: Logger, prio: Priority, validFileTypes: Seq[MimeType]): F[UploadData[F]] = {
|
||||
def parseMeta(body: Stream[F, Byte]): F[ItemUploadMeta] = {
|
||||
body.through(fs2.text.utf8Decode).
|
||||
parseJsonAs[ItemUploadMeta].
|
||||
map(_.fold(ex => {
|
||||
logger.error(ex)("Reading upload metadata failed.")
|
||||
throw ex
|
||||
}, identity))
|
||||
}
|
||||
|
||||
val meta: F[(Boolean, UploadMeta)] = mp.parts.find(_.name.exists(_ equalsIgnoreCase "meta")).
|
||||
map(p => parseMeta(p.body)).
|
||||
map(fm => fm.map(m => (m.multiple, UploadMeta(m.direction, "webapp", validFileTypes)))).
|
||||
getOrElse((true, UploadMeta(None, "webapp", validFileTypes)).pure[F])
|
||||
|
||||
val files = mp.parts.
|
||||
filter(p => p.name.forall(s => !s.equalsIgnoreCase("meta"))).
|
||||
map(p => OUpload.File(p.filename, p.headers.get(`Content-Type`).map(fromContentType), p.body))
|
||||
for {
|
||||
metaData <- meta
|
||||
_ <- Effect[F].delay(logger.debug(s"Parsed upload meta data: $metaData"))
|
||||
tracker <- Ident.randomId[F]
|
||||
} yield UploadData(metaData._1, metaData._2, files, prio, Some(tracker))
|
||||
}
|
||||
|
||||
// organization and person
|
||||
def mkOrg(v: OOrganization.OrgAndContacts): Organization = {
|
||||
val ro = v.org
|
||||
Organization(ro.oid, ro.name, Address(ro.street, ro.zip, ro.city, ro.country),
|
||||
v.contacts.map(mkContact).toList, ro.notes, ro.created)
|
||||
}
|
||||
|
||||
def newOrg[F[_]: Sync](v: Organization, cid: Ident): F[OOrganization.OrgAndContacts] = {
|
||||
def contacts(oid: Ident) =
|
||||
v.contacts.traverse(c => newContact(c, oid.some, None))
|
||||
for {
|
||||
now <- Timestamp.current[F]
|
||||
oid <- Ident.randomId[F]
|
||||
cont <- contacts(oid)
|
||||
org = ROrganization(oid, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, now)
|
||||
} yield OOrganization.OrgAndContacts(org, cont)
|
||||
}
|
||||
|
||||
def changeOrg[F[_]: Sync](v: Organization, cid: Ident): F[OOrganization.OrgAndContacts] = {
|
||||
def contacts(oid: Ident) =
|
||||
v.contacts.traverse(c => newContact(c, oid.some, None))
|
||||
for {
|
||||
cont <- contacts(v.id)
|
||||
org = ROrganization(v.id, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, v.created)
|
||||
} yield OOrganization.OrgAndContacts(org, cont)
|
||||
}
|
||||
|
||||
def mkPerson(v: OOrganization.PersonAndContacts): Person = {
|
||||
val ro = v.person
|
||||
Person(ro.pid, ro.name, Address(ro.street, ro.zip, ro.city, ro.country),
|
||||
v.contacts.map(mkContact).toList, ro.notes, ro.concerning, ro.created)
|
||||
}
|
||||
|
||||
def newPerson[F[_]: Sync](v: Person, cid: Ident): F[OOrganization.PersonAndContacts] = {
|
||||
def contacts(pid: Ident) =
|
||||
v.contacts.traverse(c => newContact(c, None, pid.some))
|
||||
for {
|
||||
now <- Timestamp.current[F]
|
||||
pid <- Ident.randomId[F]
|
||||
cont <- contacts(pid)
|
||||
org = RPerson(pid, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, v.concerning, now)
|
||||
} yield OOrganization.PersonAndContacts(org, cont)
|
||||
}
|
||||
|
||||
def changePerson[F[_]: Sync](v: Person, cid: Ident): F[OOrganization.PersonAndContacts] = {
|
||||
def contacts(pid: Ident) =
|
||||
v.contacts.traverse(c => newContact(c, None, pid.some))
|
||||
for {
|
||||
cont <- contacts(v.id)
|
||||
org = RPerson(v.id, cid, v.name, v.address.street, v.address.zip, v.address.city, v.address.country, v.notes, v.concerning, v.created)
|
||||
} yield OOrganization.PersonAndContacts(org, cont)
|
||||
}
|
||||
|
||||
// contact
|
||||
def mkContact(rc: RContact): Contact =
|
||||
Contact(rc.contactId, rc.value, rc.kind)
|
||||
|
||||
def newContact[F[_]: Sync](c: Contact, oid: Option[Ident], pid: Option[Ident]): F[RContact] =
|
||||
timeId.map { case (id, now) =>
|
||||
RContact(id, c.value, c.kind, pid, oid, now)
|
||||
}
|
||||
|
||||
// users
|
||||
def mkUser(ru: RUser): User =
|
||||
User(ru.login, ru.state, None, ru.email, ru.lastLogin, ru.loginCount, ru.created)
|
||||
|
||||
def newUser[F[_]: Sync](u: User, cid: Ident): F[RUser] =
|
||||
timeId.map { case (id, now) =>
|
||||
RUser(id, u.login, cid, u.password.getOrElse(Password.empty), u.state, u.email, u.loginCount, u.lastLogin, u.created)
|
||||
}
|
||||
|
||||
def changeUser(u: User, cid: Ident): RUser =
|
||||
RUser(Ident.unsafe(""), u.login, cid, u.password.getOrElse(Password.empty), u.state, u.email, u.loginCount, u.lastLogin, u.created)
|
||||
|
||||
// tags
|
||||
|
||||
def mkTag(rt: RTag): Tag =
|
||||
Tag(rt.tagId, rt.name, rt.category, rt.created)
|
||||
|
||||
def newTag[F[_]: Sync](t: Tag, cid: Ident): F[RTag] =
|
||||
timeId.map { case (id, now) =>
|
||||
RTag(id, cid, t.name, t.category, now)
|
||||
}
|
||||
|
||||
def changeTag(t: Tag, cid: Ident): RTag =
|
||||
RTag(t.id, cid, t.name, t.category, t.created)
|
||||
|
||||
|
||||
// sources
|
||||
|
||||
def mkSource(s: RSource): Source =
|
||||
Source(s.sid, s.abbrev, s.description, s.counter, s.enabled, s.priority, s.created)
|
||||
|
||||
def newSource[F[_]: Sync](s: Source, cid: Ident): F[RSource] =
|
||||
timeId.map({ case (id, now) =>
|
||||
RSource(id, cid, s.abbrev, s.description, 0, s.enabled, s.priority, now)
|
||||
})
|
||||
|
||||
def changeSource[F[_]: Sync](s: Source, coll: Ident): RSource =
|
||||
RSource(s.id, coll, s.abbrev, s.description, s.counter, s.enabled, s.priority, s.created)
|
||||
|
||||
// equipment
|
||||
def mkEquipment(re: REquipment): Equipment =
|
||||
Equipment(re.eid, re.name, re.created)
|
||||
|
||||
def newEquipment[F[_]: Sync](e: Equipment, cid: Ident): F[REquipment] =
|
||||
timeId.map({ case (id, now) =>
|
||||
REquipment(id, cid, e.name, now)
|
||||
})
|
||||
|
||||
def changeEquipment(e: Equipment, cid: Ident): REquipment =
|
||||
REquipment(e.id, cid, e.name, e.created)
|
||||
|
||||
// idref
|
||||
|
||||
def mkIdName(ref: IdRef): IdName =
|
||||
IdName(ref.id, ref.name)
|
||||
|
||||
// basic result
|
||||
|
||||
def basicResult(cr: JobCancelResult): BasicResult =
|
||||
cr match {
|
||||
case JobCancelResult.JobNotFound => BasicResult(false, "Job not found")
|
||||
case JobCancelResult.CancelRequested => BasicResult(true, "Cancel was requested at the job executor")
|
||||
case JobCancelResult.Removed => BasicResult(true, "The job has been removed from the queue.")
|
||||
}
|
||||
|
||||
def basicResult(ar: AddResult, successMsg: String): BasicResult = ar match {
|
||||
case AddResult.Success => BasicResult(true, successMsg)
|
||||
case AddResult.EntityExists(msg) => BasicResult(false, msg)
|
||||
case AddResult.Failure(ex) => BasicResult(false, s"Internal error: ${ex.getMessage}")
|
||||
}
|
||||
|
||||
def basicResult(ur: OUpload.UploadResult): BasicResult = ur match {
|
||||
case UploadResult.Success => BasicResult(true, "Files submitted.")
|
||||
case UploadResult.NoFiles => BasicResult(false, "There were no files to submit.")
|
||||
case UploadResult.NoSource => BasicResult(false, "The source id is not valid.")
|
||||
}
|
||||
|
||||
def basicResult(cr: PassChangeResult): BasicResult = cr match {
|
||||
case PassChangeResult.Success => BasicResult(true, "Password changed.")
|
||||
case PassChangeResult.UpdateFailed => BasicResult(false, "The database update failed.")
|
||||
case PassChangeResult.PasswordMismatch => BasicResult(false, "The current password is incorrect.")
|
||||
case PassChangeResult.UserNotFound => BasicResult(false, "User not found.")
|
||||
}
|
||||
|
||||
// MIME Type
|
||||
|
||||
def fromContentType(header: `Content-Type`): MimeType =
|
||||
MimeType(header.mediaType.mainType, header.mediaType.subType)
|
||||
}
|
||||
|
||||
object Conversions extends Conversions {
|
||||
|
||||
private def timeId[F[_]: Sync]: F[(Ident, Timestamp)] =
|
||||
for {
|
||||
id <- Ident.randomId[F]
|
||||
now <- Timestamp.current
|
||||
} yield (id, now)
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package docspell.restserver.http4s
|
||||
|
||||
import cats.Applicative
|
||||
import org.http4s.{EntityEncoder, Header, Response}
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
trait ResponseGenerator[F[_]] {
|
||||
self: Http4sDsl[F] =>
|
||||
|
||||
|
||||
implicit final class EitherResponses[A,B](e: Either[A, B]) {
|
||||
def toResponse(headers: Header*)
|
||||
(implicit F: Applicative[F]
|
||||
, w0: EntityEncoder[F, A]
|
||||
, w1: EntityEncoder[F, B]): F[Response[F]] =
|
||||
e.fold(
|
||||
a => UnprocessableEntity(a),
|
||||
b => Ok(b)
|
||||
)
|
||||
}
|
||||
|
||||
implicit final class OptionResponse[A](o: Option[A]) {
|
||||
def toResponse(headers: Header*)
|
||||
(implicit F: Applicative[F]
|
||||
, w0: EntityEncoder[F, A]): F[Response[F]] =
|
||||
o.map(a => Ok(a)).getOrElse(NotFound())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object ResponseGenerator {
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data.NonEmptyList
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.backend.ops.OItem
|
||||
import docspell.common.Ident
|
||||
import org.http4s.{Header, HttpRoutes, MediaType, Response}
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.headers._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import org.http4s.headers.ETag.EntityTag
|
||||
|
||||
object AttachmentRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F]{}
|
||||
import dsl._
|
||||
|
||||
def makeByteResp(data: OItem.AttachmentData[F]): F[Response[F]] = {
|
||||
val mt = MediaType.unsafeParse(data.meta.mimetype.asString)
|
||||
val cntLen: Header = `Content-Length`.unsafeFromLong(data.meta.length)
|
||||
val eTag: Header = ETag(data.meta.checksum)
|
||||
val disp: Header = `Content-Disposition`("inline", Map("filename" -> data.ra.name.getOrElse("")))
|
||||
Ok(data.data.take(data.meta.length)).
|
||||
map(r => r.withContentType(`Content-Type`(mt)).
|
||||
withHeaders(cntLen, eTag, disp))
|
||||
}
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ GET -> Root / Ident(id) =>
|
||||
for {
|
||||
fileData <- backend.item.findAttachment(id, user.account.collective)
|
||||
inm = req.headers.get(`If-None-Match`).flatMap(_.tags)
|
||||
matches = matchETag(fileData, inm)
|
||||
resp <- if (matches) NotModified()
|
||||
else fileData.map(makeByteResp).getOrElse(NotFound(BasicResult(false, "Not found")))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / Ident(id) / "meta" =>
|
||||
for {
|
||||
rm <- backend.item.findAttachmentMeta(id, user.account.collective)
|
||||
md = rm.map(Conversions.mkAttachmentMeta)
|
||||
resp <- md.map(Ok(_)).getOrElse(NotFound(BasicResult(false, "Not found.")))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
private def matchETag[F[_]]( fileData: Option[OItem.AttachmentData[F]]
|
||||
, noneMatch: Option[NonEmptyList[EntityTag]]): Boolean =
|
||||
(fileData, noneMatch) match {
|
||||
case (Some(fd), Some(nm)) =>
|
||||
fd.meta.checksum == nm.head.tag
|
||||
case _ =>
|
||||
false
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.data._
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.auth._
|
||||
import docspell.restserver.auth._
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.server._
|
||||
|
||||
object Authenticate {
|
||||
|
||||
def authenticateRequest[F[_]: Effect](auth: String => F[Login.Result])(req: Request[F]): F[Login.Result] =
|
||||
CookieData.authenticator(req) match {
|
||||
case Right(str) => auth(str)
|
||||
case Left(err) => Login.Result.invalidAuth.pure[F]
|
||||
}
|
||||
|
||||
|
||||
def of[F[_]: Effect](S: Login[F], cfg: Login.Config)(pf: PartialFunction[AuthedRequest[F, AuthToken], F[Response[F]]]): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = getUser[F](S.loginSession(cfg))
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.authInfo)))
|
||||
|
||||
val middleware: AuthMiddleware[F, AuthToken] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes.of(pf))
|
||||
}
|
||||
|
||||
def apply[F[_]: Effect](S: Login[F], cfg: Login.Config)(f: AuthToken => HttpRoutes[F]): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
val authUser = getUser[F](S.loginSession(cfg))
|
||||
|
||||
val onFailure: AuthedRoutes[String, F] =
|
||||
Kleisli(req => OptionT.liftF(Forbidden(req.authInfo)))
|
||||
|
||||
val middleware: AuthMiddleware[F, AuthToken] =
|
||||
AuthMiddleware(authUser, onFailure)
|
||||
|
||||
middleware(AuthedRoutes(authReq => f(authReq.authInfo).run(authReq.req)))
|
||||
}
|
||||
|
||||
private def getUser[F[_]: Effect](auth: String => F[Login.Result]): Kleisli[F, Request[F], Either[String, AuthToken]] =
|
||||
Kleisli(r => authenticateRequest(auth)(r).map(_.toEither))
|
||||
}
|
@ -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.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object CollectiveRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / "insights" =>
|
||||
for {
|
||||
ins <- backend.collective.insights(user.account.collective)
|
||||
resp <- Ok(Conversions.mkItemInsights(ins))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / "settings" =>
|
||||
for {
|
||||
settings <- req.as[CollectiveSettings]
|
||||
res <- backend.collective.updateLanguage(user.account.collective, settings.language)
|
||||
resp <- Ok(Conversions.basicResult(res, "Language updated."))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / "settings" =>
|
||||
for {
|
||||
collDb <- backend.collective.find(user.account.collective)
|
||||
sett = collDb.map(c => CollectiveSettings(c.language))
|
||||
resp <- sett.toResponse()
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
for {
|
||||
collDb <- backend.collective.find(user.account.collective)
|
||||
coll = collDb.map(c => Collective(c.id, c.state, c.created))
|
||||
resp <- coll.toResponse()
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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.Ident
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object EquipmentRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F]{}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root =>
|
||||
for {
|
||||
data <- backend.equipment.findAll(user.account)
|
||||
resp <- Ok(EquipmentList(data.map(mkEquipment).toList))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[Equipment]
|
||||
equip <- newEquipment(data, user.account.collective)
|
||||
res <- backend.equipment.add(equip)
|
||||
resp <- Ok(basicResult(res, "Equipment created"))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
data <- req.as[Equipment]
|
||||
equip = changeEquipment(data, user.account.collective)
|
||||
res <- backend.equipment.update(equip)
|
||||
resp <- Ok(basicResult(res, "Equipment updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
del <- backend.equipment.delete(id, user.account.collective)
|
||||
resp <- Ok(basicResult(del, "Equipment deleted."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
}
|
@ -1,13 +1,11 @@
|
||||
package docspell.restserver
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import org.http4s._
|
||||
import cats.effect.Sync
|
||||
import docspell.restapi.model.VersionInfo
|
||||
import docspell.restserver.{BuildInfo, Config}
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.BuildInfo
|
||||
|
||||
object InfoRoutes {
|
||||
|
@ -0,0 +1,142 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.{Ident, ItemState}
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.common.syntax.all._
|
||||
import docspell.restserver.conv.Conversions
|
||||
import org.log4s._
|
||||
|
||||
object ItemRoutes {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F]{}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ POST -> Root / "search" =>
|
||||
for {
|
||||
mask <- req.as[ItemSearch]
|
||||
_ <- logger.ftrace(s"Got search mask: $mask")
|
||||
query = Conversions.mkQuery(mask, user.account.collective)
|
||||
_ <- logger.ftrace(s"Running query: $query")
|
||||
items <- backend.item.findItems(query, 100)
|
||||
resp <- Ok(Conversions.mkItemList(items))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / Ident(id) =>
|
||||
for {
|
||||
item <- backend.item.findItem(id, user.account.collective)
|
||||
result = item.map(Conversions.mkItemDetail)
|
||||
resp <- result.map(r => Ok(r)).getOrElse(NotFound(BasicResult(false, "Not found.")))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "confirm" =>
|
||||
for {
|
||||
res <- backend.item.setState(id, ItemState.Confirmed, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Item data confirmed"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "unconfirm" =>
|
||||
for {
|
||||
res <- backend.item.setState(id, ItemState.Created, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Item back to created."))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "tags" =>
|
||||
for {
|
||||
tags <- req.as[ReferenceList].map(_.items)
|
||||
res <- backend.item.setTags(id, tags.map(_.id), user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Tags updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "direction" =>
|
||||
for {
|
||||
dir <- req.as[DirectionValue]
|
||||
res <- backend.item.setDirection(id, dir.direction, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Direction updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "corrOrg" =>
|
||||
for {
|
||||
idref <- req.as[OptionalId]
|
||||
res <- backend.item.setCorrOrg(id, idref.id, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Correspondent organization updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "corrPerson" =>
|
||||
for {
|
||||
idref <- req.as[OptionalId]
|
||||
res <- backend.item.setCorrPerson(id, idref.id, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Correspondent person updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "concPerson" =>
|
||||
for {
|
||||
idref <- req.as[OptionalId]
|
||||
res <- backend.item.setConcPerson(id, idref.id, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Concerned person updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "concEquipment" =>
|
||||
for {
|
||||
idref <- req.as[OptionalId]
|
||||
res <- backend.item.setConcEquip(id, idref.id, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "notes" =>
|
||||
for {
|
||||
text <- req.as[OptionalText]
|
||||
res <- backend.item.setNotes(id, text.text, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "name" =>
|
||||
for {
|
||||
text <- req.as[OptionalText]
|
||||
res <- backend.item.setName(id, text.text.getOrElse(""), user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Concerned equipment updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "duedate" =>
|
||||
for {
|
||||
date <- req.as[OptionalDate]
|
||||
_ <- logger.fdebug(s"Setting item due date to ${date.date}")
|
||||
res <- backend.item.setItemDueDate(id, date.date, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Item due date updated"))
|
||||
} yield resp
|
||||
|
||||
case req@POST -> Root / Ident(id) / "date" =>
|
||||
for {
|
||||
date <- req.as[OptionalDate]
|
||||
_ <- logger.fdebug(s"Setting item date to ${date.date}")
|
||||
res <- backend.item.setItemDate(id, date.date, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(res, "Item date updated"))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root / Ident(id) / "proposals" =>
|
||||
for {
|
||||
ml <- backend.item.getProposals(id, user.account.collective)
|
||||
ip = Conversions.mkItemProposals(ml)
|
||||
resp <- Ok(ip)
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
n <- backend.item.delete(id, user.account.collective)
|
||||
res = BasicResult(n > 0, if (n > 0) "Item deleted" else "Item deletion failed.")
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.Ident
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object JobQueueRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root / "state" =>
|
||||
for {
|
||||
js <- backend.job.queueState(user.account.collective, 200)
|
||||
res = Conversions.mkJobQueueState(js)
|
||||
resp <- Ok(res)
|
||||
} yield resp
|
||||
|
||||
case POST -> Root / Ident(id) / "cancel" =>
|
||||
for {
|
||||
result <- backend.job.cancelJob(id, user.account.collective)
|
||||
resp <- Ok(Conversions.basicResult(result))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.auth._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver._
|
||||
import docspell.restserver.auth._
|
||||
import org.http4s._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object LoginRoutes {
|
||||
|
||||
def login[F[_]: Effect](S: Login[F], cfg: Config): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of[F] {
|
||||
case req@POST -> Root / "login" =>
|
||||
for {
|
||||
up <- req.as[UserPass]
|
||||
res <- S.loginUserPass(cfg.auth)(Login.UserPass(up.account, up.password))
|
||||
resp <- makeResponse(dsl, cfg, res, up.account)
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def session[F[_]: Effect](S: Login[F], cfg: Config): HttpRoutes[F] = {
|
||||
val dsl: Http4sDsl[F] = new Http4sDsl[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of[F] {
|
||||
case req @ POST -> Root / "session" =>
|
||||
Authenticate.authenticateRequest(S.loginSession(cfg.auth))(req).
|
||||
flatMap(res => makeResponse(dsl, cfg, res, ""))
|
||||
|
||||
case POST -> Root / "logout" =>
|
||||
Ok().map(_.addCookie(ResponseCookie(CookieData.cookieName, "", maxAge = Some(-1))))
|
||||
}
|
||||
}
|
||||
|
||||
def makeResponse[F[_]: Effect](dsl: Http4sDsl[F], cfg: Config, res: Login.Result, account: String): F[Response[F]] = {
|
||||
import dsl._
|
||||
res match {
|
||||
case Login.Result.Ok(token) =>
|
||||
for {
|
||||
cd <- AuthToken.user(token.account, cfg.auth.serverSecret).map(CookieData.apply)
|
||||
resp <- Ok(AuthResult(token.account.collective.id, token.account.user.id, true, "Login successful", Some(cd.asString), cfg.auth.sessionValid.millis)).
|
||||
map(_.addCookie(cd.asCookie(cfg)))
|
||||
} yield resp
|
||||
case _ =>
|
||||
Ok(AuthResult("", account, false, "Login failed.", None, 0L))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import ParamDecoder._
|
||||
import docspell.common.Ident
|
||||
|
||||
object OrganizationRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F]{}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root :? FullQueryParamMatcher(full) =>
|
||||
if (full.getOrElse(false)) {
|
||||
for {
|
||||
data <- backend.organization.findAllOrg(user.account)
|
||||
resp <- Ok(OrganizationList(data.map(mkOrg).toList))
|
||||
} yield resp
|
||||
} else {
|
||||
for {
|
||||
data <- backend.organization.findAllOrgRefs(user.account)
|
||||
resp <- Ok(ReferenceList(data.map(mkIdName).toList))
|
||||
} yield resp
|
||||
}
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[Organization]
|
||||
newOrg <- newOrg(data, user.account.collective)
|
||||
added <- backend.organization.addOrg(newOrg)
|
||||
resp <- Ok(basicResult(added, "New organization saved."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
data <- req.as[Organization]
|
||||
upOrg <- changeOrg(data, user.account.collective)
|
||||
update <- backend.organization.updateOrg(upOrg)
|
||||
resp <- Ok(basicResult(update, "Organization updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
delOrg <- backend.organization.deleteOrg(id, user.account.collective)
|
||||
resp <- Ok(basicResult(delOrg, "Organization deleted."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import org.http4s.QueryParamDecoder
|
||||
import org.http4s.dsl.impl.OptionalQueryParamDecoderMatcher
|
||||
|
||||
object ParamDecoder {
|
||||
|
||||
implicit val booleanDecoder: QueryParamDecoder[Boolean] =
|
||||
QueryParamDecoder.fromUnsafeCast(qp => Option(qp.value).exists(_ equalsIgnoreCase "true"))("Boolean")
|
||||
|
||||
object FullQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Boolean]("full")
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.common.Ident
|
||||
import docspell.common.syntax.all._
|
||||
import ParamDecoder._
|
||||
import org.log4s._
|
||||
|
||||
object PersonRoutes {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F]{}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root :? FullQueryParamMatcher(full) =>
|
||||
if (full.getOrElse(false)) {
|
||||
for {
|
||||
data <- backend.organization.findAllPerson(user.account)
|
||||
resp <- Ok(PersonList(data.map(mkPerson).toList))
|
||||
} yield resp
|
||||
} else {
|
||||
for {
|
||||
data <- backend.organization.findAllPersonRefs(user.account)
|
||||
resp <- Ok(ReferenceList(data.map(mkIdName).toList))
|
||||
} yield resp
|
||||
}
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[Person]
|
||||
newPer <- newPerson(data, user.account.collective)
|
||||
added <- backend.organization.addPerson(newPer)
|
||||
resp <- Ok(basicResult(added, "New person saved."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
data <- req.as[Person]
|
||||
upPer <- changePerson(data, user.account.collective)
|
||||
update <- backend.organization.updatePerson(upPer)
|
||||
resp <- Ok(basicResult(update, "Person updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
_ <- logger.fdebug(s"Deleting person ${id.id}")
|
||||
delOrg <- backend.organization.deletePerson(id, user.account.collective)
|
||||
resp <- Ok(basicResult(delOrg, "Person deleted."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.ops.OCollective.RegisterData
|
||||
import docspell.backend.signup.{NewInviteResult, SignupResult}
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.log4s._
|
||||
|
||||
object RegisterRoutes {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ POST -> Root / "register" =>
|
||||
for {
|
||||
data <- req.as[Registration]
|
||||
res <- backend.signup.register(cfg.backend.signup)(convert(data))
|
||||
resp <- Ok(convert(res))
|
||||
} yield resp
|
||||
|
||||
case req@ POST -> Root / "newinvite" =>
|
||||
for {
|
||||
data <- req.as[GenInvite]
|
||||
res <- backend.signup.newInvite(cfg.backend.signup)(data.password)
|
||||
resp <- Ok(convert(res))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
def convert(r: NewInviteResult): InviteResult = r match {
|
||||
case NewInviteResult.Success(id) =>
|
||||
InviteResult(true, "New invitation created.", Some(id))
|
||||
case NewInviteResult.InvitationDisabled =>
|
||||
InviteResult(false, "Signing up is not enabled for invitations.", None)
|
||||
case NewInviteResult.PasswordMismatch =>
|
||||
InviteResult(false, "Password is invalid.", None)
|
||||
}
|
||||
|
||||
|
||||
def convert(r: SignupResult): BasicResult = r match {
|
||||
case SignupResult.CollectiveExists =>
|
||||
BasicResult(false, "A collective with this name already exists.")
|
||||
case SignupResult.InvalidInvitationKey =>
|
||||
BasicResult(false, "Invalid invitation key.")
|
||||
case SignupResult.SignupClosed =>
|
||||
BasicResult(false, "Sorry, registration is closed.")
|
||||
case SignupResult.Failure(ex) =>
|
||||
logger.error(ex)("Error signing up")
|
||||
BasicResult(false, s"Internal error: ${ex.getMessage}")
|
||||
case SignupResult.Success =>
|
||||
BasicResult(true, "Signup successful")
|
||||
}
|
||||
|
||||
|
||||
def convert(r: Registration): RegisterData =
|
||||
RegisterData(r.collectiveName, r.login, r.password, r.invite)
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.Ident
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object SourceRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root =>
|
||||
for {
|
||||
all <- backend.source.findAll(user.account)
|
||||
res <- Ok(SourceList(all.map(mkSource).toList))
|
||||
} yield res
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[Source]
|
||||
src <- newSource(data, user.account.collective)
|
||||
added <- backend.source.add(src)
|
||||
resp <- Ok(basicResult(added, "Source added."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
data <- req.as[Source]
|
||||
src = changeSource(data, user.account.collective)
|
||||
updated <- backend.source.update(src)
|
||||
resp <- Ok(basicResult(updated, "Source updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
del <- backend.source.delete(id, user.account.collective)
|
||||
resp <- Ok(basicResult(del, "Source deleted."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.Ident
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object TagRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case GET -> Root =>
|
||||
for {
|
||||
all <- backend.tag.findAll(user.account)
|
||||
resp <- Ok(TagList(all.size, all.map(mkTag).toList))
|
||||
} yield resp
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[Tag]
|
||||
tag <- newTag(data, user.account.collective)
|
||||
res <- backend.tag.add(tag)
|
||||
resp <- Ok(basicResult(res, "Tag successfully created."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
data <- req.as[Tag]
|
||||
tag = changeTag(data, user.account.collective)
|
||||
res <- backend.tag.update(tag)
|
||||
resp <- Ok(basicResult(res, "Tag successfully updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
del <- backend.tag.delete(id, user.account.collective)
|
||||
resp <- Ok(basicResult(del, "Tag successfully deleted."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.{Ident, Priority}
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.EntityDecoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.http4s.multipart.Multipart
|
||||
import org.log4s._
|
||||
|
||||
object UploadRoutes {
|
||||
private[this] val logger = getLogger
|
||||
|
||||
def secured[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ POST -> Root / "item" =>
|
||||
for {
|
||||
multipart <- req.as[Multipart[F]]
|
||||
updata <- readMultipart(multipart, logger, Priority.High, cfg.backend.files.validMimeTypes)
|
||||
result <- backend.upload.submit(updata, user.account)
|
||||
res <- Ok(basicResult(result))
|
||||
} yield res
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
def open[F[_]: Effect](backend: BackendApp[F], cfg: Config): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ POST -> Root / "item" / Ident(id)=>
|
||||
for {
|
||||
multipart <- req.as[Multipart[F]]
|
||||
updata <- readMultipart(multipart, logger, Priority.Low, cfg.backend.files.validMimeTypes)
|
||||
result <- backend.upload.submit(updata, id)
|
||||
res <- Ok(basicResult(result))
|
||||
} yield res
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package docspell.restserver.routes
|
||||
|
||||
import cats.effect._
|
||||
import cats.implicits._
|
||||
import docspell.backend.BackendApp
|
||||
import docspell.backend.auth.AuthToken
|
||||
import docspell.common.Ident
|
||||
import docspell.restapi.model._
|
||||
import docspell.restserver.Config
|
||||
import docspell.restserver.conv.Conversions._
|
||||
import docspell.restserver.http4s.ResponseGenerator
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.circe.CirceEntityDecoder._
|
||||
import org.http4s.circe.CirceEntityEncoder._
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
|
||||
object UserRoutes {
|
||||
|
||||
def apply[F[_]: Effect](backend: BackendApp[F], cfg: Config, user: AuthToken): HttpRoutes[F] = {
|
||||
val dsl = new Http4sDsl[F] with ResponseGenerator[F] {}
|
||||
import dsl._
|
||||
|
||||
HttpRoutes.of {
|
||||
case req @ POST -> Root / "changePassword" =>
|
||||
for {
|
||||
data <- req.as[PasswordChange]
|
||||
res <- backend.collective.changePassword(user.account, data.currentPassword, data.newPassword)
|
||||
resp <- Ok(basicResult(res))
|
||||
} yield resp
|
||||
|
||||
case GET -> Root =>
|
||||
for {
|
||||
all <- backend.collective.listUser(user.account.collective)
|
||||
res <- Ok(UserList(all.map(mkUser).toList))
|
||||
} yield res
|
||||
|
||||
case req @ POST -> Root =>
|
||||
for {
|
||||
data <- req.as[User]
|
||||
nuser <- newUser(data, user.account.collective)
|
||||
added <- backend.collective.add(nuser)
|
||||
resp <- Ok(basicResult(added, "User created."))
|
||||
} yield resp
|
||||
|
||||
case req @ PUT -> Root =>
|
||||
for {
|
||||
data <- req.as[User]
|
||||
nuser = changeUser(data, user.account.collective)
|
||||
update <- backend.collective.update(nuser)
|
||||
resp <- Ok(basicResult(update, "User updated."))
|
||||
} yield resp
|
||||
|
||||
case DELETE -> Root / Ident(id) =>
|
||||
for {
|
||||
ar <- backend.collective.deleteUser(id, user.account.collective)
|
||||
resp <- Ok(basicResult(ar, "User deleted."))
|
||||
} yield resp
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package docspell.restserver.webapp
|
||||
|
||||
import _root_.io.circe._
|
||||
import _root_.io.circe.generic.semiauto._
|
||||
import docspell.common.LenientUri
|
||||
import docspell.restserver.Config
|
||||
import docspell.backend.signup.{Config => SignupConfig}
|
||||
import yamusca.imports._
|
||||
import yamusca.implicits._
|
||||
|
||||
case class Flags(appName: String, baseUrl: LenientUri, signupMode: SignupConfig.Mode)
|
||||
|
||||
object Flags {
|
||||
def apply(cfg: Config): Flags =
|
||||
Flags(cfg.appName, cfg.baseUrl, cfg.backend.signup.mode)
|
||||
|
||||
implicit val jsonEncoder: Encoder[Flags] =
|
||||
deriveEncoder[Flags]
|
||||
|
||||
implicit def yamuscaSignupModeConverter: ValueConverter[SignupConfig.Mode] =
|
||||
ValueConverter.of(m => Value.fromString(m.name))
|
||||
implicit def yamuscaUriConverter: ValueConverter[LenientUri] =
|
||||
ValueConverter.of(uri => Value.fromString(uri.asString))
|
||||
implicit def yamuscaValueConverter: ValueConverter[Flags] =
|
||||
ValueConverter.deriveConverter[Flags]
|
||||
}
|
@ -7,9 +7,7 @@ import org.http4s._
|
||||
import org.http4s.headers._
|
||||
import org.http4s.HttpRoutes
|
||||
import org.http4s.dsl.Http4sDsl
|
||||
import org.slf4j._
|
||||
import _root_.io.circe._
|
||||
import _root_.io.circe.generic.semiauto._
|
||||
import org.log4s._
|
||||
import _root_.io.circe.syntax._
|
||||
import yamusca.imports._
|
||||
import yamusca.implicits._
|
||||
@ -19,7 +17,7 @@ import java.util.concurrent.atomic.AtomicReference
|
||||
import docspell.restserver.{BuildInfo, Config}
|
||||
|
||||
object TemplateRoutes {
|
||||
private[this] val logger = LoggerFactory.getLogger(getClass)
|
||||
private[this] val logger = getLogger
|
||||
|
||||
val `text/html` = new MediaType("text", "html")
|
||||
|
||||
@ -78,27 +76,16 @@ object TemplateRoutes {
|
||||
object DocData {
|
||||
|
||||
def apply(cfg: Config): DocData =
|
||||
DocData("/app/assets" + Webjars.swaggerui, s"/app/assets/${BuildInfo.name}/${BuildInfo.version}/openapi.yml")
|
||||
DocData("/app/assets" + Webjars.swaggerui, s"/app/assets/${BuildInfo.name}/${BuildInfo.version}/docspell-openapi.yml")
|
||||
|
||||
implicit def yamuscaValueConverter: ValueConverter[DocData] =
|
||||
ValueConverter.deriveConverter[DocData]
|
||||
}
|
||||
|
||||
case class Flags(appName: String, baseUrl: String)
|
||||
|
||||
object Flags {
|
||||
def apply(cfg: Config): Flags =
|
||||
Flags(cfg.appName, cfg.baseUrl)
|
||||
|
||||
implicit val jsonEncoder: Encoder[Flags] =
|
||||
deriveEncoder[Flags]
|
||||
implicit def yamuscaValueConverter: ValueConverter[Flags] =
|
||||
ValueConverter.deriveConverter[Flags]
|
||||
}
|
||||
|
||||
case class IndexData(flags: Flags
|
||||
, cssUrls: Seq[String]
|
||||
, jsUrls: Seq[String]
|
||||
, faviconBase: String
|
||||
, appExtraJs: String
|
||||
, flagsJson: String)
|
||||
|
||||
@ -115,9 +102,9 @@ object TemplateRoutes {
|
||||
"/app/assets" + Webjars.semanticui + "/semantic.min.js",
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell-app.js"
|
||||
)
|
||||
,
|
||||
s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js"
|
||||
, Flags(cfg).asJson.spaces2 )
|
||||
, s"/app/assets/docspell-webapp/${BuildInfo.version}/favicon"
|
||||
, s"/app/assets/docspell-webapp/${BuildInfo.version}/docspell.js"
|
||||
, Flags(cfg).asJson.spaces2 )
|
||||
|
||||
implicit def yamuscaValueConverter: ValueConverter[IndexData] =
|
||||
ValueConverter.deriveConverter[IndexData]
|
||||
|
@ -22,7 +22,7 @@ object WebjarRoutes {
|
||||
}
|
||||
|
||||
def assetFilter(asset: WebjarAsset): Boolean =
|
||||
List(".js", ".css", ".html", ".jpg", ".png", ".eot", ".woff", ".woff2", ".svg", ".otf", ".ttf", ".yml").
|
||||
List(".js", ".css", ".html", ".json", ".jpg", ".png", ".eot", ".woff", ".woff2", ".svg", ".otf", ".ttf", ".yml", ".xml").
|
||||
exists(e => asset.asset.endsWith(e))
|
||||
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Swagger UI</title>
|
||||
<title>Docspell Swagger UI</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{swaggerRoot}}/swagger-ui.css" >
|
||||
<link rel="icon" type="image/png" href="{{swaggerRoot}}/favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="{{swaggerRoot}}/favicon-16x16.png" sizes="16x16" />
|
||||
|
@ -1,8 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="{{{faviconBase}}}/apple-icon-57x57.png">
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="{{{faviconBase}}}/apple-icon-60x60.png">
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="{{{faviconBase}}}/apple-icon-72x72.png">
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="{{{faviconBase}}}/apple-icon-76x76.png">
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="{{{faviconBase}}}/apple-icon-114x114.png">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="{{{faviconBase}}}/apple-icon-120x120.png">
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="{{{faviconBase}}}/apple-icon-144x144.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="{{{faviconBase}}}/apple-icon-152x152.png">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="{{{faviconBase}}}/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="{{{faviconBase}}}/android-icon-192x192.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{{faviconBase}}}/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="{{{faviconBase}}}/favicon-96x96.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="{{{faviconBase}}}/favicon-16x16.png">
|
||||
<link rel="manifest" href="{{{faviconBase}}}/manifest.json">
|
||||
<meta name="msapplication-TileColor" content="#ffffff">
|
||||
<meta name="msapplication-TileImage" content="{{{faviconBase}}}/ms-icon-144x144.png">
|
||||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<title>{{ flags.appName }}</title>
|
||||
{{# cssUrls }}
|
||||
<link rel="stylesheet" href="{{.}}"/>
|
||||
|
Reference in New Issue
Block a user