Compare commits

...
Author SHA1 Message Date
rmcamposandGitHub 80314f22d0 feat: improve security and prevent xss attacks (#30)
* feat: improve security and prevent xss attacks

* chore: fix frontend test file name location

* fix: frontend test case

* ci: add deployment connection
2026-04-16 16:49:51 -03:00
rmcampos c9c414d83f chore: add files back after renaming 2026-04-16 18:55:34 +02:00
rmcamposandGitHub 27c7455870 chore: add files and config to run on VPS and test locally (#29)
* chore: add files and config to run on VPS and test locally

* chore: add missing nginx files left behind

* chore: rename workflow files
2026-04-16 13:54:16 -03:00
github-actions[bot] 42d5eb286f chore: bump api version to 19 [skip ci] 2026-04-14 20:21:40 +00:00
rmcampos 25b04dd86b fix: gh cli wrong --commit argument 2026-04-14 22:21:05 +02:00
github-actions[bot] 62e2f5536d chore: bump api version to 18 [skip ci] 2026-04-14 20:17:46 +00:00
rmcampos 1111f961dd ci: change to reuse tag and image built on PR and avoid rebuilding on merge 2026-04-14 22:17:14 +02:00
rmcamposandGitHub 95e5336e50 faet: add url to environments 2026-04-10 16:36:59 -03:00
github-actions[bot] 2c43861d49 chore: bump api version to 17 [skip ci] 2026-04-08 22:05:26 +00:00
rmcamposandGitHub ba0a204d75 fix: wrong base url for staging env (#28)
* fix: wrong base url for staging env

* fix: wrong var changed domain
2026-04-08 19:05:00 -03:00
rmcamposandGitHub 405ebfec34 Update devcontainer for Java 2026-04-08 18:02:46 -03:00
rmcamposandGitHub 9102d88a13 Configure devcontainer with Java, Node.js, and Terraform
Added Java, Node.js, and Terraform features to the devcontainer.
2026-04-08 17:23:39 -03:00
rmcamposandGitHub 573bdf39ba chore: drop devcontainer created by ai 2026-04-08 17:17:25 -03:00
rmcamposandGitHub b08bed7977 fix: replace timestamp by git runner on staging deploy 2026-04-08 11:21:32 -03:00
rmcamposandGitHub 48b9eec4be fix: deploy to stg reading from wrong dir 2026-04-08 11:14:34 -03:00
rmcamposandGitHub 68dcb033a4 feat: fix stg deploy file 2026-04-08 11:12:43 -03:00
github-actions[bot] ca75291b39 chore: bump api version to 16 [skip ci] 2026-04-08 13:47:09 +00:00
rmcamposandGitHub 1c3e6a927d chore: bump spring version to 4.0.5 (#27)
* chore: bump spring version to 4.0.5

* feat: add docker build to CI workflow

* feat: add staging deploy - wip

* fix: change triggers to include PR changes

* feat: add server ci and deploy to stg

* chore: add app build candidate

* remove from branch, get from main

* fix: build app candidate

* fix: build app candidate

* fix: fe changes in the app

* fix: app ci workflow dispatch

* chore: improve PR ci and cd

* feat: make deployments always happen for PRs

* feat: improve workflow names
2026-04-08 10:46:41 -03:00
rmcamposandGitHub 3bb21e5315 Create build-app-candidate.yml 2026-04-07 17:59:50 -03:00
rmcamposandGitHub 19d17350fd Add GitHub Actions workflow for server candidate build
This workflow builds and pushes a Docker image for the server candidate using Maven and caches buildpack layers.
2026-04-07 17:35:15 -03:00
rmcamposandGitHub 75b2b87379 fix: drop reduntant workflow trigger and fix changes detect 2026-04-06 12:41:49 -03:00
41 changed files with 1558 additions and 365 deletions
+14 -17
View File
@@ -1,23 +1,20 @@
{
"name": "Tasknote Dev",
"image": "mcr.microsoft.com/devcontainers/typescript-node:20",
"image": "mcr.microsoft.com/devcontainers/java:25-trixie",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "25",
"jdkDistro": "temurin"
"installMaven": true,
"version": "latest",
"jdkDistro": "tem",
"gradleVersion": "latest",
"mavenVersion": "latest",
"antVersion": "latest",
"groovyVersion": "latest"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/terraform:1": {}
},
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"vscjava.vscode-java-pack",
"hashicorp.terraform",
"ms-azuretools.vscode-docker"
]
"ghcr.io/devcontainers/features/node:1": {
"installYarnUsingApt": true,
"version": "lts",
"pnpmVersion": "latest",
"nvmVersion": "latest"
}
},
"postCreateCommand": "npm install -g @nestjs/cli typescript fastify class-validator"
}
}
+57
View File
@@ -0,0 +1,57 @@
name: Build App Candidate
on:
workflow_dispatch:
jobs:
build-and-push-app:
name: Build & Push App
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/app
tags: |
type=raw,value=candidate
- name: Generate version tag
id: version
run: |
DATE=$(date +'%Y.%m.%d')
TAG="app-v${DATE}.${{ github.run_number }}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Generated tag: ${TAG}"
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./client
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VITE_BUILD=${{ steps.version.outputs.tag }}
@@ -0,0 +1,59 @@
name: Build Server Candidate
on:
workflow_dispatch:
jobs:
build-and-push-server:
name: Build & Push Server
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.ref }}
- name: Set lowercase repo name
id: repo
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
cache-dependency-path: 'server/pom.xml'
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Cache Buildpack layers
uses: actions/cache@v4
with:
path: ~/.cache/reproducible-builds
key: ${{ runner.os }}-buildpack-${{ hashFiles('server/pom.xml') }}
restore-keys: |
${{ runner.os }}-buildpack-
- name: Build Docker image
working-directory: ./server
run: |
# Use the dynamic repo name to prevent tagging errors
./mvnw -Pnative -DskipTests spring-boot:build-image \
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
- name: Tag and push candidate
run: |
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
@@ -1,4 +1,4 @@
name: Deploy to K3s
name: Main CD-Deploy to Prod
on:
workflow_dispatch:
@@ -13,10 +13,8 @@ on:
description: "Apply changes after plan"
required: false
default: "true"
push:
branches: [ main ]
workflow_run:
workflows: [ "Backend Build & Push", "Frontend Build & Push" ]
workflows: [ "Main CI-Backend", "Main CI-Frontend" ]
types: [ completed ]
jobs:
@@ -118,6 +116,9 @@ jobs:
echo "no_changes=true" >> "$GITHUB_OUTPUT"
echo "No changes to apply."
exit 0
else
echo "Changes detected. Proceeding with apply"
echo "no_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Upload plan artifact
@@ -134,6 +135,7 @@ jobs:
&& needs.terraform-plan.outputs.no_changes == 'false'
environment:
name: production
url: https://tasknote.darkroasted.vps-kinghost.net
permissions:
contents: read
steps:
+137
View File
@@ -0,0 +1,137 @@
name: Pull Request CD-Deploy to Staging
on:
workflow_dispatch:
workflow_run:
workflows: [ "Pull Request CI-Backend", "Pull Request CI-Frontend" ]
types: [ completed ]
jobs:
terraform-plan-stg:
name: Plan changs to staging
if: ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
outputs:
no_changes: ${{ steps.check-changes.outputs.no_changes }}
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Setup kubectl
uses: azure/setup-kubectl@v4
- name: Setup Kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Validate cluster access
run: |
kubectl cluster-info
kubectl get namespace tasknote-stg
- name: Determine deployment values
id: deploy-vars
run: |
backend_image="ghcr.io/rmcampos/tasknote/api:candidate"
frontend_image="ghcr.io/rmcampos/tasknote/app:candidate"
echo "backend_image=$backend_image" >> "$GITHUB_OUTPUT"
echo "frontend_image=$frontend_image" >> "$GITHUB_OUTPUT"
- name: Terraform Fmt -check -diff
working-directory: terraform-stg
run: terraform fmt -check -diff
- name: Terraform Init
working-directory: terraform-stg
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: terraform init -input=false
- name: Terraform Validate
working-directory: terraform-stg
run: terraform validate
- name: Terraform Plan
id: check-changes
working-directory: terraform-stg
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: |
timeout 1m terraform plan -input=false -out=tfplan \
-var="db_user=${{ secrets.DB_USER }}" \
-var="db_password=${{ secrets.DB_PASSWORD }}" \
-var="db_name=${{ secrets.DB_NAME }}" \
-var="security_key=${{ secrets.JWT_SECURITY_KEY }}" \
-var="mailgun_apikey=${{ secrets.MAILGUN_API_KEY }}" \
-var="backend_image=${{ steps.deploy-vars.outputs.backend_image }}" \
-var="frontend_image=${{ steps.deploy-vars.outputs.frontend_image }}" \
-var="deploy_version=${{ github.run_id }}"
terraform show -json tfplan > tfplan.json
if jq -e '.resource_changes | length == 0' tfplan.json >/dev/null; then
echo "no_changes=true" >> "$GITHUB_OUTPUT"
echo "No changes to apply."
exit 0
else
echo "Changes detected. Proceeding with apply"
echo "no_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Upload plan artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: terraform-stg/tfplan
terraform-apply:
runs-on: ubuntu-latest
needs: terraform-plan-stg
if: needs.terraform-plan-stg.outputs.no_changes == 'false'
environment:
name: staging
url: https://tasknote-stg.darkroasted.vps-kinghost.net
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Download plan artifact
uses: actions/download-artifact@v4
with:
name: tfplan
path: terraform-stg
- name: Setup Kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Terraform Init
working-directory: terraform-stg
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: terraform init -input=false
- name: Terraform Apply
working-directory: terraform-stg
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: timeout 1m terraform apply tfplan
@@ -1,4 +1,4 @@
name: Backend Build & Push
name: Main CI-Backend
on:
workflow_dispatch:
@@ -13,7 +13,7 @@ on:
jobs:
build-and-push:
name: Backend Build & Push
name: Build & Push
runs-on: ubuntu-latest
permissions:
contents: write
@@ -69,18 +69,29 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image with Spring Boot
working-directory: ./server
run: |
./mvnw -Pnative -DskipTests spring-boot:build-image \
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Tag and push Docker image
- name: Find PR number
id: find_pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }}
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:latest
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }}
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
echo "No merged PR found for this commit. Falling back to 'candidate' tag."
PR_NUMBER="candidate"
else
PR_NUMBER="pr-${PR_NUMBER}"
fi
echo "tag=${PR_NUMBER}" >> $GITHUB_OUTPUT
- name: Promote Docker image
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
--tag ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.version.outputs.version }} \
ghcr.io/${{ steps.repo.outputs.name }}/api:${{ steps.find_pr.outputs.tag }}
- name: Create and push Git tag
run: |
@@ -1,4 +1,4 @@
name: Frontend Build & Push
name: Main CI-Frontend
on:
workflow_dispatch:
@@ -19,7 +19,7 @@ on:
jobs:
build-and-push:
name: Frontend Build & Push
name: Build & Push
runs-on: ubuntu-latest
permissions:
contents: write
@@ -31,6 +31,10 @@ jobs:
with:
fetch-depth: 0
- name: Set lowercase repo name
id: repo
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
- name: Generate version tag
id: version
run: |
@@ -49,26 +53,26 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/app
tags: |
type=raw,value=${{ steps.version.outputs.tag }}
type=raw,value=latest,enable={{is_default_branch}}
- name: Find PR number
id: find_pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR_NUMBER=$(gh pr list --search "${{ github.sha }}" --state merged --json number --jq '.[0].number')
if [ -z "$PR_NUMBER" ]; then
echo "No merged PR found for this commit. Falling back to 'candidate' tag."
PR_NUMBER="candidate"
else
PR_NUMBER="pr-${PR_NUMBER}"
fi
echo "tag=${PR_NUMBER}" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./client
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VITE_BUILD=v${{ steps.version.outputs.tag }}
- name: Promote Docker image
run: |
docker buildx imagetools create \
--tag ghcr.io/${{ steps.repo.outputs.name }}/app:latest \
--tag ghcr.io/${{ steps.repo.outputs.name }}/app:${{ steps.version.outputs.tag }} \
ghcr.io/${{ steps.repo.outputs.name }}/app:${{ steps.find_pr.outputs.tag }}
- name: Create and push Git tag
run: |
+129
View File
@@ -0,0 +1,129 @@
name: Pull Request CI-Backend
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
branches:
- 'main'
paths:
- 'server/**/*.java'
- 'server/**/*.xml'
- 'server/pom.xml'
- '.github/workflows/server-ci.yml'
jobs:
run-checks:
name: Checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
cache-dependency-path: 'server/pom.xml'
- name: Run Check Style
working-directory: ./server
run: ./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
- name: Run build
working-directory: ./server
run: ./mvnw --no-transfer-progress clean compile -DskipTests
- name: Run tests
working-directory: ./server
run: ./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
build-and-push:
name: Build & Push
runs-on: ubuntu-latest
needs: ["run-checks"]
permissions:
contents: read
deployments: write
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set lowercase repo name
id: repo
run: echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
cache-dependency-path: 'server/pom.xml'
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Cache Buildpack layers
uses: actions/cache@v4
with:
path: |
~/.cache/reproducible-builds
key: ${{ runner.os }}-buildpack-${{ hashFiles('server/pom.xml') }}
restore-keys: |
${{ runner.os }}-buildpack-
- name: Build Docker image with Spring Boot
working-directory: ./server
run: |
./mvnw -Pnative -DskipTests spring-boot:build-image \
-Dspring-boot.build-image.imageName=ghcr.io/${{ steps.repo.outputs.name }}/api:latest \
-Dspring-boot.build-image.builder=paketobuildpacks/builder-jammy-tiny:latest
- name: Tag and push Docker image
run: |
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
docker tag ghcr.io/${{ steps.repo.outputs.name }}/api:latest ghcr.io/${{ steps.repo.outputs.name }}/api:pr-${{ github.event.pull_request.number }}
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:candidate
docker push ghcr.io/${{ steps.repo.outputs.name }}/api:pr-${{ github.event.pull_request.number }}
- name: Create GitHub deployment for staging
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v6
with:
script: |
const ref = context.payload.pull_request.head.sha;
const env = 'staging';
const resp = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref,
required_contexts: [],
environment: env,
description: `PR #${context.payload.pull_request.number} preview deployment`,
transient_environment: true,
auto_merge: false
});
// create a deployment status pointing to the staging URL
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: resp.data.id,
state: 'success',
environment_url: 'https://tasknote-stg.darkroasted.vps-kinghost.net'
});
+134
View File
@@ -0,0 +1,134 @@
name: Pull Request CI-Frontend
on:
workflow_dispatch:
pull_request:
types: [opened, synchronize, reopened]
branches:
- 'main'
paths:
- 'client/**/*.html'
- 'client/**/*.png'
- 'client/**/*.json'
- 'client/**/*.txt'
- 'client/**/*.ts'
- 'client/**/*.tsx'
- 'client/**/*.js'
- 'client/Dockerfile'
- 'client/Caddyfile'
- '.github/workflows/client-ci.yml'
jobs:
run-checks:
name: Checks
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
working-directory: ./client
- name: Run lint
run: npm run lint
working-directory: ./client
- name: Run build
run: npm run build
working-directory: ./client
- name: Run tests
run: npm run test:no-watch
working-directory: ./client
build-and-push:
name: Build & Push
runs-on: ubuntu-latest
needs: ["run-checks"]
permissions:
contents: write
packages: write
deployments: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/app
tags: |
type=raw,value=candidate
type=raw,value=pr-${{ github.event.pull_request.number }}
- name: Generate version tag
id: version
run: |
DATE=$(date +'%Y.%m.%d')
TAG="app-v${DATE}.${{ github.run_number }}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "Generated tag: ${TAG}"
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: ./client
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VITE_BUILD=${{ steps.version.outputs.tag }}
- name: Create GitHub deployment for staging
if: ${{ github.event_name == 'pull_request' }}
uses: actions/github-script@v6
with:
script: |
const ref = context.payload.pull_request.head.sha;
const env = 'staging';
const resp = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref,
required_contexts: [],
environment: env,
description: `PR #${context.payload.pull_request.number} preview deployment`,
transient_environment: true,
auto_merge: false
});
// create a deployment status pointing to the staging URL
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: resp.data.id,
state: 'success',
environment_url: 'https://tasknote-stg.darkroasted.vps-kinghost.net'
});
-53
View File
@@ -1,53 +0,0 @@
name: Frontend CI
on:
workflow_dispatch:
push:
branches:
- '**'
paths:
- 'client/**/*.html'
- 'client/**/*.png'
- 'client/**/*.json'
- 'client/**/*.txt'
- 'client/**/*.ts'
- 'client/**/*.tsx'
- 'client/**/*.js'
- 'client/Dockerfile'
- 'client/Caddyfile'
- '.github/workflows/client-ci.yml'
jobs:
build-and-push:
name: Frontend CI
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
working-directory: ./client
- name: Run lint
run: npm run lint
working-directory: ./client
- name: Run build
run: npm run build
working-directory: ./client
- name: Run tests
run: npm run test:no-watch
working-directory: ./client
-45
View File
@@ -1,45 +0,0 @@
name: Backend CI
on:
workflow_dispatch:
# run for all pushes, not only main
push:
branches:
- '**'
paths:
- 'server/**/*.java'
- 'server/**/*.xml'
- 'server/pom.xml'
- '.github/workflows/server-ci.yml'
jobs:
run-checks:
name: Backend CI
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '25'
cache: 'maven'
- name: Run Check Style
working-directory: ./server
run: ./mvnw --no-transfer-progress checkstyle:check -Dcheckstyle.skip=false
- name: Run build
working-directory: ./server
run: ./mvnw --no-transfer-progress clean compile -DskipTests
- name: Run tests
working-directory: ./server
run: ./mvnw --no-transfer-progress clean verify -P tests --file pom.xml
@@ -0,0 +1,35 @@
import { describe, it, expect } from 'vitest';
import { isSafeUrl } from '../../utils/UrlUtils';
describe('UrlUtils', () => {
it('should allow http:// URLs', () => {
expect(isSafeUrl('http://example.com')).toBe(true);
});
it('should allow https:// URLs', () => {
expect(isSafeUrl('https://example.com')).toBe(true);
});
it('should allow # URLs', () => {
expect(isSafeUrl('#section')).toBe(true);
});
it('should disallow javascript: URLs', () => {
expect(isSafeUrl('javascript:alert(1)')).toBe(false);
});
it('should disallow data: URLs', () => {
expect(isSafeUrl('data:text/html,<script>alert(1)</script>')).toBe(false);
});
it('should disallow empty or null URLs', () => {
expect(isSafeUrl('')).toBe(false);
expect(isSafeUrl(null)).toBe(false);
expect(isSafeUrl(undefined)).toBe(false);
});
it('should be case insensitive for protocol', () => {
expect(isSafeUrl('HTTP://example.com')).toBe(true);
expect(isSafeUrl('HTTPS://example.com')).toBe(true);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
import { env } from '../env';
const server = env.VITE_BACKEND_SERVER;
const server = env.VITE_BACKEND_SERVER || '/api';
const ApiConfig = {
+3 -2
View File
@@ -1,5 +1,6 @@
import React from 'react';
import ExternalLinkIcon from '../../assets/icons8-external-link-30.png';
import { isSafeUrl } from '../../utils/UrlUtils';
interface Props {
readonly title: string;
@@ -18,8 +19,8 @@ function NoteTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
<span className="task-title-icon">
<span className="poppins-semibold">
{props.title}
{props.noteUrl && props.noteUrl.length > 0 && (
<a href={props.noteUrl} target="_blank" rel="noreferrer" className="task-note-external-link">
{isSafeUrl(props.noteUrl) && (
<a href={props.noteUrl!} target="_blank" rel="noreferrer" className="task-note-external-link">
<img src={ExternalLinkIcon} width={20} alt="external link" />
</a>
)}
+2 -1
View File
@@ -1,5 +1,6 @@
import React from 'react';
import ExternalLinkIcon from '../../assets/icons8-external-link-30.png';
import { isSafeUrl } from '../../utils/UrlUtils';
import './style.css';
interface Props {
@@ -24,7 +25,7 @@ function TaskTitle(props: React.PropsWithChildren<Props>): React.ReactNode {
data-testid={`task-title-text-${props.title}`}
>
{props.title}
{props.taskUrl && props.taskUrl.length > 0 && (
{props.taskUrl && props.taskUrl.length > 0 && isSafeUrl(props.taskUrl[0]) && (
<a href={props.taskUrl[0]} target="_blank" rel="noreferrer" className="task-note-external-link">
<img src={ExternalLinkIcon} width={20} alt="external link" />
</a>
+14
View File
@@ -0,0 +1,14 @@
/**
* Validates if a URL is safe to be used in an <a> tag.
* Only allows http, https, and # (for internal links/placeholders).
*
* @param {string | null | undefined} url The URL to validate.
* @returns {boolean} True if the URL is safe, false otherwise.
*/
export function isSafeUrl(url: string | null | undefined): boolean {
if (!url) {
return false;
}
const safeProtocolRegex = /^(https?:\/\/|#)/i;
return safeProtocolRegex.test(url);
}
+3 -2
View File
@@ -6,6 +6,7 @@ import remarkGfm from 'remark-gfm';
import { NoteResponse } from '../../types/NoteResponse';
import api from '../../api-service/api';
import ApiConfig from '../../api-service/apiConfig';
import { isSafeUrl } from '../../utils/UrlUtils';
/**
* SharedNote component for displaying a publicly shared note.
@@ -80,9 +81,9 @@ function SharedNote(): React.ReactNode {
</Card.Header>
<Card.Body>
<Card.Title>{note.title}</Card.Title>
{note.url && (
{isSafeUrl(note.url) && (
<p>
<a href={note.url} target="_blank" rel="noopener noreferrer">
<a href={note.url!} target="_blank" rel="noopener noreferrer">
{note.url}
</a>
</p>
+2 -4
View File
@@ -11,8 +11,6 @@ services:
context: ./client
dockerfile: Dockerfile
ports: ["5000:5000"]
environment:
VITE_BACKEND_SERVER: http://localhost:8585
networks:
- tasknote-network
@@ -27,7 +25,7 @@ services:
POSTGRES_USER: tasknoteuser
POSTGRES_PASSWORD: default
POSTGRES_PORT: 5432
CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000
CORS_ALLOWED_ORIGINS: http://tasknote-web:5000, http://localhost:5000, https://flattop-depth-dropper.ngrok-free.dev
SERVER_SERVLET_CONTEXT_PATH: /
TARGET_ENV: development
SECURITY_KEY: this-is-a-very-long-security-key-for-dev
@@ -62,4 +60,4 @@ services:
networks:
tasknote-network:
driver: bridge
external: true
+24
View File
@@ -0,0 +1,24 @@
events {}
http {
server {
listen 8181;
server_name _;
location /api/ {
proxy_pass http://tasknote-api:8585/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
proxy_pass http://tasknote-web:5000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
+4
View File
@@ -0,0 +1,4 @@
#!/bin/bash
ngrok http 8181 --log=stdout > ngrok-8181.log 2>&1 &
+10
View File
@@ -0,0 +1,10 @@
#!/bin/bash
docker run -d \
--name ngrok-tasknote-proxy \
-p 127.0.0.1:8181:8181 \
-v ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro \
--restart unless-stopped \
--network tasknote-network \
nginx:stable
+2 -18
View File
@@ -1,19 +1,3 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
wrapperVersion=3.3.2
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.14/apache-maven-3.9.14-bin.zip
Vendored
+44 -8
View File
@@ -8,7 +8,7 @@
# "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
# http://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
@@ -19,7 +19,7 @@
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
@@ -105,14 +105,17 @@ trim() {
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
@@ -130,7 +133,7 @@ maven-mvnd-*bin.*)
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
@@ -227,7 +230,7 @@ if [ -n "${distributionSha256Sum-}" ]; then
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
@@ -252,8 +255,41 @@ if command -v unzip >/dev/null; then
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
+189 -149
View File
@@ -1,149 +1,189 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. 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,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+17 -10
View File
@@ -5,13 +5,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.3</version>
<version>4.0.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>br.com.tasknoteapp</groupId>
<artifactId>server</artifactId>
<version>15</version>
<version>19</version>
<name>tasknote-api</name>
<description>Java backend REST API to serve TaskNote frontend client</description>
@@ -49,6 +49,12 @@
<jacoco.output.data>${project.build.directory}/coverage-reports</jacoco.output.data>
<timestamp>${maven.build.timestamp}</timestamp>
<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss</maven.build.timestamp.format>
<failsafe.version>3.5.5</failsafe.version>
<surefire.version>3.5.5</surefire.version>
<jacoco.version>0.8.14</jacoco.version>
<checkstyle.version>3.6.0</checkstyle.version>
<springboot.version>4.0.5</springboot.version>
<jjwt.version>0.12.6</jjwt.version>
</properties>
<!-- Profiles -->
@@ -143,17 +149,17 @@
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.6</version>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.6</version>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-gson</artifactId>
<version>0.12.6</version>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
@@ -181,7 +187,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.5.5</version>
<version>${failsafe.version}</version>
<executions>
<execution>
<id>integration-tests</id>
@@ -203,7 +209,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.5</version>
<version>${surefire.version}</version>
<configuration>
<argLine>@{argLine} -Xmx1024m</argLine>
<skipTests>${skip.unit.tests}</skipTests>
@@ -215,7 +221,7 @@
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.14</version>
<version>${jacoco.version}</version>
<configuration>
<skip>${jacoco.skip}</skip>
<excludes>
@@ -324,7 +330,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<version>${checkstyle.version}</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
@@ -358,7 +364,8 @@
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${springboot.version}</version>
<configuration>
<image>
<name>ghcr.io/rmcampos/tasknote/api:latest</name>
@@ -55,6 +55,9 @@ public class UserEntity implements UserDetails {
@Column(name = "lang", nullable = true, length = 6)
private String lang;
@Column(name = "last_password_change", nullable = false)
private LocalDateTime lastPasswordChange;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
@@ -182,4 +185,12 @@ public class UserEntity implements UserDetails {
public void setLang(String lang) {
this.lang = lang;
}
public LocalDateTime getLastPasswordChange() {
return lastPasswordChange;
}
public void setLastPasswordChange(LocalDateTime lastPasswordChange) {
this.lastPasswordChange = lastPasswordChange;
}
}
@@ -1,4 +1,13 @@
package br.com.tasknoteapp.server.request;
import jakarta.validation.constraints.Pattern;
/** This record represents a note patch payload. */
public record NotePatchRequest(String title, String description, String url, String tag) {}
public record NotePatchRequest(
String title,
String description,
@Pattern(
regexp = "^(https?://.*|#.*)?$",
message = "URL must start with http://, https:// or #")
String url,
String tag) {}
@@ -1,7 +1,14 @@
package br.com.tasknoteapp.server.request;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
/** This record represents a note request to be created. */
public record NoteRequest(
@NotNull String title, @NotNull String description, String url, String tag) {}
@NotNull String title,
@NotNull String description,
@Pattern(
regexp = "^(https?://.*|#.*)?$",
message = "URL must start with http://, https:// or #")
String url,
String tag) {}
@@ -1,12 +1,18 @@
package br.com.tasknoteapp.server.request;
import jakarta.validation.constraints.Pattern;
import java.util.List;
/** This record represents a task patch payload. */
public record TaskPatchRequest(
String description,
Boolean done,
List<String> urls,
List<
@Pattern(
regexp = "^(https?://.*|#.*)?$",
message = "URL must start with http://, https:// or #")
String>
urls,
String dueDate,
Boolean highPriority,
String tag) {}
@@ -2,12 +2,18 @@ package br.com.tasknoteapp.server.request;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.util.List;
/** This record represents a task request to be created. */
public record TaskRequest(
@NotNull @NotEmpty String description,
List<String> urls,
List<
@Pattern(
regexp = "^(https?://.*|#.*)?$",
message = "URL must start with http://, https:// or #")
String>
urls,
String dueDate,
Boolean highPriority,
String tag) {}
@@ -27,6 +27,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -131,7 +132,8 @@ public class AuthService {
user.setEmail(newUser.email());
user.setPassword(passwordEncoder.encode(newUser.password()));
user.setAdmin(false);
user.setCreatedAt(LocalDateTime.now());
user.setCreatedAt(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
user.setEmailUuid(emailUuid);
user.setLang(newUser.lang());
userRepository.save(user);
@@ -332,6 +334,7 @@ public class AuthService {
}
currentUser.setPassword(passwordEncoder.encode(patchRequest.password()));
currentUser.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
shouldUpdate = true;
}
@@ -433,7 +436,8 @@ public class AuthService {
UserEntity user = userOptional.get();
user.setResetToken(resetToken);
user.setResetPasswordExpiration(LocalDateTime.now().plusHours(2L));
user.setResetPasswordExpiration(
LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS).plusHours(2L));
userRepository.save(user);
if (hasValidMailgunApiKey()) {
@@ -477,6 +481,7 @@ public class AuthService {
user.setResetToken(null);
user.setResetPasswordExpiration(null);
user.setPassword(passwordEncoder.encode(request.password()));
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
userRepository.save(user);
if (hasValidMailgunApiKey()) {
@@ -29,10 +29,10 @@ import org.springframework.web.client.RestTemplate;
@Service
public class MailgunEmailService {
private static final Logger logger = LoggerFactory.getLogger(MailgunEmailService.class);
private static final Logger logger = LoggerFactory.getLogger(MailgunEmailService.class.getName());
private final RestTemplate restTemplate;
private final String targetEnv;
private String domain;
private final String domain;
private String senderEmail;
/**
@@ -73,6 +73,8 @@ public class MailgunEmailService {
String subject = "TaskNote App confirmation email";
String link = getBaseUrl() + "/email-confirmation?identification=%s";
logger.info("New user link: {}", link);
MailgunTemplateSignUp signUpTemplate = new MailgunTemplateSignUp();
signUpTemplate.setConfirmationLink(String.format(link, user.getEmailUuid().toString()));
@@ -91,6 +93,8 @@ public class MailgunEmailService {
String subject = "TaskNote App password reset";
String link = getBaseUrl() + "/finish-reset-password?token=%s";
logger.info("Password reset link: {}", link);
MailgunTemplateResetPwd resetTemplate = new MailgunTemplateResetPwd();
resetTemplate.setResetLink(String.format(link, user.getResetToken()));
@@ -184,7 +188,10 @@ public class MailgunEmailService {
if ("development".equals(targetEnv) || Objects.isNull(targetEnv)) {
return "http://localhost:5000";
}
String stage = targetEnv.equals("stage") ? "stage." : "";
return String.format("https://%s%s", stage, domain);
String baseUrl = domain;
if (targetEnv.equals("staging")) {
baseUrl = "tasknote-stg" + domain.substring(8);
}
return String.format("https://%s", baseUrl);
}
}
@@ -51,6 +51,14 @@ class JwtServiceImpl implements JwtService {
return null;
}
private LocalDateTime extractIssuedAt(String token) {
Date date = extractClaim(token, Claims::getIssuedAt);
if (!Objects.isNull(date)) {
return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDateTime();
}
return null;
}
@Override
public String generateToken(UserEntity user) {
Map<String, Object> claims = new HashMap<>();
@@ -91,7 +99,18 @@ class JwtServiceImpl implements JwtService {
@Override
public boolean validateTokenAndUser(String token, UserDetails user) {
final String email = user.getUsername();
return !isTokenExpired(token) && email.equals(getEmailFromToken(token));
boolean basicValid = !isTokenExpired(token) && email.equals(getEmailFromToken(token));
if (basicValid && user instanceof UserEntity userEntity) {
LocalDateTime iat = extractIssuedAt(token);
if (iat != null && userEntity.getLastPasswordChange() != null) {
// Token must be issued after or at the same time as last password change
// We use isBefore to invalidate tokens issued BEFORE the change
return !iat.isBefore(userEntity.getLastPasswordChange());
}
}
return basicValid;
}
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
@@ -0,0 +1,6 @@
ALTER TABLE tasknote.users ADD COLUMN last_password_change TIMESTAMP WITHOUT TIME ZONE;
-- Initialize for existing users
UPDATE tasknote.users SET last_password_change = created_at WHERE last_password_change IS NULL;
ALTER TABLE tasknote.users ALTER COLUMN last_password_change SET NOT NULL;
@@ -276,7 +276,7 @@ class TaskControllerTest {
"""
{
"description": "Test task",
"urls": ["www.url.com"],
"urls": ["https://www.url.com"],
"highPriority": true,
"tag": "tag"
}
@@ -166,6 +166,30 @@ class JwtServiceImplTest {
assertFalse(valid);
}
@Test
void validateTokenAndUser_shouldReturnFalseIfTokenIssuedBeforeLastPasswordChange()
throws InterruptedException {
UserEntity user = new UserEntity();
user.setId(testUserId);
user.setEmail(testEmail);
user.setAdmin(false);
user.setName(testName);
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
// Token issued NOW
String token = jwtService.generateToken(user);
// Update lastPasswordChange to FUTURE (simulating a password change after token issuance)
// We wait 1 second to ensure the new timestamp is strictly after token iat (which has second
// precision)
Thread.sleep(1100);
user.setLastPasswordChange(LocalDateTime.now().truncatedTo(ChronoUnit.SECONDS));
boolean valid = jwtService.validateTokenAndUser(token, user);
assertFalse(valid, "Token issued before password change should be invalid");
}
private Claims extractClaims(String token) {
return Jwts.parser().verifyWith(getKey()).build().parseSignedClaims(token).getPayload();
}
@@ -1,6 +1,6 @@
-- Create test user
insert into users (email, password, admin, created_at, inactivated_at)
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null
insert into users (email, password, admin, created_at, inactivated_at, last_password_change)
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, current_timestamp
where not exists (select 1 from users where email = 'test@domain.com');
-- Create some tasks
@@ -1,6 +1,6 @@
-- Create test user
insert into users (email, password, admin, created_at, inactivated_at)
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null
insert into users (email, password, admin, created_at, inactivated_at, last_password_change)
select 'test@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, current_timestamp
where not exists (select 1 from users where email = 'test@domain.com');
-- Create a task
@@ -1,4 +1,4 @@
-- Create test user
insert into users (email, password, admin, created_at, inactivated_at, email_uuid, reset_token)
select 'testuuid@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, 'cc2b5506-83ed-5764-985e-611ad4ce8050', 'abc123456'
insert into users (email, password, admin, created_at, inactivated_at, email_uuid, reset_token, last_password_change)
select 'testuuid@domain.com', 'a1b2c3d4f5g6', false, current_timestamp, null, 'cc2b5506-83ed-5764-985e-611ad4ce8050', 'abc123456', current_timestamp
where not exists (select 1 from users where email = 'testuuid@domain.com');
+387
View File
@@ -0,0 +1,387 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0.0"
}
}
backend "s3" {
bucket = "tasknote-stg"
key = "kubernetes/terraform.tfstate"
region = "auto"
endpoints = { s3 = "https://d17eb09b6bce2f90e16e800bb2a6baf9.r2.cloudflarestorage.com" }
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
}
variable "db_user" {
type = string
sensitive = true
}
variable "db_password" {
type = string
sensitive = true
}
variable "db_name" {
type = string
sensitive = true
}
variable "security_key" {
type = string
sensitive = true
}
variable "mailgun_apikey" {
type = string
sensitive = true
}
variable "cors_allowed_origins" {
type = string
default = "https://tasknote-stg.darkroasted.vps-kinghost.net"
}
variable "root_log_level" {
type = string
default = "INFO"
}
variable "backend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/api:candidate"
}
variable "frontend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/app:candidate"
}
variable "deploy_version" {
type = string
default = "manual"
}
resource "kubernetes_namespace_v1" "tasknote_stg" {
metadata {
name = "tasknote-stg"
}
}
resource "kubernetes_secret_v1" "tasknote_stg_secrets" {
metadata {
name = "tasknote-stg-secrets"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
data = {
postgres_user = var.db_user
postgres_password = var.db_password
postgres_db = var.db_name
security_key = var.security_key
mailgun_apikey = var.mailgun_apikey
}
}
resource "kubernetes_persistent_volume_claim_v1" "tasknote_stg_db_data" {
metadata {
name = "postgres-data-pvc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "1Gi"
}
}
}
}
resource "kubernetes_deployment_v1" "tasknote_stg_db" {
metadata {
name = "tasknote-stg-db"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-stg-db" } }
template {
metadata { labels = { app = "tasknote-stg-db" } }
spec {
container {
image = "postgres:15.8-bookworm"
name = "postgres"
volume_mount {
name = "postgres-storage"
mount_path = "/var/lib/postgresql/data"
}
env {
name = "POSTGRES_USER"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_user"
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_password"
}
}
}
env {
name = "POSTGRES_DB"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_db"
}
}
}
port { container_port = 5432 }
}
volume {
name = "postgres-storage"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.tasknote_stg_db_data.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_stg_db_svc" {
metadata {
name = "tasknote-stg-db-svc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
selector = { app = "tasknote-stg-db" }
port { port = 5432 }
type = "ClusterIP"
}
}
resource "kubernetes_deployment_v1" "tasknote_stg_backend" {
metadata {
name = "tasknote-stg-backend"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-stg-backend" } }
template {
metadata {
labels = { app = "tasknote-stg-backend" }
annotations = {
"deploy_id" = var.deploy_version
}
}
spec {
container {
image = var.backend_image
name = "backend"
image_pull_policy = "Always"
env {
name = "POSTGRES_DB"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_db"
}
}
}
env {
name = "POSTGRES_HOST"
value = "tasknote-stg-db-svc"
}
env {
name = "POSTGRES_USER"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_user"
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "postgres_password"
}
}
}
env {
name = "POSTGRES_PORT"
value = "5432"
}
env {
name = "CORS_ALLOWED_ORIGINS"
value = var.cors_allowed_origins
}
env {
name = "SERVER_SERVLET_CONTEXT_PATH"
value = "/"
}
env {
name = "ROOT_LOG_LEVEL"
value = var.root_log_level
}
env {
name = "SECURITY_KEY"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "security_key"
}
}
}
env {
name = "TARGET_ENV"
value = "staging"
}
env {
name = "MAILGUN_APIKEY"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_stg_secrets.metadata[0].name
key = "mailgun_apikey"
}
}
}
resources {
limits = { memory = "256Mi", cpu = "500m" }
requests = { memory = "256Mi", cpu = "250m" }
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_stg_backend_svc" {
metadata {
name = "tasknote-stg-backend-svc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
selector = { app = "tasknote-stg-backend" }
port {
port = 8585
target_port = 8585
}
}
}
resource "kubernetes_deployment_v1" "tasknote_stg_frontend" {
metadata {
name = "tasknote-stg-frontend"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-stg-app" } }
template {
metadata {
labels = { app = "tasknote-stg-app" }
annotations = {
"deploy_id" = var.deploy_version
}
}
spec {
container {
image = var.frontend_image
name = "frontend"
image_pull_policy = "Always"
port { container_port = 5000 }
env {
name = "VITE_BACKEND_SERVER"
value = "https://tasknoteapi-stg.darkroasted.vps-kinghost.net"
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_stg_frontend_svc" {
metadata {
name = "tasknote-stg-frontend-svc"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
}
spec {
selector = { app = "tasknote-stg-app" }
port {
port = 5000
target_port = 5000
}
type = "ClusterIP"
}
}
# Unified Ingress for App and API
resource "kubernetes_ingress_v1" "tasknote_stg_ingress" {
metadata {
name = "tasknote-stg-ingress"
namespace = kubernetes_namespace_v1.tasknote_stg.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "traefik"
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
}
}
spec {
tls {
hosts = ["tasknote-stg.darkroasted.vps-kinghost.net", "tasknoteapi-stg.darkroasted.vps-kinghost.net"]
secret_name = "tasknote-stg-tls-certs"
}
rule {
host = "tasknote-stg.darkroasted.vps-kinghost.net"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.tasknote_stg_frontend_svc.metadata[0].name
port { number = 5000 }
}
}
}
}
}
rule {
host = "tasknoteapi-stg.darkroasted.vps-kinghost.net"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.tasknote_stg_backend_svc.metadata[0].name
port { number = 8585 }
}
}
}
}
}
}
}
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
CURRENT_DIR=$(pwd)
if [ ! -f "$CURRENT_DIR/pom.xml" ]; then
cd "$CURRENT_DIR/server"
fi
#
# Get latest version of the `maven-failsafe-plugin` plugin
#
LATEST_FAILSAFE=$(
curl -s https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-failsafe-plugin/maven-metadata.xml \
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
| sed -E 's/<\/?version>//g' \
| sort -V \
| tail -n 1
)
# Get current version from pom.xml, reading from the `failsafe.version` property
CURRENT_FAILSAFE=$(./mvnw help:evaluate -Dexpression=failsafe.version -q -DforceStdout 2>/dev/null)
if [ "$LATEST_FAILSAFE" != "$CURRENT_FAILSAFE" ]; then
echo "The maven-failsafe-plugin is outdated. Current version: $CURRENT_FAILSAFE, Latest version: $LATEST_FAILSAFE"
exit 1
else
echo "The maven-failsafe-plugin is up to date. Current version: $CURRENT_FAILSAFE"
fi
#
# Get latest version of the `maven-surefire-plugin` plugin
#
LATEST_SUREFIRE=$(
curl -s https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-surefire-plugin/maven-metadata.xml \
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
| sed -E 's/<\/?version>//g' \
| sort -V \
| tail -n 1
)
# Get current version from pom.xml, reading from the `surefire.version` property
CURRENT_SUREFIRE=$(./mvnw help:evaluate -Dexpression=surefire.version -q -DforceStdout 2>/dev/null)
if [ "$LATEST_SUREFIRE" != "$CURRENT_SUREFIRE" ]; then
echo "The maven-surefire-plugin is outdated. Current version: $CURRENT_SUREFIRE, Latest version: $LATEST_SUREFIRE"
exit 1
else
echo "The maven-surefire-plugin is up to date. Current version: $CURRENT_SUREFIRE"
fi
#
# Get latest version of the `maven-jacoco-plugin` plugin
#
LATEST_JACOCO=$(
curl -s https://repo1.maven.org/maven2/org/jacoco/jacoco-maven-plugin/maven-metadata.xml \
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
| sed -E 's/<\/?version>//g' \
| sort -V \
| tail -n 1
)
# Get current version from pom.xml, reading from the `jacoco.version` property
CURRENT_JACOCO=$(./mvnw help:evaluate -Dexpression=jacoco.version -q -DforceStdout 2>/dev/null)
if [ "$LATEST_JACOCO" != "$CURRENT_JACOCO" ]; then
echo "The maven-jacoco-plugin is outdated. Current version: $CURRENT_JACOCO, Latest version: $LATEST_JACOCO"
exit 1
else
echo "The maven-jacoco-plugin is up to date. Current version: $CURRENT_JACOCO"
fi
# --
#
# Get latest version of the `maven-checkstyle-plugin` plugin
#
LATEST_CHECKSTYLE=$(
curl -s https://repo1.maven.org/maven2/org/apache/maven/plugins/maven-checkstyle-plugin/maven-metadata.xml \
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
| sed -E 's/<\/?version>//g' \
| sort -V \
| tail -n 1
)
# Get current version from pom.xml, reading from the `checkstyle.version` property
CURRENT_CHECKSTYLE=$(./mvnw help:evaluate -Dexpression=checkstyle.version -q -DforceStdout 2> /dev/null)
if [ "$LATEST_CHECKSTYLE" != "$CURRENT_CHECKSTYLE" ]; then
echo "The maven-checksytle-plugin is outdated. Current version: $CURRENT_CHECKSTYLE, Latest version: $LATEST_CHECKSTYLE"
exit 1
else
echo "The maven-cehckstyle-plugin is up to date. Current version: $CURRENT_CHECKSTYLE"
fi
# --
#
# Get latest version of the Spring Boot Starter Web plugin
#
LATEST_SPRINGBOOT=$(
curl -s https://repo1.maven.org/maven2/org/springframework/boot/spring-boot-maven-plugin/maven-metadata.xml \
| grep -oE '<version>[0-9]+\.[0-9]+(\.[0-9]+)?</version>' \
| sed -E 's/<\/?version>//g' \
| sort -V \
| tail -n 1
)
# Get current version from pom.xml, reading from the `springboot.version` property
CURRENT_SPRINGBOOT=$(./mvnw help:evaluate -Dexpression=springboot.version -q -DforceStdout 2>/dev/null)
if [ "$LATEST_SPRINGBOOT" != "$CURRENT_SPRINGBOOT" ]; then
echo "Spring Boot is outdated. Current version: $CURRENT_SPRINGBOOT, Latest version: $LATEST_SPRINGBOOT"
exit 1
else
echo "Spring Boot is up to date. Current version: $CURRENT_SPRINGBOOT"
fi