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 work by Salman Adnan.
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_hashbefore they touch the database; login verifies withcheck_password_hashrather 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_idto 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
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.