Skip to content

Commit

Permalink
Merge branch 'master' into update/sbt-bloop-1.5.0
Browse files Browse the repository at this point in the history
  • Loading branch information
vder authored Nov 30, 2024
2 parents 7e15756 + 5a50660 commit 4d30a65
Show file tree
Hide file tree
Showing 96 changed files with 1,518 additions and 978 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ target/
.history/
project/target/
.bsp/sbt.json
**/metals.sbt
*.worksheet.sc
metals.sbt
*.worksheet.sc
.idea
*/metals.sbt
**/metals.sbt
6 changes: 4 additions & 2 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
version = "3.4.0"
runner.dialect = scala213
version = 3.5.2
runner.dialect = scala213
align.preset = more
maxColumn = 120
Binary file added Certificate-Piotr Fałdrowicz.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,57 @@
# Taskforce App

## Requirements

The task is to write a REST API for the time logging system.
### Functional requirements:
1. The system can be used by logged in users. Users are assumed to be system authenticated with a JWT that includes a user ID in UUID format. No registration or login mechanism is required, a only authentication.
2. The system allows each user to create a new project. Project consists of the:
* Unique identifier of the project in text form, given by user.
* Timestamp of project creation for accounting purposes.
3. The system enables the author of the project to change the project ID.
4. The system allows each user to log the time spent on project (hereinafter referred to as a task). The task consists of:
* Project start timestamp.
* The duration of the task.
* Optional: volume expressed as a natural number.
* Optional: comment.
5. The given user cannot save two tasks which overlap in time according to the attributes of these tasks.
6. The system allows the author of the task to delete it, but it must be a "soft delete" timestamped with removal.

7. The system allows the author of the task to change all of the above-mentioned task attributes while validating from point 5. This should be done as deleting a task (as in point 6) and creating a new one in its place.
8. The system allows the author of the project to delete it, but it must be a "soft delete" timestamped with removal. All existing deleted tasks the project is considered deleted with the same timestamp.
9. The system allows you to return information about a given project together with the associated ones tasks and the total duration of the project.
10. The system allows you to list projects together with the associated tasks from using conjugation of the following filters (each is optional):
* list of identifiers,
* from - creation timestamp,
* to - creation timestamp,
* removed / not removed,

and sort using at most one of the following criteria:
* Creation timestamp - descending / ascending,
* update timestamp - the latest added task (in case of an empty project - the creation date should be taken) - descending / ascending,

and simple pagination based on size and page number.

11. The system allows you to display the following statistics (they only count for the statistics unremoved projects and tasks):
* Total number of tasks.
* Average duration of the task.
* Average volume of the job (for jobs with the specified volume).
* Volume weighted average task duration (for tasks from given volume).

Access to statistics should be parameterized with a list of identifiers users whose tasks should be included and the from-to dates.

## Tech stack
* scala 2.13
* cats & cats effect 3
* pure config
* monix new types
* refined
* postgress
* flyway
* doobie & quill
* munit & scalacheck
* http4s & tapir

## Usage

```shell
Expand All @@ -26,4 +78,4 @@
* "http://{host}:{port}/api/v1/filters" /GET /POST
* "http://{host}:{port}/api/v1/filters/{filterId}" /GET
* "http://{host}:{port}/api/v1/filters/{filterId}/data" /GET
* "http://{host}:{port}/api/v1/stats" /GET
* "http://{host}:{port}/api/v1/stats" /GET
160 changes: 122 additions & 38 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,76 @@ import Dependencies.Libraries._
import com.typesafe.sbt.packager.docker.Cmd

ThisBuild / githubWorkflowPublishTargetBranches := Seq()
ThisBuild / organization := "com.pfl"
ThisBuild / organizationName := "pfl"
ThisBuild / scalaVersion := "2.13.8"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / versionScheme := Some("early-semver")
ThisBuild / organization := "com.pfl"
ThisBuild / organizationName := "pfl"
ThisBuild / scalaVersion := "2.13.8"
ThisBuild / version := "0.1.0-SNAPSHOT"

IntegrationTest / parallelExecution := false
IntegrationTest / parallelExecution in Global := false

lazy val root = (project in file("."))
.enablePlugins(JavaAppPackaging)
.enablePlugins(DockerPlugin)
.enablePlugins(FlywayPlugin)
.enablePlugins(AshScriptPlugin)
.enablePlugins(RevolverPlugin)
.configs(IntegrationTest.extend(Test))
.settings(
name := "taskforce",
flywayUrl := "jdbc:postgresql://localhost:54340/task",
flywayUser := "vder",
name := "taskforce",
flywayUrl := "jdbc:postgresql://localhost:54340/task",
flywayUser := "vder",
flywayPassword := "password",
Defaults.itSettings,
publish := {},
publish / skip := true,
publish := {},
publish / skip := true,
Docker / packageName := "taskforce",
dockerCommands := dockerCommands.value.flatMap {
case cmd @ Cmd("FROM", _) =>
List(cmd, Cmd("RUN", "apk update && apk add bash"))
case other => List(other)
case cmd @ Cmd("FROM", _) => List(cmd, Cmd("RUN", "apk update && apk add bash"))
case other => List(other)
},
dockerExposedPorts ++= Seq(9090),
dockerBaseImage := "openjdk:8-jre-alpine",
dockerBaseImage := "openjdk:8-jre-alpine",
dockerUpdateLatest := true,
semanticdbEnabled := true, // enable SemanticDB
semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x
semanticdbEnabled := true, // enable SemanticDB
semanticdbVersion := scalafixSemanticdb.revision, // only required for Scala 2.x
addCompilerPlugin(kindProjector),
addCompilerPlugin(betterMonadicFor),
scalacOptions ++= Seq(
"-deprecation",
"-encoding",
"UTF-8",
"-language:higherKinds",
"-language:postfixOps",
"-feature",
"-Xlint:unused",
"-Ymacro-annotations"
)
)
.dependsOn(
common % "test->test",
filters % "compile->compile;test->test",
stats % "compile->compile;test->test"
)
.aggregate(
filters,
projects,
stats,
tasks
)

lazy val common = (project in file("modules/common"))
.disablePlugins(RevolverPlugin)
.configs(IntegrationTest extend Test)
.settings(
Defaults.itSettings,
libraryDependencies ++= Seq(
catsEffect,
cats,
circe,
circeDerivation,
circeExtras,
circeFs2,
circeParser,
circeRefined,
doobie,
doobieHikari,
doobiePostgres,
doobieRefined,
doobieQuill,
flyway,
http4sCirce,
http4sClient,
http4sDsl,
http4sServer,
jwtCirce,
log4cats,
logback,
monixNewType,
monixNewTypeCirce,
Expand All @@ -63,18 +81,84 @@ lazy val root = (project in file("."))
pureConfig,
pureConfigCE,
pureConfigRefined,
refined,
refinedCats,
// quill,
scalaCheckEffect,
scalaCheckEffectMunit,
simulacrum,
slf4j,
log4cats
slf4j
).map(_.exclude("org.slf4j", "*")),
addCompilerPlugin(kindProjector),
addCompilerPlugin(betterMonadicFor),
scalacOptions ++= Seq(
scalacOptions ++= Seq("-Ymacro-annotations")
)

lazy val authentication = (project in file("modules/auth"))
.disablePlugins(RevolverPlugin)
.settings(
libraryDependencies ++= Seq(
circeParser,
doobiePostgres,
http4sServer,
jwtCirce
).map(_.exclude("org.slf4j", "*")),
addCompilerPlugin(kindProjector),
scalacOptions ++= Seq("-Ymacro-annotations")
)
.dependsOn(common)

lazy val projects = (project in file("modules/projects"))
.disablePlugins(RevolverPlugin)
.configs((IntegrationTest extend Test))
.settings(Defaults.itSettings,sharedSettings)
.dependsOn(
authentication % "compile->compile;test->test",
common % "test->test;it->it;test->it"
)

lazy val tasks = (project in file("modules/tasks"))
.disablePlugins(RevolverPlugin)
.configs((IntegrationTest extend Test))
.settings(Defaults.itSettings,sharedSettings)
.dependsOn(
authentication % "compile->compile;test->test",
common % "test->test;it->it;compile->compile;test->it"
)

lazy val filters = (project in file("modules/filters"))
.disablePlugins(RevolverPlugin)
.configs((IntegrationTest extend Test))
.settings(
Defaults.itSettings,
addCompilerPlugin(kindProjector),
scalacOptions ++= Seq("-Ymacro-annotations")
)
.dependsOn(
tasks % "compile->compile;test->test",
projects % "compile->compile;test->test"
)

lazy val stats = (project in file("modules/stats"))
.disablePlugins(RevolverPlugin)
.settings(Defaults.itSettings,sharedSettings)
.dependsOn(
authentication % "compile->compile;test->test",
common % "test->test"
)

lazy val sharedSettings = Seq(
libraryDependencies ++= Seq(
circeDerivation,
circeExtras,
circeFs2,
circeParser,
circeRefined,
doobieHikari,
doobiePostgres,
doobieRefined,
http4sClient,
refined,
refinedCats
).map(_.exclude("org.slf4j", "*")),
addCompilerPlugin(kindProjector),
scalacOptions ++= Seq(
"-deprecation",
"-encoding",
"UTF-8",
Expand All @@ -84,4 +168,4 @@ lazy val root = (project in file("."))
"-Xlint:unused",
"-Ymacro-annotations"
)
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ import org.http4s.server.AuthMiddleware
import org.http4s.{AuthScheme, AuthedRoutes, Credentials, Request}
import pdi.jwt.{JwtAlgorithm, JwtCirce}
import cats.MonadThrow
import taskforce.authentication.instances.Circe

object TaskForceAuthMiddleware {
object TaskForceAuthMiddleware extends Circe {

def apply[F[_]: MonadThrow](
def apply[F[_]: MonadThrow](
userRepo: UserRepository[F],
secret: String
): AuthMiddleware[F, UserId] = {
Expand All @@ -28,11 +29,11 @@ object TaskForceAuthMiddleware {
encodedString <-
request.headers
.get[Authorization]
.fold(s"missing Authorization header: ${request}".asLeft[String]){
.fold(s"missing Authorization header: ${request}".asLeft[String]) {
case Authorization(Credentials.Token(AuthScheme.Bearer, t)) =>
t.asRight[String]
case header => s"invalid header type $header".asLeft[String]
}
}
jwtClaim <-
JwtCirce
.decode(encodedString, secret, Seq(JwtAlgorithm.HS256))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package taskforce.authentication

final case class User(id: UserId)
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
package taskforce.authentication

import cats.effect.Sync

import doobie.implicits._
import doobie.postgres.implicits._
import doobie.util.transactor.Transactor
import cats.effect.kernel.MonadCancel
import taskforce.common.NewTypeDoobieMeta
import cats.effect.kernel.MonadCancelThrow


trait UserRepository[F[_]] {
def find(userId: UserId): F[Option[User]]
def create(user: User): F[Int]
}

final class LiveUserRepository[F[_]: MonadCancel[*[_], Throwable]](
xa: Transactor[F]
) extends UserRepository[F]
with NewTypeDoobieMeta {
object UserRepository {
def make[F[_] : MonadCancelThrow](xa: Transactor[F]) =
new UserRepository[F] with NewTypeDoobieMeta {

override def create(user: User): F[Int] =
sql.insert(user).update.run.transact(xa)
Expand All @@ -27,15 +27,11 @@ final class LiveUserRepository[F[_]: MonadCancel[*[_], Throwable]](
.option
.transact(xa)

object sql {
private object sql {
def insert(user: User) = sql"insert into users(id) values(${user.id})"
def find(userId: UserId) =
sql"select id from users where id =${userId.value}"
}

}

object LiveUserRepository {
def make[F[_]: Sync](xa: Transactor[F]) =
Sync[F].delay { new LiveUserRepository[F](xa) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package taskforce.authentication.instances

import io.circe.generic.semiauto._
import io.circe.{Decoder, Encoder}
import taskforce.authentication.User
import monix.newtypes.integrations.DerivedCirceCodec
trait Circe extends DerivedCirceCodec {

implicit val userDecoder: Decoder[User] =
deriveDecoder[User]
implicit val userEncoder: Encoder[User] =
deriveEncoder[User]

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package taskforce

import java.util.UUID
import monix.newtypes._

package object authentication {
type UserId = UserId.Type
object UserId extends NewtypeWrapped[UUID]
}
Loading

0 comments on commit 4d30a65

Please sign in to comment.