-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b710e86
commit a6c486e
Showing
25 changed files
with
890 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
harness-web-app-template/api/src/main/scala/template/api/Main.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
) | ||
} | ||
} | ||
|
||
} |
17 changes: 17 additions & 0 deletions
17
harness-web-app-template/api/src/main/scala/template/api/db/queries/Session.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } | ||
} | ||
|
||
} |
29 changes: 29 additions & 0 deletions
29
harness-web-app-template/api/src/main/scala/template/api/db/queries/User.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
} |
26 changes: 26 additions & 0 deletions
26
harness-web-app-template/api/src/main/scala/template/api/routes/Helpers.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
|
||
} |
57 changes: 57 additions & 0 deletions
57
harness-web-app-template/api/src/main/scala/template/api/routes/User.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}, | ||
) | ||
|
||
} |
55 changes: 55 additions & 0 deletions
55
harness-web-app-template/db-model/src/main/scala/template/api/db/model/Tables.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}", | ||
) | ||
|
||
} |
11 changes: 11 additions & 0 deletions
11
harness-web-app-template/model/shared/src/main/scala/template/model/user/Login.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.