Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ TZ=UTC
PORT=3000
NODE_ENV=development

# Optional: CORS (used by Application). Comma-separated origins, or * for all.
CORS_ORIGINS=*
CORS_METHODS=HEAD,GET,POST,PUT,PATCH,DELETE
CORS_ALLOWED_HEADERS=Content-Type,Authorization

STORAGE_PATH=./uploads

MYSQL_DATABASE=herbario_dev
Expand Down
47 changes: 44 additions & 3 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Run checks
run: yarn lint

test:
unit-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
Expand All @@ -29,8 +29,8 @@ jobs:
node-version: 'lts/jod'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run tests with coverage
run: yarn test --silent --coverage
- name: Run unit tests with coverage
run: yarn test:coverage
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
Expand All @@ -39,6 +39,47 @@ jobs:
retention-days: 7
overwrite: true

integration-test:
runs-on: ubuntu-latest
continue-on-error: true
env:
PG_HOST: localhost
PG_PORT: 5432
PG_DATABASE: herbario_test
PG_USERNAME: postgres
PG_PASSWORD: testpassword
PG_MIGRATION_USERNAME: postgres
PG_MIGRATION_PASSWORD: testpassword
services:
postgres:
image: postgis/postgis:18-3.6
env:
POSTGRES_DB: herbario_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpassword
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup environment
uses: actions/setup-node@v4
with:
node-version: 'lts/jod'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Apply base schema
run: psql -h $PG_HOST -p $PG_PORT -U $PG_MIGRATION_USERNAME -d $PG_DATABASE -f test/integration/setup/schema.sql
env:
PGPASSWORD: ${{ env.PG_MIGRATION_PASSWORD }}
- name: Run integration tests
run: yarn test:integration

build:
runs-on: ubuntu-latest
steps:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ reports/data/settings

*.log
npm-debug.log

NOTES.md
2 changes: 1 addition & 1 deletion .husky/pre-push
Original file line number Diff line number Diff line change
@@ -1 +1 @@
npm run test --silent
npm run test:unit -- --silent
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,9 @@ USER hcf_api

EXPOSE $PORT

CMD node dist/index.js
HEALTHCHECK \
--interval=30s --timeout=5s \
--start-period=40s --retries=3 \
CMD wget -qO- http://127.0.0.1:$PORT/health || exit 1

CMD node dist/application/index.js
32 changes: 16 additions & 16 deletions build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,20 @@ rmSync('dist', { recursive: true, force: true })
const assets = globSync('src/**/*.{ejs,html,ttf,css}')

await build({
entryPoints: [
...assets,
'src/index.js',
],
outdir: 'dist',
bundle: true,
minify: true,
platform: 'node',
target: 'node20',
packages: 'external',
loader: {
'.ejs': 'copy',
'.html': 'copy',
'.ttf': 'copy',
'.css': 'copy',
}
entryPoints: [
...assets,
'src/application/index.js',
],
outdir: 'dist',
bundle: true,
minify: true,
platform: 'node',
target: 'node20',
packages: 'external',
loader: {
'.ejs': 'copy',
'.html': 'copy',
'.ttf': 'copy',
'.css': 'copy',
}
})
14 changes: 14 additions & 0 deletions compose.integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
postgres:
image: postgis/postgis:18-3.6
container_name: herbario_postgresql_e2e
environment:
POSTGRES_DB: herbario_e2e
POSTGRES_USER: postgres
POSTGRES_PASSWORD: testpassword
ports:
- "5433:5432"
volumes:
- ./test/integration/setup/schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro
tmpfs:
- /var/lib/postgresql
16 changes: 16 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ export default defineConfig([
'@stylistic/object-property-newline': [
'error',
{ allowAllPropertiesOnSameLine: true }
],
'@stylistic/operator-linebreak': [
'warn',
'before',
{ overrides: { '=': 'after' } }
],
'@stylistic/quote-props': [
'error',
'as-needed'
],
'no-restricted-syntax': [
'warn',
{
message: 'Use EnumOf pattern instead of TypeScript enums.',
selector: 'TSEnumDeclaration'
}
]
}
},
Expand Down
18 changes: 13 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "hcf-api",
"version": "0.1.1",
"description": "Projeto para construção da API para o sistema do Herbário da UTFPR",
"main": "dist/app/index.js",
"main": "dist/application/index.js",
"author": "Elaine Sangali <e.nani@gmail.com>",
"license": "MIT",
"private": true,
Expand All @@ -15,10 +15,14 @@
"tsc:check": "tsc --noEmit",
"lint:eslint": "eslint --cache",
"lint": "run-p tsc:check lint:eslint",
"start": "tsx --watch --env-file=.env src/index.js",
"start": "tsx --watch --env-file=.env src/application/index.js",
"build": "node build.mjs",
"test": "vitest --run",
"test:coverage": "vitest --run --coverage",
"test:unit": "vitest --run --project unit",
"test:unit:watch": "vitest --project unit",
"test:integration": "vitest --run --project integration",
"test:integration:watch": "vitest --project integration",
"test:coverage": "vitest --run --project unit --coverage",
"prepare": "husky",
"audit": "npm audit --audit-level=moderate",
"audit:fix": "npm audit fix"
Expand Down Expand Up @@ -62,10 +66,14 @@
"devDependencies": {
"@eslint/js": "9.39.1",
"@stylistic/eslint-plugin": "5.5.0",
"@types/cors": "2.8.19",
"@types/express": "5.0.5",
"@types/morgan": "1.9.10",
"@types/pg": "^8.16.0",
"@types/react": "19.2.2",
"@types/react-dom": "19.2.2",
"@types/supertest": "7.2.0",
"@types/swagger-ui-express": "^4.1.8",
"@vitest/coverage-v8": "4.0.7",
"chai": "6.2.0",
"chai-http": "^5.1.2",
Expand All @@ -77,9 +85,9 @@
"husky": "9.1.7",
"npm-run-all": "4.1.5",
"tsx": "4.21.0",
"supertest": "7.2.2",
"typescript": "5.9.3",
"typescript-eslint": "8.46.3",
"vitest": "4.0.7"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
}
16 changes: 14 additions & 2 deletions script/database-backup/.env.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
DATABASE_HOST=10.0.10.80
DATABASE_HOST=127.0.0.1
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=masterkey
DATABASE_NAME=herbario_prod
DATABASE_NAME=herbario_dev

# Google Drive (Dados reais que você gerou)
GDRIVE_TOKEN={"access_token":"SEU_ACCESS_TOKEN_AQUI","token_type":"Bearer","refresh_token":"SEU_REFRESH_TOKEN_AQUI","expiry":"2026-01-01T00:00:00Z"}
Expand All @@ -14,3 +14,15 @@ RETENTION_DAILY=5
RETENTION_WEEKLY=4
CRON_SCHEDULE="0 2 * * *"
TZ=America/Sao_Paulo

# SMTP (notificação de falha do backup)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER="seu-email@gmail.com"
SMTP_PASS="sua-senha-de-app-de-16-digitos"
SMTP_FROM="Sistema HCF <no-reply@hcf.com>"

# Destinatários do alerta (1 ou mais, separados por vírgula)
NOTIFY_EMAIL_TO=ops@exemplo.com,curadoria@exemplo.com
# Desliga notificação sem remover config (default: true)
NOTIFY_EMAIL_ENABLED=true
4 changes: 2 additions & 2 deletions script/database-backup/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ RUN \
curl \
ca-certificates \
rclone \
gzip && \
gzip \
msmtp && \
curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql.gpg && \
echo "deb [signed-by=/usr/share/keyrings/postgresql.gpg] https://apt.postgresql.org/pub/repos/apt bookworm-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y \
postgresql-client-18 && \
apt-get remove -y \
curl \
ca-certificates \
gnupg && \
rm -rf /var/lib/apt/lists/*

Expand Down
19 changes: 18 additions & 1 deletion script/database-backup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Arquitetura:

Fluxo:

`cron -> backup.sh -> pg_dump -Fp -> gzip -> rclone copy -> retenção`
`cron -> backup.sh -> pg_dump -Fp -> gzip -> rclone copy -> retenção (-> notificação por e-mail em caso de falha)`

## Formato do backup

Expand All @@ -37,6 +37,23 @@ Veja `.env.example` para o contrato completo.
- `RETENTION_WEEKLY` (default: `4`)
- `CRON_SCHEDULE` (ex.: `0 2 * * *`)
- `TZ` (ex.: `America/Sao_Paulo`)
- `SMTP_HOST`
- `SMTP_PORT`
- `SMTP_USER`
- `SMTP_PASS`
- `SMTP_FROM`
- `NOTIFY_EMAIL_TO` (1 ou mais e-mails separados por vírgula)
- `NOTIFY_EMAIL_ENABLED` (default: `true`)

## Notificação de falha

O container instala e configura o cliente `msmtp` (via `/etc/msmtprc`, gerado no `entrypoint.sh` a partir das variáveis `SMTP_*`).

- O `backup.sh` captura toda a sua saída em um log temporário e usa um `trap ERR` para detectar qualquer falha (`pg_dump`, `rclone`, retenção, etc.).
- Em caso de falha, um e-mail é enviado via `msmtp` para todos os endereços listados em `NOTIFY_EMAIL_TO` (separados por vírgula), com assunto, horário (`TZ`) e as últimas linhas do log.
- Em caso de sucesso, nenhum e-mail é enviado.
- `NOTIFY_EMAIL_ENABLED=false` desliga a notificação sem precisar remover as demais variáveis.
- Se o próprio envio do e-mail falhar (SMTP fora do ar, credenciais inválidas etc.), isso é apenas logado — o script continua terminando com o código de saída original da falha do backup (a notificação nunca mascara o erro real).

## Política de retenção

Expand Down
49 changes: 49 additions & 0 deletions script/database-backup/backup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,55 @@

set -euo pipefail

LOG_FILE=$(mktemp)
exec > >(tee -a "${LOG_FILE}") 2>&1

notify_failure() {
local exit_code=$1
local line_no=$2

if [ "${NOTIFY_EMAIL_ENABLED:-true}" != "true" ]; then
return 0
fi

if [ -z "${NOTIFY_EMAIL_TO:-}" ]; then
echo "NOTIFY_EMAIL_TO não configurado, pulando notificação por e-mail" >&2
return 0
fi

IFS=',' read -ra RAW_RECIPIENTS <<< "${NOTIFY_EMAIL_TO}"
local recipients=()
for r in "${RAW_RECIPIENTS[@]}"; do
r="$(echo -n "$r" | xargs)"
[ -n "$r" ] && recipients+=("$r")
done

if [ "${#recipients[@]}" -eq 0 ]; then
echo "Nenhum destinatário válido em NOTIFY_EMAIL_TO" >&2
return 0
fi

local subject="[FALHA] Backup do banco ${DATABASE_NAME} - $(date '+%Y-%m-%d %H:%M:%S') (${TZ:-UTC})"

{
echo "Subject: ${subject}"
echo "From: ${SMTP_FROM}"
echo "To: $(IFS=,; echo "${recipients[*]}")"
echo "Content-Type: text/plain; charset=UTF-8"
echo
echo "Falha no backup do banco: ${DATABASE_NAME}"
echo "Horário da tentativa: $(date '+%Y-%m-%d %H:%M:%S') (${TZ:-UTC})"
echo "Código de saída: ${exit_code} (linha ${line_no})"
echo
echo "Últimas linhas do log:"
tail -n 50 "${LOG_FILE}" 2>/dev/null || echo "(log indisponível)"
} | msmtp "${recipients[@]}" || echo "Falha ao enviar e-mail de notificação via msmtp" >&2

return 0
}

trap 'notify_failure $? $LINENO' ERR

TIMESTAMP=$(date +%s)
FILE_NAME="${DATABASE_NAME}_${TIMESTAMP}.sql.gz"
TMP_FILE="/tmp/${FILE_NAME}"
Expand Down
11 changes: 11 additions & 0 deletions script/database-backup/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ services:
image: database-backup:local
container_name: database-backup
build: .
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
# PostgreSQL
DATABASE_HOST: "${DATABASE_HOST}"
Expand All @@ -20,3 +22,12 @@ services:

CRON_SCHEDULE: "${CRON_SCHEDULE}"
TZ: "${TZ}"

# SMTP / Notificações
SMTP_HOST: "${SMTP_HOST}"
SMTP_PORT: "${SMTP_PORT}"
SMTP_USER: "${SMTP_USER}"
SMTP_PASS: "${SMTP_PASS}"
SMTP_FROM: "${SMTP_FROM}"
NOTIFY_EMAIL_TO: "${NOTIFY_EMAIL_TO}"
NOTIFY_EMAIL_ENABLED: "${NOTIFY_EMAIL_ENABLED}"
Loading
Loading