From 67e192b3966d897c44d3111417b6618a5d145eb6 Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 04:43:51 -0500 Subject: [PATCH 1/7] update README.md --- README.md | 40 +++++----------------------------------- 1 file changed, 5 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index fef72e3..821ecc6 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,6 @@ A single-region, production-oriented URL shortener built with Spring Boot and An - Rate limiting (token bucket) - Soft delete support - Custom aliases (feature-flagged) -- Enhanced observability [![v2 HLD](diagrams/docs/architecture/00-baseline/v2/url-shortener-v2-hld.svg)](diagrams/docs/architecture/00-baseline/v2/url-shortener-v2-hld.svg) @@ -46,7 +45,7 @@ A single-region, production-oriented URL shortener built with Spring Boot and An | Migrations | Flyway | | Reverse proxy | Nginx | | Containerization | Docker, Docker Compose | -| Cloud | AWS (EC2, RDS, ALB, S3, CloudFront, Route 53, SSM) | +| Cloud | AWS (EC2, RDS, ALB, S3, CloudFront) | | CI/CD | GitHub Actions → GHCR → EC2 via SSM | | Observability | Micrometer, Prometheus, CloudWatch | @@ -58,8 +57,6 @@ A single-region, production-oriented URL shortener built with Spring Boot and An | --- | --- | --- | | `POST` | `/api/urls` | Shorten a URL | | `GET` | `/{shortCode}` | Redirect to original URL | -| `GET` | `/actuator/health` | Health check | -| `GET` | `/actuator/prometheus` | Metrics | --- @@ -69,29 +66,23 @@ A single-region, production-oriented URL shortener built with Spring Boot and An - Docker & Docker Compose - Java 21 (for running backend without Docker) -- Node 20+ (for running frontend without Docker) ### Full stack (backend + database + nginx) ```bash -# Copy and fill in required env vars -cp .env.example .env - docker compose up --build ``` App available at `http://localhost:8080`. -### Backend only (with local Postgres) +### Backend only ```bash cd tinyurl ./gradlew bootRun ``` -Backend runs on `http://localhost:8080` by default. - -### Run backend tests +### Run tests ```bash cd tinyurl @@ -102,17 +93,6 @@ cd tinyurl --- -## Environments - -| Environment | How to run | URL | -| --- | --- | --- | -| Local (full stack) | `docker compose up` from repo root | `http://localhost:8080` | -| Local (backend only) | `./gradlew bootRun` in `tinyurl/` | `http://localhost:8080` | -| Local (frontend dev) | See [tinyurl-gui/README.md](tinyurl-gui/README.md) | `http://localhost:4200` | -| Production | Auto-deploy on merge to `main` | [go.buffden.com](https://go.buffden.com) | - ---- - ## Project Structure ```text @@ -123,23 +103,13 @@ infra/ postgres/ # DB init scripts docs/ architecture/ # ADRs and architecture docs - deployment/ # AWS deployment phases (A–F) + deployment/ # AWS deployment runbook (phases A–F) diagrams/ # Architecture diagrams (SVG) docker-compose.yml # Local dev stack -docker-compose.prod.yml # Production stack (no Postgres — uses RDS) ``` --- ## Deployment -Production runs on AWS (`us-east-1`). See [`docs/deployment/`](docs/deployment/README.md) for the full deployment runbook (infrastructure provisioning, secrets, CI/CD, observability, hardening). - -```text -Route 53 - tinyurl.buffden.com → CloudFront → S3 (Angular SPA) - go.buffden.com → ALB → EC2 (Nginx + Spring Boot) → RDS PostgreSQL -``` - -Docker image: `ghcr.io/buffden/tinyurl-api` -Deploy trigger: merge to `main` via GitHub Actions +Production is deployed on AWS. See [`docs/deployment/`](docs/deployment/README.md) for the full runbook. From 7112ccc1a6098279674f51be76051f8a00fbc97d Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 05:19:19 -0500 Subject: [PATCH 2/7] add security headers for dev environment --- infra/nginx/nginx.conf | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/infra/nginx/nginx.conf b/infra/nginx/nginx.conf index 3404279..e3cebce 100644 --- a/infra/nginx/nginx.conf +++ b/infra/nginx/nginx.conf @@ -9,6 +9,11 @@ http { listen 80; server_name _; + add_header X-Frame-Options "DENY" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "0" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + location / { proxy_pass http://backend_upstream; proxy_http_version 1.1; From 98313d43fd2976d22e47996b838ee809f572521f Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 05:22:08 -0500 Subject: [PATCH 3/7] add user in dockerfile for principle of least privilege --- tinyurl/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tinyurl/Dockerfile b/tinyurl/Dockerfile index 82a1374..c4b554a 100644 --- a/tinyurl/Dockerfile +++ b/tinyurl/Dockerfile @@ -13,8 +13,14 @@ RUN chmod +x gradlew && ./gradlew --no-daemon bootJar FROM eclipse-temurin:21-jre-alpine WORKDIR /app +RUN addgroup -S appgroup && adduser -S appuser -G appgroup + COPY --from=build /workspace/build/libs/*.jar /app/app.jar +RUN chown appuser:appgroup /app/app.jar + +USER appuser + EXPOSE 8080 ENTRYPOINT ["java", "-jar", "/app/app.jar"] From e6a07c4a1a4206e9c020cbce81d8b2813987d799 Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 05:22:25 -0500 Subject: [PATCH 4/7] add clean and secure practice while db migration --- infra/postgres/init/002_app_user.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/infra/postgres/init/002_app_user.sh b/infra/postgres/init/002_app_user.sh index c6dc2e2..e313c83 100755 --- a/infra/postgres/init/002_app_user.sh +++ b/infra/postgres/init/002_app_user.sh @@ -6,14 +6,18 @@ set -e +# Escape single quotes in credentials to prevent SQL injection +APP_USER="${SPRING_DATASOURCE_USERNAME//\'/\'\'}" +APP_PASS="${SPRING_DATASOURCE_PASSWORD//\'/\'\'}" + psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL - CREATE USER $SPRING_DATASOURCE_USERNAME WITH PASSWORD '$SPRING_DATASOURCE_PASSWORD'; - GRANT CONNECT ON DATABASE $POSTGRES_DB TO $SPRING_DATASOURCE_USERNAME; - GRANT USAGE ON SCHEMA public TO $SPRING_DATASOURCE_USERNAME; - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE url_mappings TO $SPRING_DATASOURCE_USERNAME; - GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO $SPRING_DATASOURCE_USERNAME; + CREATE USER "$APP_USER" WITH PASSWORD '$APP_PASS'; + GRANT CONNECT ON DATABASE "$POSTGRES_DB" TO "$APP_USER"; + GRANT USAGE ON SCHEMA public TO "$APP_USER"; + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE url_mappings TO "$APP_USER"; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "$APP_USER"; ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO $SPRING_DATASOURCE_USERNAME; + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO "$APP_USER"; ALTER DEFAULT PRIVILEGES IN SCHEMA public - GRANT USAGE, SELECT ON SEQUENCES TO $SPRING_DATASOURCE_USERNAME; + GRANT USAGE, SELECT ON SEQUENCES TO "$APP_USER"; EOSQL From 995cb0e1ece2e5b2abad396ac00e5238c195b82f Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 05:22:57 -0500 Subject: [PATCH 5/7] lock down the actuators metrics --- tinyurl/src/main/resources/application-dev.yaml | 9 +++++++++ tinyurl/src/main/resources/application-prod.yaml | 3 ++- tinyurl/src/main/resources/application.yaml | 7 ++++--- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 tinyurl/src/main/resources/application-dev.yaml diff --git a/tinyurl/src/main/resources/application-dev.yaml b/tinyurl/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..9a4332f --- /dev/null +++ b/tinyurl/src/main/resources/application-dev.yaml @@ -0,0 +1,9 @@ +# Development-only overrides. +# Activate with: SPRING_PROFILES_ACTIVE=dev +# Never use this profile in production or CI/CD environments. + +management: + endpoints: + web: + exposure: + include: health,metrics,prometheus diff --git a/tinyurl/src/main/resources/application-prod.yaml b/tinyurl/src/main/resources/application-prod.yaml index 4a400aa..1899b6b 100644 --- a/tinyurl/src/main/resources/application-prod.yaml +++ b/tinyurl/src/main/resources/application-prod.yaml @@ -12,4 +12,5 @@ management: tinyurl: cors: - allowed-origins: "https://tinyurl.buffden.com" + allowed-origins: + - "https://tinyurl.buffden.com" diff --git a/tinyurl/src/main/resources/application.yaml b/tinyurl/src/main/resources/application.yaml index 8d6dc74..6ef7ad9 100644 --- a/tinyurl/src/main/resources/application.yaml +++ b/tinyurl/src/main/resources/application.yaml @@ -29,8 +29,8 @@ management: endpoints: web: exposure: - # Default: expose metrics for dev/test. Restricted in production via application-prod.yaml - include: health,metrics,prometheus + # Restricted to health by default. Metrics/prometheus enabled via application-dev.yaml + include: health metrics: tags: application: ${spring.application.name} @@ -45,4 +45,5 @@ tinyurl: default-expiry-days: ${TINYURL_DEFAULT_EXPIRY_DAYS:180} short-code-min-length: ${TINYURL_SHORT_CODE_MIN_LENGTH:6} cors: - allowed-origins: "http://localhost:4200" + allowed-origins: + - "http://localhost:4200" From 2f204014faeb501318f68da6d80081068eef696c Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 05:32:21 -0500 Subject: [PATCH 6/7] switching CorsConfig.java from @Value to @ConfigurationProperties, which is more refactoring than needed right now. assigning dev profile for local docker compose --- docker-compose.yml | 1 + .../main/java/com/tinyurl/config/AppProperties.java | 6 +++++- .../src/main/java/com/tinyurl/config/CorsConfig.java | 10 ++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index c1f7b10..0bb38a9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: postgres: condition: service_healthy environment: + SPRING_PROFILES_ACTIVE: dev SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/tinyurl SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME:?SPRING_DATASOURCE_USERNAME is required} SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD:?SPRING_DATASOURCE_PASSWORD is required} diff --git a/tinyurl/src/main/java/com/tinyurl/config/AppProperties.java b/tinyurl/src/main/java/com/tinyurl/config/AppProperties.java index 8dcb165..becf65e 100644 --- a/tinyurl/src/main/java/com/tinyurl/config/AppProperties.java +++ b/tinyurl/src/main/java/com/tinyurl/config/AppProperties.java @@ -2,10 +2,14 @@ import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.List; + @ConfigurationProperties(prefix = "tinyurl") public record AppProperties( String baseUrl, Integer defaultExpiryDays, - Integer shortCodeMinLength + Integer shortCodeMinLength, + Cors cors ) { + public record Cors(List allowedOrigins) {} } diff --git a/tinyurl/src/main/java/com/tinyurl/config/CorsConfig.java b/tinyurl/src/main/java/com/tinyurl/config/CorsConfig.java index 8b0f0e9..dedf96f 100644 --- a/tinyurl/src/main/java/com/tinyurl/config/CorsConfig.java +++ b/tinyurl/src/main/java/com/tinyurl/config/CorsConfig.java @@ -1,6 +1,5 @@ package com.tinyurl.config; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; @@ -12,13 +11,16 @@ @Configuration public class CorsConfig { - @Value("${tinyurl.cors.allowed-origins}") - private List allowedOrigins; + private final AppProperties appProperties; + + public CorsConfig(AppProperties appProperties) { + this.appProperties = appProperties; + } @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); - config.setAllowedOrigins(allowedOrigins); + config.setAllowedOrigins(appProperties.cors().allowedOrigins()); config.setAllowedMethods(List.of("GET", "POST", "OPTIONS")); config.setAllowedHeaders(List.of("Content-Type", "Accept")); config.setAllowCredentials(false); From 731970057aa605af5ec88354c4c48cd673ec30ff Mon Sep 17 00:00:00 2001 From: Buffden Date: Sun, 29 Mar 2026 05:34:40 -0500 Subject: [PATCH 7/7] switch allowed-origins binding from @Value to @ConfigurationProperties --- .../src/test/java/com/tinyurl/service/UrlServiceImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinyurl/src/test/java/com/tinyurl/service/UrlServiceImplTest.java b/tinyurl/src/test/java/com/tinyurl/service/UrlServiceImplTest.java index c562b1d..e50cbd5 100644 --- a/tinyurl/src/test/java/com/tinyurl/service/UrlServiceImplTest.java +++ b/tinyurl/src/test/java/com/tinyurl/service/UrlServiceImplTest.java @@ -38,7 +38,7 @@ class UrlServiceImplTest { @BeforeEach void setUp() { - service = new UrlServiceImpl(urlRepository, base62Encoder, new AppProperties("http://localhost", 180, 6)); + service = new UrlServiceImpl(urlRepository, base62Encoder, new AppProperties("http://localhost", 180, 6, null)); } @Test