Skip to content

Commit 2fbc164

Browse files
authored
Merge pull request #13 from jantman/pr-11-fixes
Fix authentication issues from PR #11
2 parents 5299f99 + 723c3b4 commit 2fbc164

11 files changed

Lines changed: 110 additions & 142 deletions

File tree

‎.dockerignore‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ node_modules/
3535
# Environment files (secrets)
3636
.env
3737
.env.*
38-
!.env.example
3938
!.env.docker.example
4039

4140
# Instance data (local database and uploads)

‎.env.docker.example‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ MYSQL_ROOT_PASSWORD=change-this-root-password
1818
# MariaDB application user password (production only)
1919
MYSQL_PASSWORD=change-this-password
2020

21-
# Admin user password for initial setup
22-
KIOSK_ADMIN_PASSWORD=change-this-admin-password
21+
# Admin user password for initial setup (default matches README quick start)
22+
# IMPORTANT: Change this to a secure password for production!
23+
KIOSK_ADMIN_PASSWORD=admin
2324

2425
# =============================================================================
2526
# OPTIONAL - These have sensible defaults

‎.env.example‎

Lines changed: 0 additions & 18 deletions
This file was deleted.

‎docker-compose.prod.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Docker Compose for production deployment
22
# Usage:
33
# mkdir -p .docker-data/prod/uploads .docker-data/prod/mariadb
4-
# sudo chown -R 1000:1000 .docker-data/prod/uploads
4+
# sudo chown -R 1000:1000 .docker-data/prod/uploads .docker-data/prod/mariadb
55
# docker-compose -f docker-compose.prod.yml up -d
66
#
77
# Before running:
88
# 1. Create data directories with correct ownership:
99
# mkdir -p .docker-data/prod/uploads .docker-data/prod/mariadb
10-
# sudo chown -R 1000:1000 .docker-data/prod/uploads
10+
# sudo chown -R 1000:1000 .docker-data/prod/uploads .docker-data/prod/mariadb
1111
# 2. Copy .env.docker.example to .env and configure all required variables
1212
# 3. Generate a secure SECRET_KEY
1313
# 4. Set strong passwords for database and admin user

‎kiosk_show_replacement/auth/decorators.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def login_required(f: Callable[..., Any]) -> Callable[..., Any]:
3131
@wraps(f)
3232
def decorated_function(*args: Any, **kwargs: Any) -> Any:
3333
if not is_authenticated():
34-
return redirect(url_for("auth.login", next=request.url))
34+
return redirect(url_for("serve_admin"))
3535
return f(*args, **kwargs)
3636

3737
return decorated_function
@@ -51,7 +51,7 @@ def admin_required(f: Callable[..., Any]) -> Callable[..., Any]:
5151
@wraps(f)
5252
def decorated_function(*args: Any, **kwargs: Any) -> Any:
5353
if not is_authenticated():
54-
return redirect(url_for("auth.login", next=request.url))
54+
return redirect(url_for("serve_admin"))
5555

5656
if not is_admin():
5757
current_app.logger.warning(

‎kiosk_show_replacement/auth/views.py‎

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -44,80 +44,82 @@ def login() -> Any:
4444
"""
4545
Handle user login with real authentication.
4646
47-
GET: Display login form
48-
POST: Process login credentials (validates against database)
47+
GET: Redirect to React admin interface (which handles login UI)
48+
POST: Process login credentials (used by unit tests for session setup)
4949
5050
Returns:
51-
Response: Login form template or redirect to dashboard
51+
Response: Redirect to /admin/ for GET, or process login for POST
5252
"""
53-
if request.method == "POST":
54-
username = request.form.get("username", "").strip()
55-
password = request.form.get("password", "")
56-
57-
# Basic validation
58-
if not username:
59-
flash("Username is required", "error")
60-
return render_template("auth/login.html")
61-
62-
if not password:
63-
flash("Password is required", "error")
64-
return render_template("auth/login.html")
65-
66-
# Real authentication - validate credentials
67-
try:
68-
user = _authenticate_user(username, password)
69-
except RuntimeError as e:
70-
# Database initialization error
71-
error_message = str(e)
72-
flash(error_message, "error")
73-
return render_template("auth/login.html")
74-
75-
if user is None:
76-
# Authentication failed - invalid credentials
77-
flash("Invalid username or password", "error")
78-
return render_template("auth/login.html")
79-
80-
if user == "inactive":
81-
# User account is inactive
82-
flash("Account is inactive. Please contact an administrator.", "error")
83-
return render_template("auth/login.html")
84-
85-
# At this point, user must be a User object (not None or "inactive")
86-
# Type narrowing: cast to User for mypy
87-
authenticated_user = cast(User, user)
88-
89-
# Authentication successful
90-
authenticated_user.last_login_at = datetime.now(timezone.utc)
91-
db.session.commit()
92-
93-
# Set up session
94-
session["user_id"] = authenticated_user.id
95-
session["username"] = authenticated_user.username
96-
session["is_admin"] = authenticated_user.is_admin
53+
# GET requests redirect to React login
54+
if request.method == "GET":
55+
return redirect(url_for("serve_admin"))
9756

98-
current_app.logger.info(
99-
"User login successful",
100-
extra={
101-
"user_id": authenticated_user.id,
102-
"username": authenticated_user.username,
103-
"action": "login",
104-
"ip_address": request.remote_addr,
105-
},
106-
)
57+
# POST requests process login (for unit tests and backwards compatibility)
58+
username = request.form.get("username", "").strip()
59+
password = request.form.get("password", "")
10760

108-
flash(f"Welcome, {authenticated_user.username}!", "success")
61+
# Basic validation
62+
if not username:
63+
flash("Username is required", "error")
64+
return render_template("auth/login.html")
10965

110-
# Redirect to next page or dashboard
111-
# Validate next_page to prevent open redirect attacks
112-
next_page = request.args.get("next")
113-
if next_page:
114-
# Only allow relative URLs (starting with /) to prevent open redirects
115-
# Also reject protocol-relative URLs (starting with //)
116-
if next_page.startswith("/") and not next_page.startswith("//"):
117-
return redirect(next_page)
118-
return redirect(url_for("dashboard.index"))
66+
if not password:
67+
flash("Password is required", "error")
68+
return render_template("auth/login.html")
11969

120-
return render_template("auth/login.html")
70+
# Real authentication - validate credentials
71+
try:
72+
user = _authenticate_user(username, password)
73+
except RuntimeError as e:
74+
# Database initialization error
75+
error_message = str(e)
76+
flash(error_message, "error")
77+
return render_template("auth/login.html")
78+
79+
if user is None:
80+
# Authentication failed - invalid credentials
81+
flash("Invalid username or password", "error")
82+
return render_template("auth/login.html")
83+
84+
if user == "inactive":
85+
# User account is inactive
86+
flash("Account is inactive. Please contact an administrator.", "error")
87+
return render_template("auth/login.html")
88+
89+
# At this point, user must be a User object (not None or "inactive")
90+
# Type narrowing: cast to User for mypy
91+
authenticated_user = cast(User, user)
92+
93+
# Authentication successful
94+
authenticated_user.last_login_at = datetime.now(timezone.utc)
95+
db.session.commit()
96+
97+
# Set up session
98+
session["user_id"] = authenticated_user.id
99+
session["username"] = authenticated_user.username
100+
session["is_admin"] = authenticated_user.is_admin
101+
102+
current_app.logger.info(
103+
"User login successful",
104+
extra={
105+
"user_id": authenticated_user.id,
106+
"username": authenticated_user.username,
107+
"action": "login",
108+
"ip_address": request.remote_addr,
109+
},
110+
)
111+
112+
flash(f"Welcome, {authenticated_user.username}!", "success")
113+
114+
# Redirect to next page or dashboard
115+
# Validate next_page to prevent open redirect attacks
116+
next_page = request.args.get("next")
117+
if next_page:
118+
# Only allow relative URLs (starting with /) to prevent open redirects
119+
# Also reject protocol-relative URLs (starting with //)
120+
if next_page.startswith("/") and not next_page.startswith("//"):
121+
return redirect(next_page)
122+
return redirect(url_for("dashboard.index"))
121123

122124

123125
@bp.route("/logout")
@@ -146,7 +148,7 @@ def logout() -> Any:
146148
)
147149

148150
flash("You have been logged out successfully.", "info")
149-
return redirect(url_for("auth.login"))
151+
return redirect(url_for("serve_admin"))
150152

151153

152154
@bp.route("/status")

‎kiosk_show_replacement/dashboard/views.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ def index() -> Union[str, Response]:
3939
Main dashboard landing page.
4040
4141
Displays system overview, recent activity, and navigation options.
42-
Redirects to login if user is not authenticated.
42+
Redirects to React admin interface if user is not authenticated.
4343
4444
Returns:
4545
Union[str, Response]: Rendered template or redirect response
4646
"""
4747
current_user = get_current_user()
4848
if not current_user:
49-
return redirect(url_for("auth.login"))
49+
return redirect(url_for("serve_admin"))
5050

5151
# Get dashboard statistics
5252
try:

‎kiosk_show_replacement/main/__init__.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,11 @@ def index() -> Any:
2323
Application home page.
2424
2525
Returns:
26-
Response: Redirect to dashboard if authenticated, otherwise login
26+
Response: Redirect to React admin interface
2727
"""
28-
from ..auth.decorators import is_authenticated
29-
30-
if is_authenticated():
31-
return redirect(url_for("main.dashboard"))
32-
else:
33-
return redirect(url_for("auth.login"))
28+
# Always redirect to React admin interface
29+
# React handles authentication state and shows login if needed
30+
return redirect(url_for("serve_admin"))
3431

3532

3633
@bp.route("/dashboard")

‎kiosk_show_replacement/templates/auth/login.html‎

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,36 +29,30 @@ <h4 class="mb-0">
2929
<label for="username" class="form-label">
3030
<i class="fas fa-user me-1"></i>Username
3131
</label>
32-
<input
33-
type="text"
34-
class="form-control"
35-
id="username"
36-
name="username"
32+
<input
33+
type="text"
34+
class="form-control"
35+
id="username"
36+
name="username"
3737
required
3838
autofocus
39-
placeholder="Enter any username"
39+
placeholder="Username"
4040
value="{{ request.form.get('username', '') }}"
4141
>
42-
<div class="form-text">
43-
Any username is accepted - new users will be created automatically.
44-
</div>
4542
</div>
4643

4744
<div class="mb-3">
4845
<label for="password" class="form-label">
4946
<i class="fas fa-lock me-1"></i>Password
5047
</label>
51-
<input
52-
type="password"
53-
class="form-control"
54-
id="password"
55-
name="password"
48+
<input
49+
type="password"
50+
class="form-control"
51+
id="password"
52+
name="password"
5653
required
57-
placeholder="Enter any password"
54+
placeholder="Password"
5855
>
59-
<div class="form-text">
60-
Any password is accepted for development/testing purposes.
61-
</div>
6256
</div>
6357

6458
<div class="d-grid">
@@ -68,12 +62,6 @@ <h4 class="mb-0">
6862
</div>
6963
</form>
7064
</div>
71-
<div class="card-footer text-center text-muted">
72-
<small>
73-
<i class="fas fa-info-circle me-1"></i>
74-
Development Mode: Permissive Authentication
75-
</small>
76-
</div>
7765
</div>
7866
</div>
7967
</div>

‎tests/unit/test_auth.py‎

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,11 @@ def test_get_current_user_without_session(self, app):
8888
class TestAuthenticationViews:
8989
"""Test authentication views and login/logout functionality."""
9090

91-
def test_login_page_accessible(self, client):
92-
"""Test login page is accessible."""
91+
def test_login_page_redirects_to_admin(self, client):
92+
"""Test GET /auth/login redirects to React admin interface."""
9393
response = client.get("/auth/login")
94-
assert response.status_code == 200
95-
assert b"Login" in response.data
94+
assert response.status_code == 302
95+
assert "/admin" in response.location
9696

9797
def test_login_fails_for_nonexistent_user(self, client, app):
9898
"""Test login fails for a user that doesn't exist."""
@@ -200,16 +200,16 @@ def test_login_post_empty_password(self, client):
200200
assert response.status_code == 200
201201
assert b"Password is required" in response.data
202202

203-
def test_logout_redirects_to_login(self, client):
204-
"""Test logout redirects to login page."""
203+
def test_logout_redirects_to_admin(self, client):
204+
"""Test logout redirects to React admin interface."""
205205
# First login
206206
with client.session_transaction() as sess:
207207
sess["user_id"] = 123
208208
sess["username"] = "testuser"
209209

210-
response = client.get("/auth/logout", follow_redirects=True)
211-
assert response.status_code == 200
212-
assert b"Login" in response.data
210+
response = client.get("/auth/logout")
211+
assert response.status_code == 302
212+
assert "/admin" in response.location
213213

214214
def test_logout_clears_session(self, client):
215215
"""Test logout clears session data."""
@@ -272,7 +272,7 @@ def test_dashboard_requires_authentication(self, client):
272272
"""Test dashboard redirects to login when not authenticated."""
273273
response = client.get("/")
274274
assert response.status_code == 302
275-
assert "/auth/login" in response.location
275+
assert "/admin" in response.location
276276

277277
def test_dashboard_accessible_when_authenticated(self, client, app):
278278
"""Test dashboard accessible when authenticated."""
@@ -290,12 +290,11 @@ def test_dashboard_accessible_when_authenticated(self, client, app):
290290
response = client.get("/")
291291
assert response.status_code == 200
292292

293-
def test_protected_route_preserves_next_parameter(self, client):
294-
"""Test login preserves next parameter for protected routes."""
293+
def test_protected_route_redirects_to_admin(self, client):
294+
"""Test protected Flask routes redirect to React admin interface."""
295295
response = client.get("/profile")
296296
assert response.status_code == 302
297-
assert "next=" in response.location
298-
assert "/profile" in response.location
297+
assert "/admin" in response.location
299298

300299
def test_admin_route_requires_admin_privileges(self, client, app):
301300
"""Test admin routes require admin privileges."""

0 commit comments

Comments
 (0)