feat: Add mobile responsive layout, open room access, and admin room management

Mobile Responsive Layout:
- Add useMediaQuery, useIsMobile, useIsTablet, useIsDesktop hooks for device detection
- Create MobileHeader component with hamburger menu and action drawer
- Create BottomToolbar for mobile navigation (Files, Members)
- Create SlidePanel component for full-screen mobile sidebars
- Update RoomDetail.tsx with mobile/desktop conditional rendering
- Update RoomList.tsx with single-column grid and touch-friendly buttons
- Add CSS custom properties for safe areas and touch targets (min 44px)
- Add mobile viewport meta tags for notched devices

Open Room Access:
- All authenticated users can view all rooms (not just their own)
- Users can join active rooms they're not members of
- Add is_member field to room responses
- Update room list API to return all rooms by default

Admin Room Management:
- Add permanent delete functionality for system admins
- Add delete confirmation dialog with room title verification
- Broadcast room deletion via WebSocket to connected users
- Add users search API for adding members

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-05 09:12:10 +08:00
parent 1e44a63a8e
commit 1d5d4d447d
48 changed files with 3505 additions and 401 deletions

View File

@@ -6,7 +6,8 @@
3. AD token 自動刷新5 分鐘內過期時)
4. 重試計數器管理(最多 3 次)
"""
from fastapi import Request, HTTPException, status
from fastapi import Request, status
from fastapi.responses import JSONResponse
from datetime import datetime, timedelta
from app.core.database import SessionLocal
from app.core.config import get_settings
@@ -25,15 +26,19 @@ class AuthMiddleware:
async def __call__(self, request: Request, call_next):
"""Process request through authentication checks"""
# Skip auth for login/logout endpoints
if request.url.path in ["/api/auth/login", "/api/auth/logout", "/docs", "/openapi.json"]:
# Skip auth for non-API routes (frontend), login/logout, and docs
path = request.url.path
if not path.startswith("/api") or path in ["/api/auth/login", "/api/auth/logout", "/api/health"]:
return await call_next(request)
if path in ["/docs", "/openapi.json", "/redoc"]:
return await call_next(request)
# Extract token from Authorization header
authorization = request.headers.get("Authorization")
if not authorization or not authorization.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Authentication required"
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Authentication required"}
)
internal_token = authorization.replace("Bearer ", "")
@@ -44,32 +49,35 @@ class AuthMiddleware:
# Query session
user_session = session_service.get_session_by_token(db, internal_token)
if not user_session:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token"
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid or expired token"}
)
# Check 3-day inactivity timeout
inactivity_limit = datetime.utcnow() - timedelta(days=settings.SESSION_INACTIVITY_DAYS)
if user_session.last_activity < inactivity_limit:
session_service.delete_session(db, user_session.id)
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Session expired due to inactivity. Please login again.",
content={"detail": "Session expired due to inactivity. Please login again."}
)
# Check if refresh attempts exceeded
if user_session.refresh_attempt_count >= settings.MAX_REFRESH_ATTEMPTS:
session_service.delete_session(db, user_session.id)
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Session expired due to authentication failures. Please login again.",
content={"detail": "Session expired due to authentication failures. Please login again."}
)
# Check if AD token needs refresh (< 5 minutes until expiry)
time_until_expiry = user_session.ad_token_expires_at - datetime.utcnow()
if time_until_expiry < timedelta(minutes=settings.TOKEN_REFRESH_THRESHOLD_MINUTES):
# Auto-refresh AD token
await self._refresh_ad_token(db, user_session)
refresh_error = await self._refresh_ad_token(db, user_session)
if refresh_error:
return refresh_error
# Update last_activity
session_service.update_activity(db, user_session.id)
@@ -87,7 +95,11 @@ class AuthMiddleware:
return await call_next(request)
async def _refresh_ad_token(self, db, user_session):
"""Auto-refresh AD token using stored encrypted password"""
"""Auto-refresh AD token using stored encrypted password
Returns:
JSONResponse on error, None on success
"""
try:
# Decrypt password
password = encryption_service.decrypt_password(user_session.encrypted_password)
@@ -101,6 +113,7 @@ class AuthMiddleware:
)
logger.info(f"AD token refreshed successfully for user: {user_session.username}")
return None # Success
except (ValueError, ConnectionError) as e:
# Refresh failed, increment counter
@@ -117,14 +130,14 @@ class AuthMiddleware:
logger.error(
f"Session terminated for {user_session.username} after {new_count} failed refresh attempts"
)
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Session terminated. Your password may have been changed. Please login again.",
content={"detail": "Session terminated. Your password may have been changed. Please login again."}
)
else:
raise HTTPException(
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token refresh failed. Please try again or re-login if issue persists.",
content={"detail": "Token refresh failed. Please try again or re-login if issue persists."}
)