Skip to content

Commit

Permalink
Create HARNESS-WEB-TEMPLATE
Browse files Browse the repository at this point in the history
  • Loading branch information
Kalin-Rudnicki committed Sep 24, 2022
1 parent b710e86 commit a6c486e
Show file tree
Hide file tree
Showing 25 changed files with 890 additions and 3 deletions.
1 change: 0 additions & 1 deletion .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
name: CI
on:
pull_request:
push:
jobs:
test:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,6 @@ target/
project/metals.sbt
project/project/metals.sbt
project/project/project/metals.sbt

harness-web-app-template/res/js
harness-web-app-template/res/css
123 changes: 121 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

// =====| Shared Settings |=====

val Scala_3 = "3.1.2-RC3"
enablePlugins(GitVersioning)
git.gitTagToVersionNumber := { tag =>
if (tag.matches("^\\d+\\..*$")) Some(tag)
else None
}

val Scala_3 = "3.1.3"

val MyOrg = "io.github.kalin-rudnicki"

Expand Down Expand Up @@ -48,6 +54,7 @@ lazy val `harness-root` =
`harness-sql`,
`harness-web`.js,
`harness-web`.jvm,
`harness-web-app-template`,
)

lazy val `harness-test` =
Expand Down Expand Up @@ -114,7 +121,7 @@ lazy val `harness-sql` =
testSettings,
libraryDependencies ++= Seq(
"dev.zio" %%% "zio" % "2.0.0",
"dev.zio" %%% "zio-json" % "0.3.0-RC10",
"dev.zio" %%% "zio-json" % "0.3.0",
"org.typelevel" %%% "cats-core" % "2.8.0",
"org.typelevel" %% "shapeless3-deriving" % "3.0.1",
"org.postgresql" % "postgresql" % "42.5.0" % Test,
Expand All @@ -140,3 +147,115 @@ lazy val `harness-web` =
),
)
.dependsOn(`harness-zio` % "test->test;compile->compile")

// =====| |=====

lazy val `harness-web-app-template` =
project
.in(file("harness-web-app-template"))
.aggregate(
`harness-web-app-template--model`.jvm,
`harness-web-app-template--model`.js,
`harness-web-app-template--api`,
`harness-web-app-template--ui-web`,
)

lazy val `harness-web-app-template--model` =
crossProject(JSPlatform, JVMPlatform)
.in(file("harness-web-app-template/model"))
.settings(
name := "harness-web-app-template--model",
publish / skip := true,
miscSettings,
testSettings,
)
.dependsOn(`harness-web`)

lazy val `harness-web-app-template--db-model` =
project
.in(file("harness-web-app-template/db-model"))
.settings(
name := "harness-web-app-template--db-model",
publish / skip := true,
miscSettings,
testSettings,
libraryDependencies ++= Seq(
"org.postgresql" % "postgresql" % "42.5.0",
),
)
.dependsOn(`harness-sql`)

lazy val `harness-web-app-template--api` =
project
.in(file("harness-web-app-template/api"))
.settings(
name := "harness-web-app-template--api",
publish / skip := true,
miscSettings,
testSettings,
libraryDependencies ++= Seq(
"org.mindrot" % "jbcrypt" % "0.4",
),
)
.dependsOn(`harness-web-app-template--model`.jvm, `harness-web-app-template--db-model`)

lazy val buildUI: InputKey[Unit] = inputKey("build UI")

lazy val `harness-web-app-template--ui-web` =
project
.in(file("harness-web-app-template/ui-web"))
.enablePlugins(ScalaJSPlugin)
.settings(
name := "harness-web-app-template--ui-web",
publish / skip := true,
miscSettings,
testSettings,
scalaJSUseMainModuleInitializer := true,
buildUI :=
Def.inputTaskDyn {
import complete.DefaultParsers._

val resDir = file("/home/kalin/dev/current/harness/harness-web-app-template/res")

lazy val fast = (fastLinkJS, "fastopt")
lazy val full = (fullLinkJS, "opt")

val args: List[String] = spaceDelimited("<arg>").parsed.toList
val (t, s) =
if (args.contains("-F")) full
else fast
val m = !args.contains("-m")

Def.sequential(
Def.inputTask { println("Running 'webComp'...") }.toTask(""),
Compile / t,
Def
.inputTask {
def jsFile(fName: String): File = {
val crossTargetDir = (crossTarget in (Compile / t)).value
val projectName = normalizedName.value
file(s"$crossTargetDir/$projectName-$s/$fName")
}

val moveToDir = resDir / "js"

val files =
jsFile("main.js") ::
(if (m) jsFile("main.js.map") :: Nil else Nil)

moveToDir.mkdirs()
moveToDir.listFiles.foreach { f =>
if (f.name.contains("main.js"))
f.delete()
}
files.foreach { f =>
IO.copyFile(f, new File(moveToDir, f.getName))
}

()
}
.toTask(""),
)
}.evaluated,
)
.dependsOn(`harness-web-app-template--model`.js, `harness-web`.js)
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package template.api

import harness.core.*
import harness.sql.*
import harness.sql.autoSchema.*
import harness.web.*
import harness.web.server.{given, *}
import harness.zio.*
import template.api.routes as R
import zio.*

object Main extends ExecutableApp {

override val executable: Executable =
Executable
.withParser(ServerConfig.parser)
.withLayer {
ZLayer.succeed(ConnectionFactory("jdbc:postgresql:postgres", "kalin", "psql-pass"))
}
.withEffectNel { config =>
PostgresMeta
.schemaDiff(Tables(db.model.User.tableSchema, db.model.Session.tableSchema))
.mapErrorToNel(HError.SystemFailure("Failed to execute schema diff", _)) *>
Server.start(config) {
Route.stdRoot(config)(
R.User.routes,
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package template.api.db.queries

import harness.sql.*
import harness.sql.query.{given, *}
import template.api.db.model as M

object Session extends TableQueries[M.Session.Id, M.Session] {

val fromSessionToken: QueryIO[String, M.Session.Identity] =
Prepare.selectIO { Input[String] } { token =>
Select
.from[M.Session]("s")
.where { s => s.token === token }
.returning { s => s }
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package template.api.db.queries

import harness.sql.*
import harness.sql.query.{given, *}
import template.api.db.model as M

object User extends TableQueries[M.User.Id, M.User] {

val fromSessionToken: QueryIO[String, M.User.Identity] =
Prepare.selectIO { Input[String] } { token =>
Select
.from[M.Session]("s")
.join[M.User]("u")
.on { case (s, u) => s.userId === u.id }
.where { case (s, _) => s.token === token }
.returning { case (_, u) => u }
}

val byUsername: QueryIO[String, M.User.Identity] =
Prepare
.selectIO { Input[String] } { username =>
Select
.from[M.User]("u")
.where { u => u.lowerUsername === username }
.returning { u => u }
}
.cmap[String](_.toLowerCase)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package template.api.routes

import harness.core.*
import harness.sql.*
import harness.web.server.*
import harness.zio.*
import template.api.db.{model as M, queries as Q}
import zio.*

private[routes] object Helpers {

// TODO (KR) : name this on a project specific basis
val SessionToken: String = "Template-Session-Token"

val userFromSession: HRION[ConnectionFactory & HttpRequest, M.User.Identity] =
HttpRequest.cookie.get[String](Helpers.SessionToken).flatMap { tok =>
Q.User.fromSessionToken(tok).single.mapErrorToNel(HError.UserError("error getting user session", _))
}

val userFromSessionOptional: HRION[ConnectionFactory & HttpRequest, Option[M.User.Identity]] =
HttpRequest.cookie.find[String](Helpers.SessionToken).flatMap {
case Some(tok) => Q.User.fromSessionToken(tok).single.asSome.mapErrorToNel(HError.UserError("error getting user session", _))
case None => ZIO.none
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package template.api.routes

import harness.core.*
import harness.sql.*
import harness.web.*
import harness.web.server.{given, *}
import harness.zio.*
import org.mindrot.jbcrypt.BCrypt
import template.api.db.{model as M, queries as Q}
import template.model as D
import zio.*

object User {

val routes: Route[ConnectionFactory] =
"user" /: Route.oneOf(
(HttpMethod.GET / "from-session-token").implement { _ =>
for {
dbUser <- Helpers.userFromSession
user = D.user.User(dbUser.firstName, dbUser.lastName, dbUser.username, dbUser.email)
} yield HttpResponse.encodeJson(user)
},
(HttpMethod.GET / "from-session-token-optional").implement { _ =>
for {
dbUser <- Helpers.userFromSessionOptional
user = dbUser.map { dbUser => D.user.User(dbUser.firstName, dbUser.lastName, dbUser.username, dbUser.email) }
} yield HttpResponse.encodeJson(user)
},
(HttpMethod.POST / "login").implement { _ =>
for {
body <- HttpRequest.jsonBody[D.user.Login]
user <- Q.User.byUsername(body.username).single.mapErrorToNel(HError.InternalDefect("user by username", _))
_ <- ZIO.failNel(HError.UserError("Invalid Password")).unless(BCrypt.checkpw(body.password, user.encryptedPassword))
session = M.Session.newForUser(user)
_ <- Q.Session.insert(session).mapErrorToNel(HError.InternalDefect("create session", _))
} yield HttpResponse("OK").withCookie(Cookie(Helpers.SessionToken, session.token).rootPath.secure)
},
(HttpMethod.POST / "log-out").implement { _ =>
for {
tok <- HttpRequest.cookie.get[String](Helpers.SessionToken)
dbSession <- Q.Session.fromSessionToken(tok).single.mapErrorToNel(HError.UserError("error getting user session", _))
_ <- Q.Session.deleteById(dbSession.id).single.mapErrorToNel(HError.InternalDefect("delete session", _))
} yield HttpResponse("OK").withCookie(Cookie.unset(Helpers.SessionToken).rootPath.secure)
},
(HttpMethod.POST / "sign-up").implement { _ =>
for {
body <- HttpRequest.jsonBody[D.user.SignUp]
encryptedPassword = BCrypt.hashpw(body.password, BCrypt.gensalt)
user = new M.User.Identity(M.User.Id.gen, body.firstName, body.lastName, body.username, body.username.toLowerCase, encryptedPassword, body.email)
session = M.Session.newForUser(user)
_ <- Q.User.insert(user).mapErrorToNel(HError.InternalDefect("create user", _))
_ <- Q.Session.insert(session).mapErrorToNel(HError.InternalDefect("create session", _))
} yield HttpResponse("OK").withCookie(Cookie(Helpers.SessionToken, session.token).rootPath.secure)
},
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package template.api.db.model

import harness.sql.*
import java.util.UUID

final case class User[F[_]](
id: F[User.Id],
firstName: F[String],
lastName: F[String],
username: F[String],
lowerUsername: F[String],
encryptedPassword: F[String],
email: F[String],
) extends Table.WithId[F, User.Id]
object User extends Table.Companion.WithId[User] {

override implicit lazy val tableSchema: TableSchema[User] =
TableSchema.derived[User]("user") {
new User.Cols(
id = User.Id.col("id").primaryKey,
firstName = Col.string("first_name"),
lastName = Col.string("last_name"),
username = Col.string("username"),
lowerUsername = Col.string("lower_username"),
encryptedPassword = Col.string("encrypted_password"),
email = Col.string("email"),
)
}

}

final case class Session[F[_]](
id: F[Session.Id],
userId: F[User.Id],
token: F[String],
) extends Table.WithId[F, Session.Id]
object Session extends Table.Companion.WithId[Session] {

override implicit lazy val tableSchema: TableSchema[Session] =
TableSchema.derived[Session]("session") {
new Session.Cols(
id = Session.Id.col("id").primaryKey,
userId = User.Id.col("user_id").references(ForeignKeyRef("user", "id")),
token = Col.string("token"),
)
}

def newForUser(user: User.Identity): Session.Identity =
new Session.Identity(
id = Session.Id.gen,
userId = user.id,
token = s"${UUID.randomUUID}:${UUID.randomUUID}",
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package template.model.user

import zio.json.*

final case class Login(
username: String,
password: String,
)
object Login {
implicit val jsonCodec: JsonCodec[Login] = DeriveJsonCodec.gen
}
Loading

0 comments on commit a6c486e

Please sign in to comment.