A small full-stack task manager: ASP.NET Core (C#) API + React (TypeScript) frontend, with real per-user auth and ownership enforcement, SQLite persistence, and timezone-aware due dates.
simple-task-manager/
├── README.md
├── backend/
│ ├── TodoApi/ # ASP.NET Core Web API — controllers, services, repositories, EF Core models/migrations
│ └── TodoApi.Tests/ # xUnit unit tests (Unit/) and integration tests (Integration/)
└── frontend/ # React + TypeScript SPA (Vite)
└── src/
├── api/ # fetch wrapper + shared types
├── auth/ # AuthContext, login/register forms
└── tasks/ # task list view, shared task form, task row
cd backend/TodoApi
dotnet run- Runs on
http://localhost:5080by default (seeProperties/launchSettings.jsonif you want the HTTPS profile instead). - Applies EF Core migrations automatically on startup (
Database.Migrate()), creatingtodo.db(SQLite) next to the project on first run. No manual migration step needed. - JWT signing secret lives in
appsettings.jsonunderJwt:Secret. It's a placeholder development-only value checked into source for convenience — in a real deployment this would move to an environment variable ordotnet user-secretsand be rotated.
cd frontend
npm install && npm run dev- Runs on
http://localhost:5173(Vite default), configured via CORS on the backend to allow that origin. - Copy
.env.exampleto.envif the backend isn't athttp://localhost:5080.
cd backend/TodoApi.Tests
dotnet testTwo suites, split by folder:
Unit/Services/—TaskService/AuthServicetested in isolation with mocked repositories (Moq), no database involved. Fast (~200ms for the whole suite).Integration/— real integration tests againstWebApplicationFactory+ a temp SQLite file per test class (not EF InMemory) — see "What Was Built" for why. Exercises ownership, validation, and the auth+timezone happy path end-to-end over HTTP.
Run just one suite with dotnet test --filter "FullyQualifiedName~Unit" or ~Integration.
- Auth: register/login with hashed passwords (
PasswordHasher<User>), JWT bearer tokens,[Authorize]on every task endpoint. - Data model:
User→TaskList(one per user, enforced by a unique index) →TaskItem. The list layer is invisible in the UI for now; it's there so "multiple lists per user" is a schema no-op later instead of a migration. - Task CRUD: create/read (list + get-by-id)/update/delete, all resolved through the current user's list — a task lookup never bypasses list-based ownership scoping, even for the get-by-id endpoint.
- Ownership enforcement: every task read/write is scoped to
TaskListRepository.GetByUserIdAsync(currentUserId). A task that exists but belongs to someone else returns404, not403— this avoids confirming the row exists at all. - Timezone-aware due dates: the client sends a plain calendar date (
dueDate) plus its IANA timezone id (timeZoneId, fromIntl.DateTimeFormat().resolvedOptions().timeZone). The backend computes local midnight in that timezone and converts it to a UTC instant viaTimeZoneInfo.ConvertTimeToUtc. That UTC instant is the only thing stored. Display re-derives the calendar date from that instant using the viewing browser's current timezone. - Persistence: SQLite via EF Core (not EF InMemory) — data survives a backend restart.
- Repository layer: a real generic
IRepository<T>/BaseRepository<T>(abstract), withIUserRepository(+GetByEmailAsync) andITaskListRepository(+GetByUserIdAsync) adding entity-specific query methods.ITaskRepositorydoesn't need any extra methods, since every task lookup goes through the owningTaskList, never a bare task ID. - Frontend: React + TypeScript (Vite), client-side routing (login/register/tasks), an
AuthContextstoring the JWT inlocalStorage, and optimistic-feeling UI updates (list state updates from the response, no full refetch needed after create/edit/delete/toggle). Create and edit share a singleTaskFormcomponent: the create form is hidden behind an "Add Task" button, and only one form (add or edit) is ever open at a time. Loading and error states are covered for create/edit/delete;
- One task list per user. Not a UI concept yet — every user's list is auto-created on register and never surfaced. The unique index on
TaskList.UserIdis what actually enforces this, not just app-level convention. - Cross-timezone display is expected behavior, not a bug. If a task's due date was set while the browser was in one timezone and later viewed from another, the displayed date reflects the current browser's timezone at read time, because only the UTC instant is stored. E.g., a task due "Aug 1" set from New York, viewed from Tokyo, could show as Aug 1 or Aug 2 depending on the exact UTC offset difference — this is correct per the stored instant, not a translation bug.
- JWTs don't rotate or refresh. A 7-day expiry is hardcoded; there's no refresh-token flow.
- No rate limiting or lockout on login/register. not fine for a real deployment with a real attack surface.
- Password reset / email verification
- Roles or multi-tenancy beyond per-user ownership (no admin, no shared lists)
- Multiple lists per user (schema supports it; no UI or endpoint surfaces it)
- Pagination, sorting, or filtering on the task list
- Refresh tokens / token revocation
- Add a
PATCH /api/tasks/{id}/completeendpoint instead of routing toggle-complete through the fullPUT, to avoid resending the whole task body for a one-field change. - Surface the
TaskListlayer in the UI (multiple named lists per user) — the schema is already there. - Add a few frontend component tests (React Testing Library) around the validation and error-display paths, which are currently only covered by manual/E2E verification, not automated frontend tests.