Skip to content

Commit 24f7a78

Browse files
committedMar 20, 2025
git push -u origin main
Initial commit
1 parent 941290c commit 24f7a78

24 files changed

+443
-1
lines changed
 

‎README.md

+26-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,26 @@
1-
# projects
1+
<<<<<<< HEAD
2+
# projects
3+
=======
4+
# Habit Tracker
5+
6+
#### Video Demo: https://youtu.be/HcHheAYiY9w
7+
8+
#### Description:
9+
10+
# Secure Habit Tracker
11+
12+
This project is a Flask-based habit tracker web application that enables users to register, log in, and add personal habits securely. The application has been built with a focus on modern security practices and includes the following key features:
13+
14+
- **Secure User Authentication:**
15+
Users register and log in using a system that securely hashes passwords with Werkzeug's `generate_password_hash` and verifies them with `check_password_hash`. This ensures that plain-text passwords are never stored.
16+
17+
- **Data Encryption:**
18+
All habit data entered by the user is encrypted using the Cryptography library's Fernet module before being stored in the SQLite database. This ensures that sensitive user information remains confidential, even if the database is compromised.
19+
20+
- **SQL Injection Prevention:**
21+
The application uses parameterized SQL queries for all database interactions, effectively mitigating the risk of SQL injection attacks.
22+
23+
- **User-Friendly Interface:**
24+
The web interface is built using Flask templates and includes clear navigation for registration, login, adding habits, and viewing your dashboard.
25+
26+
This secure habit tracker demonstrates practical experience in secure web development and is an excellent example of applying security best practices in a real-world application.

‎__pycache__/app.cpython-312.pyc

7.07 KB
Binary file not shown.

‎__pycache__/helpers.cpython-312.pyc

726 Bytes
Binary file not shown.

‎app.py

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import sqlite3
2+
from flask import Flask, render_template, request, redirect, session, flash
3+
from flask_session import Session
4+
from werkzeug.security import generate_password_hash, check_password_hash
5+
from functools import wraps
6+
from security.encryption import encrypt_data, decrypt_data
7+
8+
app = Flask(__name__)
9+
10+
# Configure session
11+
app.config["SESSION_PERMANENT"] = False
12+
app.config["SESSION_TYPE"] = "filesystem"
13+
app.secret_key = "YOUR_SECRET_KEY_HERE" # Replace with your secret key
14+
Session(app)
15+
16+
# Database helper function
17+
def get_db():
18+
conn = sqlite3.connect("habit_tracker.db")
19+
conn.row_factory = sqlite3.Row # Enables dict-like access to rows
20+
return conn
21+
22+
# Custom login_required decorator
23+
def login_required(f):
24+
@wraps(f)
25+
def decorated_function(*args, **kwargs):
26+
if session.get("user_id") is None:
27+
flash("Please log in to continue.")
28+
return redirect("/login")
29+
return f(*args, **kwargs)
30+
return decorated_function
31+
32+
@app.route("/")
33+
@login_required
34+
def index():
35+
"""Display all habits for the logged-in user (decrypted)."""
36+
db = get_db()
37+
user_id = session["user_id"]
38+
rows = db.execute("SELECT id, habit, date FROM habits WHERE user_id = ?", (user_id,)).fetchall()
39+
db.close()
40+
habits = []
41+
for row in rows:
42+
# Decrypt habit text before display
43+
habits.append({
44+
"id": row["id"],
45+
"habit": decrypt_data(row["habit"]),
46+
"date": row["date"]
47+
})
48+
return render_template("index.html", habits=habits)
49+
50+
@app.route("/add_habit", methods=["GET", "POST"])
51+
@login_required
52+
def add_habit():
53+
"""Add a new habit (encrypted) for the logged-in user."""
54+
if request.method == "POST":
55+
habit_text = request.form.get("habit")
56+
if not habit_text:
57+
return render_template("apology.html", message="Must provide a habit description.")
58+
59+
user_id = session["user_id"]
60+
# Encrypt the habit text before storing it in the database
61+
encrypted_habit = encrypt_data(habit_text)
62+
63+
db = get_db()
64+
db.execute("INSERT INTO habits (user_id, habit, date) VALUES (?, ?, DATE('now'))",
65+
(user_id, encrypted_habit))
66+
db.commit()
67+
db.close()
68+
69+
flash("Habit added successfully!")
70+
return redirect("/")
71+
return render_template("add_habit.html")
72+
73+
@app.route("/register", methods=["GET", "POST"])
74+
def register():
75+
"""Register a new user with a hashed password (stored in the 'hash' column)."""
76+
if request.method == "POST":
77+
username = request.form.get("username")
78+
password = request.form.get("password")
79+
confirmation = request.form.get("confirmation")
80+
81+
if not username or not password or not confirmation:
82+
return render_template("apology.html", message="Must provide username and password.")
83+
if password != confirmation:
84+
return render_template("apology.html", message="Passwords do not match.")
85+
86+
hash_pw = generate_password_hash(password)
87+
db = get_db()
88+
try:
89+
# Note: We use column "hash" instead of "password"
90+
db.execute("INSERT INTO users (username, hash) VALUES (?, ?)", (username, hash_pw))
91+
db.commit()
92+
except sqlite3.IntegrityError:
93+
return render_template("apology.html", message="Username already taken.")
94+
finally:
95+
db.close()
96+
97+
flash("Registered successfully! Please log in.")
98+
return redirect("/login")
99+
return render_template("register.html")
100+
101+
@app.route("/login", methods=["GET", "POST"])
102+
def login():
103+
"""Log in an existing user by verifying the hashed password stored in 'hash'."""
104+
session.clear()
105+
if request.method == "POST":
106+
username = request.form.get("username")
107+
password = request.form.get("password")
108+
if not username or not password:
109+
return render_template("apology.html", message="Must provide username and password.")
110+
111+
db = get_db()
112+
user = db.execute("SELECT * FROM users WHERE username = ?", (username,)).fetchone()
113+
db.close()
114+
if user is None:
115+
return render_template("apology.html", message="Invalid username or password.")
116+
# Note: Use user["hash"] to check the password
117+
if not check_password_hash(user["hash"], password):
118+
return render_template("apology.html", message="Invalid username or password.")
119+
120+
session["user_id"] = user["id"]
121+
flash("Logged in successfully!")
122+
return redirect("/")
123+
return render_template("login.html")
124+
125+
@app.route("/logout")
126+
def logout():
127+
"""Log out the current user."""
128+
session.clear()
129+
flash("Logged out successfully!")
130+
return redirect("/login")
131+
132+
@app.route("/apology")
133+
def apology():
134+
"""Generic apology page."""
135+
message = request.args.get("message", "Something went wrong.")
136+
return render_template("apology.html", message=message)
137+
138+
if __name__ == "__main__":
139+
app.run(debug=True)

‎database.db

Whitespace-only changes.
9 Bytes
Binary file not shown.

‎habit_tracker.db

20 KB
Binary file not shown.

‎helpers.py

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from functools import wraps
2+
from flask import redirect, session
3+
4+
def login_required(f):
5+
@wraps(f)
6+
def decorated_function(*args, **kwargs):
7+
if session.get("user_id") is None:
8+
return redirect("/login")
9+
return f(*args, **kwargs)
10+
return decorated_function

‎init_db.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# init_db.py
2+
import sqlite3
3+
4+
def create_tables():
5+
conn = sqlite3.connect('database.db')
6+
cursor = conn.cursor()
7+
# Create users table
8+
cursor.execute("""
9+
CREATE TABLE IF NOT EXISTS users (
10+
id INTEGER PRIMARY KEY AUTOINCREMENT,
11+
username TEXT UNIQUE NOT NULL,
12+
password TEXT NOT NULL
13+
)
14+
""")
15+
# Create habits table with encrypted data
16+
cursor.execute("""
17+
CREATE TABLE IF NOT EXISTS habits (
18+
id INTEGER PRIMARY KEY AUTOINCREMENT,
19+
user_id INTEGER NOT NULL,
20+
habit_data BLOB NOT NULL,
21+
FOREIGN KEY (user_id) REFERENCES users (id)
22+
)
23+
""")
24+
conn.commit()
25+
conn.close()
26+
27+
if __name__ == "__main__":
28+
create_tables()
29+
print("Database initialized.")

‎requirements.txt

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Flask==2.2.3
2+
Flask-Session==0.4.0
3+
cs50
4+
Werkzeug==2.2.3
1.16 KB
Binary file not shown.

‎security/auth.py

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# security/auth.py
2+
import bcrypt
3+
import sqlite3
4+
5+
def hash_password(password: str) -> bytes:
6+
"""
7+
Hash a password for storing.
8+
"""
9+
# Generate a salt and hash the password
10+
salt = bcrypt.gensalt()
11+
hashed = bcrypt.hashpw(password.encode('utf-8'), salt)
12+
return hashed
13+
14+
def verify_password(stored_hash: bytes, password_attempt: str) -> bool:
15+
"""
16+
Check if the provided password matches the stored hash.
17+
"""
18+
return bcrypt.checkpw(password_attempt.encode('utf-8'), sa parameterized query to get the hashed password for the user
19+
cursor.execute("SELECT password FROM users WHERE username = ?", (username,))
20+
result = cursor.fetchone()
21+
conn.close()
22+
23+
if result:
24+
stored_hash = result[0]
25+
# stored_hash might be stored as a string; convert to bytes if necessary
26+
if isinstance(stored_hash, str):
27+
stored_hash = stored_hash.encode('utf-8')
28+
return verify_password(stored_hash, password)
29+
else:
30+
return False

‎security/encryption.py

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# security/encryption.py
2+
from cryptography.fernet import Fernet
3+
4+
# NOTE: In a real-world application, generate the key once and store it securely.
5+
# For this project, you can generate a key and save it in a file or environment variable.
6+
def generate_key() -> bytes:
7+
"""
8+
Generates a new encryption key.
9+
"""
10+
return Fernet.generate_key()
11+
12+
# For demonstration purposes, we create a cipher_suite with a fixed key.
13+
# You can replace 'your-secret-key' with a securely stored key.
14+
# To generate a key, run: key = generate_key(); print(key)
15+
key = b'8B4uFHrRas7fnpmbRjTkfBI5shHRbVKuuY8zXbbkAL4=' # Replace with a valid 32-byte key
16+
cipher_suite = Fernet(key)
17+
18+
def encrypt_data(plain_text: str) -> bytes:
19+
"""
20+
Encrypts the provided plain text data.
21+
"""
22+
return cipher_suite.encrypt(plain_text.encode('utf-8'))
23+
24+
def decrypt_data(encrypted_data: bytes) -> str:
25+
"""
26+
Decrypts the provided encrypted data.
27+
"""
28+
return cipher_suite.decrypt(encrypted_data).decode('utf-8')

‎static/script.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
document.addEventListener("DOMContentLoaded", function() {
2+
console.log("Script loaded!");
3+
4+
// You can add additional JavaScript for interactivity if needed.
5+
// For instance, you could add simple client-side form validation.
6+
});

‎static/styles.css

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
body {
2+
font-family: Arial, sans-serif;
3+
margin: 0;
4+
padding: 0;
5+
background: #f4f4f4;
6+
}
7+
8+
header {
9+
background: #333;
10+
color: #fff;
11+
padding: 1rem;
12+
text-align: center;
13+
}
14+
15+
nav a {
16+
color: #fff;
17+
margin: 0 0.5rem;
18+
text-decoration: none;
19+
}
20+
21+
main {
22+
padding: 1rem;
23+
}
24+
25+
.flashes {
26+
list-style-type: none;
27+
padding: 0;
28+
margin: 1rem 0;
29+
color: green;
30+
}
31+
32+
form {
33+
margin: 1rem 0;
34+
}
35+
36+
form label {
37+
display: block;
38+
margin-top: 0.5rem;
39+
}
40+
41+
form input[type="text"],
42+
form input[type="password"] {
43+
width: 100%;
44+
padding: 0.5rem;
45+
margin-top: 0.25rem;
46+
}
47+
48+
form button {
49+
margin-top: 1rem;
50+
padding: 0.5rem 1rem;
51+
}

‎templates/add_habit.html

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{% extends "layout.html" %}
2+
{% block title %}Add New Habit{% endblock %}
3+
{% block body %}
4+
<h2>Add a New Habit</h2>
5+
<form action="{{ url_for('add_habit') }}" method="post">
6+
<label for="habit">Habit Description:</label>
7+
<input type="text" id="habit" name="habit" placeholder="Enter your habit" required>
8+
<button type="submit">Add Habit</button>
9+
</form>
10+
{% endblock %}

‎templates/apology.html

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% extends "layout.html" %}
2+
{% block title %}Error{% endblock %}
3+
{% block body %}
4+
<h2>Oops!</h2>
5+
<p>{{ message }}</p>
6+
<p><a href="{{ url_for('index') }}">Return to Dashboard</a></p>
7+
{% endblock %}

‎templates/dashboard.html

Whitespace-only changes.

‎templates/dashbord.html

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% extends "layout.html" %}
2+
3+
{% block title %}Dashboard{% endblock %}
4+
5+
{% block main %}
6+
<h1>Welcome, {{ username }}!</h1>
7+
<h2>Your Habit Entries</h2>
8+
{% if habits %}
9+
<ul>
10+
{% for habit in habits %}
11+
<li>{{ habit.habit }} – {{ habit.date }}</li>
12+
{% endfor %}
13+
</ul>
14+
{% else %}
15+
<p>You haven't added any habits yet!</p>
16+
{% endif %}
17+
{% endblock %}

‎templates/index.html

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% extends "layout.html" %}
2+
{% block title %}Dashboard{% endblock %}
3+
{% block body %}
4+
<h2>Your Habits</h2>
5+
{% if habits %}
6+
<ul>
7+
{% for habit in habits %}
8+
<li>{{ habit.habit }} (Added on: {{ habit.date }})</li>
9+
{% endfor %}
10+
</ul>
11+
{% else %}
12+
<p>No habits added yet.</p>
13+
{% endif %}
14+
{% endblock %}

0 commit comments

Comments
 (0)
Please sign in to comment.