feat: updates (#631)
* build(deps): bump org.springframework.boot:spring-boot-starter-parent Bumps [org.springframework.boot:spring-boot-starter-parent](https://github.com/spring-projects/spring-boot) from 3.5.4 to 3.5.5. - [Release notes](https://github.com/spring-projects/spring-boot/releases) - [Commits](https://github.com/spring-projects/spring-boot/compare/v3.5.4...v3.5.5) --- updated-dependencies: - dependency-name: org.springframework.boot:spring-boot-starter-parent dependency-version: 3.5.5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump io.jsonwebtoken:jjwt-jackson in /server Bumps io.jsonwebtoken:jjwt-jackson from 0.12.7 to 0.13.0. --- updated-dependencies: - dependency-name: io.jsonwebtoken:jjwt-jackson dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump io.jsonwebtoken:jjwt-impl in /server Bumps [io.jsonwebtoken:jjwt-impl](https://github.com/jwtk/jjwt) from 0.12.7 to 0.13.0. - [Release notes](https://github.com/jwtk/jjwt/releases) - [Changelog](https://github.com/jwtk/jjwt/blob/master/CHANGELOG.md) - [Commits](https://github.com/jwtk/jjwt/compare/0.12.7...0.13.0) --- updated-dependencies: - dependency-name: io.jsonwebtoken:jjwt-impl dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * build(deps): bump io.jsonwebtoken:jjwt-api in /server Bumps [io.jsonwebtoken:jjwt-api](https://github.com/jwtk/jjwt) from 0.12.7 to 0.13.0. - [Release notes](https://github.com/jwtk/jjwt/releases) - [Changelog](https://github.com/jwtk/jjwt/blob/master/CHANGELOG.md) - [Commits](https://github.com/jwtk/jjwt/compare/0.12.7...0.13.0) --- updated-dependencies: - dependency-name: io.jsonwebtoken:jjwt-api dependency-version: 0.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * feat: add Taskfile * feat: add files, tasks and more automations * feat: disable email sending if not configured * chore: rename client to tasknote-web * feat: add devtools for faster development * feat: Re-tag Docker image with 'latest' tag * feat: update wording and improve taskfile * Implement ApplicationRunner for startup logging * fix: add reachability json file * feat: drop old config for JJWT * Revert "feat: drop old config for JJWT" This reverts commit ed29cf4684454b0d5e3f1a284b479e2bb6872ad7. * chore: JJWT finally working now * test: fix test cases * ci: fix permissions * test: add more test cases --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
parent
caa0b8b9ea
commit
2da0a9f113
@@ -1,4 +1,4 @@
|
||||
name: Main Client
|
||||
name: Main Tasknote-web
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -17,8 +17,8 @@ on:
|
||||
- 'client/Caddyfile'
|
||||
|
||||
jobs:
|
||||
client-code-checks:
|
||||
name: Client Code Checks
|
||||
tasknote-web-code-checks:
|
||||
name: Tasknote-web Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -74,20 +74,20 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image with source tag
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:candidate
|
||||
|
||||
- name: Extract PR number using docker inspect
|
||||
id: inspect
|
||||
run: |
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
echo "SOURCE_PR=$SOURCE_PR" >> $GITHUB_ENV
|
||||
echo "source_pr=$SOURCE_PR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Re-tag the image
|
||||
run: docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
run: docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Push new tag
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Checking cURL version
|
||||
run: curl --version
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d '{
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-${{ steps.inspect.outputs.source_pr }}",
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:prod-${{ steps.inspect.outputs.source_pr }}",
|
||||
"applicationId": "'"${CLIENT_APPID}"'",
|
||||
"username": "'"${GHCR_USERNAME}"'",
|
||||
"password": "'"${GHCR_PASSWORD}"'",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Main Server
|
||||
name: Main Tasknote-api
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -12,7 +12,7 @@ on:
|
||||
|
||||
jobs:
|
||||
java-code-checks:
|
||||
name: Server Code Checks
|
||||
name: Tasknote-api Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -77,20 +77,22 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Pull image with source tag
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate
|
||||
run: docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate
|
||||
|
||||
- name: Extract PR number using docker inspect
|
||||
id: inspect
|
||||
run: |
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
SOURCE_PR=$(docker inspect ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p')
|
||||
echo "SOURCE_PR=$SOURCE_PR" >> $GITHUB_ENV
|
||||
echo "source_pr=$SOURCE_PR" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Re-tag the image
|
||||
run: docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/server:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
run: |
|
||||
docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
docker tag ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:latest
|
||||
|
||||
- name: Push new tag
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/server:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
run: docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:prod-${{ steps.inspect.outputs.source_pr }}
|
||||
|
||||
- name: Checking cURL version
|
||||
run: curl --version
|
||||
@@ -108,7 +110,7 @@ jobs:
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d '{
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/server:prod-${{ steps.inspect.outputs.source_pr }}",
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:prod-${{ steps.inspect.outputs.source_pr }}",
|
||||
"applicationId": "'"${SERVER_APPID}"'",
|
||||
"username": "'"${GHCR_USERNAME}"'",
|
||||
"password": "'"${GHCR_PASSWORD}"'",
|
||||
@@ -171,4 +173,4 @@ jobs:
|
||||
exit 1
|
||||
else
|
||||
echo "Prod environment is healthy"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
name: PR Client
|
||||
name: PR Tasknote-web
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -6,12 +8,12 @@ on:
|
||||
- 'client/**'
|
||||
|
||||
concurrency:
|
||||
group: client-${{ github.event.number }}
|
||||
group: tasknote-web-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
client-code-checks:
|
||||
name: Client Code Checks
|
||||
tasknote-web-code-checks:
|
||||
name: Tasknote-web Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -46,12 +48,15 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
|
||||
client-docker-build:
|
||||
name: Build Client Docker image
|
||||
tasknote-web-docker-build:
|
||||
name: Build Tasknote-web Docker image
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-code-checks
|
||||
needs: tasknote-web-code-checks
|
||||
env:
|
||||
VITE_BUILD: client:${{ github.event.number }}
|
||||
VITE_BUILD: tasknote-web:${{ github.event.number }}
|
||||
VITE_BACKEND_SERVER: ${{ secrets.SERVER_ADDRESS }}/server
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
@@ -74,17 +79,17 @@ jobs:
|
||||
with:
|
||||
push: true
|
||||
context: ./client
|
||||
tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
|
||||
tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:candidate
|
||||
build-args: |
|
||||
VITE_BUILD=v${{ github.event.number }}-${{ steps.date.outputs.date }}
|
||||
SOURCE_PR=v${{ github.event.number }}-${{ github.run_id }}-${{ steps.date.outputs.date }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
client-stage-deployments:
|
||||
tasknote-web-stage-deployments:
|
||||
name: Deploy Changes to Stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-docker-build
|
||||
needs: tasknote-web-docker-build
|
||||
if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request'
|
||||
env:
|
||||
DEPLOY_DOMAIN: ${{ vars.DEPLOY_DOMAIN }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: PR Server
|
||||
name: PR Tasknote-api
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -6,12 +6,12 @@ on:
|
||||
- 'server/**'
|
||||
|
||||
concurrency:
|
||||
group: server-${{ github.event.number }}
|
||||
group: tasknote-api-${{ github.event.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
java-code-checks:
|
||||
name: Server Code Checks
|
||||
name: Tasknote-api Code Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
./mvnw -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=ricardo-campos-org_react-typescript-todolist_server -Dsonar.coverage.jacoco.xmlReportPaths=target/coverage-reports/merged-test-report/jacoco.xml -Dsonar.exclusions='**/config/**,**/entity/**,**/exception/**,**/filter/**,**/**Builder*,**/RestExceptionEndpoint.*,**/JavaApiApiApplication.*'
|
||||
|
||||
java-docker-build:
|
||||
name: Build Server Docker image
|
||||
name: Build Tasknote-api Docker image
|
||||
runs-on: ubuntu-latest
|
||||
needs: java-code-checks
|
||||
steps:
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
with:
|
||||
push: true
|
||||
context: ./server
|
||||
tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/server:candidate
|
||||
tags: ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate
|
||||
build-args: |
|
||||
BUILD=v${{ github.event.number }}-${{ steps.date.outputs.date }}
|
||||
SOURCE_PR=v${{ github.event.number }}-${{ github.run_id }}-${{ steps.date.outputs.date }}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# https://taskfile.dev
|
||||
|
||||
version: '3'
|
||||
|
||||
vars:
|
||||
GREETING: Hello, World!
|
||||
|
||||
tasks:
|
||||
docker-build-web:
|
||||
desc: Build the tasknote-web prod-ready docker image, tagging it as candidate
|
||||
cmd: docker build --no-cache --build-arg VITE_BUILD="v999-$(date '+%Y-%m-%d-%H%M%S')" --build-arg SOURCE_PR="v999-123456789-$(date '+%Y-%m-%d-%H%M%S')" -t ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:candidate ./client
|
||||
|
||||
docker-build-api:
|
||||
desc: Build the tasknote-api prod-ready docker image, tagging it as candidate
|
||||
cmd: docker build --no-cache --build-arg BUILD="v999-$(date '+%Y-%m-%d-%H%M%S')" --build-arg SOURCE_PR="v999-123456789-$(date '+%Y-%m-%d-%H%M%S')" -t ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate ./server
|
||||
|
||||
prod-up-web:
|
||||
desc: Speed up the tasknote-web prod-like image, building it if required
|
||||
cmd: docker compose -f docker-compose.prod.yml up -d tasknote-web
|
||||
|
||||
prod-up-api:
|
||||
desc: Speed up the tasknote-api prod-like image, building it if required
|
||||
cmd: docker compose -f docker-compose.prod.yml up -d tasknote-api
|
||||
|
||||
prod-up-db:
|
||||
desc: Start the database container
|
||||
cmd: docker compose -f docker-compose.prod.yml up -d tasknote-db
|
||||
|
||||
prod-up:
|
||||
desc: Speed up the prod-like images for both tasknote-api and tasknote-web, building them if required
|
||||
cmd: docker compose -f docker-compose.prod.yml up -d
|
||||
|
||||
prod-stop:
|
||||
desc: Stop prod containers
|
||||
cmd: docker compose -f docker-compose.prod.yml stop
|
||||
|
||||
prod-tier-down:
|
||||
desc: Stop and remove prod containers
|
||||
cmd: docker compose -f docker-compose.prod.yml down --remove-orphans
|
||||
|
||||
dev-run-web:
|
||||
desc: Get the tasknote-web up and running locally inside docker ready for development
|
||||
cmd: docker compose -f docker-compose.dev.yml up -d tasknote-web
|
||||
|
||||
dev-run-api:
|
||||
desc: Get the tasknote-api up and running locally inside docker ready for development
|
||||
cmd: docker compose -f docker-compose.dev.yml up -d tasknote-api
|
||||
|
||||
dev-run-db:
|
||||
desc: Get the tasknote-db up and running locally inside docker ready for development
|
||||
cmd: docker compose -f docker-compose.dev.yml up -d tasknote-db
|
||||
|
||||
dev-run:
|
||||
desc: Get both tasknote-web and tasknote-api up and running locally inside docker ready for development
|
||||
cmd: docker compose -f docker-compose.dev.yml up -d
|
||||
|
||||
dev-stop:
|
||||
desc: Stop dev containers
|
||||
cmd: docker compose -f docker-compose.dev.yml stop
|
||||
|
||||
dev-tier-down:
|
||||
desc: Stop and remove dev containers
|
||||
cmd: docker compose -f docker-compose.dev.yml down --remove-orphans
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:20.18-bookworm
|
||||
FROM node:22.14-bookworm-slim
|
||||
WORKDIR /app
|
||||
HEALTHCHECK CMD timeout 10s bash -c 'true > /dev/tcp/127.0.0.1/5000'
|
||||
CMD ["sh", "run-from-docker.sh"]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
---
|
||||
|
||||
services:
|
||||
client:
|
||||
container_name: client
|
||||
image: node:20.18-bookworm
|
||||
tasknote-web:
|
||||
container_name: tasknote-web
|
||||
image: node:22.14-bookworm-slim
|
||||
ports:
|
||||
- "5000:5000"
|
||||
entrypoint: sh -c "npm i --no-update-notifier && npm start"
|
||||
user: root
|
||||
environment:
|
||||
VITE_BACKEND_SERVER: http://localhost:8585
|
||||
VITE_BUILD: nightly
|
||||
volumes:
|
||||
- "./client:/app"
|
||||
working_dir: /app
|
||||
@@ -18,22 +20,22 @@ services:
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
server:
|
||||
container_name: server
|
||||
tasknote-api:
|
||||
container_name: tasknote-api
|
||||
depends_on:
|
||||
db:
|
||||
tasknote-db:
|
||||
condition: service_started
|
||||
environment:
|
||||
POSTGRES_DB: tasknote
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_HOST: tasknote-db
|
||||
POSTGRES_USER: tasknoteuser
|
||||
POSTGRES_PASSWORD: default
|
||||
POSTGRES_PORT: 5432
|
||||
CORS_ALLOWED_ORIGINS: http://localhost:4200, http://localhost:5000
|
||||
CORS_ALLOWED_ORIGINS: http://localhost:5000, http://tasknote-web:5000
|
||||
SERVER_SERVLET_CONTEXT_PATH: /
|
||||
SECURITY_KEY: ${SECURITY_KEY}
|
||||
MAILGUN_APIKEY: ${MAILGUN_APIKEY}
|
||||
TARGET_ENV: development
|
||||
SECURITY_KEY: this-is-a-very-long-security-key-for-dev
|
||||
MAILGUN_APIKEY: invalid-api-key-only-placeholder
|
||||
ports:
|
||||
- "8585:8585"
|
||||
- "5005:5005"
|
||||
@@ -58,13 +60,13 @@ services:
|
||||
- "./schemaspy/output:/output"
|
||||
- "./schemaspy/postgres.properties:/schemaspy.properties"
|
||||
depends_on:
|
||||
db:
|
||||
tasknote-db:
|
||||
condition: service_healthy
|
||||
server:
|
||||
tasknote-api:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
container_name: db
|
||||
tasknote-db:
|
||||
container_name: tasknote-db
|
||||
image: postgres:15.8-bookworm
|
||||
environment:
|
||||
POSTGRES_DB: tasknote
|
||||
+25
-21
@@ -1,46 +1,50 @@
|
||||
---
|
||||
|
||||
services:
|
||||
client:
|
||||
container_name: client
|
||||
tasknote-web:
|
||||
container_name: tasknote-web
|
||||
depends_on:
|
||||
server:
|
||||
tasknote-api:
|
||||
condition: service_healthy
|
||||
image: ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:candidate
|
||||
build:
|
||||
context: client
|
||||
dockerfile: Dockerfile
|
||||
ports: ["5000:5000"]
|
||||
environment:
|
||||
VITE_BACKEND_SERVER: http://localhost:8585
|
||||
VITE_BUILD: snapshot
|
||||
image: ghcr.io/ricardo-campos-org/react-typescript-todolist/client:candidate
|
||||
ports:
|
||||
- "5000:5000"
|
||||
VITE_BUILD: nightly
|
||||
|
||||
server:
|
||||
container_name: server
|
||||
tasknote-api:
|
||||
container_name: tasknote-api
|
||||
depends_on:
|
||||
db:
|
||||
tasknote-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
POSTGRES_DB: tasknote
|
||||
POSTGRES_HOST: db
|
||||
POSTGRES_HOST: tasknote-db
|
||||
POSTGRES_USER: tasknoteuser
|
||||
POSTGRES_PASSWORD: default
|
||||
POSTGRES_PORT: 5432
|
||||
CORS_ALLOWED_ORIGINS: http://localhost:5000
|
||||
CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000
|
||||
SERVER_SERVLET_CONTEXT_PATH: /
|
||||
SECURITY_KEY: ${SECURITY_KEY}
|
||||
TARGET_ENV: production
|
||||
ports:
|
||||
- "8585:8585"
|
||||
image: server:candidate
|
||||
TARGET_ENV: development
|
||||
SECURITY_KEY: this-is-a-very-long-security-key-for-dev
|
||||
MAILGUN_APIKEY: invalid-api-key-only-placeholder
|
||||
ports: ["8585:8585"]
|
||||
image: ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:candidate
|
||||
build:
|
||||
context: server
|
||||
dockerfile: Dockerfile
|
||||
|
||||
db:
|
||||
container_name: db
|
||||
tasknote-db:
|
||||
container_name: tasknote-db
|
||||
image: postgres:15.8-bookworm
|
||||
environment:
|
||||
POSTGRES_DB: tasknote
|
||||
POSTGRES_USER: tasknoteuser
|
||||
POSTGRES_PASSWORD: default
|
||||
ports:
|
||||
- "5432:5432"
|
||||
ports: ["5432:5432"]
|
||||
healthcheck:
|
||||
test: psql -q -U $${POSTGRES_USER} -d $${POSTGRES_DB} -c 'SELECT 1'
|
||||
interval: 1m30s
|
||||
|
||||
+12
-4
@@ -119,7 +119,15 @@
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- OPs & Tools -->
|
||||
<!-- Devtools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- OOps & Tools -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
@@ -176,17 +184,17 @@
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<version>0.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<version>0.13.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.12.6</version>
|
||||
<version>0.13.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
package br.com.tasknoteapp.server;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/** Entrypoint of the Java API service application. */
|
||||
@SpringBootApplication
|
||||
public class JavaApiApplication {
|
||||
public class JavaApiApplication implements ApplicationRunner {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(JavaApiApplication.class);
|
||||
|
||||
@Value("${br.com.tasknote.server.version}")
|
||||
private String apiBuildInfo;
|
||||
|
||||
/**
|
||||
* Main method of the application.
|
||||
@@ -15,4 +25,9 @@ public class JavaApiApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(JavaApiApplication.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
logger.info("Task Note API started successfully - Version: {}", apiBuildInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,9 @@ import org.springframework.context.annotation.ImportRuntimeHints;
|
||||
io.jsonwebtoken.impl.io.StandardCompressionAlgorithms.class,
|
||||
io.jsonwebtoken.impl.DefaultClaimsBuilder.class,
|
||||
io.jsonwebtoken.impl.DefaultJwtParserBuilder.class,
|
||||
io.jsonwebtoken.impl.DefaultJwtBuilder.class,
|
||||
io.jsonwebtoken.impl.DefaultJwtBuilder.Supplier.class,
|
||||
io.jsonwebtoken.lang.Supplier.class,
|
||||
org.flywaydb.core.internal.publishing.PublishingConfigurationExtension.class,
|
||||
})
|
||||
@ImportRuntimeHints(value = {HttpServletRequestRuntimeHint.class})
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@@ -64,6 +65,8 @@ public class AuthService {
|
||||
|
||||
private final MailgunEmailService mailgunEmailService;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
/**
|
||||
* Create a new user in the app.
|
||||
*
|
||||
@@ -104,7 +107,9 @@ public class AuthService {
|
||||
user.setLang(newUser.lang());
|
||||
userRepository.save(user);
|
||||
|
||||
mailgunEmailService.sendNewUser(user);
|
||||
if (hasValidMailgunApiKey()) {
|
||||
mailgunEmailService.sendNewUser(user);
|
||||
}
|
||||
|
||||
log.info("User created! ID {}", user.getId());
|
||||
return UserResponseWithToken.fromEntity(user, null, getGravatarImageUrl(newUser.email()));
|
||||
@@ -305,7 +310,7 @@ public class AuthService {
|
||||
userRepository.save(currentUser);
|
||||
}
|
||||
|
||||
if (emailChanged) {
|
||||
if (emailChanged && hasValidMailgunApiKey()) {
|
||||
// send email to older and new account
|
||||
log.info("Email changed from {} to {}", email, patchRequest.email());
|
||||
mailgunEmailService.sendEmailChangedNotification(currentUser, email);
|
||||
@@ -373,7 +378,9 @@ public class AuthService {
|
||||
|
||||
UserEntity user = userOptional.get();
|
||||
|
||||
mailgunEmailService.sendNewUser(user);
|
||||
if (hasValidMailgunApiKey()) {
|
||||
mailgunEmailService.sendNewUser(user);
|
||||
}
|
||||
|
||||
log.info("Confirmation email re-sent!");
|
||||
}
|
||||
@@ -400,7 +407,9 @@ public class AuthService {
|
||||
user.setResetPasswordExpiration(LocalDateTime.now().plusHours(2L));
|
||||
|
||||
userRepository.save(user);
|
||||
mailgunEmailService.sendResetPassword(user);
|
||||
if (hasValidMailgunApiKey()) {
|
||||
mailgunEmailService.sendResetPassword(user);
|
||||
}
|
||||
|
||||
log.info("Password reset request succeeded");
|
||||
}
|
||||
@@ -441,7 +450,9 @@ public class AuthService {
|
||||
user.setPassword(passwordEncoder.encode(request.password()));
|
||||
|
||||
userRepository.save(user);
|
||||
mailgunEmailService.sendPasswordResetConfirmation(user);
|
||||
if (hasValidMailgunApiKey()) {
|
||||
mailgunEmailService.sendPasswordResetConfirmation(user);
|
||||
}
|
||||
|
||||
log.info("New password set for token {}", request.token());
|
||||
}
|
||||
@@ -487,4 +498,10 @@ public class AuthService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasValidMailgunApiKey() {
|
||||
String apiKey = environment.getProperty("MAILGUN_APIKEY");
|
||||
return Optional.ofNullable(apiKey).isPresent()
|
||||
&& !"invalid-api-key-only-placeholder".equals(apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
{
|
||||
"reflection": [
|
||||
{
|
||||
"type": "io.jsonwebtoken.Claims"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.Header"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.Identifiable"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.JwsHeader"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.ProtectedHeader"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultClaims",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultClaimsBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultHeader",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultJwsHeader",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultJwtBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultJwtHeaderBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultJwtParserBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.DefaultProtectedHeader",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.ParameterMap",
|
||||
"allDeclaredFields": true
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.io.StandardCompressionAlgorithms",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.lang.Nameable"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.lang.ParameterReadable"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.security.DefaultKeyOperationBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.security.DefaultKeyOperationPolicyBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.security.StandardEncryptionAlgorithms",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.security.StandardKeyAlgorithms",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.security.StandardKeyOperations",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.impl.security.StandardSecureDigestAlgorithms",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.jackson.io.JacksonDeserializer"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.jackson.io.JacksonSerializer"
|
||||
},
|
||||
{
|
||||
"type": "io.jsonwebtoken.security.X509Accessor"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
{
|
||||
"glob": "META-INF/services/io.jsonwebtoken.io.Deserializer"
|
||||
},
|
||||
{
|
||||
"glob": "META-INF/services/io.jsonwebtoken.io.Serializer"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -625,5 +625,41 @@
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "io.jsonwebtoken.impl.DefaultJwtHeaderBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "io.jsonwebtoken.impl.DefaultClaimsBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "io.jsonwebtoken.impl.security.DefaultKeyOperationBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "io.jsonwebtoken.impl.security.DefaultKeyOperationPolicyBuilder$Supplier",
|
||||
"methods": [
|
||||
{
|
||||
"name": "<init>",
|
||||
"parameterTypes": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -34,6 +34,7 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.domain.Sort.Direction;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
@@ -59,6 +60,8 @@ class AuthServiceTest {
|
||||
|
||||
@Mock private MailgunEmailService mailgunEmailService;
|
||||
|
||||
@Mock private Environment environment;
|
||||
|
||||
private AuthService authService;
|
||||
|
||||
@BeforeEach
|
||||
@@ -71,7 +74,8 @@ class AuthServiceTest {
|
||||
authenticationManager,
|
||||
authUtil,
|
||||
userPwdLimitRepository,
|
||||
mailgunEmailService);
|
||||
mailgunEmailService,
|
||||
environment);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -509,6 +513,7 @@ class AuthServiceTest {
|
||||
existing.setId(919L);
|
||||
existing.setEmail(email);
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
|
||||
when(environment.getProperty("MAILGUN_APIKEY")).thenReturn("abc");
|
||||
|
||||
doNothing().when(mailgunEmailService).sendNewUser(existing);
|
||||
|
||||
@@ -516,6 +521,22 @@ class AuthServiceTest {
|
||||
verify(mailgunEmailService, times(1)).sendNewUser(existing);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Resend email confirmation no mailgun api key should succeed")
|
||||
void resendEmailConfirmation_noMailgunApiKey_shouldSucceed() {
|
||||
String email = "user@domain.com";
|
||||
|
||||
UserEntity existing = new UserEntity();
|
||||
existing.setId(919L);
|
||||
existing.setEmail(email);
|
||||
when(userRepository.findByEmail(email)).thenReturn(Optional.of(existing));
|
||||
|
||||
doNothing().when(mailgunEmailService).sendNewUser(existing);
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> authService.resendEmailConfirmation(email));
|
||||
verify(mailgunEmailService, times(0)).sendNewUser(existing);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Resend email confirmation with non-existent email should fail")
|
||||
void resendEmailConfirmation_nonExistentEmail_shouldFail() {
|
||||
@@ -540,6 +561,7 @@ class AuthServiceTest {
|
||||
|
||||
doNothing().when(mailgunEmailService).sendResetPassword(any());
|
||||
when(userRepository.save(any())).thenReturn(existing);
|
||||
when(environment.getProperty("MAILGUN_APIKEY")).thenReturn("abc");
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> authService.resetPasswordForUser(email));
|
||||
verify(userRepository, times(1)).save(existing);
|
||||
@@ -574,6 +596,7 @@ class AuthServiceTest {
|
||||
|
||||
when(userRepository.findByResetToken(token)).thenReturn(Optional.of(user));
|
||||
when(authUtil.validatePassword(newPassword)).thenReturn(Optional.empty());
|
||||
when(environment.getProperty("MAILGUN_APIKEY")).thenReturn("abc");
|
||||
|
||||
PasswordResetRequest request = new PasswordResetRequest(token, newPassword, newPassword);
|
||||
|
||||
@@ -586,6 +609,34 @@ class AuthServiceTest {
|
||||
Assertions.assertNotNull(user.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Confirm reset password no mailgun api token should succeed")
|
||||
void confirmResetPasswordForUser_noMailgunApiToken_shouldSucceed() {
|
||||
String token = "validToken";
|
||||
UserEntity user = new UserEntity();
|
||||
user.setResetToken(token);
|
||||
user.setResetPasswordExpiration(LocalDateTime.now().plusMinutes(30));
|
||||
|
||||
String newPassword = "NewPassword@123";
|
||||
|
||||
String encodedPassword = "hash@abcxasd123!";
|
||||
when(passwordEncoder.encode(newPassword)).thenReturn(encodedPassword);
|
||||
user.setPassword(encodedPassword);
|
||||
|
||||
when(userRepository.findByResetToken(token)).thenReturn(Optional.of(user));
|
||||
when(authUtil.validatePassword(newPassword)).thenReturn(Optional.empty());
|
||||
|
||||
PasswordResetRequest request = new PasswordResetRequest(token, newPassword, newPassword);
|
||||
|
||||
Assertions.assertDoesNotThrow(() -> authService.confirmResetPasswordForUser(request));
|
||||
|
||||
verify(userRepository, times(1)).save(user);
|
||||
verify(mailgunEmailService, times(0)).sendPasswordResetConfirmation(user);
|
||||
Assertions.assertNull(user.getResetToken());
|
||||
Assertions.assertNull(user.getResetPasswordExpiration());
|
||||
Assertions.assertNotNull(user.getPassword());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Confirm reset password with expired token should fail")
|
||||
void confirmResetPasswordForUser_expiredToken_shouldFail() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
DOCKER_IMG="server:nightly"
|
||||
DOCKER_IMG="tasknote-api:nightly"
|
||||
|
||||
docker image inspect $DOCKER_IMG --format="ignore me"
|
||||
if [ $? -eq 1 ]; then
|
||||
@@ -19,10 +19,10 @@ if [ $? -eq 1 ]; then
|
||||
echo "Done!"
|
||||
|
||||
cd ..
|
||||
docker build --file server/Dockerfile.dev --tag server:nightly .
|
||||
docker build --file server/Dockerfile.dev --tag tasknote-api:nightly .
|
||||
else
|
||||
echo "Image found! Let's keep going."
|
||||
fi
|
||||
|
||||
|
||||
docker run -it --rm --name server -e CHECK="TRUE" -v ./server:/app server:nightly
|
||||
docker run -it --rm --name tasknote-api -e CHECK="TRUE" -v ./server:/app tasknote-api:nightly
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
docker run -it --rm --name client -e CHECK="TRUE" -v ./client:/app client:nightly
|
||||
docker run -it --rm --name tasknote-web -e CHECK="TRUE" -v ./client:/app tasknote-web:nightly
|
||||
|
||||
@@ -41,7 +41,7 @@ response=$(curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-d '{
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/client:prod-v359",
|
||||
"dockerImage": "ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:prod-v359",
|
||||
"applicationId": "'"${CLIENT_APPID}"'",
|
||||
"username": "'"${GHCR_USERNAME}"'",
|
||||
"password": "'"${GHCR_PASSWORD}"'",
|
||||
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
SERVICE="$1"
|
||||
|
||||
# Get the IP address of the Docker container running the database
|
||||
IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $SERVICE)
|
||||
|
||||
# Output the IP address
|
||||
echo "$IP"
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Checks if a given image and tag are present locally to prevent running when it's not
|
||||
|
||||
TARGET="$1"
|
||||
|
||||
IS=$(docker images $TARGET:candidate | wc -l)
|
||||
|
||||
if [ $IS -eq 0 ]; then
|
||||
echo "Image $TARGET:candidate not found locally, please build it"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
echo "Image $TARGET:candidate found"
|
||||
exit 0
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Check if a given service is not running
|
||||
# Return 0 (success) if it's not
|
||||
|
||||
TARGET="$1"
|
||||
echo "Check for running image for $TARGET"
|
||||
|
||||
IS=$(docker ps --filter name=$TARGET --filter status=running | grep $TARGET | wc -l)
|
||||
|
||||
if [ $IS -eq 0 ]; then
|
||||
echo "Service $TARGET is not running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Service $TARGET is running"
|
||||
exit 1
|
||||
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
TARGET="$1"
|
||||
echo "Check for running image for $TARGET"
|
||||
|
||||
IS=$(docker ps --filter name=$TARGET --filter status=running | grep $TARGET | wc -l)
|
||||
|
||||
if [ $IS -eq 1 ]; then
|
||||
echo "Service $TARGET is running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Service $TARGET not running"
|
||||
exit 1
|
||||
@@ -11,13 +11,15 @@ if [ "$TARGET" == "back" ]; then
|
||||
echo "Creating basic env file for database and back-end"
|
||||
cd server
|
||||
|
||||
echo "POSTGRES_DB=postgres" >> .env
|
||||
echo "POSTGRES_HOST=localhost" >> .env
|
||||
echo "POSTGRES_USER=postgres" >> .env
|
||||
echo "POSTGRES_DB=tasknote" >> .env
|
||||
echo "POSTGRES_HOST=db" >> .env
|
||||
echo "POSTGRES_USER=tasknoteuser" >> .env
|
||||
echo "POSTGRES_PASSWORD=default" >> .env
|
||||
echo "POSTGRES_PORT=5432" >> .env
|
||||
echo "SERVER_SERVLET_CONTEXT_PATH=/" >> .env
|
||||
echo "CORS_ALLOWED_ORIGINS=http://localhost:5000" >> .env
|
||||
echo "SECURITY_KEY=this-is-a-very-long-security-key-for-dev" >> .env
|
||||
echo "MAILGUN_APIKEY=invalid-api-key-only-placeholder" >> .env
|
||||
elif [ "$TARGET" == "front" ]; then
|
||||
if [ -f "client/.env" ]; then
|
||||
echo "Env file in place. Leaving..."
|
||||
|
||||
@@ -21,10 +21,10 @@ if [ $? -eq 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVER_HOST="http://"$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' server)":8585"
|
||||
SERVER_HOST="http://"$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' tasknote-api):8585"
|
||||
|
||||
if [ "$SERVER_HOST" == "http://:8585" ]; then
|
||||
echo "Back-end server not running. Make sure to run it before starting the web app."
|
||||
echo "Back-end tasknote-api not running. Make sure to run it before starting the web app."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ echo "Done!"
|
||||
|
||||
cd ..
|
||||
|
||||
docker build --file server/Dockerfile.dev --tag server:nightly .
|
||||
docker build --file server/Dockerfile.dev --tag tasknote-api:nightly .
|
||||
|
||||
DB_HOST=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' db)
|
||||
|
||||
docker run -d --rm \
|
||||
--name server \
|
||||
--name tasknote-api \
|
||||
-p 8585:8585 \
|
||||
-p 5005:5005 \
|
||||
-e POSTGRES_DB=$POSTGRES_DB \
|
||||
@@ -30,4 +30,4 @@ docker run -d --rm \
|
||||
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
|
||||
-e SERVER_SERVLET_CONTEXT_PATH=$SERVER_SERVLET_CONTEXT_PATH \
|
||||
-v ./server:/app \
|
||||
server:nightly
|
||||
tasknote-api:nightly
|
||||
|
||||
+13
-40
@@ -9,7 +9,7 @@ All kind of tools and useful links and commands can be found here!
|
||||
|
||||
## Building locally
|
||||
|
||||
**Client - Frontend Web App:**
|
||||
**Tasknote-web - Frontend Web App:**
|
||||
|
||||
Before building, you need define some env vars:
|
||||
|
||||
@@ -30,13 +30,13 @@ If you want to build with Docker:
|
||||
docker build --no-cache \
|
||||
--build-arg VITE_BUILD="v999-$(date '+%Y-%m-%d-%H%M%S')" \
|
||||
--build-arg SOURCE_PR="v999-123456789-$(date '+%Y-%m-%d-%H%M%S')" \
|
||||
-t client:candidate \
|
||||
-t tasknote-web:candidate \
|
||||
./client
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
**Server - Backend REST API:**
|
||||
**Tasknote-api - Backend REST API:**
|
||||
|
||||
For the backend there's a Dockerfile ready, just run (from the project root):
|
||||
|
||||
@@ -44,30 +44,12 @@ For the backend there's a Dockerfile ready, just run (from the project root):
|
||||
docker build --no-cache \
|
||||
--build-arg BUILD="v999-$(date '+%Y-%m-%d-%H%M%S')" \
|
||||
--build-arg SOURCE_PR="v999-123456789-$(date '+%Y-%m-%d-%H%M%S')" \
|
||||
-t server:candidate \
|
||||
-t tasknote-api:candidate \
|
||||
./server
|
||||
```
|
||||
|
||||
That's it!
|
||||
|
||||
## Deploying manually
|
||||
|
||||
1. Connect into the server via SSH
|
||||
2. Run deploy scripts
|
||||
|
||||
Here are a few steps:
|
||||
|
||||
```bash
|
||||
export SERVER_IP=
|
||||
export SERVER_ADDRESS=
|
||||
|
||||
npm ci --ignore-scripts --no-update-notifier --omit=dev
|
||||
export VITE_BACKEND_SERVER=$SERVER_ADDRESS/server
|
||||
npm run build
|
||||
zip -r "client_$VERSION.zip" dist/
|
||||
scp "client_$VERSION.zip" root@$SERVER_IP:/root/
|
||||
```
|
||||
|
||||
## Running with Docker
|
||||
|
||||
**DB:**
|
||||
@@ -81,23 +63,23 @@ docker run -d -p 5432:5432 --rm \
|
||||
postgres:15.8-bookworm
|
||||
```
|
||||
|
||||
**Server:**
|
||||
**Tasknote-api:**
|
||||
|
||||
```bash
|
||||
docker run -d -p 8585:8585 --rm \
|
||||
--name server \
|
||||
--name tasknote-api \
|
||||
-e POSTGRES_DB=$POSTGRES_DB \
|
||||
-e POSTGRES_USER=$POSTGRES_USER \
|
||||
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||
-e POSTGRES_PORT=$POSTGRES_PORT \
|
||||
-e POSTGRES_HOST=$POSTGRES_HOST \
|
||||
-e CORS_ALLOWED_ORIGINS=$CORS_ALLOWED_ORIGINS \
|
||||
ghcr.io/ricardo-campos-org/react-typescript-todolist/server:<PR-Number>
|
||||
ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:<PR-Number>
|
||||
```
|
||||
|
||||
Build Cloud Native: `./mvnw -B package -Pnative -DskipTests`
|
||||
|
||||
**Client:**
|
||||
**Tasknote-web:**
|
||||
|
||||
The frontend app will run on Nginx.
|
||||
|
||||
@@ -113,13 +95,13 @@ echo $CR_PAT | docker login ghcr.io -u RMCampos --password-stdin
|
||||
**Pulling images:**
|
||||
|
||||
```sh
|
||||
docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/client:50
|
||||
docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/server:50
|
||||
docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-web:50
|
||||
docker pull ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:50
|
||||
```
|
||||
|
||||
**Pushing images:**
|
||||
```sh
|
||||
docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/server:316
|
||||
docker push ghcr.io/ricardo-campos-org/react-typescript-todolist/tasknote-api:316
|
||||
```
|
||||
|
||||
- Get container IP
|
||||
@@ -133,12 +115,12 @@ docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' db
|
||||
docker inspect --format "{{json .State.Health}}" container_name_or_id | jq
|
||||
|
||||
# Or to see it live
|
||||
while true; do clear; docker inspect --format "{{json .State.Health}}" server | jq; sleep 1; done
|
||||
while true; do clear; docker inspect --format "{{json .State.Health}}" tasknote-api | jq; sleep 1; done
|
||||
```
|
||||
|
||||
- Extract Env value from docker image (without running)
|
||||
```bash
|
||||
docker inspect client:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p'
|
||||
docker inspect tasknote-web:candidate | jq -r '.[0].Config.Env[] | select(startswith("SOURCE_PR="))' | sed -n 's/SOURCE_PR=\(v[0-9]*\).*/\1/p'
|
||||
```
|
||||
|
||||
## Restoring DB for testing
|
||||
@@ -157,12 +139,3 @@ docker run \
|
||||
docker exec -i my-postgres-db pg_restore -U <user> -d <db-name> < 2025-04-26T20_00_00.114Z.sql
|
||||
|
||||
```
|
||||
|
||||
<div className="py-5 text-center">
|
||||
<div className="d-block d-sm-none bg-primary text-white p-3">XS (Extra Small): less than 576px</div>
|
||||
<div className="d-none d-sm-block d-md-none bg-secondary text-white p-3">SM (Small): ≥576px</div>
|
||||
<div className="d-none d-md-block d-lg-none bg-success text-white p-3">MD (Medium): ≥768px</div>
|
||||
<div className="d-none d-lg-block d-xl-none bg-warning text-dark p-3">LG (Large): ≥992px</div>
|
||||
<div className="d-none d-xl-block d-xxl-none bg-danger text-white p-3">XL (Extra Large): ≥1200px</div>
|
||||
<div className="d-none d-xxl-block bg-dark text-white p-3">XXL (Extra Extra Large): ≥1400px</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user