From bee3a3b80f38b8d3424ecc5aab1c02e3d665f0df Mon Sep 17 00:00:00 2001 From: rbolivar Date: Tue, 24 Jun 2025 15:58:29 -0500 Subject: [PATCH 01/15] bff and auth services --- auth-backend-service/.gitattributes | 3 + auth-backend-service/.gitignore | 37 +++ auth-backend-service/build.gradle | 92 +++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + auth-backend-service/gradlew | 251 ++++++++++++++++++ auth-backend-service/gradlew.bat | 94 +++++++ auth-backend-service/settings.gradle | 1 + .../aws/ws/AuthBackendServiceApplication.java | 13 + .../aws/ws/application/config/BeanConfig.java | 21 ++ .../ws/application/config/DynamoDBConfig.java | 38 +++ .../ws/application/config/SecurityConfig.java | 67 +++++ .../ws/application/config/SwaggerConfig.java | 65 +++++ .../com/aws/ws/domain/api/UserAdapter.java | 14 + .../domain/exception/BusinessException.java | 21 ++ .../exception/DuplicateResourceException.java | 16 ++ .../exception/InvalidValueException.java | 7 + .../domain/exception/NoContentException.java | 10 + .../ws/domain/exception/TechnicalMessage.java | 40 +++ .../java/com/aws/ws/domain/model/User.java | 19 ++ .../aws/ws/domain/spi/UserServicePort.java | 12 + .../aws/ws/domain/usecase/UserUseCase.java | 52 ++++ .../persistence/UserPersistenceAdapter.java | 127 +++++++++ .../persistence/entity/UserEntity.java | 32 +++ .../persistence/mapper/UserEntityMapper.java | 7 + .../exception/DatabaseResourceException.java | 13 + .../common/exception/NotFoundException.java | 17 ++ .../common/exception/ProcessorException.java | 14 + .../common/exception/TechnicalException.java | 14 + .../exception/UnauthorizedException.java | 9 + .../common/handler/APIResponse.java | 20 ++ .../common/handler/ErrorDTO.java | 22 ++ .../common/handler/GlobalErrorHandler.java | 144 ++++++++++ .../common/handler/JwtAuthFilter.java | 62 +++++ .../common/handler/MessageHeaderHandler.java | 23 ++ .../infrastructure/common/util/Constants.java | 11 + .../common/util/Converters.java | 30 +++ .../infrastructure/inbound/RouterConfig.java | 88 ++++++ .../infrastructure/inbound/dto/UserDTO.java | 25 ++ .../inbound/handler/UserHandler.java | 53 ++++ .../inbound/mapper/UserMapper.java | 19 ++ .../src/main/resources/application.properties | 14 + .../src/main/resources/application.yml | 12 + .../AuthBackendServiceApplicationTests.java | 13 + bff-aws-localstack/.gitattributes | 3 + bff-aws-localstack/.gitignore | 37 +++ bff-aws-localstack/build.gradle | 102 +++++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + bff-aws-localstack/gradlew | 251 ++++++++++++++++++ bff-aws-localstack/gradlew.bat | 94 +++++++ bff-aws-localstack/settings.gradle | 1 + .../java/com/aws/ws/ApiGwApplication.java | 13 + .../ws/config/AllowedOriginsProperties.java | 15 ++ .../java/com/aws/ws/config/CustomHeader.java | 18 ++ .../aws/ws/filters/GlobalPostFiltering.java | 21 ++ .../aws/ws/filters/GlobalPreFiltering.java | 68 +++++ .../aws/ws/ssl/HostnameVerifierConfig.java | 16 ++ .../java/com/aws/ws/ssl/SSLCertConfig.java | 50 ++++ .../src/main/resources/application.yml | 43 +++ .../src/main/resources/routes.yml | 24 ++ .../com/aws/ws/ApiGwApplicationTests.java | 13 + dashboard-ui/proxy.conf.js | 6 +- 63 files changed, 2430 insertions(+), 1 deletion(-) create mode 100644 auth-backend-service/.gitattributes create mode 100644 auth-backend-service/.gitignore create mode 100644 auth-backend-service/build.gradle create mode 100644 auth-backend-service/gradle/wrapper/gradle-wrapper.jar create mode 100644 auth-backend-service/gradle/wrapper/gradle-wrapper.properties create mode 100644 auth-backend-service/gradlew create mode 100644 auth-backend-service/gradlew.bat create mode 100644 auth-backend-service/settings.gradle create mode 100644 auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/application/config/DynamoDBConfig.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/application/config/SwaggerConfig.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/exception/BusinessException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/exception/DuplicateResourceException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/exception/InvalidValueException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/exception/NoContentException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/exception/TechnicalMessage.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/DatabaseResourceException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/ProcessorException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/TechnicalException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/UnauthorizedException.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/APIResponse.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/ErrorDTO.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/GlobalErrorHandler.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/JwtAuthFilter.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/MessageHeaderHandler.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Constants.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Converters.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java create mode 100644 auth-backend-service/src/main/resources/application.properties create mode 100644 auth-backend-service/src/main/resources/application.yml create mode 100644 auth-backend-service/src/test/java/com/aws/ws/AuthBackendServiceApplicationTests.java create mode 100644 bff-aws-localstack/.gitattributes create mode 100644 bff-aws-localstack/.gitignore create mode 100644 bff-aws-localstack/build.gradle create mode 100644 bff-aws-localstack/gradle/wrapper/gradle-wrapper.jar create mode 100644 bff-aws-localstack/gradle/wrapper/gradle-wrapper.properties create mode 100644 bff-aws-localstack/gradlew create mode 100644 bff-aws-localstack/gradlew.bat create mode 100644 bff-aws-localstack/settings.gradle create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/ApiGwApplication.java create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/config/AllowedOriginsProperties.java create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/config/CustomHeader.java create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPostFiltering.java create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPreFiltering.java create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/ssl/HostnameVerifierConfig.java create mode 100644 bff-aws-localstack/src/main/java/com/aws/ws/ssl/SSLCertConfig.java create mode 100644 bff-aws-localstack/src/main/resources/application.yml create mode 100644 bff-aws-localstack/src/main/resources/routes.yml create mode 100644 bff-aws-localstack/src/test/java/com/aws/ws/ApiGwApplicationTests.java diff --git a/auth-backend-service/.gitattributes b/auth-backend-service/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/auth-backend-service/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/auth-backend-service/.gitignore b/auth-backend-service/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/auth-backend-service/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/auth-backend-service/build.gradle b/auth-backend-service/build.gradle new file mode 100644 index 0000000..037910e --- /dev/null +++ b/auth-backend-service/build.gradle @@ -0,0 +1,92 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.0' + id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' +} + +group = 'com.aws' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', "2025.0.0") +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.7.0' + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j' + implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' + implementation 'org.springframework.boot:spring-boot-starter-security' + + // AWS SDK dependencies + implementation 'software.amazon.awssdk:auth' + implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.658' + implementation 'software.amazon.awssdk:dynamodb:2.24.2' + implementation 'software.amazon.awssdk:dynamodb-enhanced:2.24.2' + implementation 'org.springframework.data:spring-data-commons:3.5.0' + + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.core:jackson-core' + implementation 'com.fasterxml.jackson.core:jackson-annotations' + + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mockito:mockito-core:5.12.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.12.0' + testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +jacoco { + toolVersion = "0.8.10" +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + csv.required = false + html.outputLocation = layout.buildDirectory.dir("jacocoHtml") + } +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.80 + } + } + } +} + +check.dependsOn jacocoTestCoverageVerification + +tasks.named('test') { + useJUnitPlatform() +} \ No newline at end of file diff --git a/auth-backend-service/gradle/wrapper/gradle-wrapper.jar b/auth-backend-service/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/auth-backend-service/gradlew.bat b/auth-backend-service/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/auth-backend-service/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/auth-backend-service/settings.gradle b/auth-backend-service/settings.gradle new file mode 100644 index 0000000..f7d2793 --- /dev/null +++ b/auth-backend-service/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'auth-backend-service' diff --git a/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java b/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java new file mode 100644 index 0000000..b58983e --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java @@ -0,0 +1,13 @@ +package com.aws.ws; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AuthBackendServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AuthBackendServiceApplication.class, args); + } + +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java new file mode 100644 index 0000000..24a1d4b --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java @@ -0,0 +1,21 @@ +package com.aws.ws.application.config; + +import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.usecase.UserUseCase; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BeanConfig { + + @Bean + public UserUseCase catalogUseCase(UserAdapter userAdapter) { + return new UserUseCase(userAdapter); + } + +// @Bean +// public CatalogPersistenceAdapter catalogPersistenceAdapter(DynamoDbAsyncClient client) { +// return new CatalogPersistenceAdapter(client); +// } + +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/DynamoDBConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/DynamoDBConfig.java new file mode 100644 index 0000000..6ecf623 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/DynamoDBConfig.java @@ -0,0 +1,38 @@ +package com.aws.ws.application.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; + +import java.net.URI; + +@Configuration +public class DynamoDBConfig { + + @Value("${aws.region}") + private String region; + + @Value("${aws.dynamodb.endpoint}") + private String dynamoEndpoint; + + @Value("${aws.secret.access-key}") + private String secretAccessKey; + + @Value("${aws.secret.key-id}") + private String accessKeyId; + + @Bean + public DynamoDbAsyncClient dynamoDbAsyncClient() { + return DynamoDbAsyncClient.builder() + .region(Region.of(region)) + .endpointOverride(URI.create(dynamoEndpoint)) // LocalStack + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create(accessKeyId, secretAccessKey) + )) + .build(); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java new file mode 100644 index 0000000..5bd4d72 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java @@ -0,0 +1,67 @@ +package com.aws.ws.application.config; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.web.server.SecurityWebFilterChain; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; + +@Configuration +@EnableWebFluxSecurity +public class SecurityConfig { + +// @Value("${spring.security.keycloak.jwk-set-uri}") +// private String jwkSetUri; +// +// @Value("${jwt.public.key}") +// private RSAPublicKey publicKey; +// +// @Value("${jwt.private.key}") +// private RSAPrivateKey privateKey; + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { + return http + .csrf(ServerHttpSecurity.CsrfSpec::disable) + .authorizeExchange(exchange -> exchange + .pathMatchers("/swagger-ui/**").permitAll() + .pathMatchers("/v3/api-docs/**").permitAll() + .pathMatchers("/webjars/**").permitAll() + .pathMatchers("/api-docs/**").permitAll() + .pathMatchers("/actuator/**").permitAll() + .pathMatchers(HttpMethod.POST, "/v1/users").authenticated() + .pathMatchers(HttpMethod.GET, "/v1/users/{email}").authenticated() + .anyExchange().authenticated() + ) +// .oauth2ResourceServer(oauth2 -> oauth2 +// .jwt(jwt -> jwt +// .jwkSetUri(jwkSetUri) +// ) +// ) + .build(); + } + +// @Bean +// JwtDecoder jwtDecoder() { +// return NimbusJwtDecoder.withPublicKey(this.publicKey).build(); +// } +// +// @Bean +// JwtEncoder jwtEncoder() { +// JWK jwk = new RSAKey.Builder(this.publicKey).privateKey(this.privateKey).build(); +// return new NimbusJwtEncoder(new ImmutableJWKSet<>(new JWKSet(jwk))); +// } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/SwaggerConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/SwaggerConfig.java new file mode 100644 index 0000000..8e1c32f --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/SwaggerConfig.java @@ -0,0 +1,65 @@ +package com.aws.ws.application.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI myOpenAPI( + @Value("${openapi.service.title}") String serviceTitle, + @Value("${openapi.service.version}") String serviceVersion, + @Value("${openapi.service.description}") String description, + @Value("${openapi.service.contact.email}") String contactEmail, + @Value("${openapi.service.contact.name}") String contactName, + @Value("${openapi.service.host}") String host) { + + // Security scheme name for JWT authentication + final String securitySchemeName = "bearerAuth"; + + // Contact information for the service + Contact contact = new Contact() + .email(contactEmail) + .name(contactName); + + // License information for the service + License mitLicense = new License() + .name("MIT License") + .url("https://opensource.org/licenses/MIT"); + + // Service information for the OpenAPI specification + Info info = new Info() + .title(serviceTitle) + .version(serviceVersion) + .contact(contact) + .description(description) + .termsOfService("https://opensource.org/licenses/MIT") + .license(mitLicense); + + return new OpenAPI() + .info(info) + .servers(List.of(new Server().url(host).description(description))) + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + // Define the security scheme for JWT authentication + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } + +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java new file mode 100644 index 0000000..7f7ae62 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java @@ -0,0 +1,14 @@ +package com.aws.ws.domain.api; + +import com.aws.ws.domain.model.User; +import reactor.core.publisher.Mono; + +public interface UserAdapter { +// Mono existsByEmail(String email); +// +// Mono getByUserId(String userId); + + Mono createUser(User domain); + + Mono findUserByEmail(String email); +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/exception/BusinessException.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/BusinessException.java new file mode 100644 index 0000000..5b4718b --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/BusinessException.java @@ -0,0 +1,21 @@ +package com.aws.ws.domain.exception; + +import lombok.Getter; + +@Getter +public class BusinessException extends RuntimeException { + private final String code; + private final String parameter; + + public BusinessException(TechnicalMessage message, String parameter) { + super(message.getMessage()); + this.code = "BUSINESS_ERROR"; + this.parameter = parameter; + } + + public BusinessException(String message, String code, String parameter) { + super(message); + this.code = code; + this.parameter = parameter; + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/exception/DuplicateResourceException.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/DuplicateResourceException.java new file mode 100644 index 0000000..55c6a43 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/DuplicateResourceException.java @@ -0,0 +1,16 @@ +package com.aws.ws.domain.exception; + +import lombok.Getter; + +@Getter +public class DuplicateResourceException extends RuntimeException { + + private final String code; + private final String parameter; + + public DuplicateResourceException(TechnicalMessage message, String code, String parameter) { + super(message.getMessage()); + this.code = code; + this.parameter = parameter; + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/exception/InvalidValueException.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/InvalidValueException.java new file mode 100644 index 0000000..e56be3a --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/InvalidValueException.java @@ -0,0 +1,7 @@ +package com.aws.ws.domain.exception; + +public class InvalidValueException extends BusinessException { + public InvalidValueException(String parameter, String message) { + super(message, "INVALID_VALUE", parameter); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NoContentException.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NoContentException.java new file mode 100644 index 0000000..e2cc7ea --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NoContentException.java @@ -0,0 +1,10 @@ +package com.aws.ws.domain.exception; + +import com.aws.ws.infrastructure.common.exception.TechnicalException; + +public class NoContentException extends TechnicalException { + + public NoContentException(TechnicalMessage message) { + super(message); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/exception/TechnicalMessage.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/TechnicalMessage.java new file mode 100644 index 0000000..a7e8ed0 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/TechnicalMessage.java @@ -0,0 +1,40 @@ +package com.aws.ws.domain.exception; + +import lombok.Getter; + +@Getter +public enum TechnicalMessage { + INTERNAL_SERVER_ERROR("500", "Internal server error", ""), + INTERNAL_ERROR_IN_ADAPTERS("503", "Internal error in adapters", ""), + MINIMUM_OR_MAXIMUM_CAPACITY("400", "A capability must have 3 to 20 unique technologies.", ""), + BAD_REQUEST("400", "Bad request", ""), + NOT_FOUND("404", "Not found", ""), + NO_CONTENT("204", "No content", ""), + INVALID_REQUEST("400", "Request null or incomplete", ""), + ALREADY_EXISTS("409", "Already exists", ""), + NOT_ONLY_NUMBERS("400", "The field must contain only numbers", ""), + NAME_CHARACTER_LIMIT("400", "Name must be between 3 and 50 characters", ""), + DESCRIPTION_CHARACTER_LIMIT("400", "Description must be between 3 and 100 characters", ""), + RESOURCE_NOT_FOUND("404", "Resource not found", ""), + RESOURCE_DELETION_FAILED("500", "Resource deletion failed", ""), + RESOURCE_ALREADY_EXISTS("409", "Resource already exists", ""), + RESOURCE_CREATION_FAILED("500", "Resource creation failed", ""), + UNKNOWN_ERROR("500", "Unknown error occurred", ""), + RESOURCE_CREATED("201", "Resource created successfully", ""), + INVALID_PARAMETERS("400", "Invalid parameters provided", ""), + INTERNAL_ERROR("500", "An internal error occurred", ""), + RESOURCE_ERROR("500", "An error occurred while processing the technology", ""), + DATABASE_ERROR("500", "Database error occurred", ""), + X_MESSAGE_ID("X-Message-ID", "Unique identifier for the message", ""), + UNAUTHORIZED("401", "Unauthorized access", ""); + + private final String code; + private final String message; + private final String parameter; + + TechnicalMessage(String code, String message, String parameter) { + this.code = code; + this.message = message; + this.parameter = parameter; + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java b/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java new file mode 100644 index 0000000..67deb6f --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java @@ -0,0 +1,19 @@ +package com.aws.ws.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class User { + private String ID; + private String email; + private String firstName; + private String lastName; + private String password; + private String role; +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java new file mode 100644 index 0000000..ef8c58f --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java @@ -0,0 +1,12 @@ +package com.aws.ws.domain.spi; + +import com.aws.ws.domain.model.User; +import reactor.core.publisher.Mono; + +public interface UserServicePort { +// Mono getByUserId(String userId); + + Mono createUser(User domain); + + Mono findUserByEmail(String email); +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java new file mode 100644 index 0000000..f53ce3c --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -0,0 +1,52 @@ +package com.aws.ws.domain.usecase; + +import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.exception.DuplicateResourceException; +import com.aws.ws.domain.exception.InvalidValueException; +import com.aws.ws.domain.exception.TechnicalMessage; +import com.aws.ws.domain.model.User; +import com.aws.ws.domain.spi.UserServicePort; +import reactor.core.publisher.Mono; + +public class UserUseCase implements UserServicePort { + + private final UserAdapter userAdapter; + + public UserUseCase(UserAdapter userAdapter) { + this.userAdapter = userAdapter; + } + + @Override + public Mono createUser(User domain) { + if (domain.getEmail() == null || domain.getPassword() == null || + domain.getFirstName() == null || domain.getLastName() == null || domain.getRole() == null) { + return Mono.error(new InvalidValueException("User ID, firstName, lastName, email, password and Role", "must not be null")); + } + + return userAdapter.findUserByEmail(domain.getEmail()) + .flatMap(exists -> { + if (exists != null) { + return Mono.error(new DuplicateResourceException( + TechnicalMessage.ALREADY_EXISTS, "User already exists with email: ", domain.getEmail())); + } else { + return userAdapter.createUser(domain); + } + }); + } + + @Override + public Mono findUserByEmail(String email) { + if (email == null || email.isEmpty()) { + return Mono.error(new IllegalArgumentException("Email must not be null or empty")); + } + + return userAdapter.findUserByEmail(email) + .switchIfEmpty(Mono.error(new RuntimeException("User not found with email: " + email))); + } + +// @Override +// public Mono getByUserId(String userId) { +// return userAdapter.getByUserId(userId) +// .switchIfEmpty(Mono.error(new RuntimeException("User not found with id: " + userId))); +// } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java new file mode 100644 index 0000000..0301860 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -0,0 +1,127 @@ +package com.aws.ws.infrastructure.adapters.persistence; + +import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.model.User; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.*; + +import java.util.Map; +import java.util.UUID; + +@Service +public class UserPersistenceAdapter implements UserAdapter { + + @Value("${aws.dynamodb.table-name}") + private String tableName; + + private final DynamoDbAsyncClient client; + private final ObjectMapper mapper = new ObjectMapper(); + + public UserPersistenceAdapter(DynamoDbAsyncClient client) { + this.client = client; + } + + @Override + public Mono createUser(User user) { + if (user.getID() == null) { + user.setID(UUID.randomUUID().toString()); + } + + PutItemRequest request = PutItemRequest.builder() + .tableName(tableName) + .item(Map.of( + "ID", AttributeValue.builder().s(user.getID()).build(), + "email", AttributeValue.builder().s(user.getEmail()).build(), + "firstName", AttributeValue.builder().s(user.getFirstName()).build(), + "lastName", AttributeValue.builder().s(user.getLastName()).build(), + "password", AttributeValue.builder().s(user.getPassword()).build(), + "role", AttributeValue.builder().s(user.getRole()).build() + )) + .build(); + + return Mono.fromFuture(() -> client.putItem(request)) + .thenReturn(user); + } + + @Override + public Mono findUserByEmail(String email) { + ScanRequest request = ScanRequest.builder() + .tableName(tableName) + .filterExpression("email = :emailVal") + .expressionAttributeValues(Map.of(":emailVal", AttributeValue.builder().s(email).build())) + .build(); + + return Mono.fromFuture(() -> client.scan(request)) + .map(ScanResponse::items) + .mapNotNull(items -> items.isEmpty() ? null : convert(items.getFirst())); + } + + private User convert(Map item) { + User user = new User(); + user.setID(item.get("ID").s()); + user.setEmail(item.get("email").s()); + user.setFirstName(item.get("firstName").s()); + user.setLastName(item.get("lastName").s()); + user.setPassword(item.get("password").s()); + user.setRole(item.get("role").s()); + return user; + } + +// @Override +// public Mono existsByEmail(String email) { +// Map key = new HashMap<>(); +// key.put("email", AttributeValue.builder().s(email).build()); +// +// GetItemRequest request = GetItemRequest.builder() +// .tableName(tableName) +// .key(key) +// .build(); +// +// CompletableFuture future = client.getItem(request); +// +// return Mono.fromFuture(future) +// .map(GetItemResponse::hasItem) +// .onErrorReturn(false); // Si ocurre un error, asumimos que el usuario no existe +// } +// +// @Override +// public Mono getByUserId(String userId) { +// Map key = new HashMap<>(); +// key.put("ID", AttributeValue.builder().s(userId).build()); +// +// GetItemRequest request = GetItemRequest.builder() +// .tableName(tableName) +// .key(key) +// .build(); +// +// CompletableFuture future = client.getItem(request); +// +// return Mono.fromFuture(future) +// .handle((response, sink) -> { +// if (!response.hasItem()) { +// sink.error(new NotFoundException( +// TechnicalMessage.NOT_FOUND, "Catalog not found with catalogId: ", userId)); +// return; +// } +// +// Map item = response.item(); +// +// User user = new User(); +// //user.setCatalogId(item.get("ID").s()); // ✅ ID for catalogId +// // user.setCatalogName(item.get("catalogName").s()); +// +// try { +// String itemsJson = item.get("items").s(); +// //user.setItems(mapper.readValue(itemsJson, new TypeReference<>() {})); +// } catch (Exception e) { +// sink.error(new RuntimeException("Error parsing items", e)); +// return; +// } +// sink.next(user); +// }); +// } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java new file mode 100644 index 0000000..c45be33 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java @@ -0,0 +1,32 @@ +package com.aws.ws.infrastructure.adapters.persistence.entity; + +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; +import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; + +@Getter +@Setter +@DynamoDBTable(tableName = "${aws.dynamodb.table-name}") +public class UserEntity { + @Id + @DynamoDBHashKey(attributeName = "ID") + private String ID; + + @DynamoDBAttribute + private String email; + + @DynamoDBAttribute + private String firstName; + + @DynamoDBAttribute + private String lastName; + + @DynamoDBAttribute + private String password; + + @DynamoDBAttribute + private String role; +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java new file mode 100644 index 0000000..a5b944f --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java @@ -0,0 +1,7 @@ +package com.aws.ws.infrastructure.adapters.persistence.mapper; + +import org.springframework.stereotype.Component; + +@Component +public class UserEntityMapper { +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/DatabaseResourceException.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/DatabaseResourceException.java new file mode 100644 index 0000000..843e84e --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/DatabaseResourceException.java @@ -0,0 +1,13 @@ +package com.aws.ws.infrastructure.common.exception; + +public class DatabaseResourceException extends RuntimeException { + + public DatabaseResourceException(String message, Throwable cause) { + super(message, cause); + } + + public DatabaseResourceException(String message) { + super(message); + } +} + diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java new file mode 100644 index 0000000..efc9cd2 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java @@ -0,0 +1,17 @@ +package com.aws.ws.infrastructure.common.exception; + +import com.aws.ws.domain.exception.TechnicalMessage; +import lombok.Getter; + +@Getter +public class NotFoundException extends RuntimeException { + + private final String code; + private final String parameter; + + public NotFoundException(TechnicalMessage message, String code, String parameter) { + super(message.getMessage()); + this.code = code; + this.parameter = parameter; + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/ProcessorException.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/ProcessorException.java new file mode 100644 index 0000000..a436dff --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/ProcessorException.java @@ -0,0 +1,14 @@ +package com.aws.ws.infrastructure.common.exception; + +import com.aws.ws.domain.exception.TechnicalMessage; + +public class ProcessorException extends TechnicalException { + + public ProcessorException(TechnicalMessage message) { + super(message); + } + + public ProcessorException(TechnicalMessage message, Throwable cause) { + super(message, cause); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/TechnicalException.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/TechnicalException.java new file mode 100644 index 0000000..33e3acd --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/TechnicalException.java @@ -0,0 +1,14 @@ +package com.aws.ws.infrastructure.common.exception; + +import com.aws.ws.domain.exception.TechnicalMessage; + +public class TechnicalException extends RuntimeException { + + public TechnicalException(TechnicalMessage message) { + super(message.getMessage()); + } + + public TechnicalException(TechnicalMessage message, Throwable cause) { + super(message.getMessage(), cause); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/UnauthorizedException.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/UnauthorizedException.java new file mode 100644 index 0000000..46b9da5 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/UnauthorizedException.java @@ -0,0 +1,9 @@ +package com.aws.ws.infrastructure.common.exception; + +import com.aws.ws.domain.exception.TechnicalMessage; + +public class UnauthorizedException extends RuntimeException { + public UnauthorizedException(TechnicalMessage message) { + super(message.getMessage()); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/APIResponse.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/APIResponse.java new file mode 100644 index 0000000..c16add5 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/APIResponse.java @@ -0,0 +1,20 @@ +package com.aws.ws.infrastructure.common.handler; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +@ToString +public class APIResponse { + private int code; + private String message; + private String identifier; + private String timestamp; + private List errors; +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/ErrorDTO.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/ErrorDTO.java new file mode 100644 index 0000000..d56a938 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/ErrorDTO.java @@ -0,0 +1,22 @@ +package com.aws.ws.infrastructure.common.handler; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ErrorDTO { + private String message; + private String parameter; + + public static ErrorDTO of(String message, String parameter) { + return ErrorDTO.builder() + .message(message) + .parameter(parameter) + .build(); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/GlobalErrorHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/GlobalErrorHandler.java new file mode 100644 index 0000000..993aefa --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/GlobalErrorHandler.java @@ -0,0 +1,144 @@ +package com.aws.ws.infrastructure.common.handler; + +import com.aws.ws.domain.exception.*; +import com.aws.ws.infrastructure.common.exception.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import java.time.Instant; +import java.util.List; + +@Component +@Slf4j +@Order(-2) +public class GlobalErrorHandler { + + public Mono handle(Throwable throwable, String messageId) { + log.error("Exception captured globally: {}", throwable.toString()); + + return switch (throwable) { + + case UnauthorizedException ex -> buildErrorResponse( + HttpStatus.UNAUTHORIZED, + messageId, + TechnicalMessage.UNAUTHORIZED, + List.of(ErrorDTO.of( + ex.getMessage(), + TechnicalMessage.UNAUTHORIZED.getParameter() + )) + ); + + case BusinessException ex -> buildErrorResponse( + HttpStatus.BAD_REQUEST, + messageId, + TechnicalMessage.BAD_REQUEST, + List.of(ErrorDTO.of( + ex.getMessage(), + ex.getParameter() + )) + ); + + case DuplicateResourceException ex -> buildErrorResponse( + HttpStatus.CONFLICT, + messageId, + TechnicalMessage.ALREADY_EXISTS, + List.of(ErrorDTO.of( + ex.getMessage(), + ex.getParameter() + )) + ); + + case NotFoundException ex -> buildErrorResponse( + HttpStatus.NOT_FOUND, + messageId, + TechnicalMessage.NOT_FOUND, + List.of(ErrorDTO.of( + ex.getMessage(), + ex.getParameter() + )) + ); + + case NoContentException ex -> buildErrorResponse( + HttpStatus.NO_CONTENT, + messageId, + TechnicalMessage.NO_CONTENT, + List.of(ErrorDTO.of( + ex.getMessage(), + TechnicalMessage.NO_CONTENT.getParameter() + )) + ); + + case ProcessorException ex -> buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + messageId, + TechnicalMessage.INTERNAL_SERVER_ERROR, + List.of(ErrorDTO.of( + ex.getMessage(), + TechnicalMessage.INTERNAL_SERVER_ERROR.getParameter() + )) + ); + + case TechnicalException ex -> buildErrorResponse( + HttpStatus.SERVICE_UNAVAILABLE, + messageId, + TechnicalMessage.INTERNAL_ERROR_IN_ADAPTERS, + List.of(ErrorDTO.of( + ex.getMessage(), + TechnicalMessage.INTERNAL_ERROR_IN_ADAPTERS.getParameter() + )) + ); + + case DatabaseResourceException ex -> buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + messageId, + TechnicalMessage.DATABASE_ERROR, + List.of(ErrorDTO.of( + ex.getMessage(), + TechnicalMessage.DATABASE_ERROR.getParameter() + )) + ); + + default -> buildErrorResponse( + HttpStatus.INTERNAL_SERVER_ERROR, + messageId, + TechnicalMessage.INTERNAL_SERVER_ERROR, + List.of(ErrorDTO.of( + throwable.getMessage(), + TechnicalMessage.INTERNAL_SERVER_ERROR.getParameter() + )) + ); + }; + } + + public Mono handleAuth(String message) { + log.error("Unauthorized access attempt detected"); + return ServerResponse.status(HttpStatus.UNAUTHORIZED) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(APIResponse.builder() + .code(HttpStatus.UNAUTHORIZED.value()) + .message(TechnicalMessage.UNAUTHORIZED.getMessage()) + .timestamp(Instant.now().toString()) + .identifier(null) // No message ID for auth errors + .build()); + } + + private Mono buildErrorResponse(HttpStatus httpStatus, + String messageId, + TechnicalMessage technicalMessage, + List errors) { + return ServerResponse.status(httpStatus) + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(APIResponse.builder() + .code(httpStatus.value()) + .message(technicalMessage.getMessage()) + .timestamp(Instant.now().toString()) + .identifier(messageId) + .errors(errors) + .build()); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/JwtAuthFilter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/JwtAuthFilter.java new file mode 100644 index 0000000..0c51ec3 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/JwtAuthFilter.java @@ -0,0 +1,62 @@ +package com.aws.ws.infrastructure.common.handler; + +//import org.springframework.security.authentication.AbstractAuthenticationToken; +//import org.springframework.security.core.context.ReactiveSecurityContextHolder; +//import org.springframework.security.oauth2.jwt.Jwt; +//import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +//import org.springframework.stereotype.Component; +//import org.springframework.web.server.ServerWebExchange; +//import org.springframework.web.server.WebFilter; +//import org.springframework.web.server.WebFilterChain; +//import reactor.core.publisher.Mono; +// +//@Component +public class JwtAuthFilter {//implements WebFilter { + +// @Override +// public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { +// return ReactiveSecurityContextHolder.getContext() +// .flatMap(securityContext -> { +// AbstractAuthenticationToken authentication = (AbstractAuthenticationToken) securityContext.getAuthentication(); +// +// if (authentication instanceof JwtAuthenticationToken jwtAuth) { +// Jwt jwt = jwtAuth.getToken(); +// +// // Puedes extraer información personalizada del JWT +// String username = jwt.getClaimAsString("preferred_username"); +// String email = jwt.getClaimAsString("email"); +// +// // Aquí puedes loggear o rechazar si es necesario +// System.out.println("Usuario autenticado: " + username + " - " + email); +// } +// +// return chain.filter(exchange); +// }) +// .switchIfEmpty(chain.filter(exchange)); // continuar si no hay contexto (por ejemplo, ruta pública) +// } + +// public static HandlerFilterFunction authorize(GlobalErrorHandler globalErrorHandler) { +// return (request, next) -> { +// String authHeader = request.headers().firstHeader(HttpHeaders.AUTHORIZATION); +// +// if (authHeader == null || !authHeader.startsWith("Bearer ")) { +// return globalErrorHandler.handleAuth("Unauthorized access"); +// } +// +// String token = authHeader.substring(7); // Remove "Bearer " +// +// if (!isValidToken(token)) { +// return globalErrorHandler.handleAuth("Invalid or expired token"); +// } +// +// return next.handle(request); +// }; +// } +// +// private static boolean isValidToken(String token) { +// // Aquí puedes validar con tu lógica JWT real. +// // Por simplicidad, aceptamos un token hardcodeado: +// return token.equals("eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJQTnJ6aDR3SDcxTF82elFsQWl0QlF4NjBsMG1seU5YQ1JXZFdpQmwwNmh3In0.eyJleHAiOjE3NDk3Njk5NDcsImlhdCI6MTc0OTc2OTY0NywianRpIjoiMzc4MGM2MDItNjMzNS00NDQzLWFhNmYtMTBjYTI2MmI0OGVlIiwiaXNzIjoiaHR0cHM6Ly9rZXljbG9hay1xYS50cmFuc2VyLmRpZ2l0YWwvcmVhbG1zL3VzZXJzIiwiYXVkIjpbInJlYWxtLW1hbmFnZW1lbnQiLCJicm9rZXIiLCJhY2NvdW50Il0sInN1YiI6ImUxNzNjZWJkLWVjYzgtNDczZS04MWUyLTRjNjViZThhNDg1NyIsInR5cCI6IkJlYXJlciIsImF6cCI6InRyYW5zZXItdXNlcnMiLCJzaWQiOiJiOGIyYWU5Yi01ZTg4LTQ2MzAtOGRhMy00NmFhNDJmMmNkMDgiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIioiXSwicmVhbG1fYWNjZXNzIjp7InJvbGVzIjpbIm9mZmxpbmVfYWNjZXNzIiwidW1hX2F1dGhvcml6YXRpb24iLCJkZWZhdWx0LXJvbGVzLXVzZXJzIl19LCJyZXNvdXJjZV9hY2Nlc3MiOnsicmVhbG0tbWFuYWdlbWVudCI6eyJyb2xlcyI6WyJ2aWV3LWlkZW50aXR5LXByb3ZpZGVycyIsInZpZXctcmVhbG0iLCJtYW5hZ2UtaWRlbnRpdHktcHJvdmlkZXJzIiwiaW1wZXJzb25hdGlvbiIsInJlYWxtLWFkbWluIiwiY3JlYXRlLWNsaWVudCIsIm1hbmFnZS11c2VycyIsInF1ZXJ5LXJlYWxtcyIsInZpZXctYXV0aG9yaXphdGlvbiIsInF1ZXJ5LWNsaWVudHMiLCJxdWVyeS11c2VycyIsIm1hbmFnZS1ldmVudHMiLCJtYW5hZ2UtcmVhbG0iLCJ2aWV3LWV2ZW50cyIsInZpZXctdXNlcnMiLCJ2aWV3LWNsaWVudHMiLCJtYW5hZ2UtYXV0aG9yaXphdGlvbiIsIm1hbmFnZS1jbGllbnRzIiwicXVlcnktZ3JvdXBzIl19LCJ0cmFuc2VyLXVzZXJzIjp7InJvbGVzIjpbImJ1c2luZXNzLXVwZGF0ZSIsInZlaGljbGVzLXVwZGF0ZSIsIm1lbnUtc2VydmljZXMtMDIiLCJsaXF1aWRhdGlvbnMtdXBkYXRlIiwibWVudS1zZXJ2aWNlcy0wMSIsIm1lbnUtc2VydmljZXMtMDMiLCJtZW51LWJ1c2luZXNzLTA0IiwibWVudS1idXNpbmVzcy0wMyIsIm1lbnUtcm91dGVzLTAxIiwiZHJpdmVycy11cGRhdGUiLCJyb3V0ZXMtdXBkYXRlIiwibWVudS12ZWhpY2xlcy0wMiIsIm1lbnUtdmVoaWNsZXMtMDEiLCJtZW51LWRyaXZlcnMtMDIiLCJtZW51LWRyaXZlcnMtMDMiLCJ1bWFfcHJvdGVjdGlvbiIsIm1lbnUtZHJpdmVycy0wMSIsImNvbXBsaWFuY2VzLXVwZGF0ZSIsImZ1ZWwtbWFuYWdlci11cGRhdGUiLCJiaWxsaW5ncy11cGRhdGUiLCJyZXNvdXJjZXMtdXBkYXRlIiwibWVudS1jbGllbnRzLTAxIiwibWVudS1jbGllbnRzLTAyIiwibWVudS1jbGllbnRzLTAzIiwic2VydmljZXMtdXBkYXRlIiwidG9sbHMtdXBkYXRlIiwiY2xpZW50cy11cGRhdGUiXX0sImJyb2tlciI6eyJyb2xlcyI6WyJyZWFkLXRva2VuIl19LCJhY2NvdW50Ijp7InJvbGVzIjpbIm1hbmFnZS1hY2NvdW50Iiwidmlldy1hcHBsaWNhdGlvbnMiLCJ2aWV3LWNvbnNlbnQiLCJ2aWV3LWdyb3VwcyIsIm1hbmFnZS1hY2NvdW50LWxpbmtzIiwibWFuYWdlLWNvbnNlbnQiLCJkZWxldGUtYWNjb3VudCIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IlF1YWxpdHkgQXNzdXJhbmNlIiwicHJlZmVycmVkX3VzZXJuYW1lIjoicWEudGVzdCIsImdpdmVuX25hbWUiOiJRdWFsaXR5IiwiZmFtaWx5X25hbWUiOiJBc3N1cmFuY2UiLCJlbWFpbCI6InFhLnRlc3RAcHJhZ21hLmNvbS5jbyJ9.kIcBrI8p8LZjBb6kPiMkzbkxxmWNKftuPm1WB8GUXrkfxj_wABOZ4H1i_cNOgc776IB4kMVvDojG8qgZH7gE-Xe4ZNToBvVU4jDFwvonMpF4Gb-YOSi7i5twAyFrBSo0cmjduR5yuoF6zBL86dNKrNZungNhidLsg94qxOPG9LtYyCV7UjHVXF0nU6Eq3FmwZJ7EfVhH5c0halw0M6zB3rt8fTsOURO_dnT4WlUEk4_fTyzp1iGQxIBtIztziDhi8AhpODX8dQGUkLyhdgrO2456DHAHBQ1XnISbzqgszNXvqITTypSE3l6cyvZZaxK687QDN1_KzRZPck3IJlDBqQ"); +// } +} + diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/MessageHeaderHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/MessageHeaderHandler.java new file mode 100644 index 0000000..9bcf3c9 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/handler/MessageHeaderHandler.java @@ -0,0 +1,23 @@ +package com.aws.ws.infrastructure.common.handler; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; + +import java.util.UUID; + +import static com.aws.ws.domain.exception.TechnicalMessage.X_MESSAGE_ID; + +@Component +@Slf4j +@Order(-2) +public class MessageHeaderHandler { + + public static String getMessageId(ServerRequest serverRequest) { + return serverRequest.headers() + .firstHeader(String.valueOf(X_MESSAGE_ID)) != null + ? serverRequest.headers().firstHeader(String.valueOf(X_MESSAGE_ID)) + : UUID.randomUUID().toString(); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Constants.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Constants.java new file mode 100644 index 0000000..331bb92 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Constants.java @@ -0,0 +1,11 @@ +package com.aws.ws.infrastructure.common.util; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class Constants { + public static final String RESOURCE_SUCCESS = "Resource retrieved successfully"; + public static final String RESOURCES_SUCCESS = "Resources retrieved successfully"; + public static final String ERROR_FETCHING_RESOURCE = "Error fetching resource by ID: {}"; + public static final String ERROR_FETCHING_RESOURCES = "Error listing resource: {}"; +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Converters.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Converters.java new file mode 100644 index 0000000..87cc527 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/Converters.java @@ -0,0 +1,30 @@ +package com.aws.ws.infrastructure.common.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.experimental.UtilityClass; + +import java.util.List; + +@UtilityClass +public class Converters { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static String listToString(List list) { + try { + return objectMapper.writeValueAsString(list); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error converting List to JSON", e); + } + } + + public static List stringToList(String json) { + try { + return objectMapper.readValue(json, new TypeReference>() {}); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error converting JSON to List", e); + } + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java new file mode 100644 index 0000000..ff625c3 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -0,0 +1,88 @@ +package com.aws.ws.infrastructure.inbound; + +import com.aws.ws.infrastructure.inbound.dto.UserDTO; +import com.aws.ws.infrastructure.inbound.handler.UserHandler; +import org.springdoc.core.annotations.RouterOperations; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +@Configuration +public class RouterConfig { + + @Bean + @RouterOperations({ + // Define the route for getting a user by ID + @org.springdoc.core.annotations.RouterOperation( + path = "/v1/users/{id}", + produces = "application/json", + method = org.springframework.web.bind.annotation.RequestMethod.GET, + beanClass = UserHandler.class, + beanMethod = "getByUserId", + operation = @io.swagger.v3.oas.annotations.Operation( + operationId = "getByUserId", + summary = "Get User by ID", + description = "Retrieve User by ID from the database.", + parameters = @io.swagger.v3.oas.annotations.Parameter( + name = "id", + description = "ID of the User to retrieve", + required = true, + in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, + example = "user_12345" // Example ID for documentation purposes + ) + ) + ), + // Create a user request UserDTO.class + @org.springdoc.core.annotations.RouterOperation( + path = "/v1/users", + produces = "application/json", + method = org.springframework.web.bind.annotation.RequestMethod.POST, + beanClass = UserHandler.class, + beanMethod = "createUser", + operation = @io.swagger.v3.oas.annotations.Operation( + operationId = "createUser", + summary = "Create User", + description = "Create a new User in the database.", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + description = "User DTO for creating a new user", + content = @io.swagger.v3.oas.annotations.media.Content( + mediaType = "application/json", + schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = UserDTO.class) + ) + ) + ) + ), + // Define the route for finding a user by email + @org.springdoc.core.annotations.RouterOperation( + path = "/v1/users/{email}", + produces = "application/json", + method = org.springframework.web.bind.annotation.RequestMethod.GET, + beanClass = UserHandler.class, + beanMethod = "findUserByEmail", + operation = @io.swagger.v3.oas.annotations.Operation( + operationId = "findUserByEmail", + summary = "Find User by Email", + description = "Retrieve User by Email from the database.", + parameters = @io.swagger.v3.oas.annotations.Parameter( + name = "email", + description = "Email of the User to retrieve", + required = true, + in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, + example = "rasysbox@hotmail.com" + ) + ) + ) + }) + public RouterFunction route(UserHandler handler) { + return org.springframework.web.reactive.function.server.RouterFunctions + .route() + .GET("/v1/users/{email}", handler::findUserByEmail) + .POST("/v1/users", handler::createUser) +// .GET("/v1/users/{id}", handler::getByUserId) +// .PUT("/users/{id}", handler::updateUser) +// .DELETE("/users/{id}", handler::deleteUser) + .build(); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java new file mode 100644 index 0000000..4e51ae4 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java @@ -0,0 +1,25 @@ +package com.aws.ws.infrastructure.inbound.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Data Transfer Object for User", title = "UserDTO") +public class UserDTO { + @Schema(description = "Email for the user", example = "rasysbox@hotmail.com") + private String email; + @Schema(description = "First name of the user", example = "Raul") + private String firstName; + @Schema(description = "Last name of the user", example = "Bolivar") + private String lastName; + @Schema(description = "Password for the user", example = "password123") + private String password; + @Schema(description = "Role of the user", example = "USER") + private String role; +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java new file mode 100644 index 0000000..2c4e23a --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -0,0 +1,53 @@ +package com.aws.ws.infrastructure.inbound.handler; + +import com.aws.ws.domain.spi.UserServicePort; +import com.aws.ws.infrastructure.common.handler.GlobalErrorHandler; +import com.aws.ws.infrastructure.inbound.dto.UserDTO; +import com.aws.ws.infrastructure.inbound.mapper.UserMapper; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.server.ServerRequest; +import org.springframework.web.reactive.function.server.ServerResponse; +import reactor.core.publisher.Mono; + +import static com.aws.ws.infrastructure.common.handler.MessageHeaderHandler.getMessageId; + +@Slf4j +@Component +@RequiredArgsConstructor +@Tag(name = "Users", description = "Users Management") +public class UserHandler { + + private final UserServicePort servicePort; + private final GlobalErrorHandler globalErrorHandler; + private final UserMapper mapper; + +// // Method to handle fetching user by ID +// public Mono getByUserId(ServerRequest request) { +// String userId = request.pathVariable("userId"); +// return servicePort.getByUserId(userId) +// .flatMap(user -> ServerResponse.ok().bodyValue(user)) +// .doOnError(error -> log.error("Error fetching user with ID {}: {}", userId, error.getMessage())) +// .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); +// } + + public Mono createUser(ServerRequest request) { + log.debug("Creating new user"); + return request.bodyToMono(UserDTO.class) + .flatMap(userDTO -> servicePort.createUser(mapper.toDomain(userDTO))) + .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user)) + .doOnError(error -> log.error("Error creating user: {}", error.getMessage())) + .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); + } + + public Mono findUserByEmail(ServerRequest request) { + String email = request.pathVariable("email"); + return servicePort.findUserByEmail(email) + .flatMap(user -> ServerResponse.ok().bodyValue(user)) + .doOnError(error -> log.error("Error finding user with email {}: {}", email, error.getMessage())) + .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java new file mode 100644 index 0000000..da3d514 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java @@ -0,0 +1,19 @@ +package com.aws.ws.infrastructure.inbound.mapper; + +import com.aws.ws.domain.model.User; +import com.aws.ws.infrastructure.inbound.dto.UserDTO; +import org.springframework.stereotype.Component; + +@Component +public class UserMapper { + public User toDomain(UserDTO userDTO) { + if (userDTO == null) return null; + return User.builder() + .email(userDTO.getEmail()) + .firstName(userDTO.getFirstName()) + .lastName(userDTO.getLastName()) + .password(userDTO.getPassword()) + .role(userDTO.getRole()) + .build(); + } +} diff --git a/auth-backend-service/src/main/resources/application.properties b/auth-backend-service/src/main/resources/application.properties new file mode 100644 index 0000000..3c75ab3 --- /dev/null +++ b/auth-backend-service/src/main/resources/application.properties @@ -0,0 +1,14 @@ +# [APPLICATION] Configuration for the Auth Backend Service +spring.application.name=auth-backend-service + +# [SWAGGER] Configuration for Swagger API documentation +springdoc.api-docs.path=/v3/api-docs +springdoc.swagger-ui.use-root-path=true + +# [OPENAPI] Configuration for OpenAPI definition +openapi.service.title=${spring.application.name} +openapi.service.host=http://localhost:\${server.port} +openapi.service.version=1.0.0 +openapi.service.description=API RESTFul operaciones sobre ${spring.application.name}. +openapi.service.contact.name=Raul Bolivar Navas @ rasysbox +openapi.service.contact.email=raul.bolivar@pragma.com.co diff --git a/auth-backend-service/src/main/resources/application.yml b/auth-backend-service/src/main/resources/application.yml new file mode 100644 index 0000000..3fed7fc --- /dev/null +++ b/auth-backend-service/src/main/resources/application.yml @@ -0,0 +1,12 @@ +# [AWS DynamoDB Local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) +aws: + region: us-east-1 + dynamodb: + endpoint: http://localhost:4566 + table-name: Users + secret: + access-key: test + key-id: test + +server: + port: 9801 diff --git a/auth-backend-service/src/test/java/com/aws/ws/AuthBackendServiceApplicationTests.java b/auth-backend-service/src/test/java/com/aws/ws/AuthBackendServiceApplicationTests.java new file mode 100644 index 0000000..80ea82c --- /dev/null +++ b/auth-backend-service/src/test/java/com/aws/ws/AuthBackendServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.aws.ws; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AuthBackendServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/bff-aws-localstack/.gitattributes b/bff-aws-localstack/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/bff-aws-localstack/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/bff-aws-localstack/.gitignore b/bff-aws-localstack/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/bff-aws-localstack/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/bff-aws-localstack/build.gradle b/bff-aws-localstack/build.gradle new file mode 100644 index 0000000..9de3901 --- /dev/null +++ b/bff-aws-localstack/build.gradle @@ -0,0 +1,102 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.5.3' + id 'io.spring.dependency-management' version '1.1.7' + id 'jacoco' +} + +group = 'com.aws' +version = '0.0.1-SNAPSHOT' +description = 'Microservicio Backend-For-Frontend para el AWS LocalStack.' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', "2025.0.0") + projectName = project.name + projectVersion = project.version + buildTimestamp = new Date().format("yyyy-MM-dd'T'HH:mm:ss") +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-webflux' + implementation 'org.springframework.cloud:spring-cloud-starter' + implementation 'org.springframework.cloud:spring-cloud-starter-gateway-server-webflux' + compileOnly 'org.projectlombok:lombok' + runtimeOnly 'io.micrometer:micrometer-registry-prometheus' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'io.projectreactor:reactor-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +test { + useJUnitPlatform() + finalizedBy jacocoTestReport +} + +jacocoTestReport { + dependsOn test + reports { + xml.required = true + html.required = true + } +} + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +bootJar { + archiveFileName = "${rootProject.name}.jar" +} + +processResources { + inputs.properties([ + 'projectName': projectName, + 'projectVersion': projectVersion, + 'buildTimestamp': buildTimestamp + ]) + + filesMatching("**/application*.yml") { + expand([ + 'projectName': projectName, + 'projectVersion': projectVersion, + 'buildTimestamp': buildTimestamp + ]) + } + + filesMatching("**/application*.properties") { + expand([ + 'projectName': projectName, + 'projectVersion': projectVersion, + 'buildTimestamp': buildTimestamp + ]) + } +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/bff-aws-localstack/gradle/wrapper/gradle-wrapper.jar b/bff-aws-localstack/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/bff-aws-localstack/gradlew.bat b/bff-aws-localstack/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/bff-aws-localstack/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/bff-aws-localstack/settings.gradle b/bff-aws-localstack/settings.gradle new file mode 100644 index 0000000..f1237e7 --- /dev/null +++ b/bff-aws-localstack/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'bff-aws-localstack' diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/ApiGwApplication.java b/bff-aws-localstack/src/main/java/com/aws/ws/ApiGwApplication.java new file mode 100644 index 0000000..56b17ef --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/ApiGwApplication.java @@ -0,0 +1,13 @@ +package com.aws.ws; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiGwApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiGwApplication.class, args); + } + +} diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/config/AllowedOriginsProperties.java b/bff-aws-localstack/src/main/java/com/aws/ws/config/AllowedOriginsProperties.java new file mode 100644 index 0000000..6bc5d04 --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/config/AllowedOriginsProperties.java @@ -0,0 +1,15 @@ +package com.aws.ws.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Data +@Component +@ConfigurationProperties(prefix = "allowed") +public class AllowedOriginsProperties { + private List origins; +} + diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/config/CustomHeader.java b/bff-aws-localstack/src/main/java/com/aws/ws/config/CustomHeader.java new file mode 100644 index 0000000..e0abb68 --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/config/CustomHeader.java @@ -0,0 +1,18 @@ +package com.aws.ws.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Data +@Component +@ConfigurationProperties(prefix = "custom") +public class CustomHeader { + private Header header; + + @Data + public static class Header { + private String name; + private String value; + } +} diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPostFiltering.java b/bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPostFiltering.java new file mode 100644 index 0000000..56520da --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPostFiltering.java @@ -0,0 +1,21 @@ +package com.aws.ws.filters; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import reactor.core.publisher.Mono; + +@Configuration +public class GlobalPostFiltering { + + private static final Logger LOGGER = LoggerFactory.getLogger(GlobalPostFiltering.class); + + @Bean + public GlobalFilter postGlobalFilter() { + return (exchange, chain) -> chain.filter(exchange) + .then(Mono.fromRunnable(() -> LOGGER.info("Global Post Filter executed"))); + } + +} diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPreFiltering.java b/bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPreFiltering.java new file mode 100644 index 0000000..6eb745e --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/filters/GlobalPreFiltering.java @@ -0,0 +1,68 @@ +package com.aws.ws.filters; + +import com.aws.ws.config.AllowedOriginsProperties; +import com.aws.ws.config.CustomHeader; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.GlobalFilter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +@Slf4j +@Component +@Configuration +@RequiredArgsConstructor +public class GlobalPreFiltering implements GlobalFilter { + + private final AllowedOriginsProperties allowedOriginsProperties; + private final CustomHeader customHeader; + + @Override + public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { + log.info("Global Pre Filter executed"); + return chain.filter(exchange); + } + + @Bean + public GlobalFilter customGlobalPreFilter() { + return (exchange, chain) -> { + ServerHttpRequest request = exchange.getRequest(); + ServerHttpResponse response = exchange.getResponse(); + + String origin = exchange.getRequest().getHeaders().getOrigin(); + String path = exchange.getRequest().getPath().toString(); + + log.info("🌐 Origin: {}", origin); + log.info("✅ Allowed: {}", allowedOriginsProperties.getOrigins()); + + if ((origin == null && !path.startsWith("/actuator")) || + (origin != null && !allowedOriginsProperties.getOrigins().contains(origin))) { + + log.warn("❌ Origin blocked: {}", origin); + exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); + return exchange.getResponse().setComplete(); + } + + String headerName = customHeader.getHeader().getName(); + String expectedValue = customHeader.getHeader().getValue(); + + String actualValue = request.getHeaders().getFirst(headerName); + log.info("🔍 Checking header {} = {}", headerName, actualValue); + + if (actualValue == null || !actualValue.equals(expectedValue)) { + log.warn("❌ Invalid or missing {} header", headerName); + response.setStatusCode(HttpStatus.BAD_REQUEST); + return response.setComplete(); + } + + return chain.filter(exchange); + }; + } +} diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/ssl/HostnameVerifierConfig.java b/bff-aws-localstack/src/main/java/com/aws/ws/ssl/HostnameVerifierConfig.java new file mode 100644 index 0000000..6954260 --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/ssl/HostnameVerifierConfig.java @@ -0,0 +1,16 @@ +package com.aws.ws.ssl; + +import org.springframework.context.annotation.Configuration; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLSession; + +@Configuration +public class HostnameVerifierConfig implements HostnameVerifier { + + @Override + public boolean verify(String s, SSLSession sslSession) { + return false; + } + +} diff --git a/bff-aws-localstack/src/main/java/com/aws/ws/ssl/SSLCertConfig.java b/bff-aws-localstack/src/main/java/com/aws/ws/ssl/SSLCertConfig.java new file mode 100644 index 0000000..e3f307f --- /dev/null +++ b/bff-aws-localstack/src/main/java/com/aws/ws/ssl/SSLCertConfig.java @@ -0,0 +1,50 @@ +package com.aws.ws.ssl; + +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.net.ssl.*; +import java.security.cert.X509Certificate; + +@Slf4j +@Configuration +public class SSLCertConfig { + + /** + * Disable SSL certificate validation (for development purposes only). + */ + @Bean + @SneakyThrows + public Boolean disableSSLValidation() { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) { + log.warn("⚠️ checkClientTrusted called: authType = {}", authType); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) { + log.warn("⚠️ checkServerTrusted called: authType = {}", authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + }; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifierConfig()); + + log.warn("⚠️ SSL Certificate validation is disabled. DO NOT USE IN PRODUCTION!"); + + return true; + } +} diff --git a/bff-aws-localstack/src/main/resources/application.yml b/bff-aws-localstack/src/main/resources/application.yml new file mode 100644 index 0000000..966c63e --- /dev/null +++ b/bff-aws-localstack/src/main/resources/application.yml @@ -0,0 +1,43 @@ +# [APP General Configuration] +server: + port: 9900 + error: + include-message: always + include-binding-errors: always +info: + project-version: "@projectVersion@" + build-timestamp: "@buildTimestamp@" + +# [SPRING CLOUD] +spring: + application: + name: "@projectName@" + config: + import: optional:classpath:routes.yml + +# [CUSTOM HEADER] +custom: + header: + name: X-Request-ID + value: f4a4fd8d-43ae-4365-bb71-038c34d06881 +allowed: + origins: + - http://localhost:4200 + - http://localhost:3000 + - https://www.rasysbox.com + +# [METRICS] +management: + endpoints: + web: + exposure: + include: health,info,prometheus,metrics + endpoint: + health: + status: + http-mapping: + down: 500 + show-details: always + http exchanges: + recording: + include: request-headers,time-taken,session-id diff --git a/bff-aws-localstack/src/main/resources/routes.yml b/bff-aws-localstack/src/main/resources/routes.yml new file mode 100644 index 0000000..02eb2eb --- /dev/null +++ b/bff-aws-localstack/src/main/resources/routes.yml @@ -0,0 +1,24 @@ +spring: + cloud: + gateway: + server: + webflux: + routes: + - id: users-local + uri: http://localhost:9801 + predicates: + - Path=/api/auth/** + filters: + - RewritePath=/api/auth/(?.*), /api/auth/\${path} + - id: dynamodb-local + uri: http://localhost:9800 + predicates: + - Path=/api/dynamodb/** + filters: + - RewritePath=/api/dynamodb/(?.*), /api/dynamodb/\${path} + - id: sqs-local + uri: http://localhost:9800 + predicates: + - Path=/api/sqs/** + filters: + - RewritePath=/api/sqs/(?.*), /api/sqs/\${path} diff --git a/bff-aws-localstack/src/test/java/com/aws/ws/ApiGwApplicationTests.java b/bff-aws-localstack/src/test/java/com/aws/ws/ApiGwApplicationTests.java new file mode 100644 index 0000000..15735e4 --- /dev/null +++ b/bff-aws-localstack/src/test/java/com/aws/ws/ApiGwApplicationTests.java @@ -0,0 +1,13 @@ +package com.aws.ws; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApiGwApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/dashboard-ui/proxy.conf.js b/dashboard-ui/proxy.conf.js index 443b8f7..724349c 100644 --- a/dashboard-ui/proxy.conf.js +++ b/dashboard-ui/proxy.conf.js @@ -1,8 +1,12 @@ module.exports = [ { context: ['/api'], - target: 'http://localhost:9800', + target: 'http://localhost:9900', secure: false, changeOrigin: true, + headers: { + 'X-Request-ID': 'f4a4fd8d-43ae-4365-bb71-038c34d06881', + 'Origin': 'http://localhost:4200', + }, }, ]; From a4ae12130359e7467606d44e73ad55349b9834fa Mon Sep 17 00:00:00 2001 From: rbolivar Date: Tue, 24 Jun 2025 16:58:47 -0500 Subject: [PATCH 02/15] login and register --- auth-backend-service/build.gradle | 4 + .../ws/application/config/SecurityConfig.java | 23 ++- .../application/filter/JwtSecurityFilter.java | 52 ++++++ .../aws/ws/domain/usecase/UserUseCase.java | 21 +-- .../persistence/UserPersistenceAdapter.java | 21 ++- .../infrastructure/common/util/JwtUtil.java | 52 ++++++ .../infrastructure/inbound/RouterConfig.java | 160 ++++++++++++------ .../inbound/dto/LoginRequest.java | 19 +++ .../inbound/handler/UserHandler.java | 40 +++++ .../src/main/resources/application.yml | 3 + 10 files changed, 330 insertions(+), 65 deletions(-) create mode 100644 auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java diff --git a/auth-backend-service/build.gradle b/auth-backend-service/build.gradle index 037910e..c0119e2 100644 --- a/auth-backend-service/build.gradle +++ b/auth-backend-service/build.gradle @@ -37,6 +37,10 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server' implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5' + // AWS SDK dependencies implementation 'software.amazon.awssdk:auth' implementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.658' diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java index 5bd4d72..e462450 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java @@ -1,5 +1,6 @@ package com.aws.ws.application.config; +import com.aws.ws.application.filter.JwtSecurityFilter; import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; @@ -9,7 +10,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtEncoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; @@ -32,6 +36,12 @@ public class SecurityConfig { // @Value("${jwt.private.key}") // private RSAPrivateKey privateKey; + private final JwtSecurityFilter jwtSecurityFilter; + + public SecurityConfig(JwtSecurityFilter jwtSecurityFilter) { + this.jwtSecurityFilter = jwtSecurityFilter; + } + @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { return http @@ -42,10 +52,14 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) .pathMatchers("/webjars/**").permitAll() .pathMatchers("/api-docs/**").permitAll() .pathMatchers("/actuator/**").permitAll() - .pathMatchers(HttpMethod.POST, "/v1/users").authenticated() - .pathMatchers(HttpMethod.GET, "/v1/users/{email}").authenticated() + .pathMatchers(HttpMethod.POST, "/api/users/login").permitAll() + .pathMatchers(HttpMethod.POST, "/api/users/register").permitAll() + .pathMatchers("/api/users/**").authenticated() +// .pathMatchers(HttpMethod.POST, "/v1/users").authenticated() +// .pathMatchers(HttpMethod.GET, "/v1/users/{email}").authenticated() .anyExchange().authenticated() ) + .addFilterAt(jwtSecurityFilter, SecurityWebFiltersOrder.AUTHENTICATION) // .oauth2ResourceServer(oauth2 -> oauth2 // .jwt(jwt -> jwt // .jwkSetUri(jwkSetUri) @@ -54,6 +68,11 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) .build(); } + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + // @Bean // JwtDecoder jwtDecoder() { // return NimbusJwtDecoder.withPublicKey(this.publicKey).build(); diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java b/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java new file mode 100644 index 0000000..b53f60d --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java @@ -0,0 +1,52 @@ +package com.aws.ws.application.filter; + +import com.aws.ws.infrastructure.common.util.JwtUtil; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +@Component +public class JwtSecurityFilter implements WebFilter { + + private final JwtUtil jwtUtil; + + public JwtSecurityFilter(JwtUtil jwtUtil) { + this.jwtUtil = jwtUtil; + } + + @Override + public Mono filter(ServerWebExchange exchange, + WebFilterChain chain) { + String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION); + + if (authHeader != null && authHeader.startsWith("Bearer ")) { + try { + String token = authHeader.substring(7); + Claims claims = jwtUtil.validateToken(token); + Authentication auth = new UsernamePasswordAuthenticationToken( + claims.getSubject(), + null, + jwtUtil.extractRoles(token).stream().map(SimpleGrantedAuthority::new).toList() + ); + return chain.filter(exchange) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)); + } catch (JwtException e) { + exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); + return exchange.getResponse().setComplete(); + } + } + + return chain.filter(exchange); + } +} + diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java index f53ce3c..f7de5bc 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -19,19 +19,20 @@ public UserUseCase(UserAdapter userAdapter) { @Override public Mono createUser(User domain) { if (domain.getEmail() == null || domain.getPassword() == null || - domain.getFirstName() == null || domain.getLastName() == null || domain.getRole() == null) { - return Mono.error(new InvalidValueException("User ID, firstName, lastName, email, password and Role", "must not be null")); + domain.getFirstName() == null || domain.getLastName() == null || domain.getRole() == null) { + return Mono.error(new InvalidValueException( + "User ID, firstName, lastName, email, password and Role", + "must not be null" + )); } return userAdapter.findUserByEmail(domain.getEmail()) - .flatMap(exists -> { - if (exists != null) { - return Mono.error(new DuplicateResourceException( - TechnicalMessage.ALREADY_EXISTS, "User already exists with email: ", domain.getEmail())); - } else { - return userAdapter.createUser(domain); - } - }); + .flatMap((User user) -> Mono.error(new DuplicateResourceException( + TechnicalMessage.ALREADY_EXISTS, + "User already exists with email: ", + domain.getEmail() + ))) + .switchIfEmpty(Mono.defer(() -> userAdapter.createUser(domain))); } @Override diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java index 0301860..b3c8e12 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -3,15 +3,18 @@ import com.aws.ws.domain.api.UserAdapter; import com.aws.ws.domain.model.User; import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.model.*; +import java.util.List; import java.util.Map; import java.util.UUID; +@Slf4j @Service public class UserPersistenceAdapter implements UserAdapter { @@ -30,6 +33,7 @@ public Mono createUser(User user) { if (user.getID() == null) { user.setID(UUID.randomUUID().toString()); } + log.info("Creating user with ID: {}", user.getID()); PutItemRequest request = PutItemRequest.builder() .tableName(tableName) @@ -52,14 +56,25 @@ public Mono findUserByEmail(String email) { ScanRequest request = ScanRequest.builder() .tableName(tableName) .filterExpression("email = :emailVal") - .expressionAttributeValues(Map.of(":emailVal", AttributeValue.builder().s(email).build())) + .expressionAttributeValues(Map.of( + ":emailVal", AttributeValue.builder().s(email).build() + )) .build(); return Mono.fromFuture(() -> client.scan(request)) - .map(ScanResponse::items) - .mapNotNull(items -> items.isEmpty() ? null : convert(items.getFirst())); + .flatMap(scanResponse -> { + List> items = scanResponse.items(); + if (items == null || items.isEmpty()) { + log.info("✅ No user found with email: {}", email); + return Mono.empty(); + } + log.info("✅ User found: {}", items.getFirst()); + return Mono.just(convert(items.getFirst())); + }) + .doOnError(e -> log.error("❌ Error scanning DynamoDB for email {}: {}", email, e.getMessage())); } + private User convert(Map item) { User user = new User(); user.setID(item.get("ID").s()); diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java new file mode 100644 index 0000000..eca0345 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java @@ -0,0 +1,52 @@ +package com.aws.ws.infrastructure.common.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.List; + +@Component +public class JwtUtil { + + @Value("${jwt.secret}") + private String SECRET_KEY; // Clave secreta para firmar el JWT, inyectada desde las propiedades de la aplicación + + private static final long EXPIRATION_TIME_MS = 3600000; // 1 hora + private final ObjectMapper objectMapper = new ObjectMapper(); + + public String generateToken(String email, List roles) { + return Jwts.builder() + .setSubject(email) + .claim("roles", roles) + .setIssuedAt(new Date()) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME_MS)) + .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) + .compact(); + } + + public Claims validateToken(String token) throws JwtException { + return Jwts.parserBuilder() + .setSigningKey(SECRET_KEY.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String extractEmail(String token) { + return validateToken(token).getSubject(); + } + + public List extractRoles(String token) { + Object roles = validateToken(token).get("roles"); + return objectMapper.convertValue(roles, new TypeReference<>() {}); + } +} + diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java index ff625c3..2c7e558 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -1,88 +1,148 @@ package com.aws.ws.infrastructure.inbound; -import com.aws.ws.infrastructure.inbound.dto.UserDTO; +import com.aws.ws.infrastructure.inbound.dto.LoginRequest; import com.aws.ws.infrastructure.inbound.handler.UserHandler; import org.springdoc.core.annotations.RouterOperations; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RequestPredicates.*; +import static org.springframework.web.reactive.function.server.RouterFunctions.route; @Configuration public class RouterConfig { @Bean @RouterOperations({ - // Define the route for getting a user by ID + // Define the route for user login @org.springdoc.core.annotations.RouterOperation( - path = "/v1/users/{id}", - produces = "application/json", - method = org.springframework.web.bind.annotation.RequestMethod.GET, - beanClass = UserHandler.class, - beanMethod = "getByUserId", - operation = @io.swagger.v3.oas.annotations.Operation( - operationId = "getByUserId", - summary = "Get User by ID", - description = "Retrieve User by ID from the database.", - parameters = @io.swagger.v3.oas.annotations.Parameter( - name = "id", - description = "ID of the User to retrieve", - required = true, - in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, - example = "user_12345" // Example ID for documentation purposes - ) - ) - ), - // Create a user request UserDTO.class - @org.springdoc.core.annotations.RouterOperation( - path = "/v1/users", + path = "/api/users/login", produces = "application/json", method = org.springframework.web.bind.annotation.RequestMethod.POST, beanClass = UserHandler.class, - beanMethod = "createUser", + beanMethod = "login", operation = @io.swagger.v3.oas.annotations.Operation( - operationId = "createUser", - summary = "Create User", - description = "Create a new User in the database.", + operationId = "loginUser", + summary = "User Login", + description = "Authenticate a user and return a JWT token.", requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( required = true, - description = "User DTO for creating a new user", + description = "Login credentials for the user", content = @io.swagger.v3.oas.annotations.media.Content( mediaType = "application/json", - schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = UserDTO.class) + schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = LoginRequest.class) ) ) ) ), - // Define the route for finding a user by email + // Define the route for user registration @org.springdoc.core.annotations.RouterOperation( - path = "/v1/users/{email}", + path = "/api/users/register", produces = "application/json", - method = org.springframework.web.bind.annotation.RequestMethod.GET, + method = org.springframework.web.bind.annotation.RequestMethod.POST, beanClass = UserHandler.class, - beanMethod = "findUserByEmail", + beanMethod = "register", operation = @io.swagger.v3.oas.annotations.Operation( - operationId = "findUserByEmail", - summary = "Find User by Email", - description = "Retrieve User by Email from the database.", - parameters = @io.swagger.v3.oas.annotations.Parameter( - name = "email", - description = "Email of the User to retrieve", + operationId = "registerUser", + summary = "User Registration", + description = "Register a new user in the system.", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( required = true, - in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, - example = "rasysbox@hotmail.com" + description = "User registration details", + content = @io.swagger.v3.oas.annotations.media.Content( + mediaType = "application/json", + schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = LoginRequest.class) + ) ) ) ) }) - public RouterFunction route(UserHandler handler) { - return org.springframework.web.reactive.function.server.RouterFunctions - .route() - .GET("/v1/users/{email}", handler::findUserByEmail) - .POST("/v1/users", handler::createUser) -// .GET("/v1/users/{id}", handler::getByUserId) -// .PUT("/users/{id}", handler::updateUser) -// .DELETE("/users/{id}", handler::deleteUser) + public RouterFunction userRoutes(UserHandler userHandler) { + return route() + .path("/api/users", builder -> builder + .POST("/register", accept(MediaType.APPLICATION_JSON), userHandler::register) + .POST("/login", accept(MediaType.APPLICATION_JSON), userHandler::login) +// .GET("/me", userHandler::getProfile) // requiere JWT +// .PUT("/update", userHandler::updateUser) // requiere JWT +// .POST("/logout", userHandler::logout) // requiere JWT + ) .build(); } + +// @Bean +// @RouterOperations({ +// // Define the route for getting a user by ID +// @org.springdoc.core.annotations.RouterOperation( +// path = "/v1/users/{id}", +// produces = "application/json", +// method = org.springframework.web.bind.annotation.RequestMethod.GET, +// beanClass = UserHandler.class, +// beanMethod = "getByUserId", +// operation = @io.swagger.v3.oas.annotations.Operation( +// operationId = "getByUserId", +// summary = "Get User by ID", +// description = "Retrieve User by ID from the database.", +// parameters = @io.swagger.v3.oas.annotations.Parameter( +// name = "id", +// description = "ID of the User to retrieve", +// required = true, +// in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, +// example = "user_12345" // Example ID for documentation purposes +// ) +// ) +// ), +// // Create a user request UserDTO.class +// @org.springdoc.core.annotations.RouterOperation( +// path = "/v1/users", +// produces = "application/json", +// method = org.springframework.web.bind.annotation.RequestMethod.POST, +// beanClass = UserHandler.class, +// beanMethod = "createUser", +// operation = @io.swagger.v3.oas.annotations.Operation( +// operationId = "createUser", +// summary = "Create User", +// description = "Create a new User in the database.", +// requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( +// required = true, +// description = "User DTO for creating a new user", +// content = @io.swagger.v3.oas.annotations.media.Content( +// mediaType = "application/json", +// schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = UserDTO.class) +// ) +// ) +// ) +// ), +// // Define the route for finding a user by email +// @org.springdoc.core.annotations.RouterOperation( +// path = "/v1/users/{email}", +// produces = "application/json", +// method = org.springframework.web.bind.annotation.RequestMethod.GET, +// beanClass = UserHandler.class, +// beanMethod = "findUserByEmail", +// operation = @io.swagger.v3.oas.annotations.Operation( +// operationId = "findUserByEmail", +// summary = "Find User by Email", +// description = "Retrieve User by Email from the database.", +// parameters = @io.swagger.v3.oas.annotations.Parameter( +// name = "email", +// description = "Email of the User to retrieve", +// required = true, +// in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, +// example = "rasysbox@hotmail.com" +// ) +// ) +// ) +// }) +// public RouterFunction route(UserHandler handler) { +// return org.springframework.web.reactive.function.server.RouterFunctions +// .route() +// .GET("/v1/users/{email}", handler::findUserByEmail) +// .POST("/v1/users", handler::createUser) +//// .GET("/v1/users/{id}", handler::getByUserId) +//// .PUT("/users/{id}", handler::updateUser) +//// .DELETE("/users/{id}", handler::deleteUser) +// .build(); +// } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java new file mode 100644 index 0000000..1c3ae61 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java @@ -0,0 +1,19 @@ +package com.aws.ws.infrastructure.inbound.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +@Schema(description = "Data Transfer Object for Login", title = "LoginRequest") +public class LoginRequest { + @Schema(description = "Email of the user", example = "rasysbox@hotmail.com") + private String email; + @Schema(description = "Password of the user", example = "password123") + private String password; +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java index 2c4e23a..ab7a31f 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -2,17 +2,23 @@ import com.aws.ws.domain.spi.UserServicePort; import com.aws.ws.infrastructure.common.handler.GlobalErrorHandler; +import com.aws.ws.infrastructure.common.util.JwtUtil; +import com.aws.ws.infrastructure.inbound.dto.LoginRequest; import com.aws.ws.infrastructure.inbound.dto.UserDTO; import com.aws.ws.infrastructure.inbound.mapper.UserMapper; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; +import java.util.List; +import java.util.Map; + import static com.aws.ws.infrastructure.common.handler.MessageHeaderHandler.getMessageId; @Slf4j @@ -24,6 +30,8 @@ public class UserHandler { private final UserServicePort servicePort; private final GlobalErrorHandler globalErrorHandler; private final UserMapper mapper; + private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; // // Method to handle fetching user by ID // public Mono getByUserId(ServerRequest request) { @@ -50,4 +58,36 @@ public Mono findUserByEmail(ServerRequest request) { .doOnError(error -> log.error("Error finding user with email {}: {}", email, error.getMessage())) .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); } + + public Mono login(ServerRequest request) { + return request.bodyToMono(LoginRequest.class) + .flatMap(login -> servicePort.findUserByEmail(login.getEmail()) + .flatMap(user -> { + if (passwordEncoder.matches(login.getPassword(), user.getPassword())) { + String token = jwtUtil.generateToken(user.getEmail(), List.of(user.getRole())); + return ServerResponse.ok().bodyValue(Map.of("token", token)); + } else { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); + } + })); + } + + public Mono register(ServerRequest request) { + return request.bodyToMono(LoginRequest.class) + .flatMap(login -> { + UserDTO userDTO = new UserDTO(); + userDTO.setEmail(login.getEmail()); + userDTO.setPassword(passwordEncoder.encode(login.getPassword())); + userDTO.setFirstName("DefaultFirstName"); + userDTO.setLastName("DefaultLastName"); + userDTO.setRole("USER"); + + return servicePort.createUser(mapper.toDomain(userDTO)) + .flatMap(createdUser -> ServerResponse.status(HttpStatus.CREATED).bodyValue(createdUser)); + }) + .doOnError(error -> log.error("❌ Error registering user: {}", error.getMessage())) + .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); + } + + } diff --git a/auth-backend-service/src/main/resources/application.yml b/auth-backend-service/src/main/resources/application.yml index 3fed7fc..ea683ae 100644 --- a/auth-backend-service/src/main/resources/application.yml +++ b/auth-backend-service/src/main/resources/application.yml @@ -10,3 +10,6 @@ aws: server: port: 9801 + +jwt: + secret: "wN!as4kdpN98A2m3V9x7QqZLrP0f98A2m3V9x7QqZLGtYz" # debe tener al menos 32 caracteres (256 bits) \ No newline at end of file From 86bcabc0851f61a34ac700da59d065e41bfbcbfe Mon Sep 17 00:00:00 2001 From: rbolivar Date: Tue, 24 Jun 2025 17:11:46 -0500 Subject: [PATCH 03/15] login and register --- .../com/aws/ws/domain/usecase/UserUseCase.java | 4 +++- .../infrastructure/inbound/dto/LoginRequest.java | 4 ++++ .../ws/infrastructure/inbound/enums/Roles.java | 15 +++++++++++++++ .../inbound/handler/UserHandler.java | 7 ++++--- 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/enums/Roles.java diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java index f7de5bc..222a5b6 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -1,11 +1,13 @@ package com.aws.ws.domain.usecase; import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.exception.BusinessException; import com.aws.ws.domain.exception.DuplicateResourceException; import com.aws.ws.domain.exception.InvalidValueException; import com.aws.ws.domain.exception.TechnicalMessage; import com.aws.ws.domain.model.User; import com.aws.ws.domain.spi.UserServicePort; +import com.aws.ws.infrastructure.common.exception.NotFoundException; import reactor.core.publisher.Mono; public class UserUseCase implements UserServicePort { @@ -42,7 +44,7 @@ public Mono findUserByEmail(String email) { } return userAdapter.findUserByEmail(email) - .switchIfEmpty(Mono.error(new RuntimeException("User not found with email: " + email))); + .switchIfEmpty(Mono.error(new NotFoundException(TechnicalMessage.NOT_FOUND, "User not found with email: ", email))); } // @Override diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java index 1c3ae61..1898ae7 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java @@ -14,6 +14,10 @@ public class LoginRequest { @Schema(description = "Email of the user", example = "rasysbox@hotmail.com") private String email; + @Schema(description = "First name of the user", example = "Raul") + private String firstName; + @Schema(description = "Last name of the user", example = "Bolivar") + private String lastName; @Schema(description = "Password of the user", example = "password123") private String password; } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/enums/Roles.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/enums/Roles.java new file mode 100644 index 0000000..36f3ffe --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/enums/Roles.java @@ -0,0 +1,15 @@ +package com.aws.ws.infrastructure.inbound.enums; + +import lombok.Getter; + +@Getter +public enum Roles { + ADMIN("ROLE_ADMIN"), + USER("ROLE_USER"); + + private final String roleName; + + Roles(String roleName) { + this.roleName = roleName; + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java index ab7a31f..10fbc8f 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -5,6 +5,7 @@ import com.aws.ws.infrastructure.common.util.JwtUtil; import com.aws.ws.infrastructure.inbound.dto.LoginRequest; import com.aws.ws.infrastructure.inbound.dto.UserDTO; +import com.aws.ws.infrastructure.inbound.enums.Roles; import com.aws.ws.infrastructure.inbound.mapper.UserMapper; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -78,9 +79,9 @@ public Mono register(ServerRequest request) { UserDTO userDTO = new UserDTO(); userDTO.setEmail(login.getEmail()); userDTO.setPassword(passwordEncoder.encode(login.getPassword())); - userDTO.setFirstName("DefaultFirstName"); - userDTO.setLastName("DefaultLastName"); - userDTO.setRole("USER"); + userDTO.setFirstName(login.getFirstName()); + userDTO.setLastName(login.getLastName()); + userDTO.setRole(Roles.USER.getRoleName()); return servicePort.createUser(mapper.toDomain(userDTO)) .flatMap(createdUser -> ServerResponse.status(HttpStatus.CREATED).bodyValue(createdUser)); From 10e613a76b8fc3b025c65b6c8b43f830ea8d3fd7 Mon Sep 17 00:00:00 2001 From: rbolivar Date: Wed, 25 Jun 2025 13:01:56 -0500 Subject: [PATCH 04/15] login and register and token --- .../aws/ws/application/config/BeanConfig.java | 5 - .../com/aws/ws/domain/api/UserAdapter.java | 6 +- .../exception/NotFoundException.java | 3 +- .../java/com/aws/ws/domain/model/Token.java | 21 +++ .../java/com/aws/ws/domain/model/User.java | 2 +- .../aws/ws/domain/spi/UserServicePort.java | 4 +- .../aws/ws/domain/usecase/UserUseCase.java | 25 ++-- .../persistence/UserPersistenceAdapter.java | 141 +++++++----------- .../persistence/constants/UserDefinition.java | 23 +++ .../persistence/entity/UserEntity.java | 32 ---- .../persistence/mapper/UserEntityMapper.java | 7 - .../mapper/UserPersistenceMapper.java | 23 +++ .../infrastructure/inbound/RouterConfig.java | 75 ---------- .../infrastructure/inbound/dto/UserDTO.java | 25 ---- .../inbound/handler/UserHandler.java | 76 ++++------ .../inbound/mapper/UserMapper.java | 20 ++- .../src/main/resources/application.yml | 4 +- 17 files changed, 192 insertions(+), 300 deletions(-) rename auth-backend-service/src/main/java/com/aws/ws/{infrastructure/common => domain}/exception/NotFoundException.java (77%) create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/constants/UserDefinition.java delete mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java delete mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserPersistenceMapper.java delete mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java index 24a1d4b..11f59ab 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java @@ -13,9 +13,4 @@ public UserUseCase catalogUseCase(UserAdapter userAdapter) { return new UserUseCase(userAdapter); } -// @Bean -// public CatalogPersistenceAdapter catalogPersistenceAdapter(DynamoDbAsyncClient client) { -// return new CatalogPersistenceAdapter(client); -// } - } diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java index 7f7ae62..0780991 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java @@ -1,14 +1,14 @@ package com.aws.ws.domain.api; +import com.aws.ws.domain.model.Token; import com.aws.ws.domain.model.User; import reactor.core.publisher.Mono; public interface UserAdapter { -// Mono existsByEmail(String email); -// -// Mono getByUserId(String userId); Mono createUser(User domain); Mono findUserByEmail(String email); + + Mono saveToken(Token token); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NotFoundException.java similarity index 77% rename from auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java rename to auth-backend-service/src/main/java/com/aws/ws/domain/exception/NotFoundException.java index efc9cd2..179ffa0 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/exception/NotFoundException.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NotFoundException.java @@ -1,6 +1,5 @@ -package com.aws.ws.infrastructure.common.exception; +package com.aws.ws.domain.exception; -import com.aws.ws.domain.exception.TechnicalMessage; import lombok.Getter; @Getter diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java b/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java new file mode 100644 index 0000000..b297f95 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java @@ -0,0 +1,21 @@ +package com.aws.ws.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Token { + private String tokenId; // puede ser UUID + private String userId; + private String jwt; + private Instant issuedAt; + private Instant expiresAt; + private boolean active; // marcarlo false en logout +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java b/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java index 67deb6f..b082dc5 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java @@ -10,7 +10,7 @@ @AllArgsConstructor @NoArgsConstructor public class User { - private String ID; + private String userId; private String email; private String firstName; private String lastName; diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java index ef8c58f..d229f0e 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java @@ -1,12 +1,14 @@ package com.aws.ws.domain.spi; +import com.aws.ws.domain.model.Token; import com.aws.ws.domain.model.User; import reactor.core.publisher.Mono; public interface UserServicePort { -// Mono getByUserId(String userId); Mono createUser(User domain); Mono findUserByEmail(String email); + + Mono saveToken(Token token); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java index 222a5b6..b33cd08 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -1,13 +1,10 @@ package com.aws.ws.domain.usecase; import com.aws.ws.domain.api.UserAdapter; -import com.aws.ws.domain.exception.BusinessException; -import com.aws.ws.domain.exception.DuplicateResourceException; -import com.aws.ws.domain.exception.InvalidValueException; -import com.aws.ws.domain.exception.TechnicalMessage; +import com.aws.ws.domain.exception.*; +import com.aws.ws.domain.model.Token; import com.aws.ws.domain.model.User; import com.aws.ws.domain.spi.UserServicePort; -import com.aws.ws.infrastructure.common.exception.NotFoundException; import reactor.core.publisher.Mono; public class UserUseCase implements UserServicePort { @@ -47,9 +44,17 @@ public Mono findUserByEmail(String email) { .switchIfEmpty(Mono.error(new NotFoundException(TechnicalMessage.NOT_FOUND, "User not found with email: ", email))); } -// @Override -// public Mono getByUserId(String userId) { -// return userAdapter.getByUserId(userId) -// .switchIfEmpty(Mono.error(new RuntimeException("User not found with id: " + userId))); -// } + @Override + public Mono saveToken(Token token) { + if (token == null || token.getUserId() == null || token.getJwt() == null) { + return Mono.error(new InvalidValueException( + "User ID and Token", + "must not be null" + )); + } + + return userAdapter.saveToken(token) + .onErrorResume(e -> Mono.error(new BusinessException( + TechnicalMessage.BAD_REQUEST.getMessage(), "Error saving token: ", e.getMessage()))); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java index b3c8e12..067d467 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -1,10 +1,15 @@ package com.aws.ws.infrastructure.adapters.persistence; import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.model.Token; import com.aws.ws.domain.model.User; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.aws.ws.infrastructure.adapters.persistence.constants.UserDefinition; +import com.aws.ws.infrastructure.adapters.persistence.mapper.UserPersistenceMapper; +import com.aws.ws.infrastructure.inbound.enums.Roles; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; @@ -16,34 +21,41 @@ @Slf4j @Service +@RequiredArgsConstructor public class UserPersistenceAdapter implements UserAdapter { - @Value("${aws.dynamodb.table-name}") - private String tableName; + @Value("${aws.dynamodb.table.users}") + private String tableUsers; - private final DynamoDbAsyncClient client; - private final ObjectMapper mapper = new ObjectMapper(); + @Value("${aws.dynamodb.table.tokens}") + private String tableTokens; - public UserPersistenceAdapter(DynamoDbAsyncClient client) { - this.client = client; - } + private final DynamoDbAsyncClient client; + private final PasswordEncoder passwordEncoder; + private final UserPersistenceMapper mapper; @Override public Mono createUser(User user) { - if (user.getID() == null) { - user.setID(UUID.randomUUID().toString()); + if (user.getUserId() == null) { + user.setUserId(UUID.randomUUID().toString()); } - log.info("Creating user with ID: {}", user.getID()); + log.info("Creating user with ID: {}", user.getUserId()); + + User newUser = new User(); + newUser.setEmail(user.getEmail()); + newUser.setPassword(passwordEncoder.encode(user.getPassword())); + newUser.setFirstName(user.getFirstName()); + newUser.setLastName(user.getLastName()); PutItemRequest request = PutItemRequest.builder() - .tableName(tableName) + .tableName(tableUsers) .item(Map.of( - "ID", AttributeValue.builder().s(user.getID()).build(), - "email", AttributeValue.builder().s(user.getEmail()).build(), - "firstName", AttributeValue.builder().s(user.getFirstName()).build(), - "lastName", AttributeValue.builder().s(user.getLastName()).build(), - "password", AttributeValue.builder().s(user.getPassword()).build(), - "role", AttributeValue.builder().s(user.getRole()).build() + UserDefinition.USER_ID, AttributeValue.builder().s(user.getUserId()).build(), + UserDefinition.USER_EMAIL, AttributeValue.builder().s(newUser.getEmail()).build(), + UserDefinition.USER_FIRSTNAME, AttributeValue.builder().s(newUser.getFirstName()).build(), + UserDefinition.USER_LASTNAME, AttributeValue.builder().s(newUser.getLastName()).build(), + UserDefinition.USER_PASSWORD, AttributeValue.builder().s(newUser.getPassword()).build(), + UserDefinition.USER_ROLE, AttributeValue.builder().s(Roles.USER.getRoleName()).build() )) .build(); @@ -54,8 +66,8 @@ public Mono createUser(User user) { @Override public Mono findUserByEmail(String email) { ScanRequest request = ScanRequest.builder() - .tableName(tableName) - .filterExpression("email = :emailVal") + .tableName(tableUsers) + .filterExpression(UserDefinition.USER_EMAIL + " = :emailVal") .expressionAttributeValues(Map.of( ":emailVal", AttributeValue.builder().s(email).build() )) @@ -69,74 +81,35 @@ public Mono findUserByEmail(String email) { return Mono.empty(); } log.info("✅ User found: {}", items.getFirst()); - return Mono.just(convert(items.getFirst())); + return Mono.just(mapper.toUser(items.getFirst())); }) .doOnError(e -> log.error("❌ Error scanning DynamoDB for email {}: {}", email, e.getMessage())); } + @Override + public Mono saveToken(Token token) { + if (token.getTokenId() == null) { + token.setTokenId(UUID.randomUUID().toString()); + } + log.info("Creating token with Token ID: {}", token.getTokenId()); - private User convert(Map item) { - User user = new User(); - user.setID(item.get("ID").s()); - user.setEmail(item.get("email").s()); - user.setFirstName(item.get("firstName").s()); - user.setLastName(item.get("lastName").s()); - user.setPassword(item.get("password").s()); - user.setRole(item.get("role").s()); - return user; - } + PutItemRequest request = PutItemRequest.builder() + .tableName(tableTokens) + .item(Map.of( + UserDefinition.TOKEN_ID, AttributeValue.builder().s(token.getTokenId()).build(), + UserDefinition.TOKEN_USER_ID, AttributeValue.builder().s(token.getUserId()).build(), + UserDefinition.TOKEN_JWT, AttributeValue.builder().s(token.getJwt()).build(), + UserDefinition.TOKEN_CREATED_DATE, AttributeValue.builder().s(token.getIssuedAt().toString()).build(), + UserDefinition.TOKEN_EXPIRATION_DATE, AttributeValue.builder().s(token.getExpiresAt().toString()).build(), + UserDefinition.TOKEN_ACTIVE, AttributeValue.builder().bool(token.isActive()).build() + )) + .build(); -// @Override -// public Mono existsByEmail(String email) { -// Map key = new HashMap<>(); -// key.put("email", AttributeValue.builder().s(email).build()); -// -// GetItemRequest request = GetItemRequest.builder() -// .tableName(tableName) -// .key(key) -// .build(); -// -// CompletableFuture future = client.getItem(request); -// -// return Mono.fromFuture(future) -// .map(GetItemResponse::hasItem) -// .onErrorReturn(false); // Si ocurre un error, asumimos que el usuario no existe -// } -// -// @Override -// public Mono getByUserId(String userId) { -// Map key = new HashMap<>(); -// key.put("ID", AttributeValue.builder().s(userId).build()); -// -// GetItemRequest request = GetItemRequest.builder() -// .tableName(tableName) -// .key(key) -// .build(); -// -// CompletableFuture future = client.getItem(request); -// -// return Mono.fromFuture(future) -// .handle((response, sink) -> { -// if (!response.hasItem()) { -// sink.error(new NotFoundException( -// TechnicalMessage.NOT_FOUND, "Catalog not found with catalogId: ", userId)); -// return; -// } -// -// Map item = response.item(); -// -// User user = new User(); -// //user.setCatalogId(item.get("ID").s()); // ✅ ID for catalogId -// // user.setCatalogName(item.get("catalogName").s()); -// -// try { -// String itemsJson = item.get("items").s(); -// //user.setItems(mapper.readValue(itemsJson, new TypeReference<>() {})); -// } catch (Exception e) { -// sink.error(new RuntimeException("Error parsing items", e)); -// return; -// } -// sink.next(user); -// }); -// } + return Mono.fromFuture(() -> client.putItem(request)) + .then(Mono.just(true)) + .onErrorResume(error -> { + log.error("❌ Error saving token: {}", error.getMessage()); + return Mono.just(false); + }); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/constants/UserDefinition.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/constants/UserDefinition.java new file mode 100644 index 0000000..7437f0c --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/constants/UserDefinition.java @@ -0,0 +1,23 @@ +package com.aws.ws.infrastructure.adapters.persistence.constants; + +public final class UserDefinition { + // Constants for User entity attributes + public static final String USER_ID = "userId"; + public static final String USER_FIRSTNAME = "userName"; + public static final String USER_LASTNAME = "userLastName"; + public static final String USER_EMAIL = "userEmail"; + public static final String USER_PASSWORD = "userPassword"; + public static final String USER_ROLE = "userRole"; + + // Constants for Token entity attributes + public static final String TOKEN_ID = "tokenId"; + public static final String TOKEN_USER_ID = "userId"; + public static final String TOKEN_JWT = "token"; + public static final String TOKEN_EXPIRATION_DATE = "expiresAt"; + public static final String TOKEN_CREATED_DATE = "issuedAt"; + public static final String TOKEN_ACTIVE = "active"; + + private UserDefinition() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java deleted file mode 100644 index c45be33..0000000 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/entity/UserEntity.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.aws.ws.infrastructure.adapters.persistence.entity; - -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute; -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey; -import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable; -import lombok.Getter; -import lombok.Setter; -import org.springframework.data.annotation.Id; - -@Getter -@Setter -@DynamoDBTable(tableName = "${aws.dynamodb.table-name}") -public class UserEntity { - @Id - @DynamoDBHashKey(attributeName = "ID") - private String ID; - - @DynamoDBAttribute - private String email; - - @DynamoDBAttribute - private String firstName; - - @DynamoDBAttribute - private String lastName; - - @DynamoDBAttribute - private String password; - - @DynamoDBAttribute - private String role; -} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java deleted file mode 100644 index a5b944f..0000000 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserEntityMapper.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.aws.ws.infrastructure.adapters.persistence.mapper; - -import org.springframework.stereotype.Component; - -@Component -public class UserEntityMapper { -} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserPersistenceMapper.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserPersistenceMapper.java new file mode 100644 index 0000000..d850047 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/mapper/UserPersistenceMapper.java @@ -0,0 +1,23 @@ +package com.aws.ws.infrastructure.adapters.persistence.mapper; + +import com.aws.ws.domain.model.User; +import com.aws.ws.infrastructure.adapters.persistence.constants.UserDefinition; +import org.springframework.stereotype.Component; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; + +import java.util.Map; + +@Component +public class UserPersistenceMapper { + + public User toUser(Map item) { + User user = new User(); + user.setUserId(item.get(UserDefinition.USER_ID).s()); + user.setEmail(item.get(UserDefinition.USER_EMAIL).s()); + user.setFirstName(item.get(UserDefinition.USER_FIRSTNAME).s()); + user.setLastName(item.get(UserDefinition.USER_LASTNAME).s()); + user.setPassword(item.get(UserDefinition.USER_PASSWORD).s()); + user.setRole(item.get(UserDefinition.USER_ROLE).s()); + return user; + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java index 2c7e558..bea25f5 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -70,79 +70,4 @@ public RouterFunction userRoutes(UserHandler userHandler) { ) .build(); } - -// @Bean -// @RouterOperations({ -// // Define the route for getting a user by ID -// @org.springdoc.core.annotations.RouterOperation( -// path = "/v1/users/{id}", -// produces = "application/json", -// method = org.springframework.web.bind.annotation.RequestMethod.GET, -// beanClass = UserHandler.class, -// beanMethod = "getByUserId", -// operation = @io.swagger.v3.oas.annotations.Operation( -// operationId = "getByUserId", -// summary = "Get User by ID", -// description = "Retrieve User by ID from the database.", -// parameters = @io.swagger.v3.oas.annotations.Parameter( -// name = "id", -// description = "ID of the User to retrieve", -// required = true, -// in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, -// example = "user_12345" // Example ID for documentation purposes -// ) -// ) -// ), -// // Create a user request UserDTO.class -// @org.springdoc.core.annotations.RouterOperation( -// path = "/v1/users", -// produces = "application/json", -// method = org.springframework.web.bind.annotation.RequestMethod.POST, -// beanClass = UserHandler.class, -// beanMethod = "createUser", -// operation = @io.swagger.v3.oas.annotations.Operation( -// operationId = "createUser", -// summary = "Create User", -// description = "Create a new User in the database.", -// requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( -// required = true, -// description = "User DTO for creating a new user", -// content = @io.swagger.v3.oas.annotations.media.Content( -// mediaType = "application/json", -// schema = @io.swagger.v3.oas.annotations.media.Schema(implementation = UserDTO.class) -// ) -// ) -// ) -// ), -// // Define the route for finding a user by email -// @org.springdoc.core.annotations.RouterOperation( -// path = "/v1/users/{email}", -// produces = "application/json", -// method = org.springframework.web.bind.annotation.RequestMethod.GET, -// beanClass = UserHandler.class, -// beanMethod = "findUserByEmail", -// operation = @io.swagger.v3.oas.annotations.Operation( -// operationId = "findUserByEmail", -// summary = "Find User by Email", -// description = "Retrieve User by Email from the database.", -// parameters = @io.swagger.v3.oas.annotations.Parameter( -// name = "email", -// description = "Email of the User to retrieve", -// required = true, -// in = io.swagger.v3.oas.annotations.enums.ParameterIn.PATH, -// example = "rasysbox@hotmail.com" -// ) -// ) -// ) -// }) -// public RouterFunction route(UserHandler handler) { -// return org.springframework.web.reactive.function.server.RouterFunctions -// .route() -// .GET("/v1/users/{email}", handler::findUserByEmail) -// .POST("/v1/users", handler::createUser) -//// .GET("/v1/users/{id}", handler::getByUserId) -//// .PUT("/users/{id}", handler::updateUser) -//// .DELETE("/users/{id}", handler::deleteUser) -// .build(); -// } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java deleted file mode 100644 index 4e51ae4..0000000 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/UserDTO.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.aws.ws.infrastructure.inbound.dto; - -import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor -@Schema(description = "Data Transfer Object for User", title = "UserDTO") -public class UserDTO { - @Schema(description = "Email for the user", example = "rasysbox@hotmail.com") - private String email; - @Schema(description = "First name of the user", example = "Raul") - private String firstName; - @Schema(description = "Last name of the user", example = "Bolivar") - private String lastName; - @Schema(description = "Password for the user", example = "password123") - private String password; - @Schema(description = "Role of the user", example = "USER") - private String role; -} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java index 10fbc8f..f92c922 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -1,11 +1,10 @@ package com.aws.ws.infrastructure.inbound.handler; +import com.aws.ws.domain.model.Token; import com.aws.ws.domain.spi.UserServicePort; import com.aws.ws.infrastructure.common.handler.GlobalErrorHandler; import com.aws.ws.infrastructure.common.util.JwtUtil; import com.aws.ws.infrastructure.inbound.dto.LoginRequest; -import com.aws.ws.infrastructure.inbound.dto.UserDTO; -import com.aws.ws.infrastructure.inbound.enums.Roles; import com.aws.ws.infrastructure.inbound.mapper.UserMapper; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -17,6 +16,7 @@ import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; +import java.time.Instant; import java.util.List; import java.util.Map; @@ -34,29 +34,11 @@ public class UserHandler { private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; -// // Method to handle fetching user by ID -// public Mono getByUserId(ServerRequest request) { -// String userId = request.pathVariable("userId"); -// return servicePort.getByUserId(userId) -// .flatMap(user -> ServerResponse.ok().bodyValue(user)) -// .doOnError(error -> log.error("Error fetching user with ID {}: {}", userId, error.getMessage())) -// .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); -// } - - public Mono createUser(ServerRequest request) { - log.debug("Creating new user"); - return request.bodyToMono(UserDTO.class) - .flatMap(userDTO -> servicePort.createUser(mapper.toDomain(userDTO))) - .flatMap(user -> ServerResponse.status(HttpStatus.CREATED).bodyValue(user)) - .doOnError(error -> log.error("Error creating user: {}", error.getMessage())) - .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); - } - - public Mono findUserByEmail(ServerRequest request) { - String email = request.pathVariable("email"); - return servicePort.findUserByEmail(email) - .flatMap(user -> ServerResponse.ok().bodyValue(user)) - .doOnError(error -> log.error("Error finding user with email {}: {}", email, error.getMessage())) + public Mono register(ServerRequest request) { + return request.bodyToMono(LoginRequest.class) + .flatMap(login -> servicePort.createUser(mapper.toDomain(login)) + .flatMap(createdUser -> ServerResponse.status(HttpStatus.CREATED).bodyValue(createdUser))) + .doOnError(error -> log.error("❌ Error registering user: {}", error.getMessage())) .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); } @@ -64,31 +46,33 @@ public Mono login(ServerRequest request) { return request.bodyToMono(LoginRequest.class) .flatMap(login -> servicePort.findUserByEmail(login.getEmail()) .flatMap(user -> { - if (passwordEncoder.matches(login.getPassword(), user.getPassword())) { - String token = jwtUtil.generateToken(user.getEmail(), List.of(user.getRole())); - return ServerResponse.ok().bodyValue(Map.of("token", token)); - } else { + if (!passwordEncoder.matches(login.getPassword(), user.getPassword())) { return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); } - })); - } - public Mono register(ServerRequest request) { - return request.bodyToMono(LoginRequest.class) - .flatMap(login -> { - UserDTO userDTO = new UserDTO(); - userDTO.setEmail(login.getEmail()); - userDTO.setPassword(passwordEncoder.encode(login.getPassword())); - userDTO.setFirstName(login.getFirstName()); - userDTO.setLastName(login.getLastName()); - userDTO.setRole(Roles.USER.getRoleName()); + String token = jwtUtil.generateToken(user.getEmail(), List.of(user.getRole())); - return servicePort.createUser(mapper.toDomain(userDTO)) - .flatMap(createdUser -> ServerResponse.status(HttpStatus.CREATED).bodyValue(createdUser)); - }) - .doOnError(error -> log.error("❌ Error registering user: {}", error.getMessage())) - .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); - } + Token tokenToSave = Token.builder() + .jwt(token) + .userId(user.getUserId()) + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .active(true) + .build(); + return servicePort.saveToken(tokenToSave) + .flatMap(success -> { + if (success) { + return ServerResponse.ok().bodyValue(Map.of( + "token", token, + "messageId", getMessageId(request) + )); + } else { + return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) + .bodyValue(Map.of("error", "Failed to save token")); + } + }); + })); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java index da3d514..c2584ce 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java @@ -1,19 +1,23 @@ package com.aws.ws.infrastructure.inbound.mapper; import com.aws.ws.domain.model.User; -import com.aws.ws.infrastructure.inbound.dto.UserDTO; +import com.aws.ws.infrastructure.inbound.dto.LoginRequest; +import com.aws.ws.infrastructure.inbound.enums.Roles; +import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; @Component +@RequiredArgsConstructor public class UserMapper { - public User toDomain(UserDTO userDTO) { - if (userDTO == null) return null; + + public User toDomain(LoginRequest user) { + if (user == null) return null; return User.builder() - .email(userDTO.getEmail()) - .firstName(userDTO.getFirstName()) - .lastName(userDTO.getLastName()) - .password(userDTO.getPassword()) - .role(userDTO.getRole()) + .email(user.getEmail()) + .firstName(user.getFirstName()) + .lastName(user.getLastName()) + .password(user.getPassword()) + .role(Roles.USER.getRoleName()) .build(); } } diff --git a/auth-backend-service/src/main/resources/application.yml b/auth-backend-service/src/main/resources/application.yml index ea683ae..da0d223 100644 --- a/auth-backend-service/src/main/resources/application.yml +++ b/auth-backend-service/src/main/resources/application.yml @@ -3,7 +3,9 @@ aws: region: us-east-1 dynamodb: endpoint: http://localhost:4566 - table-name: Users + table: + users: Users + tokens: Tokens secret: access-key: test key-id: test From 89cfeff05926b4d95247d4e7fcffab3db048269d Mon Sep 17 00:00:00 2001 From: rbolivar Date: Wed, 25 Jun 2025 13:13:43 -0500 Subject: [PATCH 05/15] al hacer login se desactivan los tokens anteriores y se deja activo el actual token generado --- .../ws/application/config/SecurityConfig.java | 6 +- .../persistence/UserPersistenceAdapter.java | 62 ++++++++++++++----- .../infrastructure/inbound/RouterConfig.java | 6 +- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java index e462450..3e3ab0a 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java @@ -52,9 +52,9 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) .pathMatchers("/webjars/**").permitAll() .pathMatchers("/api-docs/**").permitAll() .pathMatchers("/actuator/**").permitAll() - .pathMatchers(HttpMethod.POST, "/api/users/login").permitAll() - .pathMatchers(HttpMethod.POST, "/api/users/register").permitAll() - .pathMatchers("/api/users/**").authenticated() + .pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll() + .pathMatchers(HttpMethod.POST, "/api/auth/register").permitAll() +// .pathMatchers("/api/users/**").authenticated() // .pathMatchers(HttpMethod.POST, "/v1/users").authenticated() // .pathMatchers(HttpMethod.GET, "/v1/users/{email}").authenticated() .anyExchange().authenticated() diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java index 067d467..0d4c94f 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.model.*; @@ -93,23 +94,56 @@ public Mono saveToken(Token token) { } log.info("Creating token with Token ID: {}", token.getTokenId()); - PutItemRequest request = PutItemRequest.builder() + // 1. Desactivar tokens anteriores + return deactivateActiveTokensByUserId(token.getUserId()) + // 2. Guardar el nuevo token + .then(Mono.fromFuture(() -> client.putItem( + PutItemRequest.builder() + .tableName(tableTokens) + .item(Map.of( + UserDefinition.TOKEN_ID, AttributeValue.builder().s(token.getTokenId()).build(), + UserDefinition.TOKEN_USER_ID, AttributeValue.builder().s(token.getUserId()).build(), + UserDefinition.TOKEN_JWT, AttributeValue.builder().s(token.getJwt()).build(), + UserDefinition.TOKEN_CREATED_DATE, AttributeValue.builder().s(token.getIssuedAt().toString()).build(), + UserDefinition.TOKEN_EXPIRATION_DATE, AttributeValue.builder().s(token.getExpiresAt().toString()).build(), + UserDefinition.TOKEN_ACTIVE, AttributeValue.builder().bool(true).build() + )) + .build() + ))).thenReturn(true); + } + + private Mono deactivateActiveTokensByUserId(String userId) { + ScanRequest scanRequest = ScanRequest.builder() .tableName(tableTokens) - .item(Map.of( - UserDefinition.TOKEN_ID, AttributeValue.builder().s(token.getTokenId()).build(), - UserDefinition.TOKEN_USER_ID, AttributeValue.builder().s(token.getUserId()).build(), - UserDefinition.TOKEN_JWT, AttributeValue.builder().s(token.getJwt()).build(), - UserDefinition.TOKEN_CREATED_DATE, AttributeValue.builder().s(token.getIssuedAt().toString()).build(), - UserDefinition.TOKEN_EXPIRATION_DATE, AttributeValue.builder().s(token.getExpiresAt().toString()).build(), - UserDefinition.TOKEN_ACTIVE, AttributeValue.builder().bool(token.isActive()).build() + .filterExpression("userId = :uid AND active = :act") + .expressionAttributeValues(Map.of( + ":uid", AttributeValue.builder().s(userId).build(), + ":act", AttributeValue.builder().bool(true).build() )) .build(); - return Mono.fromFuture(() -> client.putItem(request)) - .then(Mono.just(true)) - .onErrorResume(error -> { - log.error("❌ Error saving token: {}", error.getMessage()); - return Mono.just(false); - }); + return Mono.fromFuture(() -> client.scan(scanRequest)) + .flatMapMany(response -> Flux.fromIterable(response.items())) + .flatMap(item -> { + String tokenId = item.get(UserDefinition.TOKEN_ID).s(); // ✅ se usa la constante correctamente + Map updates = Map.of( + "active", AttributeValueUpdate.builder() + .value(AttributeValue.builder().bool(false).build()) + .action(AttributeAction.PUT) + .build() + ); + + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .tableName(tableTokens) + .key(Map.of( + UserDefinition.TOKEN_ID, AttributeValue.builder().s(tokenId).build() // ✅ clave correcta + )) + .attributeUpdates(updates) + .build(); + + return Mono.fromFuture(() -> client.updateItem(updateRequest)).then(); + }) + .then(); // Mono } + } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java index bea25f5..d927cbf 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -18,7 +18,7 @@ public class RouterConfig { @RouterOperations({ // Define the route for user login @org.springdoc.core.annotations.RouterOperation( - path = "/api/users/login", + path = "/api/auth/login", produces = "application/json", method = org.springframework.web.bind.annotation.RequestMethod.POST, beanClass = UserHandler.class, @@ -39,7 +39,7 @@ public class RouterConfig { ), // Define the route for user registration @org.springdoc.core.annotations.RouterOperation( - path = "/api/users/register", + path = "/api/auth/register", produces = "application/json", method = org.springframework.web.bind.annotation.RequestMethod.POST, beanClass = UserHandler.class, @@ -61,7 +61,7 @@ public class RouterConfig { }) public RouterFunction userRoutes(UserHandler userHandler) { return route() - .path("/api/users", builder -> builder + .path("/api/auth", builder -> builder .POST("/register", accept(MediaType.APPLICATION_JSON), userHandler::register) .POST("/login", accept(MediaType.APPLICATION_JSON), userHandler::login) // .GET("/me", userHandler::getProfile) // requiere JWT From a6ba5dff82107d7d88e78df9fa7406840409a2a3 Mon Sep 17 00:00:00 2001 From: rbolivar Date: Wed, 25 Jun 2025 13:41:42 -0500 Subject: [PATCH 06/15] se adiciona logout --- .../com/aws/ws/domain/api/UserAdapter.java | 4 + .../aws/ws/domain/spi/UserServicePort.java | 4 + .../aws/ws/domain/usecase/UserUseCase.java | 22 +++++ .../persistence/UserPersistenceAdapter.java | 82 +++++++++++++++++++ .../infrastructure/inbound/RouterConfig.java | 2 +- .../inbound/handler/UserHandler.java | 23 ++++++ 6 files changed, 136 insertions(+), 1 deletion(-) diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java index 0780991..d8e671a 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java @@ -11,4 +11,8 @@ public interface UserAdapter { Mono findUserByEmail(String email); Mono saveToken(Token token); + + Mono existsTokenByJwt(String jwt); + + Mono logout(String jwt); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java index d229f0e..c06386d 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java @@ -11,4 +11,8 @@ public interface UserServicePort { Mono findUserByEmail(String email); Mono saveToken(Token token); + + Mono logout(String jwt); + + Mono existsTokenByJwt(String jwt); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java index b33cd08..c60ff2f 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -57,4 +57,26 @@ public Mono saveToken(Token token) { .onErrorResume(e -> Mono.error(new BusinessException( TechnicalMessage.BAD_REQUEST.getMessage(), "Error saving token: ", e.getMessage()))); } + + @Override + public Mono logout(String jwt) { + if (jwt == null || jwt.isEmpty()) { + return Mono.error(new InvalidValueException("JWT", "must not be null or empty")); + } + + return userAdapter.logout(jwt) + .onErrorResume(e -> Mono.error(new BusinessException( + TechnicalMessage.BAD_REQUEST.getMessage(), "Error during logout: ", e.getMessage()))); + } + + @Override + public Mono existsTokenByJwt(String jwt) { + if (jwt == null || jwt.isEmpty()) { + return Mono.error(new InvalidValueException("JWT", "must not be null or empty")); + } + + return userAdapter.existsTokenByJwt(jwt) + .onErrorResume(e -> Mono.error(new BusinessException( + TechnicalMessage.BAD_REQUEST.getMessage(), "Error checking token existence: ", e.getMessage()))); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java index 0d4c94f..56644b6 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -146,4 +146,86 @@ private Mono deactivateActiveTokensByUserId(String userId) { .then(); // Mono } + @Override + public Mono existsTokenByJwt(String jwt) { + ScanRequest request = ScanRequest.builder() + .tableName(tableTokens) + .filterExpression("#tk = :jwtVal") + .expressionAttributeNames(Map.of( + "#tk", UserDefinition.TOKEN_JWT // Esto debe ser "token" + )) + .expressionAttributeValues(Map.of( + ":jwtVal", AttributeValue.builder().s(jwt).build() + )) + .build(); + + return Mono.fromFuture(() -> client.scan(request)) + .flatMap(scanResponse -> { + List> items = scanResponse.items(); + if (items == null || items.isEmpty()) { + log.info("✅ No token found with JWT: {}", jwt); + return Mono.just(false); + } + log.info("✅ Token found: {}", items.getFirst()); + return Mono.just(true); + }) + .doOnError(e -> log.error("❌ Error scanning DynamoDB for JWT {}: {}", jwt, e.getMessage())); + } + + @Override + public Mono logout(String jwt) { + log.info("Logging out user with JWT: {}", jwt); + return existsTokenByJwt(jwt) + .flatMap(exists -> { + if (!exists) { + log.warn("❌ No active token found for JWT: {}", jwt); + return Mono.just(false); + } + return deactivateActiveTokensByJwt(jwt) + .thenReturn(true); + }); + } + + private Mono deactivateActiveTokensByJwt(String jwt) { +// log.info("Filter: {}", "#t = :jwtVal AND #active = :activeVal"); +// log.info("AttrNames: {}", Map.of("#t", UserDefinition.TOKEN_JWT, "#active", UserDefinition.TOKEN_ACTIVE)); +// log.info("AttrValues: {}", Map.of(":jwtVal", jwt, ":activeVal", true)); + + ScanRequest scanRequest = ScanRequest.builder() + .tableName(tableTokens) + .filterExpression("#tk = :jwtVal AND #active = :activeVal") + .expressionAttributeNames(Map.of( + "#tk", "token", + "#active", "active" + )) + .expressionAttributeValues(Map.of( + ":jwtVal", AttributeValue.builder().s(jwt).build(), + ":activeVal", AttributeValue.builder().bool(true).build() + )) + .build(); + + return Mono.fromFuture(() -> client.scan(scanRequest)) + .flatMapMany(response -> Flux.fromIterable(response.items())) + .flatMap(item -> { + String tokenId = item.get(UserDefinition.TOKEN_ID).s(); + + Map updates = Map.of( + UserDefinition.TOKEN_ACTIVE, AttributeValueUpdate.builder() + .value(AttributeValue.builder().bool(false).build()) + .action(AttributeAction.PUT) + .build() + ); + + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .tableName(tableTokens) + .key(Map.of( + UserDefinition.TOKEN_ID, AttributeValue.builder().s(tokenId).build() + )) + .attributeUpdates(updates) + .build(); + + return Mono.fromFuture(() -> client.updateItem(updateRequest)).then(); + }) + .then(Mono.just(true)); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java index d927cbf..dad2378 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -66,7 +66,7 @@ public RouterFunction userRoutes(UserHandler userHandler) { .POST("/login", accept(MediaType.APPLICATION_JSON), userHandler::login) // .GET("/me", userHandler::getProfile) // requiere JWT // .PUT("/update", userHandler::updateUser) // requiere JWT -// .POST("/logout", userHandler::logout) // requiere JWT + .POST("/logout", userHandler::logout) // requiere JWT ) .build(); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java index f92c922..940aac9 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -75,4 +75,27 @@ public Mono login(ServerRequest request) { })); } + public Mono logout(ServerRequest request) { + String token = request.headers().firstHeader("Authorization"); + if (token == null || !token.startsWith("Bearer ")) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); + } + String jwt = token.substring(7); // Remove "Bearer " prefix + return servicePort.existsTokenByJwt(jwt) + .flatMap(existingToken -> servicePort.logout(jwt) + .flatMap(success -> { + if (success) { + return ServerResponse.ok().bodyValue(Map.of( + "message", "Logout successful", + "messageId", getMessageId(request) + )); + } else { + return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) + .bodyValue(Map.of("error", "Failed to logout")); + } + })) + .switchIfEmpty(ServerResponse.status(HttpStatus.UNAUTHORIZED).build()) + .doOnError(error -> log.error("❌ Error during logout: {}", error.getMessage())) + .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); + } } From 47568ba3d280b6652c4dfc34d7b16162fda865df Mon Sep 17 00:00:00 2001 From: rbolivar Date: Wed, 25 Jun 2025 13:44:23 -0500 Subject: [PATCH 07/15] se adiciona logout 1 --- .../main/java/com/aws/ws/application/config/SecurityConfig.java | 1 + 1 file changed, 1 insertion(+) diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java index 3e3ab0a..64b5b6f 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java @@ -54,6 +54,7 @@ public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) .pathMatchers("/actuator/**").permitAll() .pathMatchers(HttpMethod.POST, "/api/auth/login").permitAll() .pathMatchers(HttpMethod.POST, "/api/auth/register").permitAll() + .pathMatchers(HttpMethod.POST, "/api/auth/logout").permitAll() // .pathMatchers("/api/users/**").authenticated() // .pathMatchers(HttpMethod.POST, "/v1/users").authenticated() // .pathMatchers(HttpMethod.GET, "/v1/users/{email}").authenticated() From 6a6a0a3431e362925a3ee62d0ad30aaac85bf9ff Mon Sep 17 00:00:00 2001 From: rbolivar Date: Wed, 25 Jun 2025 14:38:13 -0500 Subject: [PATCH 08/15] se integra el dashboard contra el login/logout en el api gateway del backend --- dashboard-ui/src/app/app.config.ts | 3 +- .../app/components/header/header.component.ts | 19 +++++- .../src/app/pages/auth/login.component.html | 20 +++--- .../src/app/pages/auth/login.component.ts | 23 ++++--- .../src/app/shared/guards/auth.guard.ts | 7 +- .../src/app/shared/guards/auth.interceptor.ts | 20 ++++++ .../app/shared/services/auth.service.spec.ts | 16 +++++ .../src/app/shared/services/auth.service.ts | 64 +++++++++++++++++++ .../app/shared/services/dynamodb.service.ts | 17 ++++- .../src/app/shared/services/sqs.service.ts | 13 +++- 10 files changed, 171 insertions(+), 31 deletions(-) create mode 100644 dashboard-ui/src/app/shared/guards/auth.interceptor.ts create mode 100644 dashboard-ui/src/app/shared/services/auth.service.spec.ts create mode 100644 dashboard-ui/src/app/shared/services/auth.service.ts diff --git a/dashboard-ui/src/app/app.config.ts b/dashboard-ui/src/app/app.config.ts index c9086b4..755df6b 100644 --- a/dashboard-ui/src/app/app.config.ts +++ b/dashboard-ui/src/app/app.config.ts @@ -5,6 +5,7 @@ import { provideToastr } from "ngx-toastr"; import { provideHttpClient } from '@angular/common/http'; import { routes } from './app.routes'; +import { AuthInterceptor } from "@shared/guards/auth.interceptor"; export const appConfig: ApplicationConfig = { providers: [ @@ -13,7 +14,7 @@ export const appConfig: ApplicationConfig = { positionClass: 'toast-bottom-right', preventDuplicates: true, }), - importProvidersFrom(BrowserAnimationsModule), + importProvidersFrom(BrowserAnimationsModule, AuthInterceptor), provideHttpClient(), provideRouter(routes) ] diff --git a/dashboard-ui/src/app/components/header/header.component.ts b/dashboard-ui/src/app/components/header/header.component.ts index 5d4b103..5f36b01 100644 --- a/dashboard-ui/src/app/components/header/header.component.ts +++ b/dashboard-ui/src/app/components/header/header.component.ts @@ -1,5 +1,7 @@ import { Component, inject, signal } from '@angular/core'; import { Router } from "@angular/router"; +import { AuthService } from "@shared/services/auth.service"; +import { ToastrService } from "ngx-toastr"; @Component({ selector: 'app-header', @@ -9,12 +11,23 @@ import { Router } from "@angular/router"; styleUrl: './header.component.scss' }) export class HeaderComponent { - router = inject(Router); + private router = inject(Router); + private authService = inject(AuthService); + public readonly toast = inject(ToastrService); + showMenu = signal(false); toggleMenu = () => this.showMenu.update(v => !v); logout() { - localStorage.removeItem('auth'); - this.router.navigate(['/login']); + this.authService.logout().subscribe({ + next: () => { + this.toast.success('Sesión cerrada con éxito'); + this.router.navigate(['/login']); + }, + error: (err) => { + this.toast.error('Error al cerrar sesión'); + console.error('❌ Error al cerrar sesión:', err); + } + }); } } diff --git a/dashboard-ui/src/app/pages/auth/login.component.html b/dashboard-ui/src/app/pages/auth/login.component.html index 96a2501..fdcb130 100644 --- a/dashboard-ui/src/app/pages/auth/login.component.html +++ b/dashboard-ui/src/app/pages/auth/login.component.html @@ -13,26 +13,26 @@
- + - @if (usernameField.invalid && usernameField.touched) { + @if (emailField.invalid && emailField.touched) {

- @if (usernameField.errors?.['required']) { - El usuario es obligatorio. + @if (emailField.errors?.['required']) { + El email es obligatorio. } - @if (usernameField.errors?.['minlength']) { + @if (emailField.errors?.['minlength']) { Mínimo 4 caracteres. } - @if (usernameField.errors?.['maxlength']) { + @if (emailField.errors?.['maxlength']) { Máximo 20 caracteres. }

diff --git a/dashboard-ui/src/app/pages/auth/login.component.ts b/dashboard-ui/src/app/pages/auth/login.component.ts index 9dc1cb8..a25262d 100644 --- a/dashboard-ui/src/app/pages/auth/login.component.ts +++ b/dashboard-ui/src/app/pages/auth/login.component.ts @@ -6,6 +6,7 @@ import { ToastrService } from "ngx-toastr"; import { HttpClient, HttpHeaders } from "@angular/common/http"; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { firstValueFrom } from 'rxjs'; +import {AuthService} from "@shared/services/auth.service"; @Component({ selector: 'app-login', @@ -18,7 +19,10 @@ import { firstValueFrom } from 'rxjs'; styleUrl: './login.component.scss' }) export class LoginComponent implements OnInit { - username: string = ''; + // Inyectamos el cliente HTTP para realizar peticiones al backend + private auth = inject(AuthService); + + email: string = ''; password: string = ''; captchaInput: string = ''; captchaSession: string = ''; @@ -63,13 +67,16 @@ export class LoginComponent implements OnInit { return; }*/ - if (this.username === 'admin' && this.password === 'admin123') { - localStorage.setItem('auth', 'true'); - this.toast.success('Inicio de sesión exitoso ✅'); - setTimeout(() => this.router.navigate(['/']), 1000); - } else { - this.toast.error('Credenciales inválidas ❌'); - } + this.auth.login(this.email, this.password).subscribe({ + next: () => { + this.toast.success('Inicio de sesión exitoso ✅'); + setTimeout(() => this.router.navigate(['/']), 1000); + }, + error: (err) => { + console.error(err); + this.toast.error('Credenciales inválidas ❌'); + }, + }); } async validateCaptcha(): Promise { diff --git a/dashboard-ui/src/app/shared/guards/auth.guard.ts b/dashboard-ui/src/app/shared/guards/auth.guard.ts index 79d5a11..a005da9 100644 --- a/dashboard-ui/src/app/shared/guards/auth.guard.ts +++ b/dashboard-ui/src/app/shared/guards/auth.guard.ts @@ -1,13 +1,12 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; +import { AuthService } from "@shared/services/auth.service"; export const authGuard: CanActivateFn = (route, state) => { const router = inject(Router); + const authService = inject(AuthService); - // Simulación: verifica si el usuario está autenticado (usa localStorage o un AuthService real) - const isAuthenticated = localStorage.getItem('auth') === 'true'; - - if (!isAuthenticated) { + if (!authService.hasValidToken()) { router.navigate(['/login']); return false; } diff --git a/dashboard-ui/src/app/shared/guards/auth.interceptor.ts b/dashboard-ui/src/app/shared/guards/auth.interceptor.ts new file mode 100644 index 0000000..2f168a6 --- /dev/null +++ b/dashboard-ui/src/app/shared/guards/auth.interceptor.ts @@ -0,0 +1,20 @@ +import { inject, Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http'; +import { AuthService } from "@shared/services/auth.service"; + +@Injectable() +export class AuthInterceptor implements HttpInterceptor { + private authService = inject(AuthService); + + intercept(req: HttpRequest, next: HttpHandler) { + const token = this.authService.getToken(); + if (token) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}`, + } + }); + } + return next.handle(req); + } +} diff --git a/dashboard-ui/src/app/shared/services/auth.service.spec.ts b/dashboard-ui/src/app/shared/services/auth.service.spec.ts new file mode 100644 index 0000000..f1251ca --- /dev/null +++ b/dashboard-ui/src/app/shared/services/auth.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + let service: AuthService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AuthService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/dashboard-ui/src/app/shared/services/auth.service.ts b/dashboard-ui/src/app/shared/services/auth.service.ts new file mode 100644 index 0000000..b4bc4fa --- /dev/null +++ b/dashboard-ui/src/app/shared/services/auth.service.ts @@ -0,0 +1,64 @@ +import { inject, Injectable, signal } from '@angular/core'; +import { HttpClient, HttpHeaders } from "@angular/common/http"; +import { tap } from "rxjs"; + +@Injectable({ + providedIn: 'root' +}) +export class AuthService { + // Inyectamos el cliente HTTP para realizar peticiones al backend + private http = inject(HttpClient); + + private readonly TOKEN_KEY = 'auth_token'; + + // Signals + token = signal(null); + isAuthenticated = signal(false); + + constructor() { + this.isAuthenticated.set(this.hasValidToken()); + } + + login(email: string, password: string) { + const headers = new HttpHeaders({ + 'Content-Type': 'application/json', + Accept: 'application/json', + }); + + return this.http.post<{ token: string }>( + '/api/auth/login', + { email, password }, + { headers } + ).pipe( + tap(response => { + this.token.set(response.token); + this.isAuthenticated.set(true); + localStorage.setItem('auth_token', response.token); + }) + ); + } + + logout() { + const token = this.getToken(); + return this.http.post('/api/auth/logout', {}, { + headers: { + Authorization: `Bearer ${token}`, + } + }).pipe( + tap(() => { + localStorage.removeItem(this.TOKEN_KEY); + this.isAuthenticated.set(false); + }) + ); + } + + getToken(): string | null { + return localStorage.getItem(this.TOKEN_KEY); + } + + hasValidToken(): boolean { + const token = this.getToken(); + // Opcional validar expiración + return !!token; + } +} diff --git a/dashboard-ui/src/app/shared/services/dynamodb.service.ts b/dashboard-ui/src/app/shared/services/dynamodb.service.ts index 197028f..4dfbf18 100644 --- a/dashboard-ui/src/app/shared/services/dynamodb.service.ts +++ b/dashboard-ui/src/app/shared/services/dynamodb.service.ts @@ -1,6 +1,6 @@ import { inject, Injectable, signal } from '@angular/core'; import { Observable } from 'rxjs'; -import { HttpClient } from "@angular/common/http"; +import { HttpClient, HttpHeaders } from "@angular/common/http"; @Injectable({ providedIn: 'root' @@ -8,14 +8,23 @@ import { HttpClient } from "@angular/common/http"; export class DynamodbService { // Inyectamos el cliente HTTP para realizar peticiones al backend private http = inject(HttpClient); + private token = localStorage.getItem('auth_token'); // Usamos signals para manejar el estado de las tablas e items tables = signal([]); items = signal[]>([]); + private getHeaders(): HttpHeaders { + return new HttpHeaders({ + 'Authorization': `Bearer ${this.token}`, + }); + } + // ✅ Ir al backend para listar las tablas de DynamoDB listTables(): Observable<{ tables: string[] }> { - return this.http.get<{ tables: string[] }>('/api/dynamodb/tables'); + return this.http.get<{ tables: string[] }>('/api/dynamodb/tables', { + headers: this.getHeaders() + }); } // ✅ Listar tablas de DynamoDB @@ -25,7 +34,9 @@ export class DynamodbService { // ✅ Ir al backend para obtener los items de una tabla específica getItems(table: string): Observable<{ items: Record[] }> { - return this.http.get<{ items: Record[] }>(`/api/dynamodb/items/${table}`); + return this.http.get<{ items: Record[] }>(`/api/dynamodb/items/${table}`, { + headers: this.getHeaders() + }); } // ✅ Obtener items de una tabla específica diff --git a/dashboard-ui/src/app/shared/services/sqs.service.ts b/dashboard-ui/src/app/shared/services/sqs.service.ts index dd2243c..5235b60 100644 --- a/dashboard-ui/src/app/shared/services/sqs.service.ts +++ b/dashboard-ui/src/app/shared/services/sqs.service.ts @@ -1,17 +1,26 @@ import { inject, Injectable, signal } from '@angular/core'; import { Observable } from 'rxjs'; -import { HttpClient } from "@angular/common/http"; +import {HttpClient, HttpHeaders} from "@angular/common/http"; @Injectable({ providedIn: 'root' }) export class SqsService { private http = inject(HttpClient); + private token = localStorage.getItem('auth_token'); queues = signal([]); + private getHeaders(): HttpHeaders { + return new HttpHeaders({ + 'Authorization': `Bearer ${this.token}`, + }); + } + listQueues(): Observable<{ queues: string[] }> { - return this.http.get<{ queues: string[] }>('/api/sqs/list'); + return this.http.get<{ queues: string[] }>('/api/sqs/list', { + headers: this.getHeaders() + }); } fetchQueues() { From c884a5d7ecf0a1148421733cdda5a7d62f9b732c Mon Sep 17 00:00:00 2001 From: rasys Date: Wed, 25 Jun 2025 22:06:26 -0500 Subject: [PATCH 09/15] aws commands --- aws-command.md | 972 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 972 insertions(+) create mode 100644 aws-command.md diff --git a/aws-command.md b/aws-command.md new file mode 100644 index 0000000..b4de5c4 --- /dev/null +++ b/aws-command.md @@ -0,0 +1,972 @@ +# AWS CLI Commands + +## S3 + +#### Create a new S3 bucket +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 s3 mb s3:// +``` + +#### Upload a file to an S3 bucket +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 s3 cp s3:/// +``` + +#### Download a file from an S3 bucket +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 s3 cp s3:/// +``` + +#### List all objects in an S3 bucket +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 s3 ls s3:// +``` + +#### Delete an object from an S3 bucket +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 s3 rm s3:/// +``` + +#### List all S3 buckets +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 s3 ls +``` + +## EC2 + +#### Launch a new EC2 instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ec2 run-instances \ + --image-id ami-12345678 \ + --count 1 \ + --instance-type t2.micro \ + --key-name \ + --security-group-ids \ + --subnet-id +``` + +#### Stop an EC2 instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ec2 stop-instances --instance-ids +``` + +#### Start an EC2 instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ec2 start-instances --instance-ids +``` + +#### Terminate an EC2 instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ec2 terminate-instances --instance-ids +``` + +#### List all EC2 instances +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,State.Name,InstanceType,PublicIpAddress]' --output table +``` + +## DynamoDB + +#### Create a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb create-table \ + --table-name \ + --attribute-definitions AttributeName=,AttributeType=S \ + --key-schema AttributeName=,KeyType=HASH \ + --provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5 +``` + +#### List all tables in DynamoDB +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb list-tables +``` + +#### Describe a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb describe-table --table-name +``` + +#### Delete a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb delete-table --table-name +``` + +#### Put an item into a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb put-item \ + --table-name \ + --item '{"": {"S": ""}}' +``` + +#### Get an item from a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb get-item \ + --table-name \ + --key '{"": {"S": ""}}' +``` + +#### Update an item in a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb update-item \ + --table-name \ + --key '{"": {"S": ""}}' \ + --update-expression "SET = :val" \ + --expression-attribute-values '{":val": {"S": ""}}' +``` + +#### Delete an item from a DynamoDB table +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 dynamodb delete-item \ + --table-name \ + --key '{"": {"S": ""}}' +``` + +## IAM + +#### Create a new IAM user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam create-user --user-name +``` + +#### List all IAM users +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-users +``` + +#### Attach a policy to an IAM user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam attach-user-policy \ + --user-name \ + --policy-arn arn:aws:iam::aws:policy/ +``` + +#### Detach a policy from an IAM user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam detach-user-policy \ + --user-name \ + --policy-arn arn:aws:iam::aws:policy/ +``` + +#### Delete an IAM user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam delete-user --user-name +``` + +#### Create an IAM role +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam create-role \ + --role-name \ + --assume-role-policy-document file://trust-policy.json +``` + +#### Attach a policy to an IAM role +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam attach-role-policy \ + --role-name \ + --policy-arn arn:aws:iam::aws:policy/ +``` + +#### Detach a policy from an IAM role +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam detach-role-policy \ + --role-name \ + --policy-arn arn:aws:iam::aws:policy/ +``` + +#### Delete an IAM role +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam delete-role --role-name +``` + +#### List all IAM roles +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-roles +``` + +#### Get details of an IAM role +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam get-role --role-name +``` + +#### Create an IAM policy +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam create-policy \ + --policy-name \ + --policy-document file://policy.json +``` + +#### List all IAM policies +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-policies +``` + +#### Get details of an IAM policy +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam get-policy --policy-arn arn:aws:iam::aws:policy/ +``` + +#### Delete an IAM policy +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam delete-policy --policy-arn arn:aws:iam::aws:policy/ +``` + +#### Create an IAM access key for a user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam create-access-key --user-name +``` + +#### List all access keys for a user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-access-keys --user-name +``` + +#### Delete an IAM access key +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam delete-access-key --user-name --access-key-id +``` + +#### Update an IAM user's password +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam update-login-profile \ + --user-name \ + --password +``` + +#### List IAM policies attached to a user +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-attached-user-policies --user-name +``` + +#### List IAM policies attached to a role +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-attached-role-policies --role-name +``` + +#### List IAM policies attached to a group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-attached-group-policies --group-name +``` + +#### Create an IAM group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam create-group --group-name +``` + +#### List all IAM groups +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-groups +``` + +#### Add a user to an IAM group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam add-user-to-group \ + --group-name \ + --user-name +``` + +#### Remove a user from an IAM group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam remove-user-from-group \ + --group-name \ + --user-name +``` + +#### Delete an IAM group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam delete-group --group-name +``` + +#### List all users in an IAM group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam get-group --group-name +``` + +#### List all policies attached to a group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 iam list-attached-group-policies --group-name +``` + +## CloudFormation + +#### Create a CloudFormation stack +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation create-stack \ + --stack-name \ + --template-body file://template.yaml \ + --parameters ParameterKey=,ParameterValue= +``` + +#### Update a CloudFormation stack +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation update-stack \ + --stack-name \ + --template-body file://template.yaml \ + --parameters ParameterKey=,ParameterValue= +``` + +#### Delete a CloudFormation stack +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation delete-stack --stack-name +``` + +#### List all CloudFormation stacks +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE +``` + +#### Describe a CloudFormation stack +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation describe-stacks --stack-name +``` + +#### Get CloudFormation stack events +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation describe-stack-events --stack-name +``` + +#### Get CloudFormation stack resources +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudformation describe-stack-resources --stack-name +``` + +## Lambda + +#### Create a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-function \ + --function-name \ + --runtime nodejs14.x \ + --role arn:aws:iam:::role/ \ + --handler index.handler \ + --zip-file fileb://function.zip +``` + +#### Update a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-function-code \ + --function-name \ + --zip-file fileb://function.zip +``` + +#### Invoke a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda invoke \ + --function-name \ + --payload '{"key": "value"}' \ + response.json +``` + +#### List all Lambda functions +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda list-functions +``` + +#### Delete a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda delete-function --function-name +``` + +#### Get details of a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda get-function --function-name +``` + +#### Add a permission to a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda add-permission \ + --function-name \ + --principal \ + --statement-id \ + --action lambda:InvokeFunction +``` + +#### Remove a permission from a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda remove-permission \ + --function-name \ + --statement-id +``` + +#### List permissions for a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda get-policy --function-name +``` + +#### Create an alias for a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-alias \ + --function-name \ + --name \ + --function-version +``` + +#### Update a Lambda alias +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-alias \ + --function-name \ + --name \ + --function-version +``` + +#### Delete a Lambda alias +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda delete-alias \ + --function-name \ + --name +``` + +## CloudWatch + +#### Create a CloudWatch log group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 logs create-log-group --log-group-name +``` + +#### Create a CloudWatch log stream +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 logs create-log-stream \ + --log-group-name \ + --log-stream-name +``` + +#### Put log events into a CloudWatch log stream +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 logs put-log-events \ + --log-group-name \ + --log-stream-name \ + --log-events timestamp=$(date +%s%3N),message="Log message" +``` + +#### Get log events from a CloudWatch log stream +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 logs get-log-events \ + --log-group-name \ + --log-stream-name +``` + +#### List all CloudWatch log groups +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 logs describe-log-groups +``` + +#### List all CloudWatch log streams in a log group +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 logs describe-log-streams --log-group-name +``` + +#### Create a CloudWatch metric +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudwatch put-metric-data \ + --namespace \ + --metric-name \ + --value \ + --unit +``` + +#### List all CloudWatch metrics +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudwatch list-metrics --namespace +``` + +## Route 53 + +#### Create a Route 53 hosted zone +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 create-hosted-zone \ + --name \ + --caller-reference +``` + +#### List all Route 53 hosted zones +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 list-hosted-zones +``` + +#### Create a record set in a Route 53 hosted zone +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 change-resource-record-sets \ + --hosted-zone-id \ + --change-batch '{ + "Changes": [{ + "Action": "CREATE", + "ResourceRecordSet": { + "Name": "", + "Type": "A", + "TTL": 300, + "ResourceRecords": [{"Value": ""}] + } + }] + }' +``` + +#### Delete a record set in a Route 53 hosted zone +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 change-resource-record-sets \ + --hosted-zone-id \ + --change-batch '{ + "Changes": [{ + "Action": "DELETE", + "ResourceRecordSet": { + "Name": "", + "Type": "A", + "TTL": 300, + "ResourceRecords": [{"Value": ""}] + } + }] + }' +``` + +#### Get details of a Route 53 hosted zone +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 get-hosted-zone --id +``` + +#### List all record sets in a Route 53 hosted zone +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 list-resource-record-sets --hosted-zone-id +``` + +#### Delete a Route 53 hosted zone +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 route53 delete-hosted-zone --id +``` + +## SNS + +#### Create an SNS topic +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns create-topic --name +``` + +#### List all SNS topics +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns list-topics +``` + +#### Publish a message to an SNS topic +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns publish \ + --topic-arn arn:aws:sns:us-east-1:: \ + --message "Hello, SNS!" +``` + +#### Subscribe an email endpoint to an SNS topic +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns subscribe \ + --topic-arn arn:aws:sns:us-east-1:: \ + --protocol email \ + --notification-endpoint +``` + +#### Confirm an SNS subscription +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns confirm-subscription \ + --topic-arn arn:aws:sns:us-east-1:: \ + --token +``` + +#### List all subscriptions for an SNS topic +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns list-subscriptions-by-topic \ + --topic-arn arn:aws:sns:us-east-1:: +``` + +#### Unsubscribe from an SNS topic +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns unsubscribe --subscription-arn arn:aws:sns:us-east-1::: +``` + +#### Delete an SNS topic +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sns delete-topic --topic-arn arn:aws:sns:us-east-1:: +``` + +## SQS + +#### Create an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs create-queue --queue-name +``` + +#### List all SQS queues +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs list-queues +``` + +#### Send a message to an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs send-message \ + --queue-url http://localhost:4566/000000000000/ \ + --message-body "Hello, SQS!" +``` + +#### Receive messages from an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs receive-message \ + --queue-url http://localhost:4566/000000000000/ \ + --max-number-of-messages 10 +``` + +#### Delete a message from an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs delete-message \ + --queue-url http://localhost:4566/000000000000/ \ + --receipt-handle +``` + +#### Get attributes of an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs get-queue-attributes \ + --queue-url http://localhost:4566/000000000000/ \ + --attribute-names All +``` + +#### Set attributes of an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs set-queue-attributes \ + --queue-url http://localhost:4566/000000000000/ \ + --attributes '{"VisibilityTimeout": "30"}' +``` + +#### Delete an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs delete-queue --queue-url http://localhost:4566/000000000000/ +``` + +#### Purge an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs purge-queue --queue-url http://localhost:4566/000000000000/ +``` + +#### Get the URL of an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs get-queue-url --queue-name +``` + +#### Create a dead-letter queue for an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs create-queue \ + --queue-name \ + --attributes '{"RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1::\",\"maxReceiveCount\":\"5\"}"}' +``` + +#### Set a dead-letter queue for an SQS queue +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 sqs set-queue-attributes \ + --queue-url http://localhost:4566/000000000000/ \ + --attributes '{"RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1::\",\"maxReceiveCount\":\"5\"}"}' +``` + +## RDS + +#### Create an RDS instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds create-db-instance \ + --db-instance-identifier \ + --db-instance-class db.t2.micro \ + --engine mysql \ + --master-username \ + --master-user-password \ + --allocated-storage 20 +``` + +#### List all RDS instances +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds describe-db-instances +``` + +#### Delete an RDS instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds delete-db-instance \ + --db-instance-identifier \ + --skip-final-snapshot +``` + +#### Modify an RDS instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds modify-db-instance \ + --db-instance-identifier \ + --allocated-storage 30 \ + --apply-immediately +``` + +#### Describe an RDS instance +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds describe-db-instances --db-instance-identifier +``` + +#### Create an RDS snapshot +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds create-db-snapshot \ + --db-snapshot-identifier \ + --db-instance-identifier +``` + +#### List all RDS snapshots +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds describe-db-snapshots --db-instance-identifier +``` + +#### Restore an RDS instance from a snapshot +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 rds restore-db-instance-from-db-snapshot \ + --db-instance-identifier \ + --db-snapshot-identifier +``` + +## ECS + +#### Create an ECS cluster +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs create-cluster --cluster-name +``` + +#### List all ECS clusters +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs list-clusters +``` + +#### Create an ECS task definition +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs register-task-definition \ + --family \ + --network-mode bridge \ + --container-definitions '[{ + "name": "", + "image": "", + "memory": 512, + "cpu": 256, + "essential": true, + "portMappings": [{ + "containerPort": 80, + "hostPort": 80 + }] + }]' +``` + +#### List all ECS task definitions +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs list-task-definitions +``` + +#### Run an ECS task +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs run-task \ + --cluster \ + --task-definition \ + --count 1 \ + --launch-type EC2 +``` + +#### List all ECS tasks in a cluster +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs list-tasks --cluster +``` + +#### Describe an ECS task +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs describe-tasks \ + --cluster \ + --tasks +``` + +#### Stop an ECS task +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs stop-task \ + --cluster \ + --task +``` + +#### Create an ECS service +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs create-service \ + --cluster \ + --service-name \ + --task-definition \ + --desired-count 1 \ + --launch-type EC2 +``` + +#### List all ECS services in a cluster +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs list-services --cluster +``` + +#### Describe an ECS service +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs describe-services \ + --cluster \ + --services +``` + +#### Update an ECS service +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs update-service \ + --cluster \ + --service \ + --desired-count 2 +``` + +#### Delete an ECS service +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs delete-service \ + --cluster \ + --service \ + --force +``` + +#### Delete an ECS cluster +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecs delete-cluster --cluster +``` + +## ECR + +#### Create an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr create-repository --repository-name +``` + +#### List all ECR repositories +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr describe-repositories +``` + +#### Push an image to an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr get-login-password | docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com/ +docker tag .dkr.ecr.us-east-1.amazonaws.com/: +docker push .dkr.ecr.us-east-1.amazonaws.com/: +``` + +#### Pull an image from an ECR repository +```text +docker pull .dkr.ecr.us-east-1.amazonaws.com/: +``` + +#### Delete an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr delete-repository \ + --repository-name \ + --force +``` + +#### List images in an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr list-images --repository-name +``` + +#### Delete an image from an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr batch-delete-image \ + --repository-name \ + --image-ids imageTag= +``` + +#### Describe an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr describe-repositories --repository-names +``` + +#### Get the URI of an ECR repository +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr describe-repositories --repository-names --query 'repositories[0].repositoryUri' --output text +``` + +#### Get the ECR repository policy +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 ecr get-repository-policy --repository-name +``` + +## CloudFront + +#### Create a CloudFront distribution +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudfront create-distribution \ + --distribution-config '{ + "CallerReference": "", + "Origins": { + "Items": [{ + "Id": "", + "DomainName": "", + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginProtocolPolicy": "http-only" + } + }], + "Quantity": 1 + }, + "DefaultCacheBehavior": { + "TargetOriginId": "", + "ViewerProtocolPolicy": "allow-all", + "ForwardedValues": { + "QueryString": false, + "Cookies": { + "Forward": "none" + } + }, + "TrustedSigners": { + "Enabled": false, + "Quantity": 0 + }, + "MinTTL": 0 + }, + "Enabled": true + }' +``` + +#### List all CloudFront distributions +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudfront list-distributions +``` + +#### Get details of a CloudFront distribution +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudfront get-distribution --id +``` + +#### Update a CloudFront distribution +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudfront update-distribution \ + --id \ + --distribution-config '{ + "CallerReference": "", + "Origins": { + "Items": [{ + "Id": "", + "DomainName": "", + "CustomOriginConfig": { + "HTTPPort": 80, + "HTTPSPort": 443, + "OriginProtocolPolicy": "http-only" + } + }], + "Quantity": 1 + }, + "DefaultCacheBehavior": { + "TargetOriginId": "", + "ViewerProtocolPolicy": "allow-all", + "ForwardedValues": { + "QueryString": false, + "Cookies": { + "Forward": "none" + } + }, + "TrustedSigners": { + "Enabled": false, + "Quantity": 0 + }, + "MinTTL": 0 + }, + "Enabled": true + }' +``` + +#### Delete a CloudFront distribution +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 cloudfront delete-distribution --id +``` + + + From 47406ca01d9015942670135db7b5e8929c1056f9 Mon Sep 17 00:00:00 2001 From: rasys Date: Wed, 25 Jun 2025 22:34:40 -0500 Subject: [PATCH 10/15] aws commands --- aws-command.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/aws-command.md b/aws-command.md index b4de5c4..9b69d3f 100644 --- a/aws-command.md +++ b/aws-command.md @@ -1,5 +1,30 @@ # AWS CLI Commands +Example: +Crear una instancia EC2 con un script de usuario para instalar Apache y PHP. + +Security Group: +```text +aws ec2 create-security-group \ + --endpoint-url http://localhost:4566 \ + --group-name my-sg \ + --description "My test SG" +``` + + +🛠️ Opción usando AWS CLI estándar con endpoint +```text +aws ec2 run-instances \ + --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + --image-id ami-df5de72bdb3b \ + --count 1 \ + --instance-type t3.nano \ + --key-name my-key \ + --security-group-ids \ + --user-data file://./user_script.sh +``` + ## S3 #### Create a new S3 bucket From 331265374c77421e7625de0309b89a0baaaa1984 Mon Sep 17 00:00:00 2001 From: rbolivar Date: Thu, 26 Jun 2025 06:22:24 -0500 Subject: [PATCH 11/15] ajuste se adiciona cronjob para deactivation de tokens expirados --- .../aws/ws/AuthBackendServiceApplication.java | 2 + .../aws/ws/application/config/BeanConfig.java | 7 +- .../ws/application/config/TimeZoneConfig.java | 17 +++ .../application/filter/JwtSecurityFilter.java | 12 +- .../com/aws/ws/domain/api/JwtAdapterPort.java | 12 ++ ...{UserAdapter.java => UserAdapterPort.java} | 4 +- .../java/com/aws/ws/domain/model/Token.java | 6 +- .../aws/ws/domain/spi/UserServicePort.java | 4 +- .../aws/ws/domain/usecase/UserUseCase.java | 28 +++-- .../adapters/jwt/JwtAdapter.java | 91 +++++++++++++++ .../adapters/jwt/TokenCleanupScheduler.java | 25 +++++ .../persistence/UserPersistenceAdapter.java | 104 +++++++++++++----- .../infrastructure/common/util/JwtUtil.java | 52 --------- .../infrastructure/inbound/RouterConfig.java | 5 +- .../inbound/handler/UserHandler.java | 87 ++++++++++----- .../src/main/resources/application.yml | 7 +- 16 files changed, 331 insertions(+), 132 deletions(-) create mode 100644 auth-backend-service/src/main/java/com/aws/ws/application/config/TimeZoneConfig.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/domain/api/JwtAdapterPort.java rename auth-backend-service/src/main/java/com/aws/ws/domain/api/{UserAdapter.java => UserAdapterPort.java} (78%) create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/JwtAdapter.java create mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/TokenCleanupScheduler.java delete mode 100644 auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java diff --git a/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java b/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java index b58983e..6a076e6 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java +++ b/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableScheduling public class AuthBackendServiceApplication { public static void main(String[] args) { diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java index 11f59ab..9c2c804 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java @@ -1,6 +1,7 @@ package com.aws.ws.application.config; -import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.api.JwtAdapterPort; +import com.aws.ws.domain.api.UserAdapterPort; import com.aws.ws.domain.usecase.UserUseCase; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -9,8 +10,8 @@ public class BeanConfig { @Bean - public UserUseCase catalogUseCase(UserAdapter userAdapter) { - return new UserUseCase(userAdapter); + public UserUseCase catalogUseCase(UserAdapterPort userAdapter, JwtAdapterPort jwtAdapter) { + return new UserUseCase(userAdapter, jwtAdapter); } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/config/TimeZoneConfig.java b/auth-backend-service/src/main/java/com/aws/ws/application/config/TimeZoneConfig.java new file mode 100644 index 0000000..c611628 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/TimeZoneConfig.java @@ -0,0 +1,17 @@ +package com.aws.ws.application.config; + +import jakarta.annotation.PostConstruct; +import org.springframework.context.annotation.Configuration; + +import java.util.TimeZone; + +@Configuration +public class TimeZoneConfig { + + @PostConstruct + public void init() { + TimeZone.setDefault(TimeZone.getTimeZone("America/Bogota")); + System.setProperty("user.timezone", "America/Bogota"); + System.out.println("✅ Zona horaria configurada globalmente: " + TimeZone.getDefault()); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java b/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java index b53f60d..8a55593 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/application/filter/JwtSecurityFilter.java @@ -1,6 +1,6 @@ package com.aws.ws.application.filter; -import com.aws.ws.infrastructure.common.util.JwtUtil; +import com.aws.ws.infrastructure.adapters.jwt.JwtAdapter; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import org.springframework.http.HttpHeaders; @@ -18,10 +18,10 @@ @Component public class JwtSecurityFilter implements WebFilter { - private final JwtUtil jwtUtil; + private final JwtAdapter jwtAdapter; - public JwtSecurityFilter(JwtUtil jwtUtil) { - this.jwtUtil = jwtUtil; + public JwtSecurityFilter(JwtAdapter jwtAdapter) { + this.jwtAdapter = jwtAdapter; } @Override @@ -32,11 +32,11 @@ public Mono filter(ServerWebExchange exchange, if (authHeader != null && authHeader.startsWith("Bearer ")) { try { String token = authHeader.substring(7); - Claims claims = jwtUtil.validateToken(token); + Claims claims = jwtAdapter.validateToken(token); Authentication auth = new UsernamePasswordAuthenticationToken( claims.getSubject(), null, - jwtUtil.extractRoles(token).stream().map(SimpleGrantedAuthority::new).toList() + jwtAdapter.extractRoles(token).stream().map(SimpleGrantedAuthority::new).toList() ); return chain.filter(exchange) .contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth)); diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/api/JwtAdapterPort.java b/auth-backend-service/src/main/java/com/aws/ws/domain/api/JwtAdapterPort.java new file mode 100644 index 0000000..5a4389a --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/api/JwtAdapterPort.java @@ -0,0 +1,12 @@ +package com.aws.ws.domain.api; + +import com.aws.ws.domain.model.Token; +import reactor.core.publisher.Mono; + +import java.util.List; + +public interface JwtAdapterPort { + Mono generateToken(String email, List roles); + + Mono validateJwt(String jwt); +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapterPort.java similarity index 78% rename from auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java rename to auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapterPort.java index d8e671a..b47c767 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapterPort.java @@ -4,13 +4,13 @@ import com.aws.ws.domain.model.User; import reactor.core.publisher.Mono; -public interface UserAdapter { +public interface UserAdapterPort { Mono createUser(User domain); Mono findUserByEmail(String email); - Mono saveToken(Token token); + Mono saveToken(Token token, String email); Mono existsTokenByJwt(String jwt); diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java b/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java index b297f95..c4dc228 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java @@ -5,7 +5,7 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.time.Instant; +import java.time.ZonedDateTime; @Data @Builder @@ -15,7 +15,7 @@ public class Token { private String tokenId; // puede ser UUID private String userId; private String jwt; - private Instant issuedAt; - private Instant expiresAt; + private ZonedDateTime issuedAt; + private ZonedDateTime expiresAt; private boolean active; // marcarlo false en logout } diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java index c06386d..7213bed 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java @@ -10,9 +10,11 @@ public interface UserServicePort { Mono findUserByEmail(String email); - Mono saveToken(Token token); + Mono saveToken(Token token, String email); Mono logout(String jwt); Mono existsTokenByJwt(String jwt); + + Mono validateJwt(String jwt); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java index c60ff2f..babea9e 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -1,6 +1,7 @@ package com.aws.ws.domain.usecase; -import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.api.JwtAdapterPort; +import com.aws.ws.domain.api.UserAdapterPort; import com.aws.ws.domain.exception.*; import com.aws.ws.domain.model.Token; import com.aws.ws.domain.model.User; @@ -9,10 +10,12 @@ public class UserUseCase implements UserServicePort { - private final UserAdapter userAdapter; + private final UserAdapterPort userAdapter; + private final JwtAdapterPort jwtAdapter; - public UserUseCase(UserAdapter userAdapter) { + public UserUseCase(UserAdapterPort userAdapter, JwtAdapterPort jwtAdapter) { this.userAdapter = userAdapter; + this.jwtAdapter = jwtAdapter; } @Override @@ -45,15 +48,15 @@ public Mono findUserByEmail(String email) { } @Override - public Mono saveToken(Token token) { - if (token == null || token.getUserId() == null || token.getJwt() == null) { + public Mono saveToken(Token token, String email) { + if (token.getJwt() == null || email == null) { return Mono.error(new InvalidValueException( - "User ID and Token", + "Email and Token", "must not be null" )); } - return userAdapter.saveToken(token) + return userAdapter.saveToken(token, email) .onErrorResume(e -> Mono.error(new BusinessException( TechnicalMessage.BAD_REQUEST.getMessage(), "Error saving token: ", e.getMessage()))); } @@ -79,4 +82,15 @@ public Mono existsTokenByJwt(String jwt) { .onErrorResume(e -> Mono.error(new BusinessException( TechnicalMessage.BAD_REQUEST.getMessage(), "Error checking token existence: ", e.getMessage()))); } + + @Override + public Mono validateJwt(String jwt) { + if (jwt == null || jwt.isEmpty()) { + return Mono.error(new InvalidValueException("JWT", "must not be null or empty")); + } + + return jwtAdapter.validateJwt(jwt) + .onErrorResume(e -> Mono.error(new BusinessException( + TechnicalMessage.BAD_REQUEST.getMessage(), "Error validating JWT: ", e.getMessage()))); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/JwtAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/JwtAdapter.java new file mode 100644 index 0000000..785002c --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/JwtAdapter.java @@ -0,0 +1,91 @@ +package com.aws.ws.infrastructure.adapters.jwt; + +import com.aws.ws.domain.api.JwtAdapterPort; +import com.aws.ws.domain.model.Token; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Date; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAdapter implements JwtAdapterPort { + + public static final ZoneId ZONE_ID = ZoneId.of("America/Bogota"); + + @Value("${jwt.secret}") + private String SECRET_KEY; // Clave secreta para firmar el JWT, inyectada desde las propiedades de la aplicación + + @Value("${jwt.expiration-time-sec}") + private long EXPIRATION_TIME_SEC; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Override + public Mono generateToken(String email, List roles) { + ZonedDateTime now = ZonedDateTime.now(ZONE_ID); + ZonedDateTime expiresAt = now.plusSeconds(EXPIRATION_TIME_SEC); + + log.info("Generating JWT for email: {}, roles: {}, expires at: {}", email, roles, expiresAt); + + String jwt = Jwts.builder() + .setSubject(email) + .claim("roles", roles) + .setIssuedAt(Date.from(now.toInstant())) + .setExpiration(Date.from(expiresAt.toInstant())) + .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) + .compact(); + + return Mono.just(Token.builder() + .jwt(jwt) + .issuedAt(now) + .expiresAt(expiresAt) + .build()); + } + + @Override + public Mono validateJwt(String jwt) { + try { + Claims claims = validateToken(jwt); + + boolean isExpired = claims.getExpiration().before(new Date()); + return Mono.just(!isExpired); + } catch (JwtException | IllegalArgumentException e) { + log.warn("❌ Invalid JWT: {}", e.getMessage()); + return Mono.just(false); + } + } + + public Claims validateToken(String token) throws JwtException { + return Jwts.parserBuilder() + .setSigningKey(SECRET_KEY.getBytes()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public String extractEmail(String token) { + return validateToken(token).getSubject(); + } + + public List extractRoles(String token) { + Object roles = validateToken(token).get("roles"); + return objectMapper.convertValue(roles, new TypeReference<>() {}); + } + +} + diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/TokenCleanupScheduler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/TokenCleanupScheduler.java new file mode 100644 index 0000000..a8bf4b6 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/jwt/TokenCleanupScheduler.java @@ -0,0 +1,25 @@ +package com.aws.ws.infrastructure.adapters.jwt; + +import com.aws.ws.infrastructure.adapters.persistence.UserPersistenceAdapter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TokenCleanupScheduler { + + private final UserPersistenceAdapter tokenAdapter; + + // Ejecutar cada hora (puedes ajustar esto) + //@Scheduled(cron = "0 0 * * * *") // cada hora en punto + @Scheduled(cron = "*/30 * * * * *") // cada 30 segundos + public void cleanExpiredTokens() { + log.info("⏳ Starting scheduled cleanup of expired JWT tokens..."); + tokenAdapter.deactivateExpiredTokens() + .doOnError(e -> log.error("❌ Error cleaning expired tokens: {}", e.getMessage())) + .subscribe(); + } +} diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java index 56644b6..fbca352 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -1,6 +1,6 @@ package com.aws.ws.infrastructure.adapters.persistence; -import com.aws.ws.domain.api.UserAdapter; +import com.aws.ws.domain.api.UserAdapterPort; import com.aws.ws.domain.model.Token; import com.aws.ws.domain.model.User; import com.aws.ws.infrastructure.adapters.persistence.constants.UserDefinition; @@ -16,14 +16,15 @@ import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; import software.amazon.awssdk.services.dynamodb.model.*; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; @Slf4j @Service @RequiredArgsConstructor -public class UserPersistenceAdapter implements UserAdapter { +public class UserPersistenceAdapter implements UserAdapterPort { @Value("${aws.dynamodb.table.users}") private String tableUsers; @@ -88,30 +89,36 @@ public Mono findUserByEmail(String email) { } @Override - public Mono saveToken(Token token) { - if (token.getTokenId() == null) { - token.setTokenId(UUID.randomUUID().toString()); - } - log.info("Creating token with Token ID: {}", token.getTokenId()); - - // 1. Desactivar tokens anteriores - return deactivateActiveTokensByUserId(token.getUserId()) - // 2. Guardar el nuevo token - .then(Mono.fromFuture(() -> client.putItem( - PutItemRequest.builder() - .tableName(tableTokens) - .item(Map.of( - UserDefinition.TOKEN_ID, AttributeValue.builder().s(token.getTokenId()).build(), - UserDefinition.TOKEN_USER_ID, AttributeValue.builder().s(token.getUserId()).build(), - UserDefinition.TOKEN_JWT, AttributeValue.builder().s(token.getJwt()).build(), - UserDefinition.TOKEN_CREATED_DATE, AttributeValue.builder().s(token.getIssuedAt().toString()).build(), - UserDefinition.TOKEN_EXPIRATION_DATE, AttributeValue.builder().s(token.getExpiresAt().toString()).build(), - UserDefinition.TOKEN_ACTIVE, AttributeValue.builder().bool(true).build() - )) - .build() - ))).thenReturn(true); + public Mono saveToken(Token token, String email) { + return findUserByEmail(email) + .switchIfEmpty(Mono.defer(() -> { + log.error("❌ User not found for email: {}", email); + return Mono.empty(); + })) + .flatMap(user -> { + log.info("✅ User found for email: {}", email); + + // 1. Desactivar tokens anteriores + return deactivateActiveTokensByUserId(user.getUserId()) + // 2. Guardar el nuevo token + .then(Mono.fromFuture(() -> client.putItem( + PutItemRequest.builder() + .tableName(tableTokens) + .item(Map.of( + UserDefinition.TOKEN_ID, AttributeValue.builder().s(UUID.randomUUID().toString()).build(), + UserDefinition.TOKEN_USER_ID, AttributeValue.builder().s(user.getUserId()).build(), + UserDefinition.TOKEN_JWT, AttributeValue.builder().s(token.getJwt()).build(), + UserDefinition.TOKEN_CREATED_DATE, AttributeValue.builder().s(String.valueOf(token.getIssuedAt())).build(), + UserDefinition.TOKEN_EXPIRATION_DATE, AttributeValue.builder().s(String.valueOf(token.getExpiresAt())).build(), + UserDefinition.TOKEN_ACTIVE, AttributeValue.builder().bool(true).build() + )) + .build() + ))).thenReturn(true); + }) + .defaultIfEmpty(false); // Devuelve false si el usuario no fue encontrado } + private Mono deactivateActiveTokensByUserId(String userId) { ScanRequest scanRequest = ScanRequest.builder() .tableName(tableTokens) @@ -228,4 +235,47 @@ private Mono deactivateActiveTokensByJwt(String jwt) { }) .then(Mono.just(true)); } + + public Mono deactivateExpiredTokens() { + ZonedDateTime now = ZonedDateTime.now(ZoneId.systemDefault()); // America/Bogota + log.info("Deactivating expired JWT: {}", now); + + ScanRequest scanRequest = ScanRequest.builder() + .tableName(tableTokens) + .filterExpression("active = :activeVal") + .expressionAttributeValues(Map.of( + ":activeVal", AttributeValue.builder().bool(true).build() + )) + .build(); + + return Mono.fromFuture(() -> client.scan(scanRequest)) + .flatMapMany(response -> Flux.fromIterable(response.items())) + .filter(item -> { + String expiresAtStr = item.get(UserDefinition.TOKEN_EXPIRATION_DATE).s(); + ZonedDateTime expiresAt = ZonedDateTime.parse(expiresAtStr, DateTimeFormatter.ISO_ZONED_DATE_TIME); + return expiresAt.isBefore(now); + }) + .flatMap(expiredItem -> { + String tokenId = expiredItem.get(UserDefinition.TOKEN_ID).s(); + Map updates = Map.of( + UserDefinition.TOKEN_ACTIVE, AttributeValueUpdate.builder() + .value(AttributeValue.builder().bool(false).build()) + .action(AttributeAction.PUT) + .build() + ); + + UpdateItemRequest updateRequest = UpdateItemRequest.builder() + .tableName(tableTokens) + .key(Map.of( + UserDefinition.TOKEN_ID, AttributeValue.builder().s(tokenId).build() + )) + .attributeUpdates(updates) + .build(); + + return Mono.fromFuture(() -> client.updateItem(updateRequest)).then(); + }) + .doOnComplete(() -> log.info("✅ Expired tokens deactivated successfully")) + .doOnError(error -> log.error("❌ Error during token cleanup: {}", error.getMessage())) + .then(); + } } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java deleted file mode 100644 index eca0345..0000000 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/common/util/JwtUtil.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.aws.ws.infrastructure.common.util; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.security.Keys; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import java.util.Date; -import java.util.List; - -@Component -public class JwtUtil { - - @Value("${jwt.secret}") - private String SECRET_KEY; // Clave secreta para firmar el JWT, inyectada desde las propiedades de la aplicación - - private static final long EXPIRATION_TIME_MS = 3600000; // 1 hora - private final ObjectMapper objectMapper = new ObjectMapper(); - - public String generateToken(String email, List roles) { - return Jwts.builder() - .setSubject(email) - .claim("roles", roles) - .setIssuedAt(new Date()) - .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME_MS)) - .signWith(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()), SignatureAlgorithm.HS256) - .compact(); - } - - public Claims validateToken(String token) throws JwtException { - return Jwts.parserBuilder() - .setSigningKey(SECRET_KEY.getBytes()) - .build() - .parseClaimsJws(token) - .getBody(); - } - - public String extractEmail(String token) { - return validateToken(token).getSubject(); - } - - public List extractRoles(String token) { - Object roles = validateToken(token).get("roles"); - return objectMapper.convertValue(roles, new TypeReference<>() {}); - } -} - diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java index dad2378..a64ce8b 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -64,9 +64,8 @@ public RouterFunction userRoutes(UserHandler userHandler) { .path("/api/auth", builder -> builder .POST("/register", accept(MediaType.APPLICATION_JSON), userHandler::register) .POST("/login", accept(MediaType.APPLICATION_JSON), userHandler::login) -// .GET("/me", userHandler::getProfile) // requiere JWT -// .PUT("/update", userHandler::updateUser) // requiere JWT - .POST("/logout", userHandler::logout) // requiere JWT + .GET("/validate", userHandler::validateJwt) // requiere JWT + .POST("/logout", userHandler::logout) // requiere JWT ) .build(); } diff --git a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java index 940aac9..5db5ec6 100644 --- a/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -1,9 +1,8 @@ package com.aws.ws.infrastructure.inbound.handler; -import com.aws.ws.domain.model.Token; import com.aws.ws.domain.spi.UserServicePort; import com.aws.ws.infrastructure.common.handler.GlobalErrorHandler; -import com.aws.ws.infrastructure.common.util.JwtUtil; +import com.aws.ws.infrastructure.adapters.jwt.JwtAdapter; import com.aws.ws.infrastructure.inbound.dto.LoginRequest; import com.aws.ws.infrastructure.inbound.mapper.UserMapper; import io.swagger.v3.oas.annotations.tags.Tag; @@ -16,7 +15,6 @@ import org.springframework.web.reactive.function.server.ServerResponse; import reactor.core.publisher.Mono; -import java.time.Instant; import java.util.List; import java.util.Map; @@ -32,7 +30,7 @@ public class UserHandler { private final GlobalErrorHandler globalErrorHandler; private final UserMapper mapper; private final PasswordEncoder passwordEncoder; - private final JwtUtil jwtUtil; + private final JwtAdapter jwtAdapter; public Mono register(ServerRequest request) { return request.bodyToMono(LoginRequest.class) @@ -49,29 +47,20 @@ public Mono login(ServerRequest request) { if (!passwordEncoder.matches(login.getPassword(), user.getPassword())) { return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); } - - String token = jwtUtil.generateToken(user.getEmail(), List.of(user.getRole())); - - Token tokenToSave = Token.builder() - .jwt(token) - .userId(user.getUserId()) - .issuedAt(Instant.now()) - .expiresAt(Instant.now().plusSeconds(3600)) - .active(true) - .build(); - - return servicePort.saveToken(tokenToSave) - .flatMap(success -> { - if (success) { - return ServerResponse.ok().bodyValue(Map.of( - "token", token, - "messageId", getMessageId(request) - )); - } else { - return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) - .bodyValue(Map.of("error", "Failed to save token")); - } - }); + // Aquí generamos el token JWT + return jwtAdapter.generateToken(user.getEmail(), List.of(user.getRole())) + .flatMap(token -> servicePort.saveToken(token, user.getEmail()) + .flatMap(success -> { + if (success) { + return ServerResponse.ok().bodyValue(Map.of( + "token", token.getJwt(), + "messageId", getMessageId(request) + )); + } else { + return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) + .bodyValue(Map.of("error", "Failed to save token")); + } + })); })); } @@ -98,4 +87,48 @@ public Mono logout(ServerRequest request) { .doOnError(error -> log.error("❌ Error during logout: {}", error.getMessage())) .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); } + + public Mono validateJwt(ServerRequest request) { + String token = request.headers().firstHeader("Authorization"); + if (token == null || !token.startsWith("Bearer ")) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); + } + + String jwt = token.substring(7); // Remove "Bearer " prefix + + return servicePort.existsTokenByJwt(jwt) + .flatMap(exists -> { + if (!exists) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue(Map.of( + "valid", false, + "messageId", getMessageId(request) + )); + } + + // Si existe, validamos la firma y expiración + return servicePort.validateJwt(jwt) + .flatMap(valid -> { + if (valid) { + return ServerResponse.ok().bodyValue(Map.of( + "valid", true, + "messageId", getMessageId(request) + )); + } else { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue(Map.of( + "valid", false, + "messageId", getMessageId(request) + )); + } + }); + }) + .switchIfEmpty( // Si no existe el token en DB + ServerResponse.status(HttpStatus.UNAUTHORIZED).bodyValue(Map.of( + "valid", false, + "messageId", getMessageId(request) + )) + ) + .doOnError(error -> log.error("❌ Error validating JWT: {}", error.getMessage())) + .onErrorResume(exception -> globalErrorHandler.handle(exception, getMessageId(request))); + } + } diff --git a/auth-backend-service/src/main/resources/application.yml b/auth-backend-service/src/main/resources/application.yml index da0d223..c793b83 100644 --- a/auth-backend-service/src/main/resources/application.yml +++ b/auth-backend-service/src/main/resources/application.yml @@ -14,4 +14,9 @@ server: port: 9801 jwt: - secret: "wN!as4kdpN98A2m3V9x7QqZLrP0f98A2m3V9x7QqZLGtYz" # debe tener al menos 32 caracteres (256 bits) \ No newline at end of file + secret: "wN!as4kdpN98A2m3V9x7QqZLrP0f98A2m3V9x7QqZLGtYz" # debe tener al menos 32 caracteres (256 bits) + expiration-time-sec: 30 # 30 segundos + +spring: + jackson: + time-zone: America/Bogota \ No newline at end of file From 189728586e3e9622df03648327b9bda4faee8c29 Mon Sep 17 00:00:00 2001 From: rbolivar Date: Thu, 26 Jun 2025 06:35:57 -0500 Subject: [PATCH 12/15] ajuste dashboard-ui auth guard se lleva a signals y validation de token --- .../src/app/shared/guards/auth.guard.ts | 20 +++++++++---- .../src/app/shared/services/auth.service.ts | 29 +++++++++++++++---- 2 files changed, 37 insertions(+), 12 deletions(-) diff --git a/dashboard-ui/src/app/shared/guards/auth.guard.ts b/dashboard-ui/src/app/shared/guards/auth.guard.ts index a005da9..685dd88 100644 --- a/dashboard-ui/src/app/shared/guards/auth.guard.ts +++ b/dashboard-ui/src/app/shared/guards/auth.guard.ts @@ -1,15 +1,23 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { AuthService } from "@shared/services/auth.service"; +import {catchError, map, of} from "rxjs"; export const authGuard: CanActivateFn = (route, state) => { const router = inject(Router); const authService = inject(AuthService); - if (!authService.hasValidToken()) { - router.navigate(['/login']); - return false; - } - - return true; + return authService.hasValidToken().pipe( + map((valid) => { + if (!valid) { + router.navigate(['/login']); + return false; + } + return true; + }), + catchError((err) => { + router.navigate(['/login']); + return of(false); + }) + ); }; diff --git a/dashboard-ui/src/app/shared/services/auth.service.ts b/dashboard-ui/src/app/shared/services/auth.service.ts index b4bc4fa..bcfe0bb 100644 --- a/dashboard-ui/src/app/shared/services/auth.service.ts +++ b/dashboard-ui/src/app/shared/services/auth.service.ts @@ -1,6 +1,6 @@ -import { inject, Injectable, signal } from '@angular/core'; +import {effect, inject, Injectable, signal} from '@angular/core'; import { HttpClient, HttpHeaders } from "@angular/common/http"; -import { tap } from "rxjs"; +import {catchError, map, Observable, of, tap} from "rxjs"; @Injectable({ providedIn: 'root' @@ -16,7 +16,11 @@ export class AuthService { isAuthenticated = signal(false); constructor() { - this.isAuthenticated.set(this.hasValidToken()); + effect(() => { + this.hasValidToken().subscribe(valid => { + this.isAuthenticated.set(valid); + }); + }); } login(email: string, password: string) { @@ -56,9 +60,22 @@ export class AuthService { return localStorage.getItem(this.TOKEN_KEY); } - hasValidToken(): boolean { + hasValidToken(): Observable { const token = this.getToken(); - // Opcional validar expiración - return !!token; + if (!token) return of(false); + + const headers = new HttpHeaders({ + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }); + + return this.http.get<{ valid: boolean }>('/api/auth/validate', { headers }).pipe( + map(response => response.valid), + catchError(err => { + console.warn('❌ Token validation failed:', err); + return of(false); + }) + ); } } From 1145388aaeebd190c960aea3a843881b0d68e1d7 Mon Sep 17 00:00:00 2001 From: Raul Bolivar Navas <71100949+raulrobinson@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:20:50 -0500 Subject: [PATCH 13/15] Document AWS Lambda management commands Added commands for managing AWS Lambda functions and SSO configuration. --- aws-lambda-cmd | 102 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 aws-lambda-cmd diff --git a/aws-lambda-cmd b/aws-lambda-cmd new file mode 100644 index 0000000..6c60b3a --- /dev/null +++ b/aws-lambda-cmd @@ -0,0 +1,102 @@ +## Lambda + +#### Create a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-function \ + --function-name \ + --runtime nodejs14.x \ + --role arn:aws:iam:::role/ \ + --handler index.handler \ + --zip-file fileb://function.zip +``` + +#### Update a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-function-code \ + --function-name \ + --zip-file fileb://function.zip +``` + +#### Invoke a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda invoke \ + --function-name \ + --payload '{"key": "value"}' \ + response.json +``` + +#### List all Lambda functions +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda list-functions +``` + +#### Delete a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda delete-function --function-name +``` + +#### Get details of a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda get-function --function-name +``` + +#### Add a permission to a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda add-permission \ + --function-name \ + --principal \ + --statement-id \ + --action lambda:InvokeFunction +``` + +#### Remove a permission from a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda remove-permission \ + --function-name \ + --statement-id +``` + +#### List permissions for a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda get-policy --function-name +``` + +#### Create an alias for a Lambda function +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-alias \ + --function-name \ + --name \ + --function-version +``` + +#### Update a Lambda alias +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-alias \ + --function-name \ + --name \ + --function-version +``` + +#### Delete a Lambda alias +```text +aws --endpoint-url http://localhost:4566 --region us-east-1 lambda delete-alias \ + --function-name \ + --name +``` + +--- + +#### AWS SSO Configure + +```text +aws configure sso +aws configure list-profiles +aws sso login --profile XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +``` + +#### Find a Lambda by name with Profile +```text +aws --region us-east-1 lambda list-functions \ + --profile XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX \ + --query "Functions[?FunctionName=='xxx-xxx-params-xxx']" +``` From d35a9884ad13275eeef5585b6f61ecfa223676bc Mon Sep 17 00:00:00 2001 From: Raul Bolivar Navas <71100949+raulrobinson@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:21:18 -0500 Subject: [PATCH 14/15] Rename aws-lambda-cmd to aws-lambda-cmd.md --- aws-lambda-cmd => aws-lambda-cmd.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename aws-lambda-cmd => aws-lambda-cmd.md (100%) diff --git a/aws-lambda-cmd b/aws-lambda-cmd.md similarity index 100% rename from aws-lambda-cmd rename to aws-lambda-cmd.md From 8c679d46e7e0eefd0ff1eae005990af746157a2c Mon Sep 17 00:00:00 2001 From: Raul Bolivar Navas <71100949+raulrobinson@users.noreply.github.com> Date: Thu, 4 Dec 2025 07:24:11 -0500 Subject: [PATCH 15/15] Refactor AWS Lambda command examples for clarity Updated command formatting for AWS Lambda operations to improve readability. --- aws-lambda-cmd.md | 48 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/aws-lambda-cmd.md b/aws-lambda-cmd.md index 6c60b3a..8947c22 100644 --- a/aws-lambda-cmd.md +++ b/aws-lambda-cmd.md @@ -2,7 +2,9 @@ #### Create a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-function \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda create-function \ --function-name \ --runtime nodejs14.x \ --role arn:aws:iam:::role/ \ @@ -12,14 +14,18 @@ aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-functi #### Update a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-function-code \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda update-function-code \ --function-name \ --zip-file fileb://function.zip ``` #### Invoke a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda invoke \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda invoke \ --function-name \ --payload '{"key": "value"}' \ response.json @@ -27,22 +33,30 @@ aws --endpoint-url http://localhost:4566 --region us-east-1 lambda invoke \ #### List all Lambda functions ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda list-functions +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda list-functions ``` #### Delete a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda delete-function --function-name +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda delete-function --function-name ``` #### Get details of a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda get-function --function-name +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda get-function --function-name ``` #### Add a permission to a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda add-permission \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda add-permission \ --function-name \ --principal \ --statement-id \ @@ -51,19 +65,25 @@ aws --endpoint-url http://localhost:4566 --region us-east-1 lambda add-permissio #### Remove a permission from a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda remove-permission \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda remove-permission \ --function-name \ --statement-id ``` #### List permissions for a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda get-policy --function-name +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda get-policy --function-name ``` #### Create an alias for a Lambda function ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-alias \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda create-alias \ --function-name \ --name \ --function-version @@ -71,7 +91,9 @@ aws --endpoint-url http://localhost:4566 --region us-east-1 lambda create-alias #### Update a Lambda alias ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-alias \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda update-alias \ --function-name \ --name \ --function-version @@ -79,7 +101,9 @@ aws --endpoint-url http://localhost:4566 --region us-east-1 lambda update-alias #### Delete a Lambda alias ```text -aws --endpoint-url http://localhost:4566 --region us-east-1 lambda delete-alias \ +aws --endpoint-url http://localhost:4566 \ + --region us-east-1 \ + lambda delete-alias \ --function-name \ --name ```