A full-stack Todo application that starts with familiar CRUD workflows and grows into a more production-oriented system: cookie-based authentication, OTP email verification, dashboard reporting, Redis caching, Quartz background jobs, Docker deployment, and a modern React interface.
This is a personal learning project. It is public for reference, but it is not currently maintained as a community contribution project.
- Features
- Tech Stack
- Architecture
- Project Structure
- Getting Started
- Configuration
- Docker
- API Overview
- Troubleshooting
- What's Changed
- Security Notes
- License
- Author
- Todo list CRUD with search, pagination, and progress counts.
- Todo item CRUD with priority, due date, completion status, and filtering.
- Dashboard analytics with completion rate, overdue tasks, trends, and priority distribution.
- Cookie-based authentication using
AuthTokenandRefreshTokenHttpOnly cookies. - Email OTP flows for account verification and password changes.
- Fixed OTP verification flow and optimized post-login redirect to the dashboard.
- Refresh-token rotation and logout support.
- Role-based authorization for admin-only reports/jobs.
- Redis-backed caching for expensive report data, with memory-cache fallback.
- Quartz.NET background jobs for reports, reminders, summaries, and cleanup.
- Lazy-loaded React routes with authenticated-route preloading for a smoother login experience.
- Responsive Ant Design UI for mobile, tablet, laptop, and desktop.
- Polished Login/Register pages with auth illustrations, compact registration form layout, and route preload hints.
- Shared SCSS tokens, mixins, and layout utilities for cleaner frontend styling.
- Docker-ready backend, frontend, MySQL, Redis, and Nginx setup.
Frontend
- React 19
- TypeScript
- Vite
- React Router
- Ant Design
- Axios
- Day.js
- SCSS
Backend
- .NET 8
- ASP.NET Core Web API
- ASP.NET Core Identity
- Entity Framework Core
- MySQL
- Redis / Distributed Cache
- Quartz.NET
- MailKit
- Serilog
Infrastructure
- Docker Compose
- Nginx reverse proxy
- MySQL 8
- Redis 7
The backend follows a layered structure:
Todo.API: controllers, middleware configuration, CORS, auth, Swagger, Redis, Quartz.Todo.Services: business handlers for auth, todos, reports, cache, and background jobs.Todo.Repositories: repository abstractions and implementations.Todo.Models: EF Core entities, configurations, DbContext, and migrations.Todo.DTOs: request and response contracts.Todo.Commons: shared enums and helpers.MayNghien.Infrastructures: shared infrastructure helpers.
The frontend is feature-oriented:
src/routes: lazy route definitions.src/routes/preload.ts: preloads authenticated route chunks from auth pages.src/pages: route-level pages.src/apis: API wrappers.src/components: reusable UI components.src/commons: shared frontend utilities and enums.src/interfaces: typed request/response contracts.src/layouts: shared layout shells.
TodoApp_BasicToModern/
├── TodoApp.Client/
│ ├── src/
│ │ ├── apis/
│ │ ├── commons/
│ │ ├── components/
│ │ ├── configs/
│ │ ├── interfaces/
│ │ ├── layouts/
│ │ ├── pages/
│ │ └── routes/
│ ├── Dockerfile
│ ├── package.json
│ └── vite.config.ts
├── TodoApp.Server/
│ └── src/
│ ├── MayNghien.Infrastructures/
│ ├── Todo.API/
│ ├── Todo.Commons/
│ ├── Todo.DTOs/
│ ├── Todo.Models/
│ ├── Todo.Repositories/
│ ├── Todo.Services/
│ └── src.sln
├── nginx/
├── .env.example
├── docker-compose.yml.example
├── LICENSE.txt
└── README.md
- .NET 8 SDK
- Node.js 18 or newer
- MySQL 8
- Redis 7, optional but recommended for report caching
- Docker Desktop, optional
From the repository root:
cd TodoApp.Server/src
dotnet restoreConfigure local secrets from Todo.API:
cd Todo.API
dotnet user-secrets init
dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=localhost;Database=TodoApp_BToM;User=root;Password=your_password;"
dotnet user-secrets set "Jwt:Key" "replace-with-a-long-random-secret"
dotnet user-secrets set "Jwt:Issuer" "https://localhost:7196"
dotnet user-secrets set "Jwt:Audience" "https://localhost:7196"Optional Redis cache for development:
dotnet user-secrets set "RedisSettings:Enabled" "true"
dotnet user-secrets set "ConnectionStrings:RedisConnection" "localhost:6379,abortConnect=false,connectTimeout=5000,syncTimeout=5000,asyncTimeout=5000,connectRetry=3,keepAlive=60"Run migrations:
cd ..
dotnet ef database update --project Todo.Models --startup-project Todo.APIStart the API:
dotnet run --project Todo.APIDefault local API URLs:
- HTTP API:
http://localhost:5133 - HTTPS API:
https://localhost:7196 - Swagger:
https://localhost:7196/swagger
cd TodoApp.Client
npm install
npm run devThe client defaults to relative API routes. For direct backend calls, create a local .env in TodoApp.Client if needed:
VITE_API_BASE_URL=https://localhost:7196
During Vite development, vite.config.ts proxies these relative routes to http://localhost:5133:
/authentication/todo-items/todo-lists/reports/jobs
Keep real secrets out of Git.
Use these files as templates only:
.env.exampledocker-compose.yml.examplenginx/conf.d/todoapp.conf.example
Create local files when needed:
Copy-Item .env.example .env
Copy-Item docker-compose.yml.example docker-compose.yml
Copy-Item nginx/conf.d/todoapp.conf.example nginx/conf.d/todoapp.confRecommended local secret storage:
- Backend development:
dotnet user-secrets - Docker deployment:
.env - Production server: environment variables or a secret manager
Gmail requires an App Password, not your normal Gmail password.
dotnet user-secrets set "EmailSettings:SmtpServer" "smtp.gmail.com"
dotnet user-secrets set "EmailSettings:SmtpPort" "587"
dotnet user-secrets set "EmailSettings:SmtpUsername" "your-email@gmail.com"
dotnet user-secrets set "EmailSettings:SmtpPassword" "your-gmail-app-password"
dotnet user-secrets set "EmailSettings:FromEmail" "your-email@gmail.com"
dotnet user-secrets set "EmailSettings:FromName" "TodoApp"
dotnet user-secrets set "EmailSettings:RecipientEmail" "recipient@example.com"Copy examples first:
Copy-Item .env.example .env
Copy-Item docker-compose.yml.example docker-compose.ymlEdit .env, then start services:
docker compose up -dFor local development with only Redis:
docker run -d --name todoapp-redis -p 127.0.0.1:6379:6379 redis:7-alpine redis-server --appendonly yesUseful commands:
docker compose ps
docker compose logs -f backend
docker compose logs -f redis
docker compose down| Method | Route | Description |
|---|---|---|
POST |
/authentication/login |
Login and set auth cookies |
POST |
/authentication/register |
Register a user and send verification OTP |
POST |
/authentication/send-otp |
Send OTP for email verification or password change |
POST |
/authentication/verify-otp |
Verify OTP |
POST |
/authentication/change-password |
Change password after OTP verification |
POST |
/authentication/refresh-token |
Refresh auth cookies |
POST |
/authentication/logout |
Revoke session and clear cookies |
| Method | Route | Description |
|---|---|---|
POST |
/todo-lists/search |
Search todo lists |
GET |
/todo-lists/{id} |
Get list by id |
POST |
/todo-lists |
Create list |
PUT |
/todo-lists |
Update list |
DELETE |
/todo-lists/{id} |
Delete list |
| Method | Route | Description |
|---|---|---|
POST |
/todo-items/search |
Search todo items |
GET |
/todo-items/{id} |
Get item by id |
POST |
/todo-items |
Create item |
PUT |
/todo-items |
Update item |
DELETE |
/todo-items/{id} |
Delete item |
| Method | Route | Description |
|---|---|---|
POST |
/reports/progress |
Get dashboard progress report |
POST |
/reports/snapshot |
Create daily snapshot, admin only |
POST |
/jobs/trigger/daily-report |
Trigger daily report, admin only |
POST |
/jobs/trigger/weekly-summary |
Trigger weekly summary, admin only |
POST |
/jobs/trigger/task-reminder |
Trigger reminder job, admin only |
POST |
/jobs/pause/{jobName} |
Pause a Quartz job, admin only |
POST |
/jobs/resume/{jobName} |
Resume a Quartz job, admin only |
GET |
/jobs/scheduler/info |
Scheduler info, admin only |
Check whether RefreshToken is stored and sent by the browser. In development, cookie settings depend on whether requests are made through HTTP proxy or direct HTTPS backend calls.
Also verify:
withCredentials: trueis enabled in Axios.- Backend was restarted after cookie configuration changes.
- Old localhost cookies were cleared.
AllowedOriginsincludes the frontend origin when calling the API directly.
The client preloads authenticated route chunks from Login/Register and skips an immediate duplicate refresh check right after a successful login. If the redirect still feels slow, check:
- Network latency of
POST /authentication/login. - Whether the dashboard report endpoint is cold and waiting for DB/cache work.
- Browser DevTools network waterfall for large chunks such as
chartsorantd. - Redis availability when report caching is enabled.
/reports/progress is an expensive endpoint on cache miss.
Recommended checks:
- Ensure Redis is running if
RedisSettings:Enabled=true. - Ensure the Redis connection string matches whether Redis uses a password.
- Restart backend after changing user-secrets.
- Check API logs for slow DB queries or Redis connection errors.
Gmail rejected SMTP authentication. Use a Gmail App Password and restart the backend after updating user-secrets.
If Redis is started with --requirepass, the backend connection string must include:
password=your_redis_password
If Redis is only bound to 127.0.0.1 for local development, running without a password is acceptable for this project.
- Added authentication API wrappers on the React client.
- Added Login, Register, and Change Password pages.
- Added auth illustrations and aligned Login/Register image/form layout.
- Compact Register form into a desktop grid while preserving mobile stacking.
- Added cookie-based private routing and lazy-loaded route boundaries.
- Added authenticated-route preloading from auth pages for faster post-login navigation.
- Avoided an unnecessary immediate refresh-token request right after successful login.
- Added OTP-based registration verification and password-change flow.
- Fixed OTP handling and login redirect behavior on the React client.
- Refined the React UI for mobile, tablet, laptop, and desktop breakpoints.
- Extracted shared SCSS tokens, mixins, page shells, cards, and utility classes.
- Replaced deprecated Sass
@importusage and split large frontend vendor chunks. - Added Redis-backed progress report caching with memory fallback.
- Optimized progress report generation to reduce in-memory work.
- Improved local development cookie behavior for direct HTTPS API calls and proxy-based HTTP calls.
- Updated Docker example configuration for Redis password and localhost port exposure.
- Stopped tracking local
docker-compose.yml; usedocker-compose.yml.exampleas the template. - Cleaned up Ant Design warnings for
Spin,Card, and staticmessageusage.
- Todo list and todo item CRUD.
- Dashboard analytics and progress reporting.
- Quartz.NET background jobs for reports, reminders, summaries, and cleanup.
- MySQL persistence with EF Core migrations.
- Docker and Nginx deployment templates.
- Do not commit
.env,docker-compose.yml,appsettings.Development.json, SMTP credentials, JWT secrets, or database passwords. - Rotate any credential that was pasted into chat, terminal logs, or committed history.
- Use Gmail App Passwords for SMTP.
- Keep admin-only endpoints behind role checks and network controls.
This project is licensed under the MIT License.
Built by Rainy.