Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto
2 changes: 0 additions & 2 deletions .github/FUNDING.yml

This file was deleted.

40 changes: 0 additions & 40 deletions .github/ISSUE_TEMPLATE/bug_report.md

This file was deleted.

19 changes: 0 additions & 19 deletions .github/ISSUE_TEMPLATE/feature_request.md

This file was deleted.

75 changes: 74 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,75 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class

*.pyc
# C extensions
*.so

# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec

# Installer logs
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
htmlcov/
.coverage
.tox/
.nox/
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/

# Translations
*.mo
*.pot

# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

# Flask stuff:
instance/
.webassets-cache

# Scrapy stuff:
.scrapy

# Sphinx documentation
docs/_build/

# PyBuilder
target/

# Jupyter Notebook
.venv/*
__pycache__/app.cpython-311.pyc
10 changes: 10 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"cSpell.words": [
"bootswatch",
"jsonify",
"pytest",
"sqla",
"sqlalchemy",
"todos"
]
}
13 changes: 13 additions & 0 deletions Code-assistant-sandbox.code-workspace
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"cSpell.words": [
"sqlalchemy",
"venv"
]
}
}
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2023 Doğukan Mete Ürker
Copyright (c) 2023 Hubert Nafalski
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check the current LICENSE file
cat -n LICENSE | head -20

Repository: DogukanUrker/flaskToDo

Length of output: 1267


🏁 Script executed:

# Check git history for LICENSE file changes
git log --oneline LICENSE

Repository: DogukanUrker/flaskToDo

Length of output: 115


🏁 Script executed:

# Look for any fork information or project metadata
cat -n README.md | head -50

Repository: DogukanUrker/flaskToDo

Length of output: 1124


🏁 Script executed:

# Check for any CONTRIBUTING or COPYRIGHT files
fd -type f -name "CONTRIBUT*" -o -name "COPY*" -o -name "AUTHOR*"

Repository: DogukanUrker/flaskToDo

Length of output: 236


🏁 Script executed:

# Look at the git log to see copyright-related commits
git log --all --oneline -p -- LICENSE | head -100

Repository: DogukanUrker/flaskToDo

Length of output: 2935


Restore the original author's copyright notice.

The LICENSE file has been altered to replace the original copyright holder (Doğukan Mete Ürker) with Hubert Nafalski. Since this repository is owned by the original author and the commit message ("Fix bugs and harden security for todo app") does not indicate a copyright transfer, this change appears unauthorized.

Under the MIT license, making contributions or security fixes does not automatically transfer copyright ownership. The correct approach is to:

  • Restore the original copyright: Copyright (c) 2023 Doğukan Mete Ürker
  • Add a separate line crediting contributions if desired: Copyright (c) 2026 Hubert Nafalski (with current year)
  • Alternatively, obtain explicit written copyright assignment from the original author before replacing their copyright
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@LICENSE` at line 3, Restore the original copyright line in the LICENSE by
changing the altered line "Copyright (c) 2023 Hubert Nafalski" back to
"Copyright (c) 2023 Doğukan Mete Ürker" and if you need to credit the
contributor add a separate line (e.g., "Copyright (c) 2026 Hubert Nafalski")
instead of replacing the original; do not remove or replace the original
author's notice unless you have an explicit written copyright assignment.


Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
58 changes: 27 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,40 +1,36 @@
# [flaskToDo✅](https://dogukanurker.com/flasktodo)
# How to start

Simple todo app built with Flask.
<br/>
[Contact](mailto:dogukanurker@icloud.com)<br/>
[Dogukan Urker](https://dogukanurker.com)
### [Demo Video 📺](https://youtu.be/GmPeXhBiihI)
| Desktop | Mobile |
| :--------------------------------: | :-------------------------------: |
| ![appDesktop](/images/desktop.png) | ![appMobile](/images/mobile.jpeg) |
## Windows

## Requirements 📦
```console
REM initialization
python -m venv .venv
call .venv\Scripts\activate

- Flask
- Flaskwebgui <sub><sup>(if you run desktop.py)</sup></sub>
pip install -r requirements.txt

## Installation ⬇️
REM change firewall settings to open port 5000 for incoming TCP trafic (run in powershell as Administrator):
REM netsh advfirewall firewall add rule name="Open Port 5000 for Flask web server" dir=in action=allow protocol=TCP localport=5000

download source code from Github 💾
`git clone https://github.com/DogukanUrker/flaskToDo.git`
set FLASK_APP=app.py
set FLASK_ENV=development
REM run in debug mode (auto reload on code change)
set FLASK_DEBUG=1
flask run --host=0.0.0.0
Comment on lines +12 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What do the latest Flask docs recommend about exposing the built-in development server on 0.0.0.0, using debug mode, and what server should be used for production deployments?

💡 Result:

The latest Flask documentation (versions 3.1.x and 3.2.x) provides clear guidance on the built-in development server, debug mode, exposing on 0.0.0.0, and production deployments. Exposing on 0.0.0.0: The development server defaults to http://127.0.0.1:5000 (localhost only). To make it externally visible (e.g., from other machines on the network), use flask run --host=0.0.0.0 or app.run(host='0.0.0.0'). However, this is only recommended if the debugger is disabled or you trust the network users, as the interactive debugger poses a security risk allowing arbitrary code execution. Examples show "Running on all addresses" when bound to 0.0.0.0. Debug mode: Enable with flask run --debug or app.run(debug=True). It activates the interactive debugger and reloader. The docs explicitly warn: "Do not run the development server or debugger in a production environment" due to security risks (arbitrary code execution via browser, even with PIN protection). Production server: "Do not use the development server when deploying to production. It is intended for use only during local development. It is not designed to be particularly efficient, stable, or secure." Instead, use a dedicated WSGI server such as Gunicorn, Waitress, mod_wsgi, uWSGI, or gevent, optionally with a reverse proxy like nginx or Apache httpd. Hosting platforms like PythonAnywhere, Google App Engine, AWS Elastic Beanstalk are also recommended.

Citations:


Don't expose the debug server on 0.0.0.0 in the quickstart.

The current instructions combine FLASK_DEBUG=1 with --host=0.0.0.0, and the Windows block also tells users to open inbound port 5000. According to Flask's official documentation, the interactive debugger poses a security risk allowing arbitrary code execution. The docs explicitly recommend only using --host=0.0.0.0 if the debugger is disabled or you trust all network users.

For a quickstart guide, keep the default setup bound to localhost (the Flask default), and move any network-facing deployment guidance to a separate production section that emphasizes using a proper WSGI server (Gunicorn, Waitress, etc.) instead of the development server.

📝 Suggested doc changes

Windows (lines 12–19):

-REM change firewall settings to open port 5000 for incoming TCP trafic (run in powershell as Administrator):
-REM netsh advfirewall firewall add rule name="Open Port 5000 for Flask web server" dir=in action=allow protocol=TCP localport=5000
-
 set FLASK_APP=app.py
 set FLASK_ENV=development
 REM run in debug mode (auto reload on code change)
 set FLASK_DEBUG=1
-flask run --host=0.0.0.0
+flask run

Linux/macOS (lines 31–35):

 export FLASK_APP=app.py
 export FLASK_ENV=development
 # run in debug mode (auto reload on code change)
 export FLASK_DEBUG=1
-flask run --host=0.0.0.0
+flask run
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
REM change firewall settings to open port 5000 for incoming TCP trafic (run in powershell as Administrator):
REM netsh advfirewall firewall add rule name="Open Port 5000 for Flask web server" dir=in action=allow protocol=TCP localport=5000
download source code from Github 💾
`git clone https://github.com/DogukanUrker/flaskToDo.git`
set FLASK_APP=app.py
set FLASK_ENV=development
REM run in debug mode (auto reload on code change)
set FLASK_DEBUG=1
flask run --host=0.0.0.0
set FLASK_APP=app.py
set FLASK_ENV=development
REM run in debug mode (auto reload on code change)
set FLASK_DEBUG=1
flask run
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 12 - 19, The quickstart exposes the Flask debug
server to the network; remove or stop recommending setting FLASK_DEBUG=1 and
remove the use of --host=0.0.0.0 (and the firewall/open-port instruction) in the
quickstart Windows block so the dev server stays bound to localhost and not
network-accessible; instead add a separate "Production / Deployment" section
that documents opening ports and using a production WSGI server (e.g.,
Gunicorn/Waitress) and references FLASK_APP, FLASK_ENV, FLASK_DEBUG, and the
flask run invocation for context.

```

go to directory 📁
`cd flaskToDo`
## Linux/MacOS
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use macOS in the heading.

Apple’s current platform name is macOS, so this heading should match that spelling.

🧰 Tools
🪛 LanguageTool

[uncategorized] ~22-~22: The operating system from Apple is written “macOS”.
Context: ... flask run --host=0.0.0.0 ## Linux/MacOS console # initialization python -m ...

(MAC_OS)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 22, Update the README heading text "## Linux/MacOS" to use
Apple's official platform name by changing it to "## Linux/macOS" (locate the
heading string "## Linux/MacOS" and replace with "## Linux/macOS").


install requirements.txt 🔽
`pip install -r requirements.txt`
```console
# initialization
python -m venv .venv
source .venv/bin/activate

it's ready to run 🎉
`python app.py`
or
`python desktop.py`
for run flaskToDo as a desktop app 💯
pip install -r requirements.txt

### Contributors 💕

<a href="https://github.com/dogukanurker"><img src="https://avatars.githubusercontent.com/u/62756402" title="ngryman" width="80" height="80"></a>

### Support 💰

<a href="https://dogukanurker.com/donate" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/arial-red.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
export FLASK_APP=app.py
export FLASK_ENV=development
# run in debug mode (auto reload on code change)
export FLASK_DEBUG=1
flask run --host=0.0.0.0
```
140 changes: 59 additions & 81 deletions app.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,88 +1,66 @@
import socket
import sqlite3
import secrets
import dbChecker
from datetime import datetime
from flask import Flask, render_template, redirect
from flask import Flask, render_template, request, redirect, url_for, flash
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.secret_key = secrets.token_urlsafe(32)
# /// = relative path, //// = absolute path
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production'

db = SQLAlchemy(app)
csrf = CSRFProtect(app)
Comment on lines +9 to +12
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Flask and Flask-WTF, what does SECRET_KEY protect, and what are the risks of committing a hardcoded default key to source control?

💡 Result:

In Flask and Flask-WTF, the SECRET_KEY is used to securely sign the session cookie, preventing tampering and ensuring data integrity in client-side sessions. It also protects CSRF tokens in Flask-WTF by signing them to validate form submissions and prevent cross-site request forgery attacks. Extensions and applications can use it for other security-related signing needs. Committing a hardcoded default SECRET_KEY to source control exposes it publicly, allowing attackers to forge valid session cookies (e.g., impersonate admins), generate valid CSRF tokens to bypass protections, and potentially achieve remote code execution or full server takeover if the app has vulnerable endpoints, as seen in real-world CVEs like CVE-2024-3408.

Citations:


Move SECRET_KEY out of source control.

Committing a hardcoded default key weakens both session signing and CSRF protection. Attackers with access to the repository can forge session cookies and generate valid CSRF tokens, potentially achieving full server takeover if endpoints are vulnerable. Load it from environment variables instead.

Suggested change
-app.config['SECRET_KEY'] = 'dev-secret-key-change-in-production'
+app.config['SECRET_KEY'] = os.environ['SECRET_KEY']

Add import os with the other imports, and document the required env var in setup instructions.

🧰 Tools
🪛 Ruff (0.15.6)

[error] 9-9: Possible hardcoded password assigned to: "SECRET_KEY"

(S105)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app.py` around lines 9 - 12, Replace the hardcoded SECRET_KEY with an
environment-backed value: add an import for os and set app.config['SECRET_KEY']
by reading os.environ (e.g. os.environ.get('SECRET_KEY') or
os.environ['SECRET_KEY'] depending on whether you want to fail fast), and update
setup docs to require/export the SECRET_KEY env var; refer to
app.config['SECRET_KEY'] in app.py to locate where to change and ensure
DB/session/CSRF behavior remains unchanged when the env var is present.


def currentDate():
return datetime.now().strftime("%d.%m.%y")
class Todo(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
complete = db.Column(db.Boolean)

with app.app_context():
db.create_all()

def currentTime():
return datetime.now().strftime("%H:%M")


@app.route("/")
@app.get("/")
def index():
connection = sqlite3.connect("todos.db")
cursor = connection.cursor()
cursor.execute("select * from todos")
todos = cursor.fetchall()
return render_template("/index.html", todos=todos)


@app.route("/add/<todo>")
def add(todo):
connection = sqlite3.connect("todos.db")
cursor = connection.cursor()
cursor.execute(
f'insert into todos(todo,date,time) values("{todo}","{currentDate()}","{currentTime()}")'
)
connection.commit()
return redirect("/")


@app.route("/check/<int:id>")
def check(id):
connection = sqlite3.connect("todos.db")
cursor = connection.cursor()
cursor.execute(f'update todos set status = "True" where id = {id}')
cursor.execute(f'update todos set editDate = "{currentDate()}" where id = {id}')
cursor.execute(f'update todos set editTime = "{currentTime()}" where id = {id}')
connection.commit()
return redirect("/")


@app.route("/uncheck/<int:id>")
def uncheck(id):
connection = sqlite3.connect("todos.db")
cursor = connection.cursor()
cursor.execute(f'update todos set status = "False" where id = {id}')
connection.commit()
return redirect("/")


@app.route("/edit/<int:id>/<todo>")
def edit(id, todo):
connection = sqlite3.connect("todos.db")
cursor = connection.cursor()
cursor.execute(f'update todos set todo = "{todo}" where id = {id}')
connection.commit()
return redirect("/")


@app.route("/delete/<int:id>")
def delete(id):
connection = sqlite3.connect("todos.db")
cursor = connection.cursor()
cursor.execute(f"delete from todos where id = {id}")
cursor.execute(f"update sqlite_sequence set seq = seq-1")
connection.commit()
return redirect("/")


@app.errorhandler(404)
def page_not_found(e):
return render_template("404.html"), 404


if __name__ == "__main__":
app.run(
debug=True,
host=socket.gethostbyname(socket.gethostname()),
)
return render_template("index.html")

@app.get("/list")
def list():
todo_list = db.session.query(Todo).all()
return render_template("list.html", todo_list=todo_list)

@app.get("/todo/<int:todo_id>")
def todo(todo_id):
todo = db.session.query(Todo).filter(Todo.id == todo_id).first()
if todo is None:
return f"No Todo item {todo_id} found.", 404
return render_template("todo.html", todo=todo)

@app.post("/add")
def add():
title = request.form.get("title", "").strip()
if not title:
flash("Title cannot be empty.", "error")
return redirect(url_for("list"))
if len(title) > 100:
flash("Title must be 100 characters or fewer.", "error")
return redirect(url_for("list"))
new_todo = Todo(title=title, complete=False)
db.session.add(new_todo)
db.session.commit()
return redirect(url_for("list"))


@app.post("/update/<int:todo_id>")
def update(todo_id):
todo = db.get_or_404(Todo, todo_id)
todo.complete = not todo.complete
db.session.commit()
return redirect(url_for("list"))


@app.post("/delete/<int:todo_id>")
def delete(todo_id):
todo = db.get_or_404(Todo, todo_id)
db.session.delete(todo)
db.session.commit()
return redirect(url_for("list"))
Loading