Skip to content

Commit

Permalink
First successful compilation of harness-web-app-template--web-server
Browse files Browse the repository at this point in the history
  • Loading branch information
Kalin-Rudnicki committed Apr 21, 2024
1 parent 7209ad0 commit 2eb8ea3
Show file tree
Hide file tree
Showing 34 changed files with 294 additions and 278 deletions.
18 changes: 17 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,22 @@ lazy val `harness-web-app-template--domain-impl` =
`harness-sql-mock` % Test,
)

lazy val `harness-web-app-template--api-impl` =
project
.in(file("harness-web-app-template/modules/api-impl"))
.settings(
name := "harness-web-app-template--api-impl",
publish / skip := true,
miscSettings,
testSettings,
)
.dependsOn(
`harness-web-app-template--domain` % testAndCompile,
`harness-web-app-template--api`.jvm % testAndCompile,
`harness-sql` % testAndCompile,
`harness-http-server` % testAndCompile,
)

lazy val `harness-web-app-template--web-server` =
project
.in(file("harness-web-app-template/modules/web-server"))
Expand All @@ -779,8 +795,8 @@ lazy val `harness-web-app-template--web-server` =
},
)
.dependsOn(
`harness-web-app-template--api-impl` % testAndCompile,
`harness-web-app-template--domain-impl` % testAndCompile,
`harness-http-server` % testAndCompile,
)

lazy val `harness-web-app-template--ui-web` =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package template.api.impl

import harness.http.server.Implementation
import harness.sql.query.Transaction
import template.api.service.*
import template.api.spec as Spec
import template.domain.model.DomainError
import template.domain.session.SessionService

object Api {

type Env = UserApi & PaymentApi & SessionService & Transaction[DomainError]

val impl: Spec.Api[Implementation.Projection[Env]] =
Spec.Api[Implementation.Projection[Env]](
user = User.impl,
payment = Payment.impl,
)

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

import harness.http.server.*
import template.api.service.*
import template.api.spec as Spec
import zio.*

object Payment {

val impl: Spec.Payment[Implementation.Projection[Api.Env]] =
Spec.Payment[Implementation.Projection[Api.Env]](
createIntent = Implementation[Spec.Payment.CreateIntent].implement { token =>
ZIO.serviceWithZIO[PaymentApi](_.createIntent(token)).toHttpResponse
},
acceptIntent = Implementation[Spec.Payment.AcceptIntent].implement { (intentId, token) =>
ZIO.serviceWithZIO[PaymentApi](_.acceptIntent(token, intentId)).toHttpResponse
},
paymentMethods = Implementation[Spec.Payment.PaymentMethods].implement { token =>
ZIO.serviceWithZIO[PaymentApi](_.paymentMethods(token)).map(_.map(_.toApi)).toHttpResponse
},
)

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

import harness.http.server.*
import harness.sql.query.Transaction
import template.api.service.*
import template.api.spec as Spec
import template.domain.session.SessionService
import zio.*

object User {

val impl: Spec.User[Implementation.Projection[Api.Env]] =
Spec.User[Implementation.Projection[Api.Env]](
get = Implementation[Spec.User.Get].implement { token =>
ZIO.serviceWithZIO[UserApi](_.get(token)).map(_.toApi).toHttpResponse
},
login = Implementation[Spec.User.Login].implement { login =>
Transaction.inTransaction {
for {
(user, token) <- ZIO.serviceWithZIO[UserApi](_.login(login))
isSecure <- ZIO.serviceWithZIO[SessionService](_.isSecure)
tokenKey <- ZIO.serviceWithZIO[SessionService](_.tokenKey)
cookie = SetCookie(tokenKey, token.value).rootPath.secure(isSecure)
} yield HttpResponse(user.toApi).withHeader(tokenKey, token.value).withCookie(cookie)
}
},
logOut = Implementation[Spec.User.LogOut].implement { token =>
for {
_ <- ZIO.serviceWithZIO[UserApi](_.logOut(token))
isSecure <- ZIO.serviceWithZIO[SessionService](_.isSecure)
tokenKey <- ZIO.serviceWithZIO[SessionService](_.tokenKey)
cookie = SetCookie.unset(tokenKey).rootPath.secure(isSecure)
} yield HttpResponse(()).withCookie(cookie)
},
signUp = Implementation[Spec.User.SignUp].implement { signUp =>
Transaction.inTransaction {
for {
(user, token) <- ZIO.serviceWithZIO[UserApi](_.signUp(signUp))
isSecure <- ZIO.serviceWithZIO[SessionService](_.isSecure)
tokenKey <- ZIO.serviceWithZIO[SessionService](_.tokenKey)
cookie = SetCookie(tokenKey, token.value).rootPath.secure(isSecure)
} yield HttpResponse(user.toApi).withHeader(tokenKey, token.value).withCookie(cookie)
}
},
verifyEmail = Implementation[Spec.User.VerifyEmail].implement { (code, token) =>
ZIO.serviceWithZIO[UserApi](_.verifyEmail(token, code)).toHttpResponse
},
resendEmailVerification = Implementation[Spec.User.ResendEmailVerification].implement { token =>
ZIO.serviceWithZIO[UserApi](_.resendEmailVerification(token)).toHttpResponse
},
)

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

import harness.http.server.ErrorHandler
import template.api.model.error.ApiError
import template.domain.model.DomainError

implicit val errorHandler: ErrorHandler.Id[DomainError, ApiError] =
ErrorHandler.id[DomainError, ApiError](
convertDecodingFailure = ???, // TODO (KR) :
convertUnexpectedError = ???, // TODO (KR) :
convertDomainError = ???, // TODO (KR) :
errorLogger = ???, // TODO (KR) :
headersAndCookiesOnError = ???, // TODO (KR) :
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package template.webServer.api
package template.api.service

import cats.syntax.option.*
import harness.payments.model.ids.*
Expand All @@ -7,12 +7,13 @@ import harness.payments.service.PaymentProcessor
import harness.zio.*
import template.api.model as Api
import template.domain.model.*
import template.domain.session.*
import template.domain.storage.*
import zio.*

final case class PaymentApi(
sessionService: SessionService,
userStorage: UserStorage,
sessionStorage: SessionStorage,
paymentMethodStorage: PaymentMethodStorage,
paymentProcessor: PaymentProcessor,
) {
Expand All @@ -28,15 +29,15 @@ final case class PaymentApi(

def createIntent(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, ClientSecret] =
for {
user <- SessionUtils.userFromSessionToken(token, sessionStorage)
user <- sessionService.getUser(token)
_ <- Logger.log.info("Attempting to create setup intent", "userId" -> user.id.toUUID)
customerId <- getOrCreateStripeCustomer(user)
setupIntent <- paymentProcessor.createSetupIntent(PM.create.SetupIntent(customerId, None)).mapError(DomainError.UnexpectedPaymentError(_))
} yield setupIntent.clientSecret

def acceptSetupIntent(token: Api.user.UserToken, setupIntentId: SetupIntentId): ZIO[HarnessEnv, DomainError, Unit] =
def acceptIntent(token: Api.user.UserToken, setupIntentId: SetupIntentId): ZIO[HarnessEnv, DomainError, Unit] =
for {
user <- SessionUtils.userFromSessionToken(token, sessionStorage)
user <- sessionService.getUser(token)
_ <- Logger.log.info("Attempting to accept setup intent", "userId" -> user.id.toUUID)
setupIntent <- paymentProcessor.getSetupIntent(setupIntentId).mapError(DomainError.UnexpectedPaymentError(_))
paymentMethod <- paymentProcessor.getPaymentMethod(setupIntent.paymentMethodId).mapError(DomainError.UnexpectedPaymentError(_))
Expand All @@ -47,15 +48,15 @@ final case class PaymentApi(

def paymentMethods(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, Chunk[PaymentMethod]] =
for {
user <- SessionUtils.userFromSessionToken(token, sessionStorage)
user <- sessionService.getUser(token)
_ <- Logger.log.info("Attempting to get payment methods", "userId" -> user.id.toUUID)
paymentMethods <- paymentMethodStorage.getForUser(user.id)
} yield paymentMethods

}
object PaymentApi {

val layer: URLayer[UserStorage & SessionStorage & PaymentMethodStorage & PaymentProcessor, PaymentApi] =
val layer: URLayer[UserStorage & PaymentMethodStorage & PaymentProcessor & SessionService, PaymentApi] =
ZLayer.fromFunction { PaymentApi.apply }

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package template.webServer.api
package template.api.service

import cats.syntax.option.*
import harness.email.SendEmail
Expand All @@ -7,20 +7,19 @@ import org.mindrot.jbcrypt.BCrypt
import template.api.model as Api
import template.domain.email.*
import template.domain.model.*
import template.domain.session.*
import template.domain.storage.*
import zio.*

final case class UserApi(
sessionService: SessionService,
userStorage: UserStorage,
sessionStorage: SessionStorage,
emailService: EmailService,
) {

def fromSessionToken(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, User] =
SessionUtils.userFromSessionTokenAllowUnverifiedEmail(token, sessionStorage)

def fromSessionTokenOptional(token: Option[Api.user.UserToken]): ZIO[HarnessEnv, DomainError, Option[User]] =
ZIO.foreach(token)(fromSessionToken)
def get(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, User] =
sessionService.getUserAllowUnverifiedEmail(token)

def login(req: Api.user.Login): ZIO[HarnessEnv, DomainError, (User, Api.user.UserToken)] =
for {
Expand All @@ -34,7 +33,7 @@ final case class UserApi(
def logOut(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, Unit] =
for {
_ <- Logger.log.info("Attempting logout")
session <- SessionUtils.sessionFromSessionToken(token, sessionStorage)
session <- sessionService.getSession(token)
_ <- sessionStorage.deleteById(session.id)
} yield ()

Expand All @@ -59,7 +58,7 @@ final case class UserApi(
def verifyEmail(token: Api.user.UserToken, code: Api.user.EmailVerificationCode): ZIO[HarnessEnv, DomainError, Unit] =
for {
_ <- Logger.log.info("Attempting to verify email")
user <- SessionUtils.userFromSessionTokenAllowUnverifiedEmail(token, sessionStorage)
user <- sessionService.getUserAllowUnverifiedEmail(token)
validCodes <- user.verificationEmailCodes match {
case Some(validCodes) => ZIO.succeed(validCodes)
case None => ZIO.fail(DomainError.EmailAlreadyVerified(user.email))
Expand All @@ -68,10 +67,10 @@ final case class UserApi(
_ <- userStorage.setEmailCodes(user.id, None)
} yield ()

def resendEmailCode(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, Unit] =
def resendEmailVerification(token: Api.user.UserToken): ZIO[HarnessEnv, DomainError, Unit] =
for {
_ <- Logger.log.info("Attempting to resend email code")
user <- SessionUtils.userFromSessionTokenAllowUnverifiedEmail(token, sessionStorage)
user <- sessionService.getUserAllowUnverifiedEmail(token)
validCodes <- user.verificationEmailCodes match {
case Some(validCodes) => ZIO.succeed(validCodes)
case None => ZIO.fail(DomainError.EmailAlreadyVerified(user.email))
Expand All @@ -88,7 +87,7 @@ final case class UserApi(
}
object UserApi {

val layer: URLayer[UserStorage & SessionStorage & EmailService, UserApi] =
val layer: URLayer[UserStorage & SessionStorage & EmailService & SessionService, UserApi] =
ZLayer.fromFunction { UserApi.apply }

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,21 @@ import zio.Chunk

final case class Payment[F[_ <: EndpointType.Any]](
createIntent: F[Payment.CreateIntent],
acceptSetupIntent: F[Payment.AcceptSetupIntent],
acceptIntent: F[Payment.AcceptIntent],
paymentMethods: F[Payment.PaymentMethods],
)
object Payment {

type CreateIntent = EndpointType[A.user.UserToken, Unit, BodyType.None, BodyType.Encoded[PM.ids.ClientSecret], ApiError]
type AcceptSetupIntent = EndpointType[(PM.ids.SetupIntentId, A.user.UserToken), PM.ids.SetupIntentId, BodyType.None, BodyType.None, ApiError]
type AcceptIntent = EndpointType[(PM.ids.SetupIntentId, A.user.UserToken), PM.ids.SetupIntentId, BodyType.None, BodyType.None, ApiError]
type PaymentMethods = EndpointType[A.user.UserToken, Unit, BodyType.None, BodyType.Encoded[Chunk[A.paymentMethod.PaymentMethod]], ApiError]

def spec(authToken: headerOrCookie[A.user.UserToken]): Payment[EndpointSpec] =
"payment" /:
Payment[EndpointSpec](
createIntent = EndpointSpec.post("Create Setup Intent") / "intent" / "create"
/# authToken /--> body.json[PM.ids.ClientSecret] /!--> errorBody.json[ApiError],
acceptSetupIntent = EndpointSpec.get("Accept Setup Intent") / "intent" / "accept"
acceptIntent = EndpointSpec.get("Accept Setup Intent") / "intent" / "accept"
/? query[PM.ids.SetupIntentId]("setup_intent") /# authToken /!--> errorBody.json[ApiError],
paymentMethods = EndpointSpec.get("Get All Payment Methods") / "method" / "all"
/# authToken /--> body.json[Chunk[A.paymentMethod.PaymentMethod]] /!--> errorBody.json[ApiError],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ final case class User[F[_ <: EndpointType.Any]](
logOut: F[User.LogOut],
signUp: F[User.SignUp],
verifyEmail: F[User.VerifyEmail],
resendEmailCode: F[User.ResendEmailCode],
resendEmailVerification: F[User.ResendEmailVerification],
)
object User {

Expand All @@ -21,7 +21,7 @@ object User {
type LogOut = EndpointType[A.user.UserToken, Unit, BodyType.None, BodyType.None, ApiError]
type SignUp = EndpointType.Basic[Unit, BodyType.Encoded[A.user.SignUp], BodyType.Encoded[A.user.User], ApiError]
type VerifyEmail = EndpointType[(A.user.EmailVerificationCode, A.user.UserToken), A.user.EmailVerificationCode, BodyType.None, BodyType.None, ApiError]
type ResendEmailCode = EndpointType[A.user.UserToken, Unit, BodyType.None, BodyType.None, ApiError]
type ResendEmailVerification = EndpointType[A.user.UserToken, Unit, BodyType.None, BodyType.None, ApiError]

def spec(authToken: headerOrCookie[A.user.UserToken]): User[EndpointSpec] =
"user" /:
Expand All @@ -36,7 +36,7 @@ object User {
/<-- body.json[A.user.SignUp] /--> body.json[A.user.User] /!--> errorBody.json[ApiError],
verifyEmail = EndpointSpec.post("Verify Email") / "email" / "verify"
/? query[A.user.EmailVerificationCode]("code") /# authToken /!--> errorBody.json[ApiError],
resendEmailCode = EndpointSpec.post("Resend Email Verification") / "email" / "resend-verification"
resendEmailVerification = EndpointSpec.post("Resend Email Verification") / "email" / "resend-verification"
/# authToken /!--> errorBody.json[ApiError],
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package template.db.model

import harness.email.EmailAddress
import harness.schema.*
import harness.sql.*
import template.api.model as Api

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package template.db.model
import harness.email.EmailAddress
import harness.payments.model.ids as StripeIds
import harness.payments.model as PM
import harness.schema.*
import harness.sql.*
import template.api.model as Api
import template.domain.model as Domain
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package template.domain.impl.session

import harness.zio.*
import template.api.model as Api
import template.domain.model.{DomainError, Session, User}
import template.domain.session.SessionService
import template.domain.storage.SessionStorage
import zio.*

final case class LiveSessionService(
storage: SessionStorage,
config: LiveSessionService.Config,
) extends SessionService {

override def isSecure: UIO[Boolean] = ZIO.succeed(config.isSecure)

override def tokenKey: UIO[String] = ZIO.succeed(config.tokenKey)

override def getUserAllowUnverifiedEmail(token: Api.user.UserToken): ZIO[Logger & Telemetry, DomainError, User] =
storage
.userFromSessionToken(token)
.someOrFail(DomainError.InvalidSessionToken)

override def getUser(token: Api.user.UserToken): ZIO[Logger & Telemetry, DomainError, User] =
getUserAllowUnverifiedEmail(token)
.tap { user => ZIO.fail(DomainError.EmailNotVerified).when(user.verificationEmailCodes.nonEmpty) }

override def getSession(token: Api.user.UserToken): ZIO[Logger & Telemetry, DomainError, Session] =
storage
.sessionFromSessionToken(token)
.someOrFail(DomainError.InvalidSessionToken)

}
object LiveSessionService {

final case class Config(isSecure: Boolean, tokenKey: String)

val layer: URLayer[SessionStorage & Config, SessionService] =
ZLayer.fromFunction { LiveSessionService.apply }

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package template.domain.model

import cats.data.NonEmptyList
import harness.email.EmailAddress
import harness.zio.ZIOJsonInstances.*
import harness.zio.json.*
import template.api.model.error.ApiError
import template.api.model as Api
import zio.json.*
Expand Down

0 comments on commit 2eb8ea3

Please sign in to comment.