From ef1536631c4bf2068e412b8870d23aa1dcbfe202 Mon Sep 17 00:00:00 2001 From: timzaak Date: Tue, 20 Jun 2023 14:56:00 +0800 Subject: [PATCH] move very.util to web-sugar --- .gitmodules | 3 + backend/build.sbt | 21 ++-- .../very/util/config/ConfigExtension.scala | 93 ---------------- .../scala/very/util/entity/Pagination.scala | 9 -- .../very/util/grpc/auth/SessionProvider.scala | 10 -- .../grpc/auth/SessionServerInterceptor.scala | 36 ------- .../util/keycloak/JWKPublicKeyLocator.scala | 26 ----- .../very/util/keycloak/JWKTokenVerifier.scala | 27 ----- .../keycloak/KeycloakJWTAuthStrategy.scala | 55 ---------- .../very/util/persistence/FlywayMigrate.scala | 12 --- .../util/persistence/quill/IDSupport.scala | 15 --- .../util/persistence/quill/PageSupport.scala | 33 ------ .../persistence/quill/ZIOJsonSupport.scala | 29 ----- .../main/scala/very/util/security/ID.scala | 22 ---- .../main/scala/very/util/web/Controller.scala | 53 --------- .../main/scala/very/util/web/LogSupport.scala | 19 ---- .../very/util/web/PaginationSupport.scala | 22 ---- .../scala/very/util/web/PingServlet.scala | 9 -- .../web/SinglePageAppResourceService.scala | 17 --- .../very/util/web/auth/AuthStrategy.scala | 9 -- .../util/web/auth/AuthStrategyProvider.scala | 10 -- .../very/util/web/auth/AuthSupport.scala | 26 ----- .../web/auth/SingleUserAuthStrategy.scala | 22 ---- .../very/util/web/json/ZIOJsonSupport.scala | 101 ------------------ .../util/web/validate/ValidationExtra.scala | 34 ------ third/web-sugar | 1 + 26 files changed, 14 insertions(+), 700 deletions(-) delete mode 100644 backend/src/main/scala/very/util/config/ConfigExtension.scala delete mode 100644 backend/src/main/scala/very/util/entity/Pagination.scala delete mode 100644 backend/src/main/scala/very/util/grpc/auth/SessionProvider.scala delete mode 100644 backend/src/main/scala/very/util/grpc/auth/SessionServerInterceptor.scala delete mode 100644 backend/src/main/scala/very/util/keycloak/JWKPublicKeyLocator.scala delete mode 100644 backend/src/main/scala/very/util/keycloak/JWKTokenVerifier.scala delete mode 100644 backend/src/main/scala/very/util/keycloak/KeycloakJWTAuthStrategy.scala delete mode 100644 backend/src/main/scala/very/util/persistence/FlywayMigrate.scala delete mode 100644 backend/src/main/scala/very/util/persistence/quill/IDSupport.scala delete mode 100644 backend/src/main/scala/very/util/persistence/quill/PageSupport.scala delete mode 100644 backend/src/main/scala/very/util/persistence/quill/ZIOJsonSupport.scala delete mode 100644 backend/src/main/scala/very/util/security/ID.scala delete mode 100644 backend/src/main/scala/very/util/web/Controller.scala delete mode 100644 backend/src/main/scala/very/util/web/LogSupport.scala delete mode 100644 backend/src/main/scala/very/util/web/PaginationSupport.scala delete mode 100644 backend/src/main/scala/very/util/web/PingServlet.scala delete mode 100644 backend/src/main/scala/very/util/web/SinglePageAppResourceService.scala delete mode 100644 backend/src/main/scala/very/util/web/auth/AuthStrategy.scala delete mode 100644 backend/src/main/scala/very/util/web/auth/AuthStrategyProvider.scala delete mode 100644 backend/src/main/scala/very/util/web/auth/AuthSupport.scala delete mode 100644 backend/src/main/scala/very/util/web/auth/SingleUserAuthStrategy.scala delete mode 100644 backend/src/main/scala/very/util/web/json/ZIOJsonSupport.scala delete mode 100644 backend/src/main/scala/very/util/web/validate/ValidationExtra.scala create mode 160000 third/web-sugar diff --git a/.gitmodules b/.gitmodules index b67b01a..b25c3c6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "third/rust-tun"] path = third/rust-tun url = https://github.com/timzaak/rust-tun.git +[submodule "third/web-sugar"] + path = third/web-sugar + url = git@github.com:ForNetCode/web-sugar.git diff --git a/backend/build.sbt b/backend/build.sbt index ab13789..1f8d2f2 100644 --- a/backend/build.sbt +++ b/backend/build.sbt @@ -1,4 +1,4 @@ -val scala3Version = "3.2.1" +val scala3Version = "3.3.0" maintainer := "timzaak" @@ -14,23 +14,22 @@ Compile / PB.targets := Seq( Compile / PB.protoSources += file("../protobuf") // zio-json default value needs this -ThisBuild / scalacOptions ++= Seq("-Yretain-trees") +//ThisBuild / scalacOptions ++= Seq("-Yretain-trees") -import Dependencies._ + + +lazy val webSugar = RootProject(file("../third/web-sugar")) lazy val app = project .in(file(".")) .settings( version := "0.0.3", scalaVersion := scala3Version, - libraryDependencies ++= grpc ++ persistence ++ logLib ++ webServer ++ configLib ++ - keycloakLib ++ httpClient ++ - Seq( - "dev.zio" %% "zio-prelude" % "1.0.0-RC16", // for validate - "com.github.seancfoley" % "ipaddress" % "5.4.0", // for ip parse - "org.bouncycastle" % "bcprov-jdk18on" % "1.72", // for x25519, - "org.hashids" % "hashids" % "1.0.3", // hashids + libraryDependencies ++= + Seq( + "org.eclipse.jetty" % "jetty-webapp" % "11.0.15" % "container;compile", + "org.bouncycastle" % "bcprov-jdk18on" % "1.72", // for x25519, // "org.keycloak" % "keycloak-servlet-filter-adapter" % "20.0.1", //keycloak "org.scalameta" %% "munit" % "0.7.29" % Test ) - ) //.enablePlugins(JlinkPlugin) + ).enablePlugins(ScalatraPlugin).enablePlugins(JavaAppPackaging).dependsOn(webSugar) //.enablePlugins(JlinkPlugin) diff --git a/backend/src/main/scala/very/util/config/ConfigExtension.scala b/backend/src/main/scala/very/util/config/ConfigExtension.scala deleted file mode 100644 index 95b2d2b..0000000 --- a/backend/src/main/scala/very/util/config/ConfigExtension.scala +++ /dev/null @@ -1,93 +0,0 @@ -package very.util.config - -import com.typesafe.config.{Config, ConfigList, ConfigMemorySize, ConfigObject} - -import java.net.{URI, URL} -import java.time.Period -import java.time.temporal.TemporalAmount -import scala.concurrent.duration.{Duration, FiniteDuration} - -//import scala.collection.JavaConverters._ -import scala.jdk.CollectionConverters._ - -trait ConfigLoader[A] { - self => - def load(config: Config, path: String = ""): A - - def map[B](f: A => B): ConfigLoader[B] = (config, path) => f(self.load(config, path)) -} - -object ConfigLoader { - def apply[A](f: Config => String => A): ConfigLoader[A] = f(_)(_) - - implicit val stringLoader: ConfigLoader[String] = ConfigLoader(_.getString) - implicit val seqStringLoader: ConfigLoader[Seq[String]] = ConfigLoader(_.getStringList).map(_.asScala.toSeq) - - implicit val intLoader: ConfigLoader[Int] = ConfigLoader(_.getInt) - implicit val seqIntLoader: ConfigLoader[Seq[Int]] = ConfigLoader(_.getIntList).map(_.asScala.map(_.toInt).toSeq) - - implicit val booleanLoader: ConfigLoader[Boolean] = ConfigLoader(_.getBoolean) - implicit val seqBooleanLoader: ConfigLoader[Seq[Boolean]] = - ConfigLoader(_.getBooleanList).map(_.asScala.map(_.booleanValue).toSeq) - - implicit val finiteDurationLoader: ConfigLoader[FiniteDuration] = - ConfigLoader(_.getDuration).map(javaDurationToScala) - - implicit val seqFiniteDurationLoader: ConfigLoader[Seq[FiniteDuration]] = - ConfigLoader(_.getDurationList).map(_.asScala.map(javaDurationToScala).toSeq) - - implicit val durationLoader: ConfigLoader[Duration] = ConfigLoader { config => - path => - if (config.getIsNull(path)) Duration.Inf - else if (config.getString(path) == "infinite") Duration.Inf - else finiteDurationLoader.load(config, path) - } - - // Note: this does not support null values but it added for convenience - implicit val seqDurationLoader: ConfigLoader[Seq[Duration]] = - seqFiniteDurationLoader.map(identity[Seq[Duration]]) - - implicit val periodLoader: ConfigLoader[Period] = ConfigLoader(_.getPeriod) - - implicit val temporalLoader: ConfigLoader[TemporalAmount] = ConfigLoader(_.getTemporal) - - implicit val doubleLoader: ConfigLoader[Double] = ConfigLoader(_.getDouble) - implicit val seqDoubleLoader: ConfigLoader[Seq[Double]] = - ConfigLoader(_.getDoubleList).map(_.asScala.map(_.doubleValue).toSeq) - - implicit val numberLoader: ConfigLoader[Number] = ConfigLoader(_.getNumber) - implicit val seqNumberLoader: ConfigLoader[Seq[Number]] = ConfigLoader(_.getNumberList).map(_.asScala.toSeq) - - implicit val longLoader: ConfigLoader[Long] = ConfigLoader(_.getLong) - implicit val seqLongLoader: ConfigLoader[Seq[Long]] = - ConfigLoader(_.getLongList).map(_.asScala.map(_.longValue).toSeq) - - implicit val bytesLoader: ConfigLoader[ConfigMemorySize] = ConfigLoader(_.getMemorySize) - implicit val seqBytesLoader: ConfigLoader[Seq[ConfigMemorySize]] = - ConfigLoader(_.getMemorySizeList).map(_.asScala.toSeq) - - implicit val configLoader: ConfigLoader[Config] = ConfigLoader(_.getConfig) - implicit val configListLoader: ConfigLoader[ConfigList] = ConfigLoader(_.getList) - implicit val configObjectLoader: ConfigLoader[ConfigObject] = ConfigLoader(_.getObject) - implicit val seqConfigLoader: ConfigLoader[Seq[Config]] = ConfigLoader(_.getConfigList).map(_.asScala.toSeq) - - //implicit val configurationLoader: ConfigLoader[Configuration] = configLoader.map(Configuration(_)) - //implicit val seqConfigurationLoader: ConfigLoader[Seq[Configuration]] = seqConfigLoader.map(_.map(Configuration(_))) - - implicit val urlLoader: ConfigLoader[URL] = ConfigLoader(_.getString).map(new URL(_)) - implicit val uriLoader: ConfigLoader[URI] = ConfigLoader(_.getString).map(new URI(_)) - - private def javaDurationToScala(javaDuration: java.time.Duration): FiniteDuration = - Duration.fromNanos(javaDuration.toNanos) - -} - -extension (underlying: Config) { - def getOptional[A](path: String)(using loader: ConfigLoader[A]): Option[A] = { - if (underlying.hasPath(path)) Some(get[A](path)) else None - } - def get[A](path: String)(using loader: ConfigLoader[A]): A = { - loader.load(underlying, path) - } -} - diff --git a/backend/src/main/scala/very/util/entity/Pagination.scala b/backend/src/main/scala/very/util/entity/Pagination.scala deleted file mode 100644 index fd99531..0000000 --- a/backend/src/main/scala/very/util/entity/Pagination.scala +++ /dev/null @@ -1,9 +0,0 @@ -package very.util.entity - -case class Pagination(page: Int, pageSize: Int) { - assert(pageSize <= 50) - assert(page > 0) - def offset: Int = (page - 1) * pageSize - def limit: Int = pageSize - -} diff --git a/backend/src/main/scala/very/util/grpc/auth/SessionProvider.scala b/backend/src/main/scala/very/util/grpc/auth/SessionProvider.scala deleted file mode 100644 index 74911a7..0000000 --- a/backend/src/main/scala/very/util/grpc/auth/SessionProvider.scala +++ /dev/null @@ -1,10 +0,0 @@ -package very.util.grpc.auth - -import io.grpc.Context - -trait SessionProvider[V] { - def createSession(data:V):String - def getData(sessionId:String):Option[V] - val SESSION_KEY:Context.Key[V] = Context.key("session") -} - diff --git a/backend/src/main/scala/very/util/grpc/auth/SessionServerInterceptor.scala b/backend/src/main/scala/very/util/grpc/auth/SessionServerInterceptor.scala deleted file mode 100644 index 19ef424..0000000 --- a/backend/src/main/scala/very/util/grpc/auth/SessionServerInterceptor.scala +++ /dev/null @@ -1,36 +0,0 @@ -package very.util.grpc.auth - -import io.grpc.{Context, Contexts, Metadata, ServerCall, ServerCallHandler, ServerInterceptor, Status} -import io.grpc.Metadata.ASCII_STRING_MARSHALLER - -//TODO: limit try time.. - -class SessionServerInterceptor[T](sessionProvider: SessionProvider[T]) extends ServerInterceptor { - - override def interceptCall[ReqT, RespT](call: ServerCall[ReqT, RespT], headers: Metadata, next: ServerCallHandler[ReqT, RespT]): ServerCall.Listener[ReqT] = { - def error(status:Status) = { - call.close(status, Metadata()) - new ServerCall.Listener[ReqT]{} - } - Option(headers.get[String](SessionServerInterceptor.AUTHORIZATION_METADATA_KEY)) match { - case None => - error(Status.UNAUTHENTICATED.withDescription("Authorization token is missing")) - case Some(value) if !value.startsWith(SessionServerInterceptor.BEARER_TYPE) => - error(Status.UNAUTHENTICATED.withDescription("Unknown authorization type")) - case Some(value) => - val token = value.substring(SessionServerInterceptor.BEARER_TYPE.length).trim() - sessionProvider.getData(token) match { - case None => error(Status.UNAUTHENTICATED.withDescription("Authorization could not find token")) - case Some(data) => - val ctx = Context.current().withValue(sessionProvider.SESSION_KEY, data); - Contexts.interceptCall(ctx, call, headers, next) - } - } - } -} - -object SessionServerInterceptor { - val AUTHORIZATION_METADATA_KEY:Metadata.Key[String] = Metadata.Key.of("Authorization", ASCII_STRING_MARSHALLER) - val BEARER_TYPE = "Bearer" - -} \ No newline at end of file diff --git a/backend/src/main/scala/very/util/keycloak/JWKPublicKeyLocator.scala b/backend/src/main/scala/very/util/keycloak/JWKPublicKeyLocator.scala deleted file mode 100644 index 653809d..0000000 --- a/backend/src/main/scala/very/util/keycloak/JWKPublicKeyLocator.scala +++ /dev/null @@ -1,26 +0,0 @@ -package very.util.keycloak - -import org.keycloak.jose.jwk.{JSONWebKeySet, JWK} -import org.keycloak.util.{JWKSUtils, JsonSerialization} - -import scala.jdk.CollectionConverters.* -import scala.util.Try -import java.security.PublicKey - -class JWKPublicKeyLocator private(currentKeys:Map[String,PublicKey]) { - - def getPublicKey(kid:String) = { - currentKeys.get(kid) - } -} - -object JWKPublicKeyLocator { - def init(keycloakBaseUri: String, realm: String):Try[JWKPublicKeyLocator] = { - Try { - val data = scala.io.Source.fromURL(s"$keycloakBaseUri/realms/${realm}/protocol/openid-connect/certs").mkString - val jwks = JsonSerialization.readValue(data, classOf[JSONWebKeySet]) - val publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG) - JWKPublicKeyLocator(publicKeys.asScala.toMap) - } - } -} diff --git a/backend/src/main/scala/very/util/keycloak/JWKTokenVerifier.scala b/backend/src/main/scala/very/util/keycloak/JWKTokenVerifier.scala deleted file mode 100644 index 0bbf29e..0000000 --- a/backend/src/main/scala/very/util/keycloak/JWKTokenVerifier.scala +++ /dev/null @@ -1,27 +0,0 @@ -package very.util.keycloak - -import org.keycloak.TokenVerifier -import org.keycloak.representations.AccessToken - -import scala.util.Try - -class JWKTokenVerifier( - publicKeyLocator: JWKPublicKeyLocator, - keycloakBaseUri: String, - realm: String, -) { - private val realmUrl = s"$keycloakBaseUri/realms/${realm}" - def verify(token: String): Try[AccessToken] = { - val tokenVerifier = TokenVerifier - .create(token, classOf[AccessToken]) - .withDefaultChecks() - .realmUrl(realmUrl) - val kid = tokenVerifier.getHeader.getKeyId - Try { - tokenVerifier.publicKey(publicKeyLocator.getPublicKey(kid).get) - tokenVerifier.verify() - // subject is user_id - }.map(_ => tokenVerifier.getToken) - - } -} diff --git a/backend/src/main/scala/very/util/keycloak/KeycloakJWTAuthStrategy.scala b/backend/src/main/scala/very/util/keycloak/KeycloakJWTAuthStrategy.scala deleted file mode 100644 index 3ff8d18..0000000 --- a/backend/src/main/scala/very/util/keycloak/KeycloakJWTAuthStrategy.scala +++ /dev/null @@ -1,55 +0,0 @@ -package very.util.keycloak - -import org.scalatra.auth.ScentrySupport -import org.scalatra.auth.strategy.BasicAuthSupport -import com.typesafe.scalalogging.LazyLogging -import org.keycloak.TokenVerifier -import com.typesafe.scalalogging.Logger -import very.util.web.auth.AuthStrategy - -import scala.util.{ Success, Failure } - -class KeycloakJWTAuthStrategy(jwkTokenVerifier: JWKTokenVerifier, adminRole: Option[String], clientRole: Option[String]) - extends AuthStrategy[String] with LazyLogging { - // JWT - def name: String = KeycloakJWTAuthStrategy.name - - def adminAuth(token: String): Option[String] = { - jwkTokenVerifier.verify(token) match { - case Success(accessToken) => - if (adminRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) { - Some(accessToken.getSubject) - } else { - logger.info( - s"the user:${accessToken.getSubject} could not pass admin auth" - ) - None - } - case Failure(exception) => - logger.debug(s"bad token:$token", exception) - None - } - } - - def clientAuth(token: String): Option[String] = { - jwkTokenVerifier.verify(token) match { - case Success(accessToken) => - if (clientRole.fold(true)(role => accessToken.getRealmAccess.getRoles.contains(role))) { - Some(accessToken.getSubject) - } else { - logger.info( - s"the user:${accessToken.getSubject} could not pass client auth" - ) - None - } - case Failure(exception) => - logger.debug(s"bad token:$token", exception) - None - } - } - -} - -object KeycloakJWTAuthStrategy { - val name: String = "Bearer" -} diff --git a/backend/src/main/scala/very/util/persistence/FlywayMigrate.scala b/backend/src/main/scala/very/util/persistence/FlywayMigrate.scala deleted file mode 100644 index d238af2..0000000 --- a/backend/src/main/scala/very/util/persistence/FlywayMigrate.scala +++ /dev/null @@ -1,12 +0,0 @@ -package very.util.persistence - -import com.typesafe.config.Config -import org.flywaydb.core.Flyway - -//ConfigFactory.load().getConfig("database.dataSource") -def pgMigrate(config:Config) = - val url = config.getString("url") - val user = config.getString("user") - val password = config.getString("password") - val flyway = Flyway.configure().dataSource(url, user, password).load() - flyway.migrate() diff --git a/backend/src/main/scala/very/util/persistence/quill/IDSupport.scala b/backend/src/main/scala/very/util/persistence/quill/IDSupport.scala deleted file mode 100644 index 0f9930e..0000000 --- a/backend/src/main/scala/very/util/persistence/quill/IDSupport.scala +++ /dev/null @@ -1,15 +0,0 @@ -package very.util.persistence.quill - -import io.getquill.context.jdbc.JdbcContextTypes -import io.getquill.{MappedEncoding, PostgresDialect} -import org.hashids.Hashids -import very.util.security.IntID - -trait IDSupport { - this: JdbcContextTypes[PostgresDialect, _] => - - given intIDEncode: MappedEncoding[IntID, Int] = MappedEncoding(_.id) - given intIDDecode(using hashId: Hashids): MappedEncoding[Int, IntID] = MappedEncoding(IntID.apply) - given intIDListEncoder: MappedEncoding[List[IntID], List[Int]] = MappedEncoding(_.map(_.id)) - -} diff --git a/backend/src/main/scala/very/util/persistence/quill/PageSupport.scala b/backend/src/main/scala/very/util/persistence/quill/PageSupport.scala deleted file mode 100644 index d7d70fd..0000000 --- a/backend/src/main/scala/very/util/persistence/quill/PageSupport.scala +++ /dev/null @@ -1,33 +0,0 @@ -package very.util.persistence.quill - -import io.getquill.context.jdbc.JdbcContextTypes -import io.getquill.* -import very.util.entity.Pagination - -trait PageSupport[+N <: NamingStrategy] { - this: PostgresJdbcContext[N] => - - - extension[T] (inline q: Query[T]) { - inline def page(using pagination: Pagination) = { - q.drop(lift(pagination.offset)).take(lift(pagination.pageSize)) - } - - // warning: sortBy should be split, because PG would report error for count(*) - inline def pageWithCount(using pagination: Pagination) = { - (this.run(quote(q.page)), this.run(quote(q.size))) - } - - inline def pageWithCount( - sort: Query[T] => Query[T] - )(using pagination: Pagination) = { - (this.run(quote(sort(q).page)), this.run(quote(q.size))) - } - - // inline def pageWithPram(param:T => Boolean)(using pagination:Pagination) = { - // q.filter(param).page - // } - inline def single = q.take(1) - } - -} diff --git a/backend/src/main/scala/very/util/persistence/quill/ZIOJsonSupport.scala b/backend/src/main/scala/very/util/persistence/quill/ZIOJsonSupport.scala deleted file mode 100644 index 3ae8f7b..0000000 --- a/backend/src/main/scala/very/util/persistence/quill/ZIOJsonSupport.scala +++ /dev/null @@ -1,29 +0,0 @@ -package very.util.persistence.quill - -import io.getquill.context.jdbc.JdbcContextTypes -import io.getquill.PostgresDialect -import org.postgresql.util.PGobject -import zio.json.* - -trait DBSerializer -trait ZIOJsonSupport { - this: JdbcContextTypes[PostgresDialect, _] => - - given encodeJsonb[T<:DBSerializer](using JsonEncoder[T]): Encoder[T] = { - encoder( - java.sql.Types.OTHER, - (index, value, row) => { - val jsonObject = new PGobject() - jsonObject.setType("jsonb") - jsonObject.setValue(value.toJson) - row.setObject(index, jsonObject) - } - ) - } - - given decodeJsonb[T<:DBSerializer](using JsonDecoder[T]):Decoder[T] = - decoder{(index,row, session) => - val data = row.getString(index) - data.fromJson[T].toOption.get - } -} diff --git a/backend/src/main/scala/very/util/security/ID.scala b/backend/src/main/scala/very/util/security/ID.scala deleted file mode 100644 index c32ef9d..0000000 --- a/backend/src/main/scala/very/util/security/ID.scala +++ /dev/null @@ -1,22 +0,0 @@ -package very.util.security - -import org.hashids.Hashids - -sealed trait ID[T] { - def id: T - def secretId: String -} - -case class IntID(id: Int, secretId: String) extends ID[Int] -case class LongID(id: Long, secretId: String) extends ID[Long] - -object IntID { - def apply(id: Int)(using hashId: Hashids): IntID = IntID(id, hashId.encode(id)) - def apply(secretId: String)(using hashId: Hashids): IntID = IntID(hashId.decode(secretId).head.toInt, secretId) - - // given Conversion[IntID, Int] = _.id - - extension (secretId: String)(using hashId: Hashids) { - def toIntID: IntID = IntID(secretId) - } -} diff --git a/backend/src/main/scala/very/util/web/Controller.scala b/backend/src/main/scala/very/util/web/Controller.scala deleted file mode 100644 index d5124a0..0000000 --- a/backend/src/main/scala/very/util/web/Controller.scala +++ /dev/null @@ -1,53 +0,0 @@ -package very.util.web - -import com.typesafe.scalalogging.LazyLogging -import org.scalatra.json.JacksonJsonSupport -import org.scalatra.* -//import org.json4s.Formats -import org.scalatra.i18n.I18nSupport -import very.util.web.json.ZIOJsonSupport -import very.util.web.validate.ValidationExtra -import zio.NonEmptyChunk -import zio.prelude.Validation -class Controller //(using val jsonFormats: Formats) - extends ScalatraServlet - with ZIOJsonSupport - with I18nSupport - with ValidationExtra - with PaginationSupport - with LazyLogging { - override def defaultFormat: Symbol = Symbol("txt") - def badResponse(msg: String): ActionResult = { - contentType = formats("txt") - BadRequest(msg) - } - def serverError(msg: String): ActionResult = { - contentType = formats("txt") - InternalServerError(msg) - } - - errorHandler = { - case _: org.json4s.MappingException | _: java.lang.NumberFormatException | _: java.lang.AssertionError => - badResponse(messages("error.parameter_error")) - case t => - logger.error("errorHandler", t) - serverError(messages("error.internal_server_error")) - } - protected override def renderPipeline: RenderPipeline = ({ - case Validation.Success(_, value) => super.renderPipeline(value) - case Validation.Failure(_, data) => - val info = badResponse(i18n("error.request_error")(data.mkString("\n"))) - super.renderPipeline(info) - }: RenderPipeline) orElse super.renderPipeline - - /* - def created(id: Long): ActionResult = { - contentType = formats("json") - Created(s"""{"id":$id}""") - } - */ - def created(id: very.util.security.ID[_]): ActionResult = { - contentType = formats("json") - Created(s"""{"id":"${id.secretId}"}""") - } -} diff --git a/backend/src/main/scala/very/util/web/LogSupport.scala b/backend/src/main/scala/very/util/web/LogSupport.scala deleted file mode 100644 index 5f7287c..0000000 --- a/backend/src/main/scala/very/util/web/LogSupport.scala +++ /dev/null @@ -1,19 +0,0 @@ -package very.util.web - -import com.typesafe.scalalogging.Logger - -import scala.util.{Failure, Try} -trait LogSupport { - lazy val logger: Logger = - com.typesafe.scalalogging.Logger(getClass.getName.stripSuffix("$")) - - inline def logTry[T](inline errorMessage: String)(inline func: T): Try[T] = { - val result = Try(func) - result match { - case Failure(exception) => - logger.warn(errorMessage, exception) - case _ => - } - result - } -} diff --git a/backend/src/main/scala/very/util/web/PaginationSupport.scala b/backend/src/main/scala/very/util/web/PaginationSupport.scala deleted file mode 100644 index e19b04d..0000000 --- a/backend/src/main/scala/very/util/web/PaginationSupport.scala +++ /dev/null @@ -1,22 +0,0 @@ -package very.util.web - -import very.util.entity.Pagination - -trait PaginationSupport { this: org.scalatra.ScalatraBase => - - private def page = params.get("page").fold(1)(_.toInt) - - private def pageSize = params.get("pageSize").fold(10)(_.toInt) - - given pagination: Pagination = Pagination(page, pageSize) - - inline def search[T]( - arguments: Map[String, String => T => Boolean] - ): Iterable[T => Boolean] = { - for { - (k, func) <- arguments - value <- params.get(k) if value.nonEmpty - } yield func(value) - - } -} diff --git a/backend/src/main/scala/very/util/web/PingServlet.scala b/backend/src/main/scala/very/util/web/PingServlet.scala deleted file mode 100644 index df68522..0000000 --- a/backend/src/main/scala/very/util/web/PingServlet.scala +++ /dev/null @@ -1,9 +0,0 @@ -package very.util.web - -import org.scalatra.ScalatraServlet - -class PingServlet extends ScalatraServlet { - get("/") { - "pong" - } -} diff --git a/backend/src/main/scala/very/util/web/SinglePageAppResourceService.scala b/backend/src/main/scala/very/util/web/SinglePageAppResourceService.scala deleted file mode 100644 index baf21dd..0000000 --- a/backend/src/main/scala/very/util/web/SinglePageAppResourceService.scala +++ /dev/null @@ -1,17 +0,0 @@ -package very.util.web - -import jakarta.servlet.http.{HttpServletRequest, HttpServletResponse} -import org.eclipse.jetty.server.ResourceService - -class SinglePageAppResourceService extends ResourceService { - override def notFound( - request: HttpServletRequest, - response: HttpServletResponse - ) = { - if (request.getRequestURI.contains(".") && request.getMethod != "GET") { - super.notFound(request, response) - } else { - response.sendRedirect("/") - } - } -} diff --git a/backend/src/main/scala/very/util/web/auth/AuthStrategy.scala b/backend/src/main/scala/very/util/web/auth/AuthStrategy.scala deleted file mode 100644 index 9b9ceb6..0000000 --- a/backend/src/main/scala/very/util/web/auth/AuthStrategy.scala +++ /dev/null @@ -1,9 +0,0 @@ -package very.util.web.auth - -import jakarta.servlet.http.HttpServletRequest - -trait AuthStrategy[User] { - def name:String - def adminAuth(token:String): Option[User] - def clientAuth(token:String): Option[User] -} diff --git a/backend/src/main/scala/very/util/web/auth/AuthStrategyProvider.scala b/backend/src/main/scala/very/util/web/auth/AuthStrategyProvider.scala deleted file mode 100644 index cf4860f..0000000 --- a/backend/src/main/scala/very/util/web/auth/AuthStrategyProvider.scala +++ /dev/null @@ -1,10 +0,0 @@ -package very.util.web.auth - -class AuthStrategyProvider[User](strategies: List[AuthStrategy[User]]) { - private val strategyMap = strategies.map(v => v.name -> v).toMap - - def getStrategy(strategyName: String): Option[AuthStrategy[User]] = { - strategyMap.get(strategyName) - } - -} diff --git a/backend/src/main/scala/very/util/web/auth/AuthSupport.scala b/backend/src/main/scala/very/util/web/auth/AuthSupport.scala deleted file mode 100644 index 40e2fdc..0000000 --- a/backend/src/main/scala/very/util/web/auth/AuthSupport.scala +++ /dev/null @@ -1,26 +0,0 @@ -package very.util.web.auth - -import org.scalatra.{ ScalatraBase, Initializable } - -import scala.util.Try - -trait AuthSupport[User](using authStrategyProvider: AuthStrategyProvider[User]) { - self: ScalatraBase => - - def auth: User = { - Option(request.getHeader("Authorization")) match { - case Some(authorization) => - Try { - val Array(strategy, token) = authorization.split(' ') - authStrategyProvider.getStrategy(strategy).flatMap { authStrategy => - authStrategy.adminAuth(token) - } - }.toOption.flatten match { - case Some(v) => v - case None => halt(org.scalatra.Unauthorized("bad token")) - } - case None => halt(org.scalatra.Unauthorized("no authorization header")) - } - - } -} diff --git a/backend/src/main/scala/very/util/web/auth/SingleUserAuthStrategy.scala b/backend/src/main/scala/very/util/web/auth/SingleUserAuthStrategy.scala deleted file mode 100644 index 35c4760..0000000 --- a/backend/src/main/scala/very/util/web/auth/SingleUserAuthStrategy.scala +++ /dev/null @@ -1,22 +0,0 @@ -package very.util.web.auth -import io.getquill.ast.CaseClass.Single.apply - -//This is the easy way to auth -class SingleUserAuthStrategy[User](selfDefinedToken: String, user: User) - extends AuthStrategy[User] { - override def name: String = SingleUserAuthStrategy.name - - override def adminAuth( - token: String - ): Option[User] = { - if (token == selfDefinedToken) { - Some(user) - } else { None } - } - - //this would never Use - override def clientAuth(token: String): Option[User] = Some(user) -} -object SingleUserAuthStrategy { - val name:String = "ST" -} diff --git a/backend/src/main/scala/very/util/web/json/ZIOJsonSupport.scala b/backend/src/main/scala/very/util/web/json/ZIOJsonSupport.scala deleted file mode 100644 index 399f1d0..0000000 --- a/backend/src/main/scala/very/util/web/json/ZIOJsonSupport.scala +++ /dev/null @@ -1,101 +0,0 @@ -package very.util.web.json - -import jakarta.servlet.http.HttpServletRequest -import org.hashids.Hashids -import org.scalatra.* -import very.util.security.IntID -import zio.json.* -import zio.json.ast.Json - -import scala.util.{ Failure, Success, Try } - -trait ZIOJsonSupport extends ApiFormats { - - private def parsedBody[T](action: T => Any)(implicit request: HttpServletRequest, decoder: JsonDecoder[T]): Any = { - request.body.fromJson[T] match { - case Right(v) => action(v) - case Left(msg) => - contentType = formats("txt") - BadRequest(msg) - } - } - - override protected def renderPipeline: RenderPipeline = ({ - case DefaultJResponse(None) | None => - contentType = formats("txt") - response.status = 404 - response.writer.write("Not Found") - case Some(v: String) => - contentType = formats("txt") - response.writer.write(v) - case v: JResponse[?] => - contentType = formats("json") - response.writer.write(v.toJson) - }: RenderPipeline) orElse super.renderPipeline - - inline def jGet[T](transformers: RouteTransformer*)( - action: => T | (List[T], Long) - )(using JsonEncoder[T]): Route = get(transformers: _*)(action.r) - - inline def jPost[T](transformers: RouteTransformer*)( - action: => T - )(using JsonEncoder[T]): Route = post(transformers: _*)(action.r) - - inline def jPost[R](transformers: RouteTransformer*)( - action: R => Any - )(using JsonDecoder[R]): Route = post(transformers: _*) { - parsedBody[R](action) - } - - inline def jPost2[R, T](transformers: RouteTransformer*)( - action: R => T - )(using JsonDecoder[R], JsonEncoder[T]): Route = post(transformers: _*) { - - parsedBody[R](p => action(p).r) - } - inline def jPut[T](transformers: RouteTransformer*)( - action: => T - )(using JsonEncoder[T]): Route = put(transformers: _*)(action.r) - - inline def jPut[R](transformers: RouteTransformer*)( - action: R => Any - )(using JsonDecoder[R]): Route = put(transformers: _*) { - parsedBody[R](action) - } - - inline def jPut2[R, T](transformers: RouteTransformer*)( - action: R => T - )(using JsonDecoder[R], JsonEncoder[T]): Route = put(transformers: _*) { - parsedBody[R](p => action(p).r) - } - - extension [T](result: T | (List[T], Long))(using JsonEncoder[T]) { - inline def r: JResponse[T] = result match { - case (body: List[T], count: Long) => PageJResponse(count, body) - case body: T => DefaultJResponse(body) - } - } -} - -trait JResponse[T] { - def toJson: String -} -case class DefaultJResponse[T](body: T)(using JsonEncoder[T]) extends JResponse[T] { - def toJson: String = body.toJson -} -case class PageJResponse[T](total: Long, body: List[T])(using JsonEncoder[T]) extends JResponse[T] { - def toJson: String = - Json.Obj("total" -> Json.Num(total), "list" -> body.toJsonAST.getOrElse(Json.Arr())).toJson - -} -case class JsonResponse[T](body: T)(using JsonEncoder[T]) { - def toJson: String = body.toJson -} - -given intIDDecoder(using hashId: Hashids): JsonDecoder[IntID] = JsonDecoder[String].mapOrFail { v => - Try(IntID.apply(v)) match { - case Success(v) => Right(v) - case Failure(_) => Left("Invalid ID") - } -} -given intIDEncoder: JsonEncoder[IntID] = JsonEncoder.string.contramap(_.secretId) diff --git a/backend/src/main/scala/very/util/web/validate/ValidationExtra.scala b/backend/src/main/scala/very/util/web/validate/ValidationExtra.scala deleted file mode 100644 index 7aa014e..0000000 --- a/backend/src/main/scala/very/util/web/validate/ValidationExtra.scala +++ /dev/null @@ -1,34 +0,0 @@ -package very.util.web.validate - -import zio.prelude.Validation -import inet.ipaddr.IPAddress.IPVersion -import inet.ipaddr.IPAddressString -import inet.ipaddr.ipv4.IPv4Address - -import java.text.MessageFormat -import scala.util.{Success, Try} - -trait ValidationExtra {this: org.scalatra.i18n.I18nSupport & org.scalatra.ScalatraBase => - - def i18n(key:String)(arguments:AnyRef*): String = { - MessageFormat.format(messages(key), arguments:_*) - } - - - def ipV4Range(n:String) = Validation.fromEither { - Try(IPAddressString(n).toAddress(IPVersion.IPV4).asInstanceOf[IPv4Address]) match { - case Success(address) if address.isPrivate && address.getPrefixLength != null => - Right(address) - case _ => Left(i18n("error.ipv4_address_range")(n)) - } - } - - def privateIpValid(n:String) = Validation.fromEither { - Try(IPAddressString(n).toAddress(IPVersion.IPV4).asInstanceOf[IPv4Address]) match { - case Success(address) if address.isPrivate && address.getPrefixLength == null => - Right(address) - case _ => Left(i18n("error.ipv4_address")(n)) - } - } - -} \ No newline at end of file diff --git a/third/web-sugar b/third/web-sugar new file mode 160000 index 0000000..c1fa62f --- /dev/null +++ b/third/web-sugar @@ -0,0 +1 @@ +Subproject commit c1fa62f8af93172d3308e2e2deb75f712b475c10