diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt index 9f6f29e..4ec6fc3 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/api/ApiClient.kt @@ -151,6 +151,6 @@ class ApiClient( companion object { const val PROD_SERVER_URL = "https://api.simplesplitapp.link" - const val DEBUG_SERVER_URL = "http://10.0.2.2:8080" + const val DEBUG_SERVER_URL = "https://10.0.2.2:8443" } } \ No newline at end of file diff --git a/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt b/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt index 8bc6559..e199fda 100644 --- a/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt +++ b/android/app/src/main/java/com/github/ai/simplesplit/android/data/settings/Settings.kt @@ -35,7 +35,7 @@ class SettingsImpl( override var httpLogLevel: LogLevel get() { - return prefs.pull(HTTP_LOG_LEVEL.key, StringUtils.EMPTY).let { name -> + return prefs.pull(HTTP_LOG_LEVEL.key, StringUtils.EMPTY).let { name -> LogLevel.entries.find { level -> level.name == name } ?: LogLevel.INFO } diff --git a/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala b/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala index 013bb5e..a569b33 100644 --- a/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala +++ b/backend/api-client/src/main/scala/com/github/ai/split/client/ApiClient.scala @@ -13,7 +13,7 @@ class ApiClient( type ApiResponse = ZIO[Scope, Throwable, Response] private val DefaultPassword = "abc123" - private val baseUrl = "http://127.0.0.1:8080" + private val baseUrl = "https://127.0.0.1:8443" def getGroup( uid: String = Groups.TripToDisneyLand, diff --git a/backend/app/src/main/scala/com/github/ai/split/Main.scala b/backend/app/src/main/scala/com/github/ai/split/Main.scala index ef880a9..0c4f760 100644 --- a/backend/app/src/main/scala/com/github/ai/split/Main.scala +++ b/backend/app/src/main/scala/com/github/ai/split/Main.scala @@ -4,6 +4,7 @@ import com.github.ai.split.data.currency.CurrencyParser import com.github.ai.split.domain.CliArgumentParser import com.github.ai.split.domain.usecases.{FillTestDataUseCase, StartUpServerUseCase} import com.github.ai.split.entity.CliArguments +import com.github.ai.split.entity.HttpProtocol.{HTTP, HTTPS} import com.github.ai.split.presentation.routes.{CurrencyRoutes, ExpenseRoutes, ExportRoutes, GroupRoutes, MemberRoutes} import io.getquill.SnakeCase import io.getquill.jdbczio.Quill @@ -11,6 +12,7 @@ import zio.* import zio.http.* import zio.logging.LogFormat import zio.logging.backend.SLF4J +import zio.direct.* object Main extends ZIOAppDefault { @@ -24,19 +26,39 @@ object Main extends ZIOAppDefault { Runtime.removeDefaultLoggers >>> SLF4J.slf4j(LogFormat.colored) } - private def application() = { - for { - startupUseCase <- ZIO.service[StartUpServerUseCase] - _ <- startupUseCase.startUpServer() - _ <- Server.serve(routes) - } yield () + private def application() = defer { + val startUpUseCase = ZIO.service[StartUpServerUseCase].run + startUpUseCase.startUpServer().run + + Server.serve(routes).run + + () + } + + private def createServerConfig( + arguments: CliArguments + ) = defer { + arguments.protocol match { + case HTTP => + Server.Config.default + .port(arguments.getPort()) + + case HTTPS => + Server.Config.default + .port(arguments.getPort()) + .ssl(SSLConfig.fromFile("dev-data/server.crt", "dev-data/server.key")) + } } override def run: ZIO[ZIOAppArgs, Throwable, Unit] = { for { arguments <- CliArgumentParser().parse() - _ <- ZIO.logInfo(s"Starting application with arguments:") - _ <- ZIO.logInfo(arguments.toReadableString()) + _ <- ZIO.logInfo(s"Starting server on port ${arguments.getPort()}") + _ <- ZIO.logInfo(s" isUseInMemoryDatabase=${arguments.isUseInMemoryDatabase}") + _ <- ZIO.logInfo(s" isPopulateTestData=${arguments.isPopulateTestData}") + _ <- ZIO.logInfo(s" protocol=${arguments.protocol}") + + serverConfig <- createServerConfig(arguments) _ <- application().provide( // Application arguments @@ -96,7 +118,8 @@ object Main extends ZIOAppDefault { // Others Layers.currencyParser, - Server.defaultWithPort(8080), + Server.live, + ZLayer.succeed(serverConfig), Quill.H2.fromNamingStrategy(SnakeCase), if (arguments.isUseInMemoryDatabase) { Quill.DataSource.fromPrefix("test-h2db") diff --git a/backend/app/src/main/scala/com/github/ai/split/domain/CliArgumentParser.scala b/backend/app/src/main/scala/com/github/ai/split/domain/CliArgumentParser.scala index 8d6615a..35a5339 100644 --- a/backend/app/src/main/scala/com/github/ai/split/domain/CliArgumentParser.scala +++ b/backend/app/src/main/scala/com/github/ai/split/domain/CliArgumentParser.scala @@ -1,27 +1,72 @@ package com.github.ai.split.domain import com.github.ai.split.utils.some -import com.github.ai.split.entity.CliArguments -import com.github.ai.split.entity.exception.DomainError +import com.github.ai.split.entity.{CliArguments, HttpProtocol} +import com.github.ai.split.entity.exception.{DomainError, ParsingError} import zio.* +import zio.direct.* + +import scala.collection.mutable class CliArgumentParser { def parse(): ZIO[ZIOAppArgs, DomainError, CliArguments] = { - for { - appArgs <- ZIO.service[ZIOAppArgs] - parsedArgs <- parseArguments(appArgs.getArgs.toArray) - } yield parsedArgs + defer { + val args = ZIO.service[ZIOAppArgs].run + parseArguments(args.getArgs.toList).run + } } - private def parseArguments(args: Array[String]): IO[DomainError, CliArguments] = { - ZIO.foldLeft(args)(CliArguments()) { (acc, arg) => - arg match { - case "--in-memory-db" => ZIO.succeed(acc.copy(isUseInMemoryDatabase = true)) - case "--populate-data" => ZIO.succeed(acc.copy(isPopulateTestData = true)) - case arg => - ZIO.fail(DomainError(message = s"Unexpected argument: $arg".some)) + private def parseArguments(args: List[String]): IO[DomainError, CliArguments] = { + ZIO + .attempt { + var useInMemoryDb = false + var populateData = false + var protocol: Option[HttpProtocol] = None + + val queue = mutable.Queue[String]() + queue.addAll(args) + + while (queue.nonEmpty) { + val optionName = queue.removeHead() + optionName match { + case CliOptions.InMemoryDb.cliName => useInMemoryDb = true + case CliOptions.PopulateData.cliName => populateData = true + case CliOptions.Protocol.cliName => { + val protocolValue = queue + .removeHeadOption() + .flatMap(value => HttpProtocol.fromString(value)) + + protocolValue match { + case Some(p) => protocol = Some(p) + case None => + throw new ParsingError( + s"Invalid option: ${CliOptions.Protocol.cliName}. Expected 'http' or 'https'" + ) + } + } + + case _ => + throw new ParsingError(s"Invalid option specified: '$optionName'") + } + } + + if (protocol.isEmpty) { + throw new ParsingError(s"Option '${CliOptions.Protocol.cliName}' is required ") + } + + CliArguments( + isUseInMemoryDatabase = useInMemoryDb, + isPopulateTestData = populateData, + protocol = protocol.get + ) } - } + .mapError(error => DomainError(cause = error.some)) + } + + private enum CliOptions(val cliName: String) { + case InMemoryDb extends CliOptions("--in-memory-db") + case PopulateData extends CliOptions("--populate-data") + case Protocol extends CliOptions("--protocol") } } diff --git a/backend/app/src/main/scala/com/github/ai/split/entity/CliArguments.scala b/backend/app/src/main/scala/com/github/ai/split/entity/CliArguments.scala index 5dc7d89..eae58ba 100644 --- a/backend/app/src/main/scala/com/github/ai/split/entity/CliArguments.scala +++ b/backend/app/src/main/scala/com/github/ai/split/entity/CliArguments.scala @@ -1,10 +1,16 @@ package com.github.ai.split.entity +import com.github.ai.split.entity.HttpProtocol.{HTTP, HTTPS} + case class CliArguments( - isUseInMemoryDatabase: Boolean = false, - isPopulateTestData: Boolean = false + isUseInMemoryDatabase: Boolean, + isPopulateTestData: Boolean, + protocol: HttpProtocol ) { - def toReadableString(): String = - s"${classOf[CliArguments]}(IN_MEMORY_DB=${isUseInMemoryDatabase}, POPULATE_DATA=${isPopulateTestData})" + def getPort(): Int = { + protocol match + case HTTP => 8080 + case HTTPS => 8443 + } } diff --git a/backend/app/src/main/scala/com/github/ai/split/entity/HttpProtocol.scala b/backend/app/src/main/scala/com/github/ai/split/entity/HttpProtocol.scala new file mode 100644 index 0000000..f69d0b9 --- /dev/null +++ b/backend/app/src/main/scala/com/github/ai/split/entity/HttpProtocol.scala @@ -0,0 +1,11 @@ +package com.github.ai.split.entity + +enum HttpProtocol { + case HTTP, HTTPS +} + +object HttpProtocol { + def fromString(name: String): Option[HttpProtocol] = { + HttpProtocol.values.find(protocol => protocol.toString.equalsIgnoreCase(name)) + } +} diff --git a/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala b/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala index 1478f3d..548e1c6 100644 --- a/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala +++ b/backend/app/src/main/scala/com/github/ai/split/entity/exception/DomainError.scala @@ -7,3 +7,7 @@ class DomainError( message.orNull, cause.orNull ) + +class ParsingError( + message: String +) extends DomainError(message = Some(message), cause = None) diff --git a/backend/app/src/main/scala/com/github/ai/split/utils/ExceptionExtensions.scala b/backend/app/src/main/scala/com/github/ai/split/utils/ExceptionExtensions.scala index 4eaf492..d93519b 100644 --- a/backend/app/src/main/scala/com/github/ai/split/utils/ExceptionExtensions.scala +++ b/backend/app/src/main/scala/com/github/ai/split/utils/ExceptionExtensions.scala @@ -3,7 +3,6 @@ package com.github.ai.split.utils import com.github.ai.split.entity.exception.DomainError import java.io.{PrintWriter, StringWriter} -import java.sql.SQLException extension (exception: Throwable) def stackTraceToString(): String = {