Compare commits

..

2 Commits

Author SHA1 Message Date
6768946b0a test: improve test coverage
Some checks failed
Publish Docker Images / build-and-publish (push) Has been cancelled
2025-11-15 23:18:16 +01:00
1e769f0b39 feat: send confirmation email to sender
When users submit a contact form, they now receive a confirmation
email acknowlledging receipt of their message. The backend also
continues to send a notification email to the configured recipient.

If the backend fails to send the acknowledgement email to the sender,
it will assume the email is not valid and will therefore not transmit
the contact request to the configured recipient.

Changes:
- Refactor `send_email()` to `send_emails()` that sends two emails:
  - Confirmation email from the submitter
  - Notification email to the configured recipient
- Add `From<T>` implementations of various errors for new error type
  `ContactError`.
- Errors now return a translation identifier for the frontend.
2025-11-15 23:03:26 +01:00
11 changed files with 137 additions and 201 deletions

View File

@@ -7,7 +7,7 @@ The `publish-docker.yml` workflow automatically builds and publishes Docker imag
### Triggers and Tagging Strategy
| Event | Condition | Published Tags | Example |
|--------------|-----------------------------|------------------------|-------------------|
|--------------+-----------------------------+------------------------+-------------------|
| Tag push | Tag pushed to `main` branch | `latest` + version tag | `latest`, `1.0.0` |
| Branch push | Push to `develop` branch | `develop` | `develop` |
| Pull request | PR opened or updated | `pr<number>` | `pr12` |
@@ -18,7 +18,7 @@ The `publish-docker.yml` workflow automatically builds and publishes Docker imag
Configure these secrets in your repository settings (`Settings``Secrets and variables``Actions`):
| Secret Name | Description | Example Value |
|---------------------|---------------------------------------------|-----------------------------------------|
|---------------------+---------------------------------------------+-----------------------------------------|
| `DOCKER_USERNAME` | Username for Docker registry authentication | `phundrak` |
| `DOCKER_PASSWORD` | Password or token for Docker registry | Personal Access Token (PAT) or password |
| `CACHIX_AUTH_TOKEN` | (Optional) Token for Cachix caching | Your Cachix auth token |
@@ -84,7 +84,7 @@ Cachix is a Nix binary cache that dramatically speeds up builds by caching build
Configure these in the workflow's `env` section or as repository variables:
| Variable | Description | Default Value | Example |
|--------------------|------------------------------------------------|---------------|--------------------|
|--------------------+------------------------------------------------+---------------+--------------------|
| `CACHIX_NAME` | Name of the Cachix cache to use | `devenv` | `phundrak-dot-com` |
| `CACHIX_SKIP_PUSH` | Whether to skip pushing artifacts to the cache | `true` | `false` |

View File

@@ -12,45 +12,12 @@ on:
env:
CACHIX_NAME: devenv
CACHIX_SKIP_PUSH: true
DOCKER_REGISTRY: labs.phundrak.com # Override in repository settings if needed
IMAGE_NAME: phundrak/phundrak-dot-com-backend
jobs:
coverage-and-sonar:
runs-on: ubuntu-latest
permissions:
content: read
pull-requests: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Nix
uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
- name: Setup Cachix
uses: cachix/cachix-action@v15
with:
name: '${{ env.CACHIX_NAME }}'
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
skipPush: ${{ github.event_name == 'pull_request' }}
- name: Coverage
run: |
nix develop --no-pure-eval --accept-flake-config --command just coverage
- name: Sonar analysis
uses: SonarSource/sonarqube-scan-action@v6
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
build-docker:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: read
@@ -71,125 +38,85 @@ jobs:
with:
name: '${{ env.CACHIX_NAME }}'
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
skipPush: ${{ github.event_name == 'pull_request' }}
skipPush: ${{ env.CACHIX_SKIP_PUSH }}
- name: Build Docker image with Nix
run: |
echo "Building Docker image..."
nix build .#backendDockerLatest --accept-flake-config
cp -L result docker-image.tar.gz
- name: Upload Docker image artifact
uses: actions/upload-artifact@v3
with:
name: docker-image
path: docker-image.tar.gz
retention-days: 1
push-docker:
needs: [coverage-and-sonar, build-docker]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Required for pushing to Phundrak Labs registry
steps:
- name: Download Docker image artifact
uses: actions/download-artifact@v3
with:
name: docker-image
- name: Load Docker image
run: |
echo "Loading Docker image into Docker daemon..."
docker load < docker-image.tar.gz
docker load < result
- name: Push Docker tags
id: push
uses: https://labs.phundrak.com/phundrak/docker-push-action@v1
with:
registry: ${{ env.DOCKER_REGISTRY }}
registry-username: ${{ secrets.DOCKER_USERNAME }}
registry-password: ${{ secrets.DOCKER_PASSWORD }}
image-name: ${{ env.IMAGE_NAME }}
local-image-name: phundrak/phundrak-dot-com-backend:latest
event-name: ${{ github.event_name }}
ref: ${{ github.ref }}
ref-type: ${{ github.ref_type }}
ref-name: ${{ github.ref_name }}
pr-number: ${{ github.event.pull_request.number }}
# - name: Log in to Docker Registry
# run: |
# echo "${{ secrets.DOCKER_PASSWORD }}" | docker login ${{ env.DOCKER_REGISTRY }} -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
- name: Log in to Docker Registry
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login ${{ env.DOCKER_REGISTRY }} -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
# - name: Determine tags and push images
# run: |
# set -euo pipefail
- name: Determine tags and push images
run: |
set -euo pipefail
# REGISTRY="${{ env.DOCKER_REGISTRY }}"
# IMAGE_NAME="${{ env.IMAGE_NAME }}"
REGISTRY="${{ env.DOCKER_REGISTRY }}"
IMAGE_NAME="${{ env.IMAGE_NAME }}"
# # The locally built image from Nix (name comes from Cargo.toml package.name)
# LOCAL_IMAGE="phundrak/phundrak-dot-com-backend:latest"
# The locally built image from Nix (name comes from Cargo.toml package.name)
LOCAL_IMAGE="phundrak/phundrak-dot-com-backend:latest"
# echo "Event: ${{ github.event_name }}"
# echo "Ref: ${{ github.ref }}"
# echo "Ref type: ${{ github.ref_type }}"
echo "Event: ${{ github.event_name }}"
echo "Ref: ${{ github.ref }}"
echo "Ref type: ${{ github.ref_type }}"
# # Determine which tags to push based on the event
# if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" ]]; then
# # Tag push on main branch → publish 'latest' and versioned tag
# echo "Tag push detected"
# TAG_VERSION="${{ github.ref_name }}"
# # Remove 'v' prefix if present (v1.0.0 → 1.0.0)
# TAG_VERSION="${TAG_VERSION#v}"
# Determine which tags to push based on the event
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref_type }}" == "tag" ]]; then
# Tag push on main branch → publish 'latest' and versioned tag
echo "Tag push detected"
TAG_VERSION="${{ github.ref_name }}"
# Remove 'v' prefix if present (v1.0.0 → 1.0.0)
TAG_VERSION="${TAG_VERSION#v}"
# echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:latest"
# docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
# docker push "${REGISTRY}/${IMAGE_NAME}:latest"
echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:latest"
docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
# echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}"
# docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}"
# docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}"
echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}"
docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}"
docker push "${REGISTRY}/${IMAGE_NAME}:${TAG_VERSION}"
# elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/develop" ]]; then
# # Push on develop branch → publish 'develop' tag
# echo "Push to develop branch detected"
elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/develop" ]]; then
# Push on develop branch → publish 'develop' tag
echo "Push to develop branch detected"
# echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:develop"
# docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:develop"
# docker push "${REGISTRY}/${IMAGE_NAME}:develop"
echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:develop"
docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:develop"
docker push "${REGISTRY}/${IMAGE_NAME}:develop"
# elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
# # Pull request → publish 'pr<number>' tag
# echo "Pull request detected"
# PR_NUMBER="${{ github.event.pull_request.number }}"
elif [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Pull request → publish 'pr<number>' tag
echo "Pull request detected"
PR_NUMBER="${{ github.event.pull_request.number }}"
# echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}"
# docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}"
# docker push "${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}"
echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}"
docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}"
docker push "${REGISTRY}/${IMAGE_NAME}:pr${PR_NUMBER}"
# elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
# # Push to main branch (not a tag) → publish 'latest'
# echo "Push to main branch detected"
elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
# Push to main branch (not a tag) → publish 'latest'
echo "Push to main branch detected"
# echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:latest"
# docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
# docker push "${REGISTRY}/${IMAGE_NAME}:latest"
echo "Tagging and pushing: ${REGISTRY}/${IMAGE_NAME}:latest"
docker tag "${LOCAL_IMAGE}" "${REGISTRY}/${IMAGE_NAME}:latest"
docker push "${REGISTRY}/${IMAGE_NAME}:latest"
# else
# echo "Unknown event or ref, skipping push"
# exit 1
# fi
else
echo "Unknown event or ref, skipping push"
exit 1
fi
# - name: Log out from Docker Registry
# if: always()
# run: docker logout ${{ env.DOCKER_REGISTRY }}
- name: Delete Docker image artifact
uses: geekyeggo/delete-artifact@v2
with:
name: docker-image
- name: Log out from Docker Registry
if: always()
run: docker logout ${{ env.DOCKER_REGISTRY }}
- name: Image published successfully
run: |

40
Cargo.lock generated
View File

@@ -134,26 +134,6 @@ dependencies = [
"fs_extra",
]
[[package]]
name = "bakit"
version = "0.1.0"
dependencies = [
"chrono",
"config",
"dotenvy",
"governor",
"lettre",
"poem",
"poem-openapi",
"serde",
"serde_json",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"validator",
]
[[package]]
name = "base64"
version = "0.21.7"
@@ -1593,6 +1573,26 @@ dependencies = [
"sha2",
]
[[package]]
name = "phundrak-dot-com-backend"
version = "0.1.0"
dependencies = [
"chrono",
"config",
"dotenvy",
"governor",
"lettre",
"poem",
"poem-openapi",
"serde",
"serde_json",
"thiserror",
"tokio",
"tracing",
"tracing-subscriber",
"validator",
]
[[package]]
name = "pin-project"
version = "0.4.30"

View File

@@ -1,5 +1,5 @@
[package]
name = "bakit"
name = "phundrak-dot-com-backend"
version = "0.1.0"
edition = "2024"
publish = false
@@ -11,7 +11,7 @@ path = "src/lib.rs"
[[bin]]
path = "src/main.rs"
name = "bakit"
name = "phundrak-dot-com-backend"
[dependencies]
chrono = { version = "0.4.42", features = ["serde"] }

View File

@@ -1,30 +1,44 @@
---
include_toc: true
gitea: none
---
# phundrak.com Backend
<h1 align="center">Bakit</h1>
<div align="center">
<strong>
A backend for my personal website
</strong>
</div>
<br/>
<!--toc:start-->
- [phundrak.com Backend](#phundrakcom-backend)
- [Features](#features)
- [API Endpoints](#api-endpoints)
- [Configuration](#configuration)
- [Configuration Example](#configuration-example)
- [Rate Limiting](#rate-limiting)
- [Development](#development)
- [Prerequisites](#prerequisites)
- [Running the Server](#running-the-server)
- [Building](#building)
- [Testing](#testing)
- [Testing Notes](#testing-notes)
- [Code Quality](#code-quality)
- [Linting](#linting)
- [Continuous Checking with Bacon](#continuous-checking-with-bacon)
- [Code Style](#code-style)
- [Error Handling](#error-handling)
- [Logging](#logging)
- [Imports](#imports)
- [Testing Conventions](#testing-conventions)
- [Project Structure](#project-structure)
- [Architecture](#architecture)
- [Application Initialization Flow](#application-initialization-flow)
- [Email Handling](#email-handling)
- [Docker Deployment](#docker-deployment)
- [Using Pre-built Images](#using-pre-built-images)
- [Available Image Tags](#available-image-tags)
- [Building Images Locally](#building-images-locally)
- [Docker Compose Example](#docker-compose-example)
- [CI/CD Pipeline](#cicd-pipeline)
- [Automated Docker Publishing](#automated-docker-publishing)
- [Workflow Details](#workflow-details)
- [Registry Configuration](#registry-configuration)
- [Required Secrets](#required-secrets)
- [License](#license)
<!--toc:end-->
<div align="center">
<a href="https://sonar.phundrak.com/dashboard?id=bakit" target="_blank">
<img src="https://sonar.phundrak.com/api/project_badges/measure?project=bakit&metric=coverage&token=sqb_bda24bf36825576d6c6b76048044e103339c3c5f" alt="Sonar Coverage" />
</a>
<a href="https://sonar.phundrak.com/dashboard?id=bakit" target="_blank">
<img src="https://sonar.phundrak.com/api/project_badges/measure?project=bakit&metric=alert_status&token=sqb_bda24bf36825576d6c6b76048044e103339c3c5f" alt="Sonar Quality Gate Status" />
</a>
<a href="#license">
<img src="https://img.shields.io/badge/License-AGPL--3.0--only-blue" alt="License" />
</a>
<a href="https://www.gnu.org/software/emacs/" target="_blank">
<img src="https://img.shields.io/badge/Made%20with-GNU%2FEmacs-blueviolet.svg?logo=GNU%20Emacs&logoColor=white" alt="Made with GNU/Emacs" />
</a>
</div>
The backend for [phundrak.com](https://phundrak.com), built with Rust and the [Poem](https://github.com/poem-web/poem) web framework.
## Features
@@ -141,14 +155,14 @@ For optimized production builds:
cargo build --release
```
The compiled binary will be at `target/release/bakit`.
The compiled binary will be at `target/release/backend`.
**With Nix:**
Build the backend binary:
```bash
nix build .#backend
# Binary available at: ./result/bin/bakit
# Binary available at: ./result/bin/backend
```
Build Docker images:
@@ -161,7 +175,7 @@ nix build .#backendDockerLatest
# Load into Docker
docker load < result
# Image will be available as: localhost/phundrak/bakit:latest
# Image will be available as: localhost/phundrak/backend-rust:latest
```
The Nix build ensures reproducible builds with all dependencies pinned.
@@ -202,7 +216,6 @@ just coverage
- Tests use `get_test_app()` helper for consistent test setup
- Telemetry is automatically disabled during tests
- Tests are organized in `#[cfg(test)]` modules within each file
- Email sending is tested using lettre's `StubTransport` for mocking SMTP operations
## Code Quality
@@ -281,15 +294,12 @@ backend/
│ ├── startup.rs # Application builder, server setup
│ ├── settings.rs # Configuration management
│ ├── telemetry.rs # Logging and tracing setup
│ ├── errors.rs # Error type re-exports
│ ├── middleware/ # Custom middleware
│ │ ├── mod.rs # Middleware module
│ │ └── rate_limit.rs # Rate limiting middleware
│ └── route/ # API route handlers
│ ├── mod.rs # Route organization
│ ├── contact/ # Contact form module
│ │ ├── mod.rs # Contact form endpoint
│ │ └── errors.rs # Contact form error types
│ ├── contact.rs # Contact form endpoint
│ ├── health.rs # Health check endpoint
│ └── meta.rs # Metadata endpoint
├── settings/ # Configuration files
@@ -336,7 +346,7 @@ Docker images are automatically built and published via GitHub Actions to the co
Pull and run the latest image:
```bash
# Pull from Phundrak Labs (labs.phundrak.com)
docker pull labs.phundrak.com/phundrak/bakit:latest
docker pull labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
# Run the container
docker run -d \
@@ -349,7 +359,7 @@ docker run -d \
-e APP__EMAIL__PASSWORD=your_password \
-e APP__EMAIL__FROM="Contact Form <noreply@example.com>" \
-e APP__EMAIL__RECIPIENT="Admin <admin@example.com>" \
labs.phundrak.com/phundrak/bakit:latest
labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
```
### Available Image Tags
@@ -367,7 +377,7 @@ Build with Nix (recommended for reproducibility):
```bash
nix build .#backendDockerLatest
docker load < result
docker run -p 3100:3100 localhost/phundrak/bakit:latest
docker run -p 3100:3100 localhost/phundrak/backend-rust:latest
```
Build with Docker directly:
@@ -383,7 +393,7 @@ version: '3.8'
services:
backend:
image: labs.phundrak.com/phundrak/bakit:latest
image: labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
ports:
- "3100:3100"
environment:
@@ -435,7 +445,7 @@ To use the published images, authenticate with the registry:
echo $GITHUB_TOKEN | docker login labs.phundrak.com -u USERNAME --password-stdin
# Pull the image
docker pull labs.phundrak.com/phundrak/bakit:latest
docker pull labs.phundrak.com/phundrak/phundrak-dot-com-backend:latest
```
### Required Secrets
@@ -449,4 +459,4 @@ See [.github/workflows/README.md](./.github/workflows/README.md) for detailed se
## License
AGPL-3.0-only - See [LICENSE.md](./LICENSE.md) for full license information.
AGPL-3.0-only - See the root repository for full license information.

View File

@@ -5,7 +5,7 @@ application:
protocol: http
host: 127.0.0.1
base_url: http://127.0.0.1:3100
name: "bakit-dev"
name: "com.phundrak.backend.dev"
email:
host: localhost

View File

@@ -2,7 +2,7 @@ debug: false
frontend_url: ""
application:
name: "bakit-prod"
name: "com.phundrak.backend.prod"
protocol: https
host: 0.0.0.0
base_url: ""

View File

@@ -1 +0,0 @@
sonar.projectKey=bakit

View File

@@ -3,5 +3,5 @@
#[cfg(not(tarpaulin_include))]
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
bakit::run(None).await
phundrak_dot_com_backend::run(None).await
}

View File

@@ -268,7 +268,7 @@ mod tests {
#[test]
fn from_validation_errors_with_name_error() {
use validator::Validate;
use validator::{Validate, ValidationError};
#[derive(Validate)]
struct TestStruct {

View File

@@ -933,7 +933,7 @@ mod tests {
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotParseSettingsEmail(_) => (),
e => panic!("Expected CouldNotParseSettingsEmail, got {e:?}"),
e => panic!("Expected CouldNotParseSettingsEmail, got {:?}", e),
}
}
@@ -964,7 +964,7 @@ mod tests {
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotParseRequestEmailAddress(_) => (),
e => panic!("Expected CouldNotParseRequestEmailAddress, got {e:?}"),
e => panic!("Expected CouldNotParseRequestEmailAddress, got {:?}", e),
}
}
@@ -996,7 +996,7 @@ mod tests {
assert!(result.is_err());
match result.unwrap_err() {
ContactError::CouldNotSendEmail(_) => (),
e => panic!("Expected CouldNotSendEmail, got {e:?}"),
e => panic!("Expected CouldNotSendEmail, got {:?}", e),
}
}
}