mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-18 20:29:44 -05:00
fix(api): support desktop app login and CORS
Allow the desktop renderer to authenticate through the app login endpoint and call API routes from its packaged origin without weakening non-API responses.
This commit is contained in:
@@ -575,6 +575,16 @@ def create_app(config=None):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@app.before_request
|
||||
def handle_api_cors_preflight():
|
||||
if request.method == "OPTIONS" and request.path.startswith("/api/v1/"):
|
||||
response = app.response_class(status=204)
|
||||
response.headers["Access-Control-Allow-Origin"] = request.headers.get("Origin") or "*"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type, Accept, X-Request-ID"
|
||||
response.headers["Access-Control-Max-Age"] = "600"
|
||||
return response
|
||||
|
||||
# Start timer for Prometheus metrics
|
||||
@app.before_request
|
||||
def prom_start_timer():
|
||||
@@ -746,6 +756,15 @@ def create_app(config=None):
|
||||
# CSRF cookie/token handling
|
||||
# If CSRF is enabled, ensure CSRF cookie exists for HTML GET responses
|
||||
# If CSRF is disabled, explicitly clear any existing CSRF cookie to avoid confusion
|
||||
try:
|
||||
if request.path.startswith("/api/v1/"):
|
||||
response.headers["Access-Control-Allow-Origin"] = request.headers.get("Origin") or "*"
|
||||
response.headers["Vary"] = "Origin"
|
||||
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
||||
response.headers["Access-Control-Allow-Headers"] = "Authorization, Content-Type, Accept, X-Request-ID"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if app.config.get("WTF_CSRF_ENABLED"):
|
||||
try:
|
||||
# Only for safe, HTML page responses
|
||||
|
||||
@@ -218,11 +218,11 @@ def health_check():
|
||||
@api_v1_bp.route("/auth/login", methods=["POST"])
|
||||
@limiter.limit("5 per minute", methods=["POST"])
|
||||
def auth_login():
|
||||
"""Login with username and password; returns an API token for mobile/app use.
|
||||
"""Login with username and password; returns an API token for app use.
|
||||
|
||||
Accepts JSON: { "username": "...", "password": "..." }.
|
||||
Returns 200 with { "token": "tt_..." } or 401 with { "error": "..." }.
|
||||
The token has scopes for basics: read:projects, read:tasks, read:time_entries, write:time_entries.
|
||||
Admin users receive admin scope; regular users receive broad read/write API scopes.
|
||||
"""
|
||||
current_app.logger.info(
|
||||
"POST /api/v1/auth/login from %s",
|
||||
@@ -239,12 +239,12 @@ def auth_login():
|
||||
if not user or not user.check_password(password):
|
||||
return jsonify({"error": "Invalid username or password"}), 401
|
||||
|
||||
scopes = "read:projects,read:tasks,read:time_entries,write:time_entries"
|
||||
scopes = "admin:all" if user.is_admin else "read:*,write:*"
|
||||
expiry_days = current_app.config.get("API_TOKEN_DEFAULT_EXPIRY_DAYS", 90)
|
||||
api_token, plain_token = ApiToken.create_token(
|
||||
user_id=user.id,
|
||||
name=f"Mobile app - {user.username}",
|
||||
description="Token issued via mobile/app login",
|
||||
name=f"App login - {user.username}",
|
||||
description="Token issued via desktop/mobile app login",
|
||||
scopes=scopes,
|
||||
expires_days=expiry_days if expiry_days else None,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user