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..c0119e2 --- /dev/null +++ b/auth-backend-service/build.gradle @@ -0,0 +1,96 @@ +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' + + 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' + 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 0000000..1b33c55 Binary files /dev/null and b/auth-backend-service/gradle/wrapper/gradle-wrapper.jar differ diff --git a/auth-backend-service/gradle/wrapper/gradle-wrapper.properties b/auth-backend-service/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ff23a68 --- /dev/null +++ b/auth-backend-service/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/auth-backend-service/gradlew b/auth-backend-service/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/auth-backend-service/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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..6a076e6 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/AuthBackendServiceApplication.java @@ -0,0 +1,15 @@ +package com.aws.ws; + +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) { + 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..9c2c804 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/BeanConfig.java @@ -0,0 +1,17 @@ +package com.aws.ws.application.config; + +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; + +@Configuration +public class BeanConfig { + + @Bean + 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/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..64b5b6f --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/application/config/SecurityConfig.java @@ -0,0 +1,87 @@ +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; +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.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; +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; + + private final JwtSecurityFilter jwtSecurityFilter; + + public SecurityConfig(JwtSecurityFilter jwtSecurityFilter) { + this.jwtSecurityFilter = jwtSecurityFilter; + } + + @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, "/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() + .anyExchange().authenticated() + ) + .addFilterAt(jwtSecurityFilter, SecurityWebFiltersOrder.AUTHENTICATION) +// .oauth2ResourceServer(oauth2 -> oauth2 +// .jwt(jwt -> jwt +// .jwkSetUri(jwkSetUri) +// ) +// ) + .build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + +// @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/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 new file mode 100644 index 0000000..8a55593 --- /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.adapters.jwt.JwtAdapter; +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 JwtAdapter jwtAdapter; + + public JwtSecurityFilter(JwtAdapter jwtAdapter) { + this.jwtAdapter = jwtAdapter; + } + + @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 = jwtAdapter.validateToken(token); + Authentication auth = new UsernamePasswordAuthenticationToken( + claims.getSubject(), + null, + jwtAdapter.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/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/UserAdapterPort.java b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapterPort.java new file mode 100644 index 0000000..b47c767 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/api/UserAdapterPort.java @@ -0,0 +1,18 @@ +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 UserAdapterPort { + + Mono createUser(User domain); + + Mono findUserByEmail(String email); + + Mono saveToken(Token token, String email); + + Mono existsTokenByJwt(String jwt); + + Mono logout(String jwt); +} 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/NotFoundException.java b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NotFoundException.java new file mode 100644 index 0000000..179ffa0 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/exception/NotFoundException.java @@ -0,0 +1,16 @@ +package com.aws.ws.domain.exception; + +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/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/Token.java b/auth-backend-service/src/main/java/com/aws/ws/domain/model/Token.java new file mode 100644 index 0000000..c4dc228 --- /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.ZonedDateTime; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Token { + private String tokenId; // puede ser UUID + private String userId; + private String jwt; + 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/model/User.java b/auth-backend-service/src/main/java/com/aws/ws/domain/model/User.java new file mode 100644 index 0000000..b082dc5 --- /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 userId; + 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..7213bed --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/spi/UserServicePort.java @@ -0,0 +1,20 @@ +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 createUser(User domain); + + Mono findUserByEmail(String email); + + 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 new file mode 100644 index 0000000..babea9e --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/domain/usecase/UserUseCase.java @@ -0,0 +1,96 @@ +package com.aws.ws.domain.usecase; + +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; +import com.aws.ws.domain.spi.UserServicePort; +import reactor.core.publisher.Mono; + +public class UserUseCase implements UserServicePort { + + private final UserAdapterPort userAdapter; + private final JwtAdapterPort jwtAdapter; + + public UserUseCase(UserAdapterPort userAdapter, JwtAdapterPort jwtAdapter) { + this.userAdapter = userAdapter; + this.jwtAdapter = jwtAdapter; + } + + @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((User user) -> Mono.error(new DuplicateResourceException( + TechnicalMessage.ALREADY_EXISTS, + "User already exists with email: ", + domain.getEmail() + ))) + .switchIfEmpty(Mono.defer(() -> 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 NotFoundException(TechnicalMessage.NOT_FOUND, "User not found with email: ", email))); + } + + @Override + public Mono saveToken(Token token, String email) { + if (token.getJwt() == null || email == null) { + return Mono.error(new InvalidValueException( + "Email and Token", + "must not be null" + )); + } + + return userAdapter.saveToken(token, email) + .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()))); + } + + @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 new file mode 100644 index 0000000..fbca352 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/adapters/persistence/UserPersistenceAdapter.java @@ -0,0 +1,281 @@ +package com.aws.ws.infrastructure.adapters.persistence; + +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; +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.Flux; +import reactor.core.publisher.Mono; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.model.*; + +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UserPersistenceAdapter implements UserAdapterPort { + + @Value("${aws.dynamodb.table.users}") + private String tableUsers; + + @Value("${aws.dynamodb.table.tokens}") + private String tableTokens; + + private final DynamoDbAsyncClient client; + private final PasswordEncoder passwordEncoder; + private final UserPersistenceMapper mapper; + + @Override + public Mono createUser(User user) { + if (user.getUserId() == null) { + user.setUserId(UUID.randomUUID().toString()); + } + 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(tableUsers) + .item(Map.of( + 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(); + + return Mono.fromFuture(() -> client.putItem(request)) + .thenReturn(user); + } + + @Override + public Mono findUserByEmail(String email) { + ScanRequest request = ScanRequest.builder() + .tableName(tableUsers) + .filterExpression(UserDefinition.USER_EMAIL + " = :emailVal") + .expressionAttributeValues(Map.of( + ":emailVal", AttributeValue.builder().s(email).build() + )) + .build(); + + return Mono.fromFuture(() -> client.scan(request)) + .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(mapper.toUser(items.getFirst())); + }) + .doOnError(e -> log.error("❌ Error scanning DynamoDB for email {}: {}", email, e.getMessage())); + } + + @Override + 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) + .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.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 + } + + @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)); + } + + 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/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/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/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/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..a64ce8b --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/RouterConfig.java @@ -0,0 +1,72 @@ +package com.aws.ws.infrastructure.inbound; + +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 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 user login + @org.springdoc.core.annotations.RouterOperation( + path = "/api/auth/login", + produces = "application/json", + method = org.springframework.web.bind.annotation.RequestMethod.POST, + beanClass = UserHandler.class, + beanMethod = "login", + operation = @io.swagger.v3.oas.annotations.Operation( + 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 = "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 = LoginRequest.class) + ) + ) + ) + ), + // Define the route for user registration + @org.springdoc.core.annotations.RouterOperation( + path = "/api/auth/register", + produces = "application/json", + method = org.springframework.web.bind.annotation.RequestMethod.POST, + beanClass = UserHandler.class, + beanMethod = "register", + operation = @io.swagger.v3.oas.annotations.Operation( + operationId = "registerUser", + summary = "User Registration", + description = "Register a new user in the system.", + requestBody = @io.swagger.v3.oas.annotations.parameters.RequestBody( + required = true, + 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 userRoutes(UserHandler userHandler) { + return route() + .path("/api/auth", builder -> builder + .POST("/register", accept(MediaType.APPLICATION_JSON), userHandler::register) + .POST("/login", accept(MediaType.APPLICATION_JSON), userHandler::login) + .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/dto/LoginRequest.java b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java new file mode 100644 index 0000000..1898ae7 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/dto/LoginRequest.java @@ -0,0 +1,23 @@ +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 = "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 new file mode 100644 index 0000000..5db5ec6 --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/handler/UserHandler.java @@ -0,0 +1,134 @@ +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.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; +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 +@Component +@RequiredArgsConstructor +@Tag(name = "Users", description = "Users Management") +public class UserHandler { + + private final UserServicePort servicePort; + private final GlobalErrorHandler globalErrorHandler; + private final UserMapper mapper; + private final PasswordEncoder passwordEncoder; + private final JwtAdapter jwtAdapter; + + 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))); + } + + 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())) { + return ServerResponse.status(HttpStatus.UNAUTHORIZED).build(); + } + // 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")); + } + })); + })); + } + + 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))); + } + + 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/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..c2584ce --- /dev/null +++ b/auth-backend-service/src/main/java/com/aws/ws/infrastructure/inbound/mapper/UserMapper.java @@ -0,0 +1,23 @@ +package com.aws.ws.infrastructure.inbound.mapper; + +import com.aws.ws.domain.model.User; +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(LoginRequest user) { + if (user == null) return null; + return User.builder() + .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.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..c793b83 --- /dev/null +++ b/auth-backend-service/src/main/resources/application.yml @@ -0,0 +1,22 @@ +# [AWS DynamoDB Local](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html) +aws: + region: us-east-1 + dynamodb: + endpoint: http://localhost:4566 + table: + users: Users + tokens: Tokens + secret: + access-key: test + key-id: test + +server: + port: 9801 + +jwt: + 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 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/aws-command.md b/aws-command.md new file mode 100644 index 0000000..9b69d3f --- /dev/null +++ b/aws-command.md @@ -0,0 +1,997 @@ +# 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 +```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 +``` + + + diff --git a/aws-lambda-cmd.md b/aws-lambda-cmd.md new file mode 100644 index 0000000..8947c22 --- /dev/null +++ b/aws-lambda-cmd.md @@ -0,0 +1,126 @@ +## 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']" +``` 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 0000000..1b33c55 Binary files /dev/null and b/bff-aws-localstack/gradle/wrapper/gradle-wrapper.jar differ diff --git a/bff-aws-localstack/gradle/wrapper/gradle-wrapper.properties b/bff-aws-localstack/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ff23a68 --- /dev/null +++ b/bff-aws-localstack/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/bff-aws-localstack/gradlew b/bff-aws-localstack/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/bff-aws-localstack/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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', + }, }, ]; 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..685dd88 100644 --- a/dashboard-ui/src/app/shared/guards/auth.guard.ts +++ b/dashboard-ui/src/app/shared/guards/auth.guard.ts @@ -1,16 +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); - // Simulación: verifica si el usuario está autenticado (usa localStorage o un AuthService real) - const isAuthenticated = localStorage.getItem('auth') === 'true'; - - if (!isAuthenticated) { - 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/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..bcfe0bb --- /dev/null +++ b/dashboard-ui/src/app/shared/services/auth.service.ts @@ -0,0 +1,81 @@ +import {effect, inject, Injectable, signal} from '@angular/core'; +import { HttpClient, HttpHeaders } from "@angular/common/http"; +import {catchError, map, Observable, of, 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() { + effect(() => { + this.hasValidToken().subscribe(valid => { + this.isAuthenticated.set(valid); + }); + }); + } + + 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(): Observable { + const token = this.getToken(); + 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); + }) + ); + } +} 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() {