Nobody has started this yet — be first.
Business impact
Nobody can log in. Every owner, with every correct password, gets a 401 invalid_credentials. This is a total outage of the only authenticated role this service has -- no owner can create forms, view submissions, download attachments, export CSVs, or view analytics. The public submission endpoint (unauthenticated) still works, so the outage is invisible from the outside until an owner actually tries to log in -- meaning it is likely to be discovered by a confused customer rather than a monitoring alert, generating a flood of "I know my password is right, your login is broken" support tickets, each of which looks at first like a user error rather than a system bug.
Problem
login() checks the submitted password against the stored hash using werkzeug's check_password_hash, but passes the two arguments in the wrong order. check_password_hash's real signature is check_password_hash(pwhash, password) -- a hash first, the raw candidate password second. The call site here passes the raw password first and the hash second.
Current behavior
POST /api/auth/login with the exact password that was just used to register the account still returns 401 {"error": "invalid_credentials"}.
Expected behavior
A correct (email, password) pair returns 200 with an access_token. An incorrect password still returns 401 invalid_credentials (this side already works and must keep working).
Steps to reproduce
cd flask/feedback_service export FLASK_ENV=testing .venv/bin/python -c " from app import create_app from app.extensions import db
app = create_app('testing') with app.app_context(): db.create_all() client = app.test_client()
client.post('/api/auth/register', json={'email': 'a@example.com', 'password': 'supersecret123'}) resp = client.post('/api/auth/login', json={'email': 'a@example.com', 'password': 'supersecret123'}) print(resp.status_code, resp.get_json()) "
Why this matters
werkzeug.security.check_password_hash(pwhash, password) tries to parse its first argument as a structured hash string (method, salt, digest) and compare the second argument's hash against it. Handed a raw password string as the "hash" to parse, it simply can never match -- it does not raise, it just always returns False. That is what makes this particular argument-order mistake so easy to ship unnoticed in a rushed manual test: it fails closed (safe-looking, no stack trace, a normal 401 response) rather than crashing loudly, so a quick "does it 500?" smoke test will not catch it. Only actually trying to log in with a real, correct password reveals the outage.
Suggested approach
Look up werkzeug's check_password_hash signature (or its sibling, generate_password_hash, used two lines above it in register() for comparison) and check the order of arguments passed at the call site in login() against it, not just that both arguments are present.
Acceptance criteria
Verification
cd flask/feedback_service && FLASK_ENV=testing .venv/bin/python -m pytest ticket_tests/test_ticket_03_login_argument_order.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b fix/login-argument-orderFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/login-argument-orderSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign inweb and worker share the same image and only differ in their command: -- web runs flask db upgrade && flask run, worker runs celery -A celery_worker.celery worker. For a prod-style stack (built image, gunicorn, no bind mount) use docker compose -f docker-compose.prod.yml up --build instead; it refuses to start without SECRET_KEY/JWT_SECRET_KEY actually set in the environment.
cd flask/feedback_service
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # defaults are fine for local dev as-is
export FLASK_APP=wsgi.py FLASK_ENV=development
flask db upgrade # applies migrations to a fresh SQLite DB under instance/
# terminal 1
flask run --port 5000
# terminal 2 (needs a local redis-server running — broker for both Celery and the rate limiter)
celery -A celery_worker.celery worker --loglevel=info
Each ticket in ticket_tests/tickets/0N_*.md names one dedicated test file under ticket_tests/:
cd flask/feedback_service
export FLASK_ENV=testing
.venv/bin/python -m pytest ticket_tests/test_ticket_0N_*.py -v # a single ticket
./ticket_tests/run_tickets.sh # all 7, PASS/FAIL scoreboard
TestingConfig runs Celery tasks eagerly (CELERY_TASK_ALWAYS_EAGER=True, no broker needed) and uses a real, per-test-flushed Redis DB for the rate limiter when Redis is reachable, falling back to Flask-Limiter's in-memory backend otherwise -- either way the real Flask-Limiter code path runs, nothing about rate limiting is mocked. Note ticket_tests/ is not collected by a plain pytest -q from the project root (pytest.ini pins testpaths = tests), so the project's own suite and this practice suite stay independent and must be run explicitly.
Level 1
Fix a bug
Read existing behaviour, correct it.