Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
ce18390
beautify QR code content preview hover cards for all types
FloB95 Mar 4, 2026
2499cf8
add detailed analytics link to scan tooltip and update translations
FloB95 Mar 4, 2026
12f7ce4
improve QR code list UX: copy buttons, truncation tooltips, and styling
FloB95 Mar 4, 2026
c92b17b
improve QR code edit page: show content type icon in name field and f…
FloB95 Mar 5, 2026
0ccb9ce
update posthog-js SDK to v1.359.1
FloB95 Mar 9, 2026
5ae1dbd
fix QR code custom domain: use correct domain in all downloads, previ…
FloB95 Mar 9, 2026
6ce601d
fix export dialog: show dynamic QR code preview for codes without sto…
FloB95 Mar 9, 2026
9fc6431
fix QR code preview images: support custom images and add regeneratio…
FloB95 Mar 9, 2026
2df593e
add qrCodeData backfill phase to preview image regeneration script
FloB95 Mar 9, 2026
d7dade6
optimize preview image regeneration: parallel processing and remove c…
FloB95 Mar 9, 2026
9c02717
add Sentry Logs integration alongside Axiom for backend logging
FloB95 Mar 10, 2026
c1bafde
add IP abuse tracking and automatic blocking
FloB95 Mar 10, 2026
290b27a
add light logo variant and PNG exports
FloB95 Mar 10, 2026
46d1139
add MySQL, Redis, and MinIO service containers to CI pipeline
FloB95 Mar 10, 2026
89827f3
allow manual workflow trigger and trigger on workflow file changes
FloB95 Mar 10, 2026
c4158ca
fix MinIO: run as docker container instead of service container
FloB95 Mar 10, 2026
ae573b4
fix CI test failures: use env var for redirect loop check, mock Umami…
FloB95 Mar 10, 2026
403c7cd
fix Umami fetch mock to only intercept Umami URLs, pass through Clerk
FloB95 Mar 10, 2026
63b2521
add status assertion to createQrCode test helper for better error rep…
FloB95 Mar 10, 2026
745d2da
Merge pull request #335 from FloB95/chore/ci-backend-test-services
FloB95 Mar 10, 2026
e49961d
fix no permission requests
FloB95 Mar 10, 2026
6946393
feat: add short URL management (CRUD, dashboard, tracking, i18n)
FloB95 Mar 10, 2026
b499b12
update claude.md
FloB95 Mar 10, 2026
63e1845
feat: add product pages, navigation dropdown, and UI refinements
FloB95 Mar 11, 2026
8a911ef
fix: resolve code review issues and UI refinements
FloB95 Mar 11, 2026
622cdfc
feat: add URL validation, improve tests, and refine UI components
FloB95 Mar 12, 2026
e8184e1
feat: add tagging support for short URLs and UI refinements
FloB95 Mar 12, 2026
e62c4bd
refactoring
FloB95 Mar 12, 2026
0c6c49b
fix: resolve code review issues - tag toast i18n, page clamping, muta…
FloB95 Mar 12, 2026
80fde57
refine: polish CrossProductCards and ProductUseCases styling
FloB95 Mar 12, 2026
5d1d0e6
fix: open footer language dropdown upward via direction prop
FloB95 Mar 12, 2026
ac5bad6
refine: reorder sidebar nav — templates before short URLs
FloB95 Mar 12, 2026
659fd45
Merge pull request #336 from FloB95/feature/short-url-management
FloB95 Mar 12, 2026
4c05e46
docs: add v1.10.0 changelog for short URL management release
FloB95 Mar 12, 2026
45c6ea7
refine: use i18n-aware navigation, move user middleware to global hoo…
FloB95 Mar 12, 2026
7d0d574
refine: improve form validation, disable submit on unchanged forms, a…
FloB95 Mar 12, 2026
dc1b8c7
fix: close NameDialog on Enter key and raise dialog z-index above header
FloB95 Mar 12, 2026
f437f38
feat: add branded QR code table loader with smooth refetch UX
FloB95 Mar 12, 2026
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
124 changes: 99 additions & 25 deletions .github/workflows/backend-test.yml
Original file line number Diff line number Diff line change
@@ -1,53 +1,109 @@
name: Backend Tests

on:
workflow_dispatch:
pull_request:
branches:
- main
paths:
- 'apps/backend/**'
- 'packages/shared/**'
- '.github/workflows/backend-test.yml'

jobs:
test-backend:
runs-on: ubuntu-latest

services:
mysql:
image: mysql:8
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: qrcodly_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1"
--health-interval=10s
--health-timeout=5s
--health-retries=5

redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd="redis-cli ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5

env:
NODE_ENV: 'test'
LOG_LEVEL: 'error'
TZ: 'Europe/Berlin'

# Database — service container
DB_HOST: '127.0.0.1'
DB_USER: 'root'
DB_PASSWORD: 'root'
DB_NAME: 'qrcodly_test'
TEST_DB_NAME: 'qrcodly_test'
DB_PORT: '3306'

# Redis — service container
REDIS_URL: 'redis://127.0.0.1:6379'

# S3 — MinIO service container
S3_ENDPOINT: 'http://127.0.0.1:9000'
S3_REGION: 'us-east-1'
S3_UPLOAD_KEY: 'minio'
S3_UPLOAD_SECRET: 'testtest1'
S3_BUCKET_NAME: 'qrcodly'

# App config
BACKEND_URL: 'http://localhost:5001'
API_HOST: '127.0.0.1'
FRONTEND_URL: 'https://www.qrcodly.de'
COOKIE_SECRET: ${{ secrets.COOKIE_SECRET }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
REDIS_URL: ${{ secrets.REDIS_URL }}
API_HOST: ${{ secrets.API_HOST }}
FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
DB_HOST: ${{ secrets.DB_HOST }}
DB_USER: ${{ secrets.DB_USER }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
DB_NAME: ${{ secrets.DB_NAME }}
TEST_DB_NAME: ${{ secrets.DB_NAME }}
DB_PORT: ${{ secrets.DB_PORT }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASS: ${{ secrets.SMTP_PASS }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_REGION: ${{ secrets.S3_REGION }}
S3_UPLOAD_KEY: ${{ secrets.S3_UPLOAD_KEY }}
S3_UPLOAD_SECRET: ${{ secrets.S3_UPLOAD_SECRET }}
S3_BUCKET_NAME: ${{ secrets.S3_BUCKET_NAME }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_ENVIRONMENT: 'development'
INTERNAL_API_SECRET: ${{ secrets.INTERNAL_API_SECRET }}

# Clerk — real API needed for test JWT tokens
CLERK_PUBLISHABLE_KEY: ${{ secrets.CLERK_PUBLISHABLE_KEY }}
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
UMAMI_HOST: ${{ secrets.UMAMI_HOST }}
UMAMI_WEBSITE: ${{ secrets.UMAMI_WEBSITE }}
UMAMI_USERNAME: ${{ secrets.UMAMI_USERNAME }}
UMAMI_PASSWORD: ${{ secrets.UMAMI_PASSWORD }}
BACKEND_URL: 'http://localhost:5001'
CLERK_WEBHOOK_SECRET_KEY: 'NOT_USED_IN_TESTS'

# Stripe — real test API needed for billing tests
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
STRIPE_PRO_PRICE_ID_MONTHLY: ${{ secrets.STRIPE_PRO_PRICE_ID_MONTHLY }}
STRIPE_PRO_PRICE_ID_ANNUAL: ${{ secrets.STRIPE_PRO_PRICE_ID_ANNUAL }}

# SMTP — not called in tests, dummy values
SMTP_HOST: 'localhost'
SMTP_PORT: '587'
SMTP_USER: 'test'
SMTP_PASS: 'test'

# Sentry — not called in tests, dummy DSN
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_ENVIRONMENT: 'development'

# Umami — mocked in tests, dummy values
UMAMI_HOST: 'http://localhost:3001'
UMAMI_WEBSITE: 'test-website-id'
UMAMI_USERNAME: 'test'
UMAMI_PASSWORD: 'test'

# Cloudflare — not called in tests
CLOUDFLARE_API_TOKEN: 'NOT_USED_IN_TESTS'
CLOUDFLARE_ZONE_ID: 'NOT_USED_IN_TESTS'
CLOUDFLARE_DCV_DELEGATION_TARGET: 'NOT_USED_IN_TESTS'

# Analytics encryption
ANALYTICS_ENCRYPTION_KEY: ${{ secrets.ANALYTICS_ENCRYPTION_KEY }}

steps:
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
Expand All @@ -61,6 +117,24 @@ jobs:
- name: Build shared package
run: pnpm --filter @shared/schemas build

- name: Start MinIO and create bucket
run: |
docker run -d --name minio \
-p 9000:9000 \
-e MINIO_ROOT_USER=minio \
-e MINIO_ROOT_PASSWORD=testtest1 \
minio/minio:latest server /data
# Wait for MinIO to be ready
for i in $(seq 1 30); do
curl -sf http://127.0.0.1:9000/minio/health/live && break
sleep 1
done
# Create bucket
curl -sL https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc
chmod +x /usr/local/bin/mc
mc alias set ci http://127.0.0.1:9000 minio testtest1
mc mb ci/qrcodly --ignore-existing

- name: Run Checks
working-directory: apps/backend
run: pnpm run pr:precheck
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ yarn.lock
.env
.vscode/*
!.vscode/settings.json
.claude
.claude-flow
.swarm
.idea
**/package-lock.json
packages/**/.turbo
Expand Down
124 changes: 70 additions & 54 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ QRcodly is a full-stack QR code generator and management platform. It's a pnpm m

```
apps/
├── backend/ # Fastify REST API (Node.js)
├── frontend/ # Next.js web application
└── browser-extension/ # Vite-based browser extension
├── backend/ # Fastify REST API (Node.js)
├── frontend/ # Next.js 16 web application (React 19)
└── browser-extension/ # Vite-based browser extension

packages/
├── shared/ # @shared/schemas - DTOs, Zod schemas, utilities shared between frontend/backend
├── shared/ # @shared/schemas - DTOs, Zod v4 schemas, utilities
├── eslint-config/
└── typescript-config/
```
Expand All @@ -39,10 +39,10 @@ pnpm run frontend:dev # Next.js at :3000 (uses turbo)
### Building

```bash
pnpm run build # Build all apps via Turbo
pnpm run build:shared-package # Build shared package only
pnpm run backend:build # Build backend (builds shared first)
pnpm run frontend:build # Build frontend (builds shared first)
pnpm run build # Build all apps via Turbo
pnpm run build:shared-package # Build shared package only (must run before app builds)
pnpm run backend:build # Build backend (builds shared first)
pnpm run frontend:build # Build frontend (builds shared first)
```

### Testing
Expand Down Expand Up @@ -82,64 +82,79 @@ pnpm run studio # Open Drizzle Studio

### Backend (`apps/backend`)

**Tech Stack**: Fastify, Drizzle ORM (MySQL), Redis, Clerk auth, S3/MinIO storage, Pino logging

**Structure**:

- `src/core/` - Framework and infrastructure
- `config/` - Environment variables and constants
- `db/` - Database connection, schema, migrations
- `cache/` - Redis caching layer
- `storage/` - S3/MinIO file uploads
- `mailer/` - Nodemailer with Handlebars templates (`templates/`)
- `error/` - Custom error classes
- `event/` - Event system for async operations
- `rate-limit/` - Rate limiting configuration
- `policies/` - Authorization policies
- `server.ts` - Fastify server setup
- `src/modules/` - Feature modules (each has http/, service/, and often `__tests__/`)
- `qr-code/` - QR code generation and management (supports URL, vCard, WiFi, Email, Event, Location, Text types)
- `custom-domain/` - Custom domain management with Cloudflare integration
- `url-shortener/` - URL shortening and analytics tracking
- `config-template/` - User-defined QR code templates
- `subscription/` - Billing and subscription management

**Dependency Injection**: Uses `tsyringe` for DI container
**Tech Stack**: Fastify, Drizzle ORM (MySQL), Redis, Clerk auth, S3/MinIO, Pino logging, tsyringe DI

**Module Structure** (`src/modules/`): 7 modules — `qr-code`, `url-shortener`, `billing`, `custom-domain`, `config-template`, `tag`, `analytics-integration`

Each module follows this layout:

- `domain/entities/` — Drizzle table definitions + TypeScript types
- `domain/repository/` — Data access extending `AbstractRepository`
- `useCase/` — Business logic classes (`*.use-case.ts`)
- `http/controller/` — Decorator-based route handlers
- `http/__tests__/` — Integration tests
- `service/` — Stateless services (optional)
- `event/handler/` — Event handlers (optional)
- `setup.ts` — Module registration via `registerRoutes()`

**DI Conventions (tsyringe)**:

- Controllers and UseCases: `@injectable()` (transient, new instance per resolution)
- Repositories and Services: `@singleton()` (single shared instance)
- All deps injected via `@inject(ClassName)` constructor params

**Route Registration**: Decorator-based (`@Get`, `@Post`, `@Put`, `@Patch`, `@Delete`) with Zod schema validation for body/query/response. Routes are registered in each module's `setup.ts` via `registerRoutes(fastify, ControllerClass, prefix)`.

**API prefix**: `/api/v1` (e.g., `/api/v1/qr-code`, `/api/v1/short-url`, `/api/v1/billing`)

**Core infrastructure** (`src/core/`):

- `db/` — Connection, schema re-exports, migrations. All entity schemas centrally re-exported from `db/schemas/index.ts`
- `cache/` — Redis caching (`KeyCache`)
- `storage/` — S3/MinIO file uploads
- `server.ts` — Fastify server setup, plugin registration, module loading

**Database patterns**: `AbstractRepository` provides pagination, caching, soft deletes. `UnitOfWork` + `TransactionContext` for multi-operation transactions.

### Frontend (`apps/frontend`)

**Tech Stack**: Next.js (App Router), React, Tailwind CSS, shadcn/ui, Zustand, TanStack Query, Clerk, next-intl (i18n)
**Tech Stack**: Next.js 16 (App Router, Turbo dev), React 19, Tailwind CSS, shadcn/ui, Zustand, TanStack Query, Clerk, next-intl

**Routing**: `src/app/[locale]/` — locale-prefixed routes. Dashboard routes under `/dashboard/` are protected via Clerk middleware.

**Middleware**: `src/proxy.ts` chains Clerk auth + next-intl middleware. Handles `/u/[shortCode]` analytics redirects.

**Structure**:
**API calls** (`src/lib/api/`): `apiRequest<T>()` utility with Bearer token auth from Clerk. Each API module exports TanStack Query hooks (e.g., `useListQrCodesQuery`, `useCreateQrCodeMutation`).

- `src/app/[locale]/` - Locale-prefixed routes (i18n)
- `src/components/` - React components
- `ui/` - shadcn/ui primitives
- `dashboard/` - Dashboard feature sections
- `qr-generator/` - QR code creation UI
- `src/store/` - Zustand state management
- `src/dictionaries/` - Translation files (en, de, es, fr, it, nl, pl, ru)
- `src/lib/api/` - API client utilities
**State**: Zustand store (`src/store/useQrCodeStore.ts`) for QR code generator form state with localStorage persistence.

**Env validation**: `@t3-oss/env-nextjs` in `src/env.js` — type-safe server/client env vars.

**i18n**: 8 locales (en, de, es, fr, it, nl, pl, ru). Translation files in `src/dictionaries/`. Uses `localePrefix: 'as-needed'`.

### Shared Package (`packages/shared`)

Published as `@shared/schemas`. Contains:
Published as `@shared/schemas`. Import via: `import { ... } from '@shared/schemas'`

Uses **Zod v4** (from pnpm catalog). Contains:

- `src/schemas/` — Source-of-truth Zod schemas (e.g., `QrCode.ts` with discriminated union for 8 content types: URL, Text, WiFi, vCard, Email, Location, Event, EPC)
- `src/dtos/` — Request/response DTOs derived from schemas via `.pick()`, `.extend()`. Organized by feature domain.
- `src/utils/` — QR code content converters, default styling options, deep diff utility

- Zod validation schemas
- DTOs for API request/response types
- Shared utilities and QR code defaults
**DTO naming**: `CreateXDto`, `UpdateXDto`, `XResponseDto`, `XPaginatedResponseDto`, `XListRequestDto`

Import via: `import { ... } from '@shared/schemas'`
Build: `tsc` → `dist/` (CommonJS). Must be built before backend/frontend (`pnpm run build:shared-package`).

## Local Development Environment

Docker Compose provides:

- MySQL (port 3306) - credentials: root/root, database: qrcodly
- Redis (port 6379)
- MinIO S3 mock (ports 9000 API, 9001 console) - credentials: minio/testtest
- phpMyAdmin (port 8081)
- Umami analytics (port 3001)
- MySQL (3306) root/root, database: qrcodly
- Redis (6379)
- MinIO S3 mock (9000 API, 9001 console) minio/testtest
- phpMyAdmin (8081)
- Umami analytics (3001)

## Testing Patterns

Expand All @@ -148,11 +163,12 @@ Backend tests use Jest with:
- Global setup/teardown for test database isolation
- `--runInBand` for sequential execution
- 30 second timeout per test
- Tests located in `__tests__/` directories within modules
- Tests in `__tests__/` directories within module `http/` dirs

## Key Conventions

- **Database naming**: snake_case for tables and columns
- **TypeScript**: Strict mode, path aliases (`@/*` for local, `@shared/schemas/*` for shared)
- **TypeScript**: Strict mode, path aliases (`@/*` for local, `@shared/schemas` or `@shared/schemas/*` for shared)
- **Formatting**: Prettier with tabs, semicolons, 100 char width
- **Git hooks**: lefthook runs Prettier on pre-commit
- **Git hooks**: lefthook runs Prettier on staged files at pre-commit
- **Locale translations**: When adding/changing user-facing strings, update all 8 locale dictionaries
9 changes: 5 additions & 4 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,17 @@
"start": "pnpm run db:migrate && TZ=Europe/Berlin node build/src/index.js",
"dev-server": "docker-compose up -d",
"dev": "pnpm run db:migrate && TZ=Europe/Berlin tsx watch --include ./src src/index.ts",
"test": "NODE_ENV=test LOG_LEVEL=error pnpm run db:migrate && jest --runInBand",
"test:coverage": "NODE_ENV=test LOG_LEVEL=error pnpm run db:migrate && jest --runInBand --coverage",
"test": "export NODE_ENV=test && export LOG_LEVEL=error && export FRONTEND_URL=https://test.qrcodly.de && pnpm run db:migrate && jest --runInBand",
"test:coverage": "export NODE_ENV=test && export LOG_LEVEL=error && export FRONTEND_URL=https://test.qrcodly.de && pnpm run db:migrate && jest --runInBand --coverage",
"test:detectOpenHandles": "jest --silent --runInBand --detectOpenHandles",
"db:push": "drizzle-kit push --config=drizzle.config.ts",
"db:migrate": "DB_MIGRATING=true tsx src/core/db/migrate.ts",
"db:generate-migration": "drizzle-kit generate --config=drizzle.config.ts",
"studio": "drizzle-kit studio",
"cli:list": "npx tsx src/cli.ts list",
"script:find-orphaned-urls": "SKIP_ENV_VALIDATION=true tsx scripts/find-orphaned-short-urls.ts",
"script:test-smtp": "tsx scripts/test-smtp.ts"
"script:test-smtp": "tsx scripts/test-smtp.ts",
"script:regenerate-previews": "SKIP_ENV_VALIDATION=true tsx scripts/regenerate-preview-images.ts"
},
"devDependencies": {
"@aws-sdk/types": "^3.936.0",
Expand Down Expand Up @@ -61,7 +62,7 @@
"dependencies": {
"@aws-sdk/client-s3": "^3.943.0",
"@aws-sdk/s3-request-presigner": "^3.943.0",
"@axiomhq/pino": "^1.3.1",
"@axiomhq/pino": "^1.4.0",
"@clerk/fastify": "^2.6.17",
"@fastify/cookie": "^11.0.2",
"@fastify/cors": "^11.1.0",
Expand Down
Loading
Loading