Salman Adnan

flask-todo-list

Flask TODO List

A to-do list you use in a web browser: each person signs in and sees only their own tasks, ticking them off without the page reloading. The story worth telling is the fix. Passwords used to be stored as plain readable words, so anyone reaching the database could read them. Now only a scrambled version is kept, and an old readable password no longer works.

Solo

Solo work by Salman Adnan.

Done and Gone: a task card gets ticked, flares, and leaves as dust; the small satisfaction of clearing a list, which is most of what the app is. Live and interactive: drag it to orbit, scroll or pinch to zoom. Open full screen
The real Flask TODO List app, showing a live-created account and tasks.
Live local run of the actual app.
2SQLAlchemy models, one-to-many
8flows verified by hand end to end

Overview

A small multi-user TODO list web app built with Flask, SQLAlchemy, and Flask-Login. Each user registers an account, logs in, and keeps their own private list of tasks, managed through an AJAX interface that falls back cleanly to plain form posts.

This was built as a practice project for wiring together the core pieces of a server-rendered Flask app: a relational model with a foreign key relationship between user and tasks, session-based auth with Flask-Login, server-side form handling with flashed validation messages, and progressive-enhancement JavaScript that upgrades the same routes to AJAX without needing a separate API layer.

Key features

  • Register and log in with a name, email, and password. Passwords are hashed with scrypt via werkzeug.security.generate_password_hash before they touch the database; login verifies with check_password_hash rather than a raw string comparison.
  • A card-based task list where ticking a task marks it complete (strikethrough, faded row) without removing it, and a separate delete button removes it entirely, backed by distinct /toggle/<id> and /delete/<id> routes.
  • Add, complete/uncomplete, and delete all run over fetch() calls that patch the DOM directly with no full page reload, but each route still accepts a plain form POST and redirects if the request didn't come from the fetch-based JS.
  • Duplicate or empty tasks are rejected with a clear message on both the AJAX and no-JS paths, instead of a raw error or a silently bad row.
  • Each user only ever sees their own tasks: every task query is scoped by author_id to the logged-in user.

Verification

Everything was verified by hand and by script against a running instance: registering a user, confirming the stored password is a hash and not the raw string, logging in with the correct password, confirming a simulated legacy plaintext password now fails to log in, adding a duplicate/empty task, toggling a task complete and back, deleting a task, and logging out, all through direct HTTP requests. There's no automated test suite yet; that's listed plainly as a gap, not glossed over.

Tech stack

  • Python
  • Flask
  • SQLAlchemy
  • Flask-Login
  • SQLite
  • JavaScript (fetch/AJAX)

A challenge worth noting

Passwords used to be stored and compared in plain text: the User.password column held the raw string and login compared it directly. That's fixed now with scrypt hashing on register and verified hashing on login, confirmed directly by simulating a pre-fix account and watching it correctly fail to log in against the new check. Separately, the original delete route queried the database by current_user.id before even checking the request was a POST or that anyone was logged in, which would throw on Flask-Login's anonymous user object; adding @login_required and a null check on the fetched row closed that gap.

Book a call

Let's talk about what you're building.

Pick a slot below. No forms, no back-and-forth emails.