Skip to content

Commit b514dab

Browse files
committed
Add authentication features and environment configuration
- Create .env.example for environment variables - Update .gitignore to exclude .env file - Add python-dotenv to requirements - Implement PrivateRoute component for protected routes - Enhance Sidebar and UserMenu components for user authentication - Update server.py to load environment variables - Add login_required decorator to theme routes - Refactor useFetch hook to include authorization token
1 parent 4d886cd commit b514dab

22 files changed

Lines changed: 919 additions & 45 deletions

File tree

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
USER_ADMIN=admin123
2+
USER_TEST=test123

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
#
44
puzzles/*
55
!puzzles/sample
6-
__pycache__/
6+
__pycache__/
7+
.env

app/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from .utils.loader import PuzzlesLoader
55

66
app = Flask(__name__, static_folder='../frontend/dist')
7+
app.secret_key = 'your_secret_key_here' # Add a secret key for sessions
8+
79
loader = PuzzlesLoader()
810

911
swagger_config = {
@@ -28,4 +30,4 @@ def serve(path):
2830
else:
2931
return send_from_directory(app.static_folder, 'index.html')
3032

31-
from .routes import health, puzzles, themes
33+
from .routes import health, puzzles, themes, auth

app/auth.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import uuid
2+
import os
3+
import hashlib
4+
import time
5+
from functools import wraps
6+
from flask import request, abort, session, jsonify
7+
8+
# In-memory user storage
9+
users = {}
10+
sessions = {}
11+
12+
def generate_session_id():
13+
return str(uuid.uuid4())
14+
15+
def hash_password(password, salt=None):
16+
if salt is None:
17+
salt = uuid.uuid4().hex
18+
hashed = hashlib.sha256((password + salt).encode()).hexdigest()
19+
return f"{salt}:{hashed}"
20+
21+
def check_password(password, hashed_password):
22+
salt, hashed = hashed_password.split(":")
23+
return hashed == hashlib.sha256((password + salt).encode()).hexdigest()
24+
25+
def register_user(username, password):
26+
if username in users:
27+
return False, "Username already exists"
28+
29+
if len(password) < 5:
30+
return False, "Password must be at least 5 characters"
31+
32+
users[username] = {
33+
"username": username,
34+
"password": hash_password(password),
35+
"created_at": time.time()
36+
}
37+
return True, "User registered successfully"
38+
39+
def authenticate_user(username, password):
40+
if username not in users:
41+
return False, "Invalid username or password"
42+
43+
user = users[username]
44+
if not check_password(password, user["password"]):
45+
return False, "Invalid username or password"
46+
47+
session_id = generate_session_id()
48+
sessions[session_id] = {
49+
"username": username,
50+
"created_at": time.time()
51+
}
52+
53+
return True, session_id
54+
55+
def logout_user(session_id):
56+
if session_id in sessions:
57+
del sessions[session_id]
58+
return True
59+
60+
def delete_user(username):
61+
if username in users:
62+
del users[username]
63+
# Remove any active sessions for this user
64+
for session_id in list(sessions.keys()):
65+
if sessions[session_id]["username"] == username:
66+
del sessions[session_id]
67+
return True
68+
return False
69+
70+
def is_authenticated(session_id):
71+
return session_id in sessions
72+
73+
def get_user_from_session(session_id):
74+
if session_id in sessions:
75+
username = sessions[session_id]["username"]
76+
return users.get(username)
77+
return None
78+
79+
def login_required(f):
80+
@wraps(f)
81+
def decorated_function(*args, **kwargs):
82+
auth_header = request.headers.get("Authorization")
83+
84+
if not auth_header or not auth_header.startswith("Bearer "):
85+
return jsonify({"error": "Unauthorized"}), 401
86+
87+
session_id = auth_header.split("Bearer ")[1]
88+
89+
if not is_authenticated(session_id):
90+
return jsonify({"error": "Unauthorized"}), 401
91+
92+
return f(*args, **kwargs)
93+
return decorated_function
94+
95+
def create_user_from_env():
96+
"""
97+
Create users from environment variables.
98+
Environment variables should be in the format USER_<username>=<password>
99+
"""
100+
user_count = 0
101+
for key, value in os.environ.items():
102+
if key.startswith("USER_"):
103+
username = key[5:].lower().replace("_", "-")
104+
password = value
105+
print(f"Registering user {username} with password {password}")
106+
register_user(username, password)
107+
user_count += 1
108+
109+
if user_count == 0:
110+
print("No users found in environment variables.")
111+
print("Using a default u:{admin} p:{admin} user.")
112+
register_user("admin", "admin")

app/routes/auth.py

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
from flask import request, jsonify
2+
from .. import app
3+
from ..auth import register_user, authenticate_user, logout_user, delete_user, get_user_from_session, is_authenticated
4+
5+
@app.route('/auth/register', methods=['POST'])
6+
def register():
7+
"""
8+
Register a new user
9+
---
10+
tags:
11+
- Authentication
12+
parameters:
13+
- name: body
14+
in: body
15+
required: true
16+
schema:
17+
type: object
18+
properties:
19+
username:
20+
type: string
21+
password:
22+
type: string
23+
responses:
24+
200:
25+
description: User registered successfully
26+
400:
27+
description: Registration failed
28+
"""
29+
data = request.json
30+
username = data.get('username')
31+
password = data.get('password')
32+
33+
if not username or not password:
34+
return jsonify({"error": "Username and password are required"}), 400
35+
36+
success, message = register_user(username, password)
37+
38+
if success:
39+
return jsonify({"message": message}), 200
40+
else:
41+
return jsonify({"error": message}), 400
42+
43+
@app.route('/auth/login', methods=['POST'])
44+
def login():
45+
"""
46+
Login a user
47+
---
48+
tags:
49+
- Authentication
50+
parameters:
51+
- name: body
52+
in: body
53+
required: true
54+
schema:
55+
type: object
56+
properties:
57+
username:
58+
type: string
59+
password:
60+
type: string
61+
responses:
62+
200:
63+
description: Login successful
64+
401:
65+
description: Login failed
66+
"""
67+
data = request.json
68+
username = data.get('username')
69+
password = data.get('password')
70+
71+
if not username or not password:
72+
return jsonify({"error": "Username and password are required"}), 400
73+
74+
success, result = authenticate_user(username, password)
75+
76+
if success:
77+
return jsonify({
78+
"message": "Login successful",
79+
"token": result,
80+
"username": username
81+
}), 200
82+
else:
83+
return jsonify({"error": result}), 401
84+
85+
@app.route('/auth/logout', methods=['POST'])
86+
def logout():
87+
"""
88+
Logout a user
89+
---
90+
tags:
91+
- Authentication
92+
parameters:
93+
- name: Authorization
94+
in: header
95+
type: string
96+
required: true
97+
description: Bearer token
98+
responses:
99+
200:
100+
description: Logout successful
101+
"""
102+
auth_header = request.headers.get('Authorization')
103+
104+
if not auth_header or not auth_header.startswith('Bearer '):
105+
return jsonify({"message": "Logged out"}), 200
106+
107+
session_id = auth_header.split('Bearer ')[1]
108+
logout_user(session_id)
109+
110+
return jsonify({"message": "Logged out successfully"}), 200
111+
112+
@app.route('/auth/user', methods=['GET'])
113+
def get_user():
114+
"""
115+
Get current user information
116+
---
117+
tags:
118+
- Authentication
119+
parameters:
120+
- name: Authorization
121+
in: header
122+
type: string
123+
required: true
124+
description: Bearer token
125+
responses:
126+
200:
127+
description: User information
128+
401:
129+
description: Not authenticated
130+
"""
131+
auth_header = request.headers.get('Authorization')
132+
133+
if not auth_header or not auth_header.startswith('Bearer '):
134+
return jsonify({"error": "Not authenticated"}), 401
135+
136+
session_id = auth_header.split('Bearer ')[1]
137+
user = get_user_from_session(session_id)
138+
139+
if not user:
140+
return jsonify({"error": "Not authenticated"}), 401
141+
142+
return jsonify({
143+
"username": user["username"],
144+
"created_at": user["created_at"]
145+
}), 200
146+
147+
@app.route('/auth/delete-account', methods=['DELETE'])
148+
def delete_account():
149+
"""
150+
Delete user account
151+
---
152+
tags:
153+
- Authentication
154+
parameters:
155+
- name: Authorization
156+
in: header
157+
type: string
158+
required: true
159+
description: Bearer token
160+
responses:
161+
200:
162+
description: Account deleted successfully
163+
401:
164+
description: Not authenticated
165+
"""
166+
auth_header = request.headers.get('Authorization')
167+
168+
if not auth_header or not auth_header.startswith('Bearer '):
169+
return jsonify({"error": "Not authenticated"}), 401
170+
171+
session_id = auth_header.split('Bearer ')[1]
172+
user = get_user_from_session(session_id)
173+
174+
if not user:
175+
return jsonify({"error": "Not authenticated"}), 401
176+
177+
username = user["username"]
178+
if delete_user(username):
179+
return jsonify({"message": "Account deleted successfully"}), 200
180+
else:
181+
return jsonify({"error": "Failed to delete account"}), 400
182+
183+
@app.route('/auth/check', methods=['GET'])
184+
def check_auth():
185+
"""
186+
Check if user is authenticated
187+
---
188+
tags:
189+
- Authentication
190+
parameters:
191+
- name: Authorization
192+
in: header
193+
type: string
194+
required: true
195+
description: Bearer token
196+
responses:
197+
200:
198+
description: Authentication status
199+
"""
200+
auth_header = request.headers.get('Authorization')
201+
202+
if not auth_header or not auth_header.startswith('Bearer '):
203+
return jsonify({"authenticated": False}), 200
204+
205+
session_id = auth_header.split('Bearer ')[1]
206+
authenticated = is_authenticated(session_id)
207+
208+
return jsonify({"authenticated": authenticated}), 200

0 commit comments

Comments
 (0)