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
77 changes: 77 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

env:
CARGO_TERM_COLOR: always

jobs:
# ── Lint & unit tests (no database needed) ──────────────────────────────────
check:
name: Lint & Unit Tests
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy

- name: Cache Cargo registry & build
uses: Swatinem/rust-cache@v2

- name: Check formatting
run: cargo fmt --all -- --check

- name: Run Clippy
run: cargo clippy -- -D warnings

- name: Build
run: cargo build --verbose

- name: Run unit tests
run: cargo test --test unit --verbose

# ── Integration tests (requires Postgres) ───────────────────────────────────
integration:
name: Integration Tests
runs-on: ubuntu-latest
needs: check

services:
postgres:
image: postgres:15
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: zcloudpass_test
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U test"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
DATABASE_URL: postgres://test:test@localhost:5432/zcloudpass_test

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Cache Cargo registry & build
uses: Swatinem/rust-cache@v2

- name: Run integration tests
run: cargo test --test integration --verbose
67 changes: 67 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Release

on:
push:
tags:
- "v*"

env:
CARGO_TERM_COLOR: always
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}

permissions:
contents: write
packages: write

jobs:
release:
name: Build & Release
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: Cache Cargo registry & build
uses: Swatinem/rust-cache@v2

- name: Build release binary
run: cargo build --release

- name: Run tests
run: cargo test --verbose

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha

- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

- name: Upload release binary
uses: softprops/action-gh-release@v2
with:
files: target/release/zcloudpass-backend
generate_release_notes: true
40 changes: 40 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# ── Builder stage ─────────────────────────────────────────────────────────────
FROM rust:1.85-slim AS builder

WORKDIR /app

# Install build dependencies for native TLS (required by sqlx native-tls)
RUN apt-get update && \
apt-get install -y --no-install-recommends pkg-config libssl-dev && \
rm -rf /var/lib/apt/lists/*

# Copy manifests first for better layer caching
COPY Cargo.toml Cargo.lock ./

# Create a dummy main to cache dependency builds
RUN mkdir src && \
echo "fn main() {}" > src/main.rs && \
echo "" > src/lib.rs && \
cargo build --release && \
rm -rf src

# Copy actual source and build
COPY src ./src
COPY tests ./tests
RUN touch src/main.rs src/lib.rs && \
cargo build --release

# ── Runtime stage ─────────────────────────────────────────────────────────────
FROM debian:bookworm-slim

RUN apt-get update && \
apt-get install -y --no-install-recommends ca-certificates libssl3 && \
rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/zcloudpass-backend /usr/local/bin/zcloudpass-backend

ENV BIND_ADDRESS=0.0.0.0:3000

EXPOSE 3000

ENTRYPOINT ["zcloudpass-backend"]
51 changes: 51 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,54 @@ pub mod middleware;
pub struct AppState {
pub db: sqlx::PgPool,
}

/// Creates the `users` and `sessions` tables if they do not already exist.
/// Also attempts to enable the `pgcrypto` extension (best-effort).
pub async fn ensure_tables(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
match sqlx::query("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
.execute(pool)
.await
{
Ok(_) => println!("pgcrypto extension ensured"),
Err(e) => eprintln!(
"notice: could not create pgcrypto extension (continuing): {:?}. \
If you need pgcrypto functionality, create the extension as a superuser.",
e
),
}

sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(255),
email VARCHAR(255) UNIQUE NOT NULL,
srp_salt TEXT,
srp_verifier TEXT,
encrypted_vault TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
account_status VARCHAR(20) DEFAULT 'active'
)
"#,
)
.execute(pool)
.await?;

sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS sessions (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
session_token TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(pool)
.await?;

Ok(())
}
52 changes: 3 additions & 49 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ async fn main() {
.await
.expect("Failed to connect to database");

ensure_tables(&pool).await.expect("Failed to ensure tables");
zcloudpass_backend::ensure_tables(&pool)
.await
.expect("Failed to ensure tables");

let app_state = AppState { db: pool.clone() };
let shared_state = Arc::new(app_state);
Expand All @@ -38,51 +40,3 @@ async fn main() {
axum::serve(listener, app).await.unwrap();
}

async fn ensure_tables(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
match sqlx::query("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
.execute(pool)
.await
{
Ok(_) => println!("pgcrypto extension ensured"),
Err(e) => eprintln!(
"notice: could not create pgcrypto extension (continuing): {:?}. \
If you need pgcrypto functionality, create the extension as a superuser.",
e
),
}

sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username VARCHAR(255),
email VARCHAR(255) UNIQUE NOT NULL,
srp_salt TEXT,
srp_verifier TEXT,
encrypted_vault TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP,
account_status VARCHAR(20) DEFAULT 'active'
)
"#,
)
.execute(pool)
.await?;

sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS sessions (
id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
session_token TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(pool)
.await?;

Ok(())
}
Loading
Loading