Skip to content

cvizzini/claw-todo

Repository files navigation

πŸ“ TodoApp

A production-ready, multi-tenant todo application built with ASP.NET Core 10 and Blazor

.NET Blazor EF Core Tests License

Features Β· Architecture Β· Getting Started Β· API Reference Β· Configuration Β· Testing Β· Contributing


✨ Features

For Users

  • πŸ“‹ Task management β€” Create, edit, complete, and organise todos with priorities (Low / Medium / High), categories, and due dates
  • πŸ” Real-time search β€” Server-side search across titles, notes, and categories with debounced input
  • πŸ“„ Pagination & sorting β€” Configurable page size, sort by title / priority / due date / last updated
  • πŸ—‘οΈ Trash & restore β€” Soft-delete with a dedicated trash bin; restore or permanently delete anytime
  • ⏰ Overdue alerts β€” Dismissible banner when tasks have slipped past their due date
  • πŸŒ™ Dark mode β€” System-aware dark/light toggle that persists across sessions
  • πŸ“± Responsive layout β€” Sidebar collapses on mobile; full-width single-column view

For Administrators

  • 🏒 Multi-tenancy β€” Full three-tier tenant isolation (Administrator β†’ TenantAdmin β†’ User)
  • πŸ‘₯ User management β€” Create users, assign roles, lock/unlock accounts, manage across tenants
  • πŸ” Role-based access β€” Granular policy enforcement at every endpoint
  • πŸ“Š Audit log β€” Every create, update, complete, delete, and restore action recorded with a full diff

For Operations

  • ❀️ Health checks β€” /health/live (liveness) and /health/ready (readiness with DB probe)
  • πŸ”­ Distributed tracing β€” OpenTelemetry with ASP.NET Core + EF Core instrumentation
  • πŸ“ˆ Metrics β€” Runtime metrics (GC, heap, thread pool) via OpenTelemetry
  • πŸ“‹ Structured logging β€” Serilog with per-request enrichment: UserId, TenantId, TraceId, SpanId
  • πŸ”‘ Refresh tokens β€” 30-day sliding refresh tokens with rotation on use
  • 🚦 Rate limiting β€” Configurable fixed-window limits per endpoint group

πŸ—οΈ Architecture

TodoApp follows Onion Architecture (also known as Clean Architecture), with strict dependency flow: outer layers depend on inner layers, never the reverse.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                         Presentation                             β”‚
β”‚                                                                  β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”‚
β”‚   β”‚    TodoApp.Web       β”‚      β”‚      TodoApp.Api          β”‚     β”‚
β”‚   β”‚  Blazor Server UI    │─────▢│  ASP.NET Core Minimal APIβ”‚     β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ JWT  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                 β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Infrastructure                             β”‚
β”‚                     TodoApp.Infrastructure                        β”‚
β”‚         EF Core Β· SQLite Β· Identity Β· JWT Β· Migrations           β”‚
β”‚                         DatabaseSeeder                           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                 β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        Application                                β”‚
β”‚                      TodoApp.Application                          β”‚
β”‚           TodoService Β· AuthService Β· DTOs Β· Interfaces          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                                                 β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           Domain                                  β”‚
β”‚                        TodoApp.Domain                             β”‚
β”‚           TodoItem Β· AppUser Β· Tenant Β· Enums Β· Interfaces       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Project Structure

πŸ“ Todo 2/
β”œβ”€β”€ πŸ“ TodoApp.Domain/               # Core entities and interfaces (no dependencies)
β”‚   β”œβ”€β”€ πŸ“ Entities/                 # TodoItem, AppUser, Tenant, AuditLog
β”‚   β”œβ”€β”€ πŸ“ Enums/                    # Priority
β”‚   └── πŸ“ Interfaces/               # ITodoRepository, etc.
β”‚
β”œβ”€β”€ πŸ“ TodoApp.Application/          # Use-case logic (depends on Domain only)
β”‚   β”œβ”€β”€ πŸ“ Services/                 # TodoService, AuthService
β”‚   β”œβ”€β”€ πŸ“ DTOs/                     # Request / response models
β”‚   └── DependencyInjection.cs       # AddApplication() extension
β”‚
β”œβ”€β”€ πŸ“ TodoApp.Infrastructure/       # Data access + external concerns
β”‚   β”œβ”€β”€ πŸ“ Data/                     # TodoDbContext, AppUser (Identity)
β”‚   β”œβ”€β”€ πŸ“ Repositories/             # TodoRepository (EF Core)
β”‚   β”œβ”€β”€ πŸ“ Migrations/               # EF Core migration history
β”‚   β”œβ”€β”€ πŸ“ Seeding/                  # DatabaseSeeder (roles + super-admin)
β”‚   └── DependencyInjection.cs       # AddInfrastructure() extension
β”‚
β”œβ”€β”€ πŸ“ TodoApp.Api/                  # Thin Minimal API layer
β”‚   β”œβ”€β”€ πŸ“ Endpoints/                # Auth, Todos, Admin, TenantAdmin
β”‚   β”œβ”€β”€ πŸ“ Middleware/               # Global exception handler
β”‚   └── Program.cs                   # AddInfrastructure() + AddApplication() + pipeline
β”‚
β”œβ”€β”€ πŸ“ TodoApp.Web/                  # Blazor Server frontend
β”‚   β”œβ”€β”€ πŸ“ Components/
β”‚   β”‚   β”œβ”€β”€ πŸ“ Pages/                # Landing, Login, Home, Trash, Admin, TenantAdmin
β”‚   β”‚   └── πŸ“ Layout/               # MainLayout, NavMenu
β”‚   β”œβ”€β”€ πŸ“ Services/                 # AuthService, TodoApiService
β”‚   β”œβ”€β”€ πŸ“ Models/                   # Request/response DTOs
β”‚   └── πŸ“ wwwroot/                  # CSS, PWA manifest
β”‚
β”œβ”€β”€ πŸ“ TodoApp.Api.Tests/            # Integration tests (WebApplicationFactory)
β”‚   β”œβ”€β”€ AuthEndpointsTests.cs
β”‚   β”œβ”€β”€ TodoEndpointsTests.cs
β”‚   β”œβ”€β”€ AdminEndpointsTests.cs
β”‚   β”œβ”€β”€ TenantAdminEndpointsTests.cs
β”‚   └── HealthTests.cs
β”‚
└── πŸ“ TodoApp.Application.Tests/    # Pure unit tests (NSubstitute, no HTTP)
    β”œβ”€β”€ TodoServiceTests.cs
    └── AuthServiceTests.cs

Multi-Tenancy Model

Administrator  (no TenantId)
  └── manages all tenants and all users platform-wide

TenantAdmin    (scoped TenantId)
  └── manages users within their own tenant only

User           (scoped TenantId)
  └── manages their own todos only

All users are created by an administrator β€” there is no public self-registration. Tenant isolation is enforced at the database query level via EF Core global query filters on every request.


πŸš€ Getting Started

Prerequisites

1. Clone the repository

git clone https://github.com/cvizzini/claw-todo.git
cd claw-todo

2. Run the API

cd TodoApp.Api
dotnet run

The API starts on http://localhost:5227. On first run it will:

  • Create the SQLite database (todos.db)
  • Run all EF Core migrations automatically
  • Seed roles and the default super-admin account

Default super-admin credentials:

Field Value
Email admin@todoapp.local
Password Admin1234!

⚠️ Change these in appsettings.json before deploying to production.

3. Run the Blazor frontend

# In a separate terminal
cd TodoApp.Web
dotnet run

Open http://localhost:5001 in your browser and sign in with the super-admin credentials above.

4. Run all tests

# Integration tests
cd TodoApp.Api.Tests
dotnet test

# Unit tests
cd TodoApp.Application.Tests
dotnet test

Expected output: 107 tests, 0 failures.

5. Explore the API docs

With the API running in Development mode, visit:

http://localhost:5227/scalar

Interactive Scalar UI with every endpoint documented and try-it-out support.

6. Add EF Core migrations

Migrations live in TodoApp.Infrastructure. Always specify both projects:

dotnet ef migrations add <MigrationName> \
  --project TodoApp.Infrastructure \
  --startup-project TodoApp.Api

dotnet ef database update \
  --project TodoApp.Infrastructure \
  --startup-project TodoApp.Api

πŸ“‘ API Reference

All endpoints are versioned under /api/v1/.

Authentication β€” /api/v1/auth

Method Endpoint Auth Description
POST /login β€” Sign in, returns JWT + refresh token
POST /refresh β€” Exchange refresh token for new token pair
GET /me βœ… Get current user profile

Todos β€” /api/v1/todos

Method Endpoint Description
GET / List todos (paginated, searchable, filterable, sortable)
POST / Create a todo
GET /{id} Get a single todo
PUT /{id} Update a todo
PATCH /{id}/toggle Toggle completion status
DELETE /{id} Soft-delete (moves to trash)
GET /trash List soft-deleted todos
POST /{id}/restore Restore from trash
DELETE /{id}/permanent Permanently delete from trash
DELETE /trash Empty trash
DELETE /completed Clear all completed todos
GET /stats Counts: total, active, completed, overdue
GET /categories Distinct categories used by this user
GET /{id}/audit Audit history for a specific todo

Pagination & filtering:

GET /api/v1/todos?page=1&pageSize=20&search=groceries&category=Personal&completed=false&sortBy=dueDate&sortDesc=false

Admin β€” /api/v1/admin (Administrator only)

Method Endpoint Description
GET /tenants List all tenants
POST /tenants Create a tenant
GET /tenants/{id} Get tenant by ID
PUT /tenants/{id} Update tenant name / active status
DELETE /tenants/{id} Delete tenant (must have no users)
GET /users List all users (optionally filter by ?tenantId=)
GET /users/{id} Get user by ID
POST /users Create user with role + tenant assignment
PUT /users/{id} Update display name, role, tenant
DELETE /users/{id} Hard delete user and all their data
PATCH /users/{id}/lock Lock account
PATCH /users/{id}/unlock Unlock account

Tenant Admin β€” /api/v1/tenant (TenantAdmin or above)

Same user management endpoints as Admin, automatically scoped to the caller's tenant. Cannot create or manage Administrators.

Health β€” /health

Endpoint Description
GET /health/live Liveness probe β€” { "status": "alive" }
GET /health/ready Readiness probe β€” includes database connectivity check
GET /health Full health report with all checks and durations

βš™οΈ Configuration

All configuration lives in appsettings.json. Key sections:

{
  "ConnectionStrings": {
    // SQLite by default β€” swap for PostgreSQL in production
    "DefaultConnection": "Data Source=todos.db"
  },
  "Jwt": {
    "Key": "CHANGE-THIS-TO-A-LONG-RANDOM-SECRET",  // min 32 chars
    "Issuer": "TodoApp.Api",
    "Audience": "TodoApp.Web",
    "ExpiryHours": "8"
  },
  "Seed": {
    // Super-admin created on first run only
    "AdminEmail": "admin@yourcompany.com",
    "AdminPassword": "ChangeMe1234!",
    "AdminDisplayName": "Super Admin"
  },
  "RateLimiting": {
    "AuthLimit": 10,    // requests per minute on /auth
    "ApiLimit": 100     // requests per minute on /api
  },
  "OpenTelemetry": {
    "ServiceName": "TodoApp.Api",
    "Endpoint": ""      // OTLP endpoint (Seq, Jaeger, etc.) β€” empty = console in Development
  }
}

Environment variables

Any appsettings.json key can be overridden via environment variable using __ as the separator:

Jwt__Key=my-super-secret-key
Seed__AdminPassword=SecurePassword123!
OpenTelemetry__Endpoint=http://localhost:4317

πŸ” Security Notes

Before going to production:

  • Replace the JWT Key with a cryptographically random secret (β‰₯ 32 characters)
  • Change the default seed admin email and password
  • Switch ConnectionStrings:DefaultConnection from SQLite to PostgreSQL
  • Set AllowedHosts to your specific domain
  • Configure HTTPS and set ASPNETCORE_URLS
  • Point OpenTelemetry:Endpoint at your observability backend

πŸ§ͺ Testing

The test suite uses xUnit + FluentAssertions with two complementary layers:

Integration Tests (TodoApp.Api.Tests)

Uses WebApplicationFactory with a shared in-memory SQLite connection β€” no mocks, real middleware pipeline. Every test gets a fully seeded environment (admin, tenant, tenant-admin, and regular user).

cd TodoApp.Api.Tests
dotnet test --verbosity normal
Suite Tests Covers
AuthEndpointsTests 8 Login, refresh, me, lockout
TodoEndpointsTests 26 Full CRUD, soft delete, audit, search, pagination
AdminEndpointsTests 22 Tenant CRUD, user management, cross-tenant isolation
TenantAdminEndpointsTests 9 Scoped admin operations, role enforcement
HealthTests 3 Liveness, readiness, full report
Subtotal 74

Unit Tests (TodoApp.Application.Tests)

Pure unit tests of the Application layer using NSubstitute for mocking. No HTTP, no database, no EF Core. Tests service logic in isolation.

cd TodoApp.Application.Tests
dotnet test --verbosity normal
Suite Tests Covers
TodoServiceTests 22 Create, update, delete, toggle, trash, restore, stats
AuthServiceTests 11 Login, refresh, JWT validation, role checks
Subtotal 33

Combined

Total: 107 tests β€” Failed: 0, Passed: 107

πŸ›£οΈ Roadmap

  • PostgreSQL support β€” production-grade database with connection pooling
  • Docker + Docker Compose β€” multi-stage build with Postgres + Seq for local dev
  • GitHub Actions CI β€” build β†’ test β†’ publish on PR
  • Password reset β€” email-based forgot-password flow
  • Problem Details β€” RFC 9457 application/problem+json on all error responses
  • Email verification β€” wire up EmailConfirmed flag
  • PWA / offline support β€” service worker + offline cache

🀝 Contributing

Contributions are welcome! Here's how to get started:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Make your changes β€” keep all 107 tests green
  4. Add tests for any new behaviour
  5. Push and open a Pull Request

Please keep PRs focused β€” one feature or fix per PR makes review much easier.


πŸ“„ License

This project is licensed under the MIT License β€” see the LICENSE file for details.


Built with ❀️ using ASP.NET Core 10 · Blazor · Entity Framework Core · OpenTelemetry

About

.NET Todo app Entirely written by Open Claw and Claude Sonnet 4.6

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors