diff --git a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala index a9f6274c..d62dddd1 100644 --- a/modules/backend/src/main/scala/docspell/backend/BackendApp.scala +++ b/modules/backend/src/main/scala/docspell/backend/BackendApp.scala @@ -35,6 +35,7 @@ trait BackendApp[F[_]] { def mail: OMail[F] def joex: OJoex[F] def userTask: OUserTask[F] + def space: OSpace[F] } object BackendApp { @@ -67,6 +68,7 @@ object BackendApp { JavaMailEmil(blocker, Settings.defaultSettings.copy(debug = cfg.mailDebug)) mailImpl <- OMail(store, javaEmil) userTaskImpl <- OUserTask(utStore, queue, joexImpl) + spaceImpl <- OSpace(store) } yield new BackendApp[F] { val login: Login[F] = loginImpl val signup: OSignup[F] = signupImpl @@ -84,6 +86,7 @@ object BackendApp { val mail = mailImpl val joex = joexImpl val userTask = userTaskImpl + val space = spaceImpl } def apply[F[_]: ConcurrentEffect: ContextShift]( diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OSpace.scala b/modules/backend/src/main/scala/docspell/backend/ops/OSpace.scala new file mode 100644 index 00000000..2962c55e --- /dev/null +++ b/modules/backend/src/main/scala/docspell/backend/ops/OSpace.scala @@ -0,0 +1,84 @@ +package docspell.backend.ops + +import cats.effect._ + +import docspell.common._ +import docspell.store.{AddResult, Store} +import docspell.store.records.RSpace + +trait OSpace[F[_]] { + + def findAll(account: AccountId, nameQuery: Option[String]): F[Vector[OSpace.SpaceItem]] + + def findById(id: Ident, collective: Ident): F[Option[OSpace.SpaceDetail]] + + def delete(id: Ident, collective: Ident): F[Int] + + def add(space: RSpace): F[AddResult] + + def changeName(space: Ident, account: AccountId, name: String): F[AddResult] + + def addMember( + space: Ident, + account: AccountId, + member: Ident + ): F[OSpace.MemberChangeResult] + def removeMember( + space: Ident, + account: AccountId, + member: Ident + ): F[OSpace.MemberChangeResult] +} + +object OSpace { + + sealed trait MemberChangeResult + object MemberChangeResult { + case object Success extends MemberChangeResult + case object NotFound extends MemberChangeResult + case object Forbidden extends MemberChangeResult + } + + final case class SpaceItem( + id: Ident, + name: String, + owner: IdRef, + created: Timestamp, + members: Int + ) + + final case class SpaceDetail( + id: Ident, + name: String, + owner: IdRef, + created: Timestamp, + members: List[IdRef] + ) + + def apply[F[_]: Effect](store: Store[F]): Resource[F, OSpace[F]] = + Resource.pure[F, OSpace[F]](new OSpace[F] { + println(s"$store") + def findAll( + account: AccountId, + nameQuery: Option[String] + ): F[Vector[OSpace.SpaceItem]] = ??? + + def findById(id: Ident, collective: Ident): F[Option[OSpace.SpaceDetail]] = ??? + def add(space: RSpace): F[AddResult] = ??? + def changeName(space: Ident, account: AccountId, name: String): F[AddResult] = ??? + def delete(id: Ident, collective: Ident): F[Int] = ??? + def addMember( + space: Ident, + account: AccountId, + member: Ident + ): F[MemberChangeResult] = + ??? + def removeMember( + space: Ident, + account: AccountId, + member: Ident + ): F[MemberChangeResult] = + ??? + + }) +} diff --git a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala index 71934336..d31becf3 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/RestServer.scala @@ -81,7 +81,8 @@ object RestServer { "usertask/notifydueitems" -> NotifyDueItemsRoutes(cfg, restApp.backend, token), "usertask/scanmailbox" -> ScanMailboxRoutes(restApp.backend, token), "calevent/check" -> CalEventCheckRoutes(), - "fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token) + "fts" -> FullTextIndexRoutes.secured(cfg, restApp.backend, token), + "space" -> SpaceRoutes(restApp.backend, token) ) def openRoutes[F[_]: Effect](cfg: Config, restApp: RestApp[F]): HttpRoutes[F] = diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/SpaceRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/SpaceRoutes.scala new file mode 100644 index 00000000..1fb9e7a3 --- /dev/null +++ b/modules/restserver/src/main/scala/docspell/restserver/routes/SpaceRoutes.scala @@ -0,0 +1,108 @@ +package docspell.restserver.routes + +import cats.data.OptionT +import cats.effect._ +import cats.implicits._ + +import docspell.backend.BackendApp +import docspell.backend.auth.AuthToken +import docspell.backend.ops.OSpace +import docspell.common._ +import docspell.store.records.RSpace +import docspell.restapi.model._ +import docspell.restserver.conv.Conversions +import docspell.restserver.http4s._ + +import org.http4s.HttpRoutes +import org.http4s.circe.CirceEntityDecoder._ +import org.http4s.circe.CirceEntityEncoder._ +import org.http4s.dsl.Http4sDsl + +object SpaceRoutes { + + def apply[F[_]: Effect](backend: BackendApp[F], user: AuthToken): HttpRoutes[F] = { + val dsl = new Http4sDsl[F] with ResponseGenerator[F] {} + import dsl._ + + HttpRoutes.of { + case GET -> Root :? QueryParam.QueryOpt(q) => + for { + all <- backend.space.findAll(user.account, q.map(_.q)) + resp <- Ok(SpaceList(all.map(mkSpace).toList)) + } yield resp + + case req @ POST -> Root => + for { + data <- req.as[NewSpace] + tag <- newSpace(data, user.account) + res <- backend.space.add(tag) + resp <- Ok(Conversions.basicResult(res, "Space successfully created.")) + } yield resp + + case GET -> Root / Ident(id) => + (for { + space <- OptionT(backend.space.findById(id, user.account.collective)) + resp <- OptionT.liftF(Ok(mkSpaceDetail(space))) + } yield resp).getOrElseF(NotFound()) + + case req @ PUT -> Root / Ident(id) => + for { + data <- req.as[NewSpace] + res <- backend.space.changeName(id, user.account, data.name) + resp <- Ok(Conversions.basicResult(res, "Space successfully updated.")) + } yield resp + + case DELETE -> Root / Ident(id) => + for { + del <- backend.space.delete(id, user.account.collective) + resp <- Ok( + if (del > 0) BasicResult(true, "Successfully deleted space") + else BasicResult(false, "Could not delete space") + ) + } yield resp + + case PUT -> Root / Ident(id) / "member" / Ident(userId) => + for { + res <- backend.space.addMember(id, user.account, userId) + resp <- Ok(mkMemberResult(res)) + } yield resp + + case DELETE -> Root / Ident(id) / "member" / Ident(userId) => + for { + res <- backend.space.removeMember(id, user.account, userId) + resp <- Ok(mkMemberResult(res)) + } yield resp + } + } + + private def newSpace[F[_]: Sync](ns: NewSpace, account: AccountId): F[RSpace] = + RSpace.newSpace(ns.name, account) + + private def mkSpace(item: OSpace.SpaceItem): SpaceItem = + SpaceItem( + item.id, + item.name, + Conversions.mkIdName(item.owner), + item.created, + item.members + ) + + private def mkSpaceDetail(item: OSpace.SpaceDetail): SpaceDetail = + SpaceDetail( + item.id, + item.name, + Conversions.mkIdName(item.owner), + item.created, + item.members.map(Conversions.mkIdName) + ) + + private def mkMemberResult(r: OSpace.MemberChangeResult): BasicResult = + r match { + case OSpace.MemberChangeResult.Success => + BasicResult(true, "Successfully changed space") + case OSpace.MemberChangeResult.NotFound => + BasicResult(false, "Space or user not found") + case OSpace.MemberChangeResult.Forbidden => + BasicResult(false, "Not allowed to edit space") + } +}