Spaces:
Runtime error
Runtime error
feat: Replace complex auto-login with simple Conference Demo button
Browse files- Add Conference Demo option to login page with prominent styling
- Remove complex auto-login logic that caused redirect loops
- Simplify auth.py with on-demand session creation
- Add one-click conference access via dedicated login button
- Clean UX for conference participants
- Maintains all demo permissions and 12-hour sessions
- app.py +14 -66
- auth.py +21 -19
- static/index.html +2 -2
- static/login.html +60 -0
- static/map.html +2 -2
- static/sw.js +1 -1
- version.json +1 -1
app.py
CHANGED
|
@@ -390,48 +390,40 @@ async def login(login_data: LoginRequest, response: Response):
|
|
| 390 |
|
| 391 |
@app.get("/api/auth/conference-login", response_model=ConferenceLoginResponse, tags=["Authentication"])
|
| 392 |
async def conference_auto_login(response: Response):
|
| 393 |
-
"""
|
| 394 |
if not settings.is_conference_mode():
|
| 395 |
raise HTTPException(
|
| 396 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 397 |
detail="Conference mode not enabled"
|
| 398 |
)
|
| 399 |
|
| 400 |
-
#
|
| 401 |
-
|
| 402 |
-
if not
|
| 403 |
-
raise HTTPException(
|
| 404 |
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 405 |
-
detail="Conference session not initialized"
|
| 406 |
-
)
|
| 407 |
-
|
| 408 |
-
# Validate the session to get user data
|
| 409 |
-
session_data = auth_manager.validate_session(conference_token)
|
| 410 |
-
if not session_data:
|
| 411 |
raise HTTPException(
|
| 412 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 413 |
-
detail="
|
| 414 |
)
|
| 415 |
|
| 416 |
# Set authentication cookie for web page requests (12 hour expiry)
|
| 417 |
response.set_cookie(
|
| 418 |
key="auth_token",
|
| 419 |
-
value=
|
| 420 |
max_age=12*60*60, # 12 hours for conference duration
|
| 421 |
httponly=True,
|
| 422 |
secure=True,
|
| 423 |
samesite="lax"
|
| 424 |
)
|
| 425 |
|
| 426 |
-
logger.info("Conference participant
|
| 427 |
|
| 428 |
return ConferenceLoginResponse(
|
| 429 |
-
token=
|
| 430 |
user={
|
| 431 |
-
"username":
|
| 432 |
-
"role":
|
| 433 |
-
"full_name":
|
| 434 |
-
"permissions":
|
| 435 |
},
|
| 436 |
is_demo_mode=True
|
| 437 |
)
|
|
@@ -502,33 +494,11 @@ async def serve_login():
|
|
| 502 |
raise HTTPException(status_code=404, detail="Login page not found")
|
| 503 |
|
| 504 |
@app.get("/", response_class=HTMLResponse, tags=["Frontend"])
|
| 505 |
-
async def read_root(request: Request
|
| 506 |
"""Serve the main application page with auth check"""
|
| 507 |
# Check if user is authenticated
|
| 508 |
user = get_current_user(request)
|
| 509 |
|
| 510 |
-
# If no user and conference mode is enabled, auto-login
|
| 511 |
-
if not user and settings.is_conference_mode():
|
| 512 |
-
# Check if we already tried to set the cookie (prevent infinite loop)
|
| 513 |
-
if not request.cookies.get('auth_token'):
|
| 514 |
-
conference_token = auth_manager.get_conference_token()
|
| 515 |
-
if conference_token:
|
| 516 |
-
# Validate the session immediately to use it
|
| 517 |
-
session = auth_manager.validate_session(conference_token)
|
| 518 |
-
if session:
|
| 519 |
-
# Set the conference session cookie
|
| 520 |
-
response.set_cookie(
|
| 521 |
-
key="auth_token",
|
| 522 |
-
value=conference_token,
|
| 523 |
-
max_age=12*60*60, # 12 hours
|
| 524 |
-
httponly=True,
|
| 525 |
-
secure=True,
|
| 526 |
-
samesite="lax"
|
| 527 |
-
)
|
| 528 |
-
# Don't redirect - serve the page directly with the new session
|
| 529 |
-
user = session
|
| 530 |
-
logger.info("Conference participant auto-logged in")
|
| 531 |
-
|
| 532 |
# Regular authentication check
|
| 533 |
if not user:
|
| 534 |
return RedirectResponse(url="/login")
|
|
@@ -543,33 +513,11 @@ async def read_root(request: Request, response: Response):
|
|
| 543 |
|
| 544 |
|
| 545 |
@app.get("/map", response_class=HTMLResponse, tags=["Frontend"])
|
| 546 |
-
async def serve_map(request: Request
|
| 547 |
"""Serve the map page with auth check"""
|
| 548 |
# Check if user is authenticated
|
| 549 |
user = get_current_user(request)
|
| 550 |
|
| 551 |
-
# If no user and conference mode is enabled, auto-login
|
| 552 |
-
if not user and settings.is_conference_mode():
|
| 553 |
-
# Check if we already tried to set the cookie (prevent infinite loop)
|
| 554 |
-
if not request.cookies.get('auth_token'):
|
| 555 |
-
conference_token = auth_manager.get_conference_token()
|
| 556 |
-
if conference_token:
|
| 557 |
-
# Validate the session immediately to use it
|
| 558 |
-
session = auth_manager.validate_session(conference_token)
|
| 559 |
-
if session:
|
| 560 |
-
# Set the conference session cookie
|
| 561 |
-
response.set_cookie(
|
| 562 |
-
key="auth_token",
|
| 563 |
-
value=conference_token,
|
| 564 |
-
max_age=12*60*60, # 12 hours
|
| 565 |
-
httponly=True,
|
| 566 |
-
secure=True,
|
| 567 |
-
samesite="lax"
|
| 568 |
-
)
|
| 569 |
-
# Don't redirect - serve the page directly with the new session
|
| 570 |
-
user = session
|
| 571 |
-
logger.info("Conference participant auto-logged in for map")
|
| 572 |
-
|
| 573 |
# Regular authentication check
|
| 574 |
if not user:
|
| 575 |
return RedirectResponse(url="/login")
|
|
|
|
| 390 |
|
| 391 |
@app.get("/api/auth/conference-login", response_model=ConferenceLoginResponse, tags=["Authentication"])
|
| 392 |
async def conference_auto_login(response: Response):
|
| 393 |
+
"""Login for conference demo mode"""
|
| 394 |
if not settings.is_conference_mode():
|
| 395 |
raise HTTPException(
|
| 396 |
status_code=status.HTTP_404_NOT_FOUND,
|
| 397 |
detail="Conference mode not enabled"
|
| 398 |
)
|
| 399 |
|
| 400 |
+
# Create a new conference session
|
| 401 |
+
result = auth_manager.create_conference_session()
|
| 402 |
+
if not result:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
raise HTTPException(
|
| 404 |
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
| 405 |
+
detail="Could not create conference session"
|
| 406 |
)
|
| 407 |
|
| 408 |
# Set authentication cookie for web page requests (12 hour expiry)
|
| 409 |
response.set_cookie(
|
| 410 |
key="auth_token",
|
| 411 |
+
value=result["token"],
|
| 412 |
max_age=12*60*60, # 12 hours for conference duration
|
| 413 |
httponly=True,
|
| 414 |
secure=True,
|
| 415 |
samesite="lax"
|
| 416 |
)
|
| 417 |
|
| 418 |
+
logger.info("Conference participant logged in via demo button")
|
| 419 |
|
| 420 |
return ConferenceLoginResponse(
|
| 421 |
+
token=result["token"],
|
| 422 |
user={
|
| 423 |
+
"username": result["user"]["username"],
|
| 424 |
+
"role": result["user"]["role"],
|
| 425 |
+
"full_name": result["user"]["full_name"],
|
| 426 |
+
"permissions": result["user"]["permissions"]
|
| 427 |
},
|
| 428 |
is_demo_mode=True
|
| 429 |
)
|
|
|
|
| 494 |
raise HTTPException(status_code=404, detail="Login page not found")
|
| 495 |
|
| 496 |
@app.get("/", response_class=HTMLResponse, tags=["Frontend"])
|
| 497 |
+
async def read_root(request: Request):
|
| 498 |
"""Serve the main application page with auth check"""
|
| 499 |
# Check if user is authenticated
|
| 500 |
user = get_current_user(request)
|
| 501 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
# Regular authentication check
|
| 503 |
if not user:
|
| 504 |
return RedirectResponse(url="/login")
|
|
|
|
| 513 |
|
| 514 |
|
| 515 |
@app.get("/map", response_class=HTMLResponse, tags=["Frontend"])
|
| 516 |
+
async def serve_map(request: Request):
|
| 517 |
"""Serve the map page with auth check"""
|
| 518 |
# Check if user is authenticated
|
| 519 |
user = get_current_user(request)
|
| 520 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
# Regular authentication check
|
| 522 |
if not user:
|
| 523 |
return RedirectResponse(url="/login")
|
auth.py
CHANGED
|
@@ -78,23 +78,20 @@ class AuthManager:
|
|
| 78 |
}
|
| 79 |
|
| 80 |
logger.info(f"AuthManager initialized with {len(self.users)} user accounts")
|
| 81 |
-
|
| 82 |
-
# Initialize conference session if needed
|
| 83 |
-
self._init_conference_session()
|
| 84 |
|
| 85 |
-
def
|
| 86 |
-
"""
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
| 93 |
|
| 94 |
-
# Extended timeout for conference (12 hours
|
| 95 |
conference_timeout = timedelta(hours=12)
|
| 96 |
|
| 97 |
-
conference_user = self.users["conference_participant"]
|
| 98 |
session_data = {
|
| 99 |
"username": "conference_participant",
|
| 100 |
"role": conference_user["role"],
|
|
@@ -106,12 +103,17 @@ class AuthManager:
|
|
| 106 |
"session_timeout": conference_timeout
|
| 107 |
}
|
| 108 |
|
| 109 |
-
self.sessions[
|
| 110 |
-
logger.info("Conference
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
def _hash_password(self, password: str) -> str:
|
| 117 |
"""Hash password using bcrypt with automatic salt generation"""
|
|
|
|
| 78 |
}
|
| 79 |
|
| 80 |
logger.info(f"AuthManager initialized with {len(self.users)} user accounts")
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
def create_conference_session(self) -> Optional[Dict[str, Any]]:
|
| 83 |
+
"""Create a new conference session when requested"""
|
| 84 |
+
try:
|
| 85 |
+
conference_user = self.users.get("conference_participant")
|
| 86 |
+
if not conference_user:
|
| 87 |
+
return None
|
| 88 |
+
|
| 89 |
+
# Create session token
|
| 90 |
+
session_token = secrets.token_urlsafe(AUTH_TOKEN_LENGTH)
|
| 91 |
|
| 92 |
+
# Extended timeout for conference (12 hours)
|
| 93 |
conference_timeout = timedelta(hours=12)
|
| 94 |
|
|
|
|
| 95 |
session_data = {
|
| 96 |
"username": "conference_participant",
|
| 97 |
"role": conference_user["role"],
|
|
|
|
| 103 |
"session_timeout": conference_timeout
|
| 104 |
}
|
| 105 |
|
| 106 |
+
self.sessions[session_token] = session_data
|
| 107 |
+
logger.info("Conference session created")
|
| 108 |
+
|
| 109 |
+
return {
|
| 110 |
+
"token": session_token,
|
| 111 |
+
"user": session_data
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
except Exception as e:
|
| 115 |
+
logger.error(f"Error creating conference session: {e}")
|
| 116 |
+
return None
|
| 117 |
|
| 118 |
def _hash_password(self, password: str) -> str:
|
| 119 |
"""Hash password using bcrypt with automatic salt generation"""
|
static/index.html
CHANGED
|
@@ -907,7 +907,7 @@
|
|
| 907 |
// Force refresh if we detect cached version
|
| 908 |
(function() {
|
| 909 |
const currentVersion = '5.1.1';
|
| 910 |
-
const timestamp = '
|
| 911 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
| 912 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
| 913 |
|
|
@@ -1152,7 +1152,7 @@
|
|
| 1152 |
</div>
|
| 1153 |
</div>
|
| 1154 |
|
| 1155 |
-
<script type="module" src="/static/js/tree-track-app.js?v=5.1.1&t=
|
| 1156 |
|
| 1157 |
<script>
|
| 1158 |
// Idle-time prefetch of map assets to speed up first navigation
|
|
|
|
| 907 |
// Force refresh if we detect cached version
|
| 908 |
(function() {
|
| 909 |
const currentVersion = '5.1.1';
|
| 910 |
+
const timestamp = '1761483327'; // Cache-busting bump
|
| 911 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
| 912 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
| 913 |
|
|
|
|
| 1152 |
</div>
|
| 1153 |
</div>
|
| 1154 |
|
| 1155 |
+
<script type="module" src="/static/js/tree-track-app.js?v=5.1.1&t=1761483327"></script>
|
| 1156 |
|
| 1157 |
<script>
|
| 1158 |
// Idle-time prefetch of map assets to speed up first navigation
|
static/login.html
CHANGED
|
@@ -294,6 +294,28 @@
|
|
| 294 |
}
|
| 295 |
}
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
/* Focus improvements */
|
| 298 |
.demo-toggle:focus,
|
| 299 |
.account-item:focus {
|
|
@@ -336,6 +358,10 @@
|
|
| 336 |
</button>
|
| 337 |
|
| 338 |
<div class="accounts-dropdown" id="accountsDropdown">
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
<button class="account-item" onclick="selectAccount('aalekh', event)">
|
| 340 |
<div class="account-role">Aalekh (Admin)</div>
|
| 341 |
<div class="account-username">Full system access</div>
|
|
@@ -374,6 +400,40 @@
|
|
| 374 |
}
|
| 375 |
}
|
| 376 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 377 |
function selectAccount(username, event) {
|
| 378 |
document.getElementById('username').value = username;
|
| 379 |
document.getElementById('password').value = '';
|
|
|
|
| 294 |
}
|
| 295 |
}
|
| 296 |
|
| 297 |
+
/* Conference Demo Styling */
|
| 298 |
+
.account-item.conference-demo {
|
| 299 |
+
background: linear-gradient(135deg, var(--primary-50), var(--primary-100));
|
| 300 |
+
border: 2px solid var(--primary-200);
|
| 301 |
+
margin-bottom: var(--space-2);
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.account-item.conference-demo:hover {
|
| 305 |
+
background: linear-gradient(135deg, var(--primary-100), var(--primary-150));
|
| 306 |
+
border-color: var(--primary-300);
|
| 307 |
+
transform: translateY(-1px);
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
.account-item.conference-demo .account-role {
|
| 311 |
+
color: var(--primary-700);
|
| 312 |
+
font-weight: var(--font-semibold);
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
.account-item.conference-demo .account-username {
|
| 316 |
+
color: var(--primary-600);
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
/* Focus improvements */
|
| 320 |
.demo-toggle:focus,
|
| 321 |
.account-item:focus {
|
|
|
|
| 358 |
</button>
|
| 359 |
|
| 360 |
<div class="accounts-dropdown" id="accountsDropdown">
|
| 361 |
+
<button class="account-item conference-demo" onclick="selectConferenceDemo(event)">
|
| 362 |
+
<div class="account-role">Conference Demo</div>
|
| 363 |
+
<div class="account-username">Public demonstration access</div>
|
| 364 |
+
</button>
|
| 365 |
<button class="account-item" onclick="selectAccount('aalekh', event)">
|
| 366 |
<div class="account-role">Aalekh (Admin)</div>
|
| 367 |
<div class="account-username">Full system access</div>
|
|
|
|
| 400 |
}
|
| 401 |
}
|
| 402 |
|
| 403 |
+
function selectConferenceDemo(event) {
|
| 404 |
+
// Directly log in to conference demo
|
| 405 |
+
event.preventDefault();
|
| 406 |
+
|
| 407 |
+
setLoading(true);
|
| 408 |
+
|
| 409 |
+
// Call conference auto-login API
|
| 410 |
+
fetch('/api/auth/conference-login', {
|
| 411 |
+
method: 'GET'
|
| 412 |
+
})
|
| 413 |
+
.then(response => response.json())
|
| 414 |
+
.then(result => {
|
| 415 |
+
if (result.token) {
|
| 416 |
+
// Store authentication token
|
| 417 |
+
localStorage.setItem('auth_token', result.token);
|
| 418 |
+
localStorage.setItem('user_info', JSON.stringify(result.user));
|
| 419 |
+
|
| 420 |
+
showMessage('Conference demo access granted! Redirecting...', 'success');
|
| 421 |
+
|
| 422 |
+
// Redirect to main app
|
| 423 |
+
setTimeout(() => {
|
| 424 |
+
window.location.href = '/';
|
| 425 |
+
}, 1500);
|
| 426 |
+
} else {
|
| 427 |
+
throw new Error('Conference mode not available');
|
| 428 |
+
}
|
| 429 |
+
})
|
| 430 |
+
.catch(error => {
|
| 431 |
+
console.error('Conference login error:', error);
|
| 432 |
+
showMessage('Conference demo is not currently available. Please try again later.');
|
| 433 |
+
setLoading(false);
|
| 434 |
+
});
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
function selectAccount(username, event) {
|
| 438 |
document.getElementById('username').value = username;
|
| 439 |
document.getElementById('password').value = '';
|
static/map.html
CHANGED
|
@@ -799,7 +799,7 @@
|
|
| 799 |
// Force refresh if we detect cached version
|
| 800 |
(function() {
|
| 801 |
const currentVersion = '5.1.1';
|
| 802 |
-
const timestamp = '
|
| 803 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
| 804 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
| 805 |
|
|
@@ -925,7 +925,7 @@ const timestamp = '1761483168'; // Current timestamp for cache busting
|
|
| 925 |
|
| 926 |
<!-- Leaflet JS -->
|
| 927 |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| 928 |
-
<script src="/static/map.js?v=5.1.1&t=
|
| 929 |
|
| 930 |
"default-state": {
|
| 931 |
gradients: [
|
|
|
|
| 799 |
// Force refresh if we detect cached version
|
| 800 |
(function() {
|
| 801 |
const currentVersion = '5.1.1';
|
| 802 |
+
const timestamp = '1761483327'; // Current timestamp for cache busting
|
| 803 |
const lastVersion = sessionStorage.getItem('treetrack_version');
|
| 804 |
const lastTimestamp = sessionStorage.getItem('treetrack_timestamp');
|
| 805 |
|
|
|
|
| 925 |
|
| 926 |
<!-- Leaflet JS -->
|
| 927 |
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
| 928 |
+
<script src="/static/map.js?v=5.1.1&t=1761483327">
|
| 929 |
|
| 930 |
"default-state": {
|
| 931 |
gradients: [
|
static/sw.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
// TreeTrack Service Worker - PWA and Offline Support
|
| 2 |
-
const VERSION =
|
| 3 |
const CACHE_NAME = `treetrack-v${VERSION}`;
|
| 4 |
const STATIC_CACHE = `static-v${VERSION}`;
|
| 5 |
const API_CACHE = `api-v${VERSION}`;
|
|
|
|
| 1 |
// TreeTrack Service Worker - PWA and Offline Support
|
| 2 |
+
const VERSION = 1761483327; // Cache busting bump - force clients to fetch new static assets and header image change
|
| 3 |
const CACHE_NAME = `treetrack-v${VERSION}`;
|
| 4 |
const STATIC_CACHE = `static-v${VERSION}`;
|
| 5 |
const API_CACHE = `api-v${VERSION}`;
|
version.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
{
|
| 2 |
"version": "5.1.1",
|
| 3 |
-
"timestamp":
|
| 4 |
}
|
|
|
|
| 1 |
{
|
| 2 |
"version": "5.1.1",
|
| 3 |
+
"timestamp": 1761483327
|
| 4 |
}
|