@@ -61,13 +61,12 @@ object ApiApp extends ZIOAppDefault {
6161 val configProviderLayer = Runtime .setConfigProvider(configProvider)
6262
6363 override val bootstrap : ZLayer [ZIOAppArgs , Any , Any ] = {
64- // val fmt = LogFormat.level |-| LogFormat.annotations |-| LogFormat.line
64+ // val fmt = LogFormat.level |-| LogFormat.annotations |-| LogFormat.line
6565 // val fmt = LogFormat.annotations |-| LogFormat.line
66- // val loggingLayer = Runtime.removeDefaultLoggers >>> SLF4J.slf4j(format = fmt)
67- // val loggingLayer = zio.logging.slf4j.bridge.Slf4jBridge.initialize
66+ // val loggingLayer = Runtime.removeDefaultLoggers >>> SLF4J.slf4j(format = fmt)
67+ // val loggingLayer = zio.logging.slf4j.bridge.Slf4jBridge.initialize
6868 val loggingLayer = Runtime .removeDefaultLoggers >>> consoleLogger()
6969
70-
7170 loggingLayer
7271 ++ configProviderLayer
7372 ++ Runtime .enableAutoBlockingExecutor // TODO identify where some blocking operation markers have been forgotten
@@ -95,10 +94,22 @@ object ApiApp extends ZIOAppDefault {
9594 // Secure endpoint bases with optional bearer authentication
9695 // When auth is disabled, endpoints work without token; when enabled, token is validated
9796
98- def securityError : EndpointOutput [ApiIssue ] = jsonBody[ApiSecurityError ].map(e => e : ApiIssue ) {
99- case e : ApiSecurityError => e
100- case _ => ApiSecurityError (" Unknown security error" )
101- }
97+ def securityError : EndpointOutput [ApiIssue ] =
98+ oneOf[ApiIssue ](
99+ oneOfVariant(StatusCode .Unauthorized , jsonBody[ApiSecurityError ].map(e => e : ApiIssue ) {
100+ case e : ApiSecurityError => e
101+ case _ => ApiSecurityError (" Unknown security error" )
102+ }.description(" Authentication failed" )),
103+ oneOfVariant(StatusCode .Forbidden , jsonBody[ApiSecurityError ].map(e => e : ApiIssue ) {
104+ case e : ApiSecurityError => e
105+ case _ => ApiSecurityError (" Unknown security error" )
106+ }.description(" Insufficient permissions" )),
107+ // Fallback for any other ApiSecurityError to 401
108+ oneOfVariant(StatusCode .Unauthorized , jsonBody[ApiSecurityError ].map(e => e : ApiIssue ) {
109+ case e : ApiSecurityError => e
110+ case _ => ApiSecurityError (" Unknown security error" )
111+ })
112+ )
102113
103114 val secureSystemEndpoint = systemEndpoint.securityIn(SecureEndpoints .bearerAuth).errorOut(securityError).zServerSecurityLogic(securityLogic)
104115 val secureAdminEndpoint = adminEndpoint.securityIn(SecureEndpoints .bearerAuth).errorOut(securityError).zServerSecurityLogic(securityLogic)
@@ -591,26 +602,26 @@ object ApiApp extends ZIOAppDefault {
591602 // We need to read the config to set the cache header, but endpoints are defined as vals.
592603 // We can wrap the header logic or rely on the fact that ApiConfig.config is a ZIO effect.
593604 // Ideally, we'd restructure to build endpoints after config, but for minimal intrusion:
594- // We'll define a placeholder or read it unsafely/default if necessary?
605+ // We'll define a placeholder or read it unsafely/default if necessary?
595606 // No, let's use a serverLogic wrapper or just a fixed value if dynamic is too hard in this structure.
596- // Actually, 'header' in tapir output is static unless mapped.
607+ // Actually, 'header' in tapir output is static unless mapped.
597608 // Let's use a Task/ZIO to build the header output? Tapir doesn't support ZIO in definition easily.
598609 // Best approach: Use a var or lazy val initialized early, OR just assume a default since we can't change the val structure easily without refactoring 'ApiApp' into a class/layer.
599-
610+
600611 // WAIT: ApiApp is an object extending ZIOAppDefault. config is available via ApiConfig.config effect.
601612 // But 'mediaContentGetOriginalEndpoint' is a val. It is evaluated at initialization time.
602613 // We cannot use the effectful config here.
603614 // HOWEVER: We can use `out(header[String]("Cache-Control"))` and provide the value in the serverLogic.
604-
615+
605616 // Let's modify the endpoints to include the header in the output definition, and then provide the value in the logic.
606-
617+
607618 // Helper to add cache control
608619 def addCacheHeader [R , E , Err ](logic : ZIO [R , E , (String , ZStream [Any , Err , Byte ])]): ZIO [R , E , (String , String , ZStream [Any , Err , Byte ])] = {
609620 for {
610- config <- ApiConfig .config.orDie
611- tuple <- logic
621+ config <- ApiConfig .config.orDie
622+ tuple <- logic
612623 (contentType, stream) = tuple
613- cacheControl = s " private, max-age= ${config.cacheMaxAgeSeconds}"
624+ cacheControl = s " private, max-age= ${config.cacheMaxAgeSeconds}"
614625 } yield (contentType, cacheControl, stream)
615626 }
616627
@@ -906,14 +917,13 @@ object ApiApp extends ZIOAppDefault {
906917 .errorOutVariantPrepend(statusForApiInternalError)
907918 .errorOutVariantPrepend(statusForApiResourceNotFound)
908919 .errorOutVariantPrepend(statusForApiInvalidRequestError)
909- .serverLogic[ApiEnv ] { user =>
910- rawFaceId =>
911- for {
912- faceId <- extractFaceId(rawFaceId)
913- _ <- MediaService
914- .faceDelete(faceId)
915- .mapError(err => ApiInternalError (" Couldn't delete face" ))
916- } yield ()
920+ .serverLogic[ApiEnv ] { user => rawFaceId =>
921+ for {
922+ faceId <- extractFaceId(rawFaceId)
923+ _ <- MediaService
924+ .faceDelete(faceId)
925+ .mapError(err => ApiInternalError (" Couldn't delete face" ))
926+ } yield ()
917927 }
918928
919929 def faceGetImageBytesLogic (faceId : FaceId ) = {
@@ -1017,11 +1027,10 @@ object ApiApp extends ZIOAppDefault {
10171027 .in(" synchronize" )
10181028 .in(query[Option [Int ]](" addedThoseLastDays" ).description(" for faster synchronize operations provide how much days back to look for new medias" ))
10191029 .errorOutVariantPrepend(statusForApiInternalError)
1020- .serverLogic[ApiEnv ] { user =>
1021- addedThoseLastDays =>
1022- MediaService
1023- .synchronizeStart(addedThoseLastDays)
1024- .mapError(err => ApiInternalError (" Couldn't synchronize" ))
1030+ .serverLogic[ApiEnv ] { user => addedThoseLastDays =>
1031+ MediaService
1032+ .synchronizeStart(addedThoseLastDays)
1033+ .mapError(err => ApiInternalError (" Couldn't synchronize" ))
10251034 }
10261035
10271036 val adminSynchronizeStatusEndpoint =
@@ -1032,12 +1041,11 @@ object ApiApp extends ZIOAppDefault {
10321041 .in(" synchronize" )
10331042 .out(jsonBody[ApiSynchronizeStatus ])
10341043 .errorOutVariantPrepend(statusForApiInternalError)
1035- .serverLogic[ApiEnv ] { user =>
1036- _ =>
1037- MediaService
1038- .synchronizeStatus()
1039- .map(_.transformInto[ApiSynchronizeStatus ])
1040- .mapError(err => ApiInternalError (" Couldn't get synchronize status" ))
1044+ .serverLogic[ApiEnv ] { user => _ =>
1045+ MediaService
1046+ .synchronizeStatus()
1047+ .map(_.transformInto[ApiSynchronizeStatus ])
1048+ .mapError(err => ApiInternalError (" Couldn't get synchronize status" ))
10411049 }
10421050
10431051 // -------------------------------------------------------------------------------------------------------------------
@@ -1491,6 +1499,33 @@ object ApiApp extends ZIOAppDefault {
14911499 .errorOutVariantPrepend(statusForApiInternalError)
14921500 .serverLogic[ApiEnv ](user => _ => serviceInfoLogic)
14931501
1502+ val serviceClientConfigEndpoint =
1503+ systemEndpoint.get
1504+ .in(" config" )
1505+ .out(jsonBody[ApiClientConfig ])
1506+ .zServerLogic[ApiEnv ] { _ =>
1507+ for {
1508+ config <- ApiConfig .config.orDie // .mapError(err => ApiInternalError("Couldn't get client configuration"))
1509+ auth = config.auth
1510+ issuer = auth.issuer
1511+ parts = issuer.split(" /realms/" )
1512+ } yield {
1513+ val (url, realm) = if (parts.length >= 2 ) {
1514+ (parts(0 ), parts(1 ))
1515+ } else {
1516+ (" http://127.0.0.1:8081" , " sotohp" )
1517+ }
1518+ ApiClientConfig (
1519+ auth = ApiClientAuth (
1520+ enabled = auth.enabled,
1521+ url = url,
1522+ realm = realm,
1523+ clientId = auth.clientId
1524+ )
1525+ )
1526+ }
1527+ }
1528+
14941529 // -------------------------------------------------------------------------------------------------------------------
14951530 val apiRoutes = List (
14961531 // -------------------------
@@ -1545,7 +1580,8 @@ object ApiApp extends ZIOAppDefault {
15451580 adminSynchronizeStatusEndpoint,
15461581 // -------------------------
15471582 serviceStatusEndpoint,
1548- serviceInfoEndpoint
1583+ serviceInfoEndpoint,
1584+ serviceClientConfigEndpoint
15491585 )
15501586
15511587 def apiDocRoutes =
@@ -1651,23 +1687,25 @@ object ApiApp extends ZIOAppDefault {
16511687 }
16521688
16531689 val securityServiceLayer : ZLayer [Any , Nothing , SecurityService ] =
1654- ZLayer .fromZIO {
1655- ApiConfig .config.map(_.auth).orDie
1656- }.flatMap { authConfigEnv =>
1657- val authConfig = authConfigEnv.get
1658- if (authConfig.enabled) {
1659- ZLayer .succeed(authConfig) >>> SecurityService .live
1660- } else {
1661- SecurityService .disabled
1690+ ZLayer
1691+ .fromZIO {
1692+ ApiConfig .config.map(_.auth).orDie
1693+ }
1694+ .flatMap { authConfigEnv =>
1695+ val authConfig = authConfigEnv.get
1696+ if (authConfig.enabled) {
1697+ ZLayer .succeed(authConfig) >>> SecurityService .live
1698+ } else {
1699+ SecurityService .disabled
1700+ }
16621701 }
1663- }
16641702
16651703 override def run = {
16661704 getArgs.flatMap { args =>
16671705 args.toList match {
16681706 case " --just-generate-openapi-specs" :: fileName :: Nil =>
16691707 generateOpenApiSpec(fileName)
1670- case _ =>
1708+ case _ =>
16711709 for {
16721710 config <- ApiConfig .config
16731711 _ <- ZIO .logInfo(s " Authentication enabled: ${config.auth.enabled}" )
0 commit comments