2026-01-14 05:29:55 +08:00
"""
Authentication API Routes
2025-12-29 03:06:49 +08:00
2026-01-14 20:42:10 +08:00
Handles login, logout, registration, password reset, and OAuth authentication.
2026-01-14 05:29:55 +08:00
Supports both multi-user (database) and single-user (legacy) modes.
"""
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
import os
2026-01-14 23:50:31 +08:00
from urllib.parse import urlencode
2026-04-09 14:30:51 +07:00
from flask import Blueprint , g , jsonify , redirect , request
2025-12-29 03:06:49 +08:00
from app.config.settings import Config
2026-04-09 14:30:51 +07:00
from app.utils.auth import authenticate_legacy , generate_token , login_required
2025-12-29 03:06:49 +08:00
from app.utils.logger import get_logger
logger = get_logger ( __name__ )
2026-04-09 14:30:51 +07:00
auth_bp = Blueprint ( "auth" , __name__ )
2026-01-14 05:29:55 +08:00
2026-01-14 23:50:31 +08:00
def _build_frontend_login_redirect ( frontend_url : str , ** params ) -> str :
"""
Build a redirect URL to frontend login page for OAuth flows.
Frontend uses Vue Router hash mode (`/#/user/login`), so redirecting to `/user/login`
will 404 on static hosting. Always normalize to ` {origin} /#/user/login`.
"""
2026-04-09 14:30:51 +07:00
base = ( frontend_url or "" ) . strip () . rstrip ( "/" )
2026-01-14 23:50:31 +08:00
if not base :
2026-04-09 14:30:51 +07:00
base = "http://localhost:8080"
2026-01-14 23:50:31 +08:00
2026-04-09 14:30:51 +07:00
if "/#/" in base :
origin = base . split ( "/#/" , 1 )[ 0 ] . rstrip ( "/" )
elif "#" in base :
origin = base . split ( "#" , 1 )[ 0 ] . rstrip ( "/" )
2026-01-14 23:50:31 +08:00
else :
origin = base
login_url = f " { origin } /#/user/login"
2026-04-09 14:30:51 +07:00
qs = urlencode ({ k : v for k , v in params . items () if v is not None and v != "" })
2026-01-14 23:50:31 +08:00
return f " { login_url } ? { qs } " if qs else login_url
2026-01-14 05:29:55 +08:00
def _is_single_user_mode () -> bool :
"""Check if system is in single-user (legacy) mode"""
2026-04-09 14:30:51 +07:00
return os . getenv ( "SINGLE_USER_MODE" , "false" ) . lower () == "true"
2026-01-14 05:29:55 +08:00
2026-01-14 20:42:10 +08:00
def _get_client_ip () -> str :
"""Get client IP address from request"""
# Check for proxy headers
2026-04-09 14:30:51 +07:00
if request . headers . get ( "X-Forwarded-For" ):
return request . headers . get ( "X-Forwarded-For" ) . split ( "," )[ 0 ] . strip ()
if request . headers . get ( "X-Real-IP" ):
return request . headers . get ( "X-Real-IP" )
return request . remote_addr or "0.0.0.0"
2026-01-14 20:42:10 +08:00
def _get_user_agent () -> str :
"""Get user agent from request"""
2026-04-09 14:30:51 +07:00
return request . headers . get ( "User-Agent" , "" )[: 500 ]
2026-01-14 20:42:10 +08:00
# =============================================================================
# Security Config Endpoint
# =============================================================================
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/security-config" , methods = [ "GET" ])
2026-01-14 20:42:10 +08:00
def get_security_config ():
"""
Get public security configuration for frontend.
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
Returns:
turnstile_enabled: bool
turnstile_site_key: str
registration_enabled: bool
oauth_google_enabled: bool
oauth_github_enabled: bool
"""
try :
from app.services.security_service import get_security_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
config = get_security_service () . get_security_config ()
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "success" , "data" : config })
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "get_security_config error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-14 20:42:10 +08:00
# =============================================================================
# Login Endpoint (Enhanced with security)
# =============================================================================
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/login" , methods = [ "POST" ])
2025-12-29 03:06:49 +08:00
def login ():
2026-01-14 05:29:55 +08:00
"""
User login endpoint.
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Request body:
username: str
password: str
2026-01-14 20:42:10 +08:00
turnstile_token: str (optional, required if Turnstile is enabled)
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
Returns:
token: JWT token
userinfo: User information
"""
2026-01-14 20:42:10 +08:00
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
try :
2026-01-14 20:42:10 +08:00
from app.services.security_service import get_security_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
security = get_security_service ()
2026-04-09 14:30:51 +07:00
2025-12-29 03:06:49 +08:00
data = request . get_json ()
if not data :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 400 , "msg" : "No data provided" , "data" : None }), 400
username = data . get ( "username" ) or data . get ( "account" )
password = data . get ( "password" )
turnstile_token = data . get ( "turnstile_token" )
2025-12-29 03:06:49 +08:00
if not username or not password :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 400 , "msg" : "Missing username/email or password" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Step 1: Verify Turnstile (if enabled)
turnstile_ok , turnstile_msg = security . verify_turnstile ( turnstile_token , ip_address )
if not turnstile_ok :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : turnstile_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Step 2: Check rate limiting
allowed , block_msg = security . check_login_allowed ( username , ip_address )
if not allowed :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : block_msg , "data" : { "blocked" : True }}), 429
2026-01-14 05:29:55 +08:00
user = None
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Step 3: Authenticate
2026-01-14 05:29:55 +08:00
if not _is_single_user_mode ():
try :
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
user = get_user_service () . authenticate ( username , password )
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Check if user has no password set (code-login user)
2026-04-09 14:30:51 +07:00
if user and user . get ( "_no_password" ):
user . pop ( "_no_password" , None )
2026-01-14 20:42:10 +08:00
# Record failed attempt
2026-04-09 14:30:51 +07:00
security . record_login_attempt ( ip_address , "ip" , False , ip_address , user_agent )
security . record_login_attempt ( username , "account" , False , ip_address , user_agent )
security . log_security_event (
"login_failed" ,
user . get ( "id" ),
ip_address ,
user_agent ,
{ "username" : username , "reason" : "no_password_set" },
)
return jsonify (
{
"code" : 0 ,
"msg" : "This account was created with email verification code and has no password set. Please use email code login or set a password first in your profile settings." ,
"data" : None ,
}
), 401
2026-01-14 05:29:55 +08:00
except Exception as e :
logger . warning ( f "Multi-user auth failed, trying legacy: { e } " )
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
# Fallback to legacy single-user mode
if not user :
user = authenticate_legacy ( username , password )
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
if not user :
2026-01-14 20:42:10 +08:00
# Record failed attempt
2026-04-09 14:30:51 +07:00
security . record_login_attempt ( ip_address , "ip" , False , ip_address , user_agent )
security . record_login_attempt ( username , "account" , False , ip_address , user_agent )
security . log_security_event (
"login_failed" , None , ip_address , user_agent , { "username" : username , "reason" : "invalid_credentials" }
)
return jsonify ({ "code" : 0 , "msg" : "Invalid credentials" , "data" : None }), 401
2026-01-14 20:42:10 +08:00
# Check user status
2026-04-09 14:30:51 +07:00
if user . get ( "status" ) == "disabled" :
security . log_security_event (
"login_blocked" , user . get ( "id" ), ip_address , user_agent , { "reason" : "account_disabled" }
)
return jsonify ({ "code" : 0 , "msg" : "Account is disabled" , "data" : None }), 403
if user . get ( "status" ) == "pending" :
return jsonify ({ "code" : 0 , "msg" : "Account is pending activation" , "data" : None }), 403
2026-01-31 02:59:49 +08:00
# Step 4: Increment token_version (invalidates old sessions for single-client login)
2026-04-09 14:30:51 +07:00
user_id = user . get ( "id" ) or user . get ( "user_id" , 1 )
2026-01-31 02:59:49 +08:00
try :
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
new_token_version = get_user_service () . increment_token_version ( user_id )
except Exception as e :
logger . warning ( f "Failed to increment token_version: { e } " )
new_token_version = 1
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# Step 5: Generate token with new token_version
2026-01-14 05:29:55 +08:00
token = generate_token (
2026-01-31 02:59:49 +08:00
user_id = user_id ,
2026-04-09 14:30:51 +07:00
username = user . get ( "username" , username ),
role = user . get ( "role" , "admin" ),
token_version = new_token_version , # Contains new token_version
2026-01-14 05:29:55 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
if not token :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 500 , "msg" : "Token generation error" , "data" : None }), 500
2026-01-31 02:59:49 +08:00
# Step 6: Record successful login
2026-04-09 14:30:51 +07:00
security . record_login_attempt ( ip_address , "ip" , True , ip_address , user_agent )
security . record_login_attempt ( username , "account" , True , ip_address , user_agent )
security . clear_login_attempts ( ip_address , "ip" )
security . clear_login_attempts ( username , "account" )
security . log_security_event ( "login_success" , user . get ( "id" ), ip_address , user_agent )
2026-01-14 05:29:55 +08:00
# Build user info for frontend
userinfo = {
2026-04-09 14:30:51 +07:00
"id" : user . get ( "id" ) or user . get ( "user_id" , 1 ),
"username" : user . get ( "username" , username ),
"nickname" : user . get ( "nickname" , "User" ),
"avatar" : user . get ( "avatar" , "/avatar2.jpg" ),
"timezone" : str ( user . get ( "timezone" ) or "" ) . strip (),
"role" : { "id" : user . get ( "role" , "admin" ), "permissions" : _get_permissions ( user . get ( "role" , "admin" ))},
2026-01-14 05:29:55 +08:00
}
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "Login successful" , "data" : { "token" : token , "userinfo" : userinfo }})
2025-12-29 03:06:49 +08:00
except Exception as e :
logger . error ( f "Login error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 500 , "msg" : str ( e ), "data" : None }), 500
2025-12-29 03:06:49 +08:00
2026-01-14 05:29:55 +08:00
2026-01-14 20:42:10 +08:00
# =============================================================================
# Email Code Login
# =============================================================================
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/login-code" , methods = [ "POST" ])
2026-01-14 20:42:10 +08:00
def login_with_code ():
"""
Login with email verification code (quick login / register).
If user doesn't exist, create a new account automatically.
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
Request body:
email: str
code: str (verification code)
turnstile_token: str (optional)
referral_code: str (optional, referrer's user ID - only for new users)
"""
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
2026-04-09 14:30:51 +07:00
from app.services.billing_service import get_billing_service
2026-01-14 20:42:10 +08:00
from app.services.email_service import get_email_service
2026-04-09 14:30:51 +07:00
from app.services.security_service import get_security_service
2026-01-14 20:42:10 +08:00
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
security = get_security_service ()
email_service = get_email_service ()
user_service = get_user_service ()
billing_service = get_billing_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
data = request . get_json ()
if not data :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "No data provided" , "data" : None }), 400
email = ( data . get ( "email" ) or "" ) . strip () . lower ()
code = data . get ( "code" , "" ) . strip ()
turnstile_token = data . get ( "turnstile_token" )
referral_code = data . get ( "referral_code" , "" ) . strip ()
2026-01-14 20:42:10 +08:00
# Validate inputs
if not email or not email_service . is_valid_email ( email ):
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Invalid email address" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
if not code :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Verification code is required" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify Turnstile
turnstile_ok , turnstile_msg = security . verify_turnstile ( turnstile_token , ip_address )
if not turnstile_ok :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : turnstile_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify email code
2026-04-09 14:30:51 +07:00
code_valid , code_msg = email_service . verify_code ( email , code , "login" )
2026-01-14 20:42:10 +08:00
if not code_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : code_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Check if user exists
user = user_service . get_user_by_email ( email )
is_new_user = False
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not user :
# Check if registration is enabled
2026-04-09 14:30:51 +07:00
if os . getenv ( "ENABLE_REGISTRATION" , "true" ) . lower () != "true" :
return jsonify ({ "code" : 0 , "msg" : "User not found and registration is disabled" , "data" : None }), 403
2026-01-14 20:42:10 +08:00
# Auto-create user with email as username
import re
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Generate username from email (before @)
2026-04-09 14:30:51 +07:00
base_username = re . sub ( r "[^a-zA-Z0-9_]" , "" , email . split ( "@" )[ 0 ])
2026-01-14 20:42:10 +08:00
if not base_username or not base_username [ 0 ] . isalpha ():
2026-04-09 14:30:51 +07:00
base_username = "user_" + base_username
2026-01-14 20:42:10 +08:00
# Make sure username is unique
username = base_username
counter = 1
while user_service . get_user_by_username ( username ):
username = f " { base_username } _ { counter } "
counter += 1
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Validate referral code (user ID)
referred_by = None
if referral_code :
try :
referrer_id = int ( referral_code )
referrer = user_service . get_user_by_id ( referrer_id )
2026-04-09 14:30:51 +07:00
if referrer and referrer . get ( "status" ) == "active" :
2026-01-14 20:42:10 +08:00
referred_by = referrer_id
except ( ValueError , TypeError ):
pass # Invalid referral code, ignore
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Create user without password (can set later)
user_id = user_service . create_user (
username = username ,
password = None , # No password for code-login users
email = email ,
nickname = username ,
2026-04-09 14:30:51 +07:00
role = "user" ,
status = "active" ,
2026-01-14 20:42:10 +08:00
email_verified = True ,
2026-04-09 14:30:51 +07:00
referred_by = referred_by ,
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not user_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Failed to create account" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# Grant registration bonus credits
2026-04-09 14:30:51 +07:00
register_bonus = int ( os . getenv ( "CREDITS_REGISTER_BONUS" , "0" ))
2026-01-14 20:42:10 +08:00
if register_bonus > 0 :
billing_service . add_credits (
2026-04-09 14:30:51 +07:00
user_id = user_id , amount = register_bonus , action = "register_bonus" , remark = "Registration bonus"
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Grant referral bonus to referrer
if referred_by :
2026-04-09 14:30:51 +07:00
referral_bonus = int ( os . getenv ( "CREDITS_REFERRAL_BONUS" , "0" ))
2026-01-14 20:42:10 +08:00
if referral_bonus > 0 :
billing_service . add_credits (
user_id = referred_by ,
amount = referral_bonus ,
2026-04-09 14:30:51 +07:00
action = "referral_bonus" ,
remark = f "Referral bonus for inviting user { username } " ,
reference_id = str ( user_id ),
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
user = user_service . get_user_by_id ( user_id )
is_new_user = True
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Log registration
2026-04-09 14:30:51 +07:00
security . log_security_event (
"register_via_code" , user_id , ip_address , user_agent , { "email" : email , "referred_by" : referred_by }
)
2026-01-14 20:42:10 +08:00
# Check user status
2026-04-09 14:30:51 +07:00
if user . get ( "status" ) == "disabled" :
security . log_security_event (
"login_blocked" , user . get ( "id" ), ip_address , user_agent , { "reason" : "account_disabled" }
)
return jsonify ({ "code" : 0 , "msg" : "Account is disabled" , "data" : None }), 403
2026-01-31 02:59:49 +08:00
# Increment token_version (invalidates old sessions for single-client login)
try :
2026-04-09 14:30:51 +07:00
new_token_version = user_service . increment_token_version ( user [ "id" ])
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . warning ( f "Failed to increment token_version: { e } " )
new_token_version = 1
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# Generate token with new token_version
2026-01-14 20:42:10 +08:00
token = generate_token (
2026-04-09 14:30:51 +07:00
user_id = user [ "id" ],
username = user [ "username" ],
role = user . get ( "role" , "user" ),
token_version = new_token_version ,
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not token :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 500 , "msg" : "Token generation error" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# Update last login time
try :
from app.utils.db import get_db_connection
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
with get_db_connection () as db :
cur = db . cursor ()
2026-04-09 14:30:51 +07:00
cur . execute ( "UPDATE qd_users SET last_login_at = NOW() WHERE id = ?" , ( user [ "id" ],))
2026-01-14 20:42:10 +08:00
db . commit ()
2026-01-25 01:14:54 +08:00
affected = cur . rowcount
2026-01-14 20:42:10 +08:00
cur . close ()
2026-01-25 01:14:54 +08:00
if affected == 0 :
logger . error ( f "Failed to update last_login_at: no rows affected for user_id= { user [ 'id' ] } " )
else :
logger . info ( f "Updated last_login_at for user_id= { user [ 'id' ] } " )
2026-01-14 20:42:10 +08:00
except Exception as e :
2026-01-25 01:14:54 +08:00
logger . error ( f "Failed to update last_login_at for user_id= { user . get ( 'id' ) } : { e } " )
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Log login
2026-04-09 14:30:51 +07:00
security . log_security_event ( "login_via_code" , user [ "id" ], ip_address , user_agent )
return jsonify (
{
"code" : 1 ,
"msg" : "Login successful" + ( " (new account created)" if is_new_user else "" ),
"data" : {
"token" : token ,
"is_new_user" : is_new_user ,
"userinfo" : {
"id" : user [ "id" ],
"username" : user [ "username" ],
"nickname" : user . get ( "nickname" , user [ "username" ]),
"email" : user . get ( "email" ),
"avatar" : user . get ( "avatar" , "/avatar2.jpg" ),
"timezone" : str ( user . get ( "timezone" ) or "" ) . strip (),
"role" : {
"id" : user . get ( "role" , "user" ),
"permissions" : _get_permissions ( user . get ( "role" , "user" )),
},
},
},
2026-01-14 20:42:10 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "login_with_code error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Login failed" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# =============================================================================
# Registration Endpoints
# =============================================================================
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/send-code" , methods = [ "POST" ])
2026-01-14 20:42:10 +08:00
def send_verification_code ():
"""
Send verification code to email.
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
Request body:
email: str
type: str (register, reset_password, change_password, change_email)
turnstile_token: str (optional)
"""
ip_address = _get_client_ip ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
from app.services.email_service import get_email_service
2026-04-09 14:30:51 +07:00
from app.services.security_service import get_security_service
2026-01-14 20:42:10 +08:00
security = get_security_service ()
email_service = get_email_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
data = request . get_json ()
if not data :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "No data provided" , "data" : None }), 400
email = ( data . get ( "email" ) or "" ) . strip () . lower ()
code_type = data . get ( "type" , "register" )
turnstile_token = data . get ( "turnstile_token" )
2026-01-14 20:42:10 +08:00
# Validate email
if not email or not email_service . is_valid_email ( email ):
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Invalid email address" , "data" : None }), 400
2026-01-31 22:34:26 +08:00
# For change_password type with logged-in user, skip Turnstile verification
# because user already authenticated
skip_turnstile = False
2026-04-09 14:30:51 +07:00
if code_type == "change_password" :
2026-01-31 22:34:26 +08:00
# Try to get user_id from token (this route doesn't require login)
from app.utils.auth import verify_token
2026-04-09 14:30:51 +07:00
auth_header = request . headers . get ( "Authorization" )
2026-01-31 22:34:26 +08:00
if auth_header :
parts = auth_header . split ()
2026-04-09 14:30:51 +07:00
if len ( parts ) == 2 and parts [ 0 ] . lower () == "bearer" :
2026-01-31 22:34:26 +08:00
payload = verify_token ( parts [ 1 ])
2026-04-09 14:30:51 +07:00
if payload and payload . get ( "user_id" ):
2026-01-31 22:34:26 +08:00
skip_turnstile = True
2026-04-09 14:30:51 +07:00
2026-01-31 22:34:26 +08:00
# Verify Turnstile (skip for authenticated change_password requests)
if not skip_turnstile :
turnstile_ok , turnstile_msg = security . verify_turnstile ( turnstile_token , ip_address )
if not turnstile_ok :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : turnstile_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Check rate limit
can_send , rate_msg = security . can_send_verification_code ( email , ip_address )
if not can_send :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : rate_msg , "data" : None }), 429
2026-01-14 20:42:10 +08:00
# For registration, check if email already exists
2026-04-09 14:30:51 +07:00
if code_type == "register" :
2026-01-14 20:42:10 +08:00
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
existing = get_user_service () . get_user_by_email ( email )
if existing :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Email already registered" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# For login type - always allow (will auto-create if not exists)
# No special check needed
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# For reset_password, check if email exists
2026-04-09 14:30:51 +07:00
if code_type == "reset_password" :
2026-01-14 20:42:10 +08:00
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
existing = get_user_service () . get_user_by_email ( email )
if not existing :
# Don't reveal if email exists or not (security best practice)
# But still return success to prevent email enumeration
2026-04-09 14:30:51 +07:00
return jsonify (
{ "code" : 1 , "msg" : "If the email exists, a verification code has been sent" , "data" : None }
)
2026-01-14 20:42:10 +08:00
# Send verification code
success , msg = email_service . send_verification_code ( email , code_type , ip_address )
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if success :
2026-04-09 14:30:51 +07:00
security . log_security_event (
"verification_code_sent" , None , ip_address , _get_user_agent (), { "email" : email , "type" : code_type }
)
return jsonify ({ "code" : 1 , "msg" : "Verification code sent" , "data" : None })
2026-01-14 20:42:10 +08:00
else :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : msg , "data" : None }), 500
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "send_verification_code error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Failed to send verification code" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/register" , methods = [ "POST" ])
2026-01-14 20:42:10 +08:00
def register ():
"""
Register new user with email verification.
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
Request body:
email: str
code: str (verification code)
username: str
password: str
turnstile_token: str (optional)
referral_code: str (optional, referrer's user ID)
"""
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
# Check if registration is enabled
2026-04-09 14:30:51 +07:00
if os . getenv ( "ENABLE_REGISTRATION" , "true" ) . lower () != "true" :
return jsonify ({ "code" : 0 , "msg" : "Registration is disabled" , "data" : None }), 403
from app.services.billing_service import get_billing_service
2026-01-14 20:42:10 +08:00
from app.services.email_service import get_email_service
2026-04-09 14:30:51 +07:00
from app.services.security_service import get_security_service
2026-01-14 20:42:10 +08:00
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
security = get_security_service ()
email_service = get_email_service ()
user_service = get_user_service ()
billing_service = get_billing_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
data = request . get_json ()
if not data :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "No data provided" , "data" : None }), 400
email = ( data . get ( "email" ) or "" ) . strip () . lower ()
code = data . get ( "code" , "" ) . strip ()
username = ( data . get ( "username" ) or "" ) . strip ()
password = data . get ( "password" , "" )
turnstile_token = data . get ( "turnstile_token" )
referral_code = data . get ( "referral_code" , "" ) . strip ()
2026-01-14 20:42:10 +08:00
# Validate inputs
if not email or not email_service . is_valid_email ( email ):
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Invalid email address" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
if not code :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Verification code is required" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
if not username or len ( username ) < 3 or len ( username ) > 30 :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Username must be 3-30 characters" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Validate username format (alphanumeric and underscore only)
import re
2026-04-09 14:30:51 +07:00
if not re . match ( r "^[a-zA-Z][a-zA-Z0-9_]*$" , username ):
return jsonify (
{
"code" : 0 ,
"msg" : "Username must start with letter and contain only letters, numbers, and underscores" ,
"data" : None ,
}
), 400
2026-01-14 20:42:10 +08:00
# Validate password strength
pwd_valid , pwd_msg = security . validate_password_strength ( password )
if not pwd_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : pwd_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify Turnstile
turnstile_ok , turnstile_msg = security . verify_turnstile ( turnstile_token , ip_address )
if not turnstile_ok :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : turnstile_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify email code
2026-04-09 14:30:51 +07:00
code_valid , code_msg = email_service . verify_code ( email , code , "register" )
2026-01-14 20:42:10 +08:00
if not code_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : code_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Check if username already exists
existing_user = user_service . get_user_by_username ( username )
if existing_user :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Username already taken" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Check if email already exists
existing_email = user_service . get_user_by_email ( email )
if existing_email :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Email already registered" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Validate referral code (user ID)
referred_by = None
if referral_code :
try :
referrer_id = int ( referral_code )
referrer = user_service . get_user_by_id ( referrer_id )
2026-04-09 14:30:51 +07:00
if referrer and referrer . get ( "status" ) == "active" :
2026-01-14 20:42:10 +08:00
referred_by = referrer_id
except ( ValueError , TypeError ):
pass # Invalid referral code, ignore
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Create user
user_id = user_service . create_user (
username = username ,
password = password ,
email = email ,
nickname = username ,
2026-04-09 14:30:51 +07:00
role = "user" ,
status = "active" ,
2026-01-14 20:42:10 +08:00
email_verified = True ,
2026-04-09 14:30:51 +07:00
referred_by = referred_by ,
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not user_id :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Failed to create account" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# Grant registration bonus credits
2026-04-09 14:30:51 +07:00
register_bonus = int ( os . getenv ( "CREDITS_REGISTER_BONUS" , "0" ))
2026-01-14 20:42:10 +08:00
if register_bonus > 0 :
billing_service . add_credits (
2026-04-09 14:30:51 +07:00
user_id = user_id , amount = register_bonus , action = "register_bonus" , remark = "Registration bonus"
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Grant referral bonus to referrer
if referred_by :
2026-04-09 14:30:51 +07:00
referral_bonus = int ( os . getenv ( "CREDITS_REFERRAL_BONUS" , "0" ))
2026-01-14 20:42:10 +08:00
if referral_bonus > 0 :
billing_service . add_credits (
user_id = referred_by ,
amount = referral_bonus ,
2026-04-09 14:30:51 +07:00
action = "referral_bonus" ,
remark = f "Referral bonus for inviting user { username } " ,
reference_id = str ( user_id ),
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Log registration
2026-04-09 14:30:51 +07:00
security . log_security_event (
"register" , user_id , ip_address , user_agent , { "email" : email , "referred_by" : referred_by }
)
2026-01-31 02:59:49 +08:00
# Auto login after registration (get token_version for new user)
try :
new_token_version = user_service . get_token_version ( user_id )
except Exception as e :
logger . warning ( f "Failed to get token_version: { e } " )
new_token_version = 1
2026-04-09 14:30:51 +07:00
token = generate_token ( user_id = user_id , username = username , role = "user" , token_version = new_token_version )
return jsonify (
{
"code" : 1 ,
"msg" : "Registration successful" ,
"data" : {
"token" : token ,
"userinfo" : {
"id" : user_id ,
"username" : username ,
"nickname" : username ,
"email" : email ,
"avatar" : "/avatar2.jpg" ,
"timezone" : "" ,
"role" : { "id" : "user" , "permissions" : _get_permissions ( "user" )},
},
},
2026-01-14 20:42:10 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "register error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Registration failed" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/reset-password" , methods = [ "POST" ])
2026-01-14 20:42:10 +08:00
def reset_password ():
"""
Reset password with email verification.
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
Request body:
email: str
code: str (verification code)
new_password: str
turnstile_token: str (optional)
"""
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
from app.services.email_service import get_email_service
2026-04-09 14:30:51 +07:00
from app.services.security_service import get_security_service
2026-01-14 20:42:10 +08:00
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
security = get_security_service ()
email_service = get_email_service ()
user_service = get_user_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
data = request . get_json ()
if not data :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "No data provided" , "data" : None }), 400
email = ( data . get ( "email" ) or "" ) . strip () . lower ()
code = data . get ( "code" , "" ) . strip ()
new_password = data . get ( "new_password" , "" )
turnstile_token = data . get ( "turnstile_token" )
2026-01-14 20:42:10 +08:00
# Validate inputs
if not email or not code or not new_password :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing required fields" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Validate password strength
pwd_valid , pwd_msg = security . validate_password_strength ( new_password )
if not pwd_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : pwd_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify Turnstile
turnstile_ok , turnstile_msg = security . verify_turnstile ( turnstile_token , ip_address )
if not turnstile_ok :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : turnstile_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify email code
2026-04-09 14:30:51 +07:00
code_valid , code_msg = email_service . verify_code ( email , code , "reset_password" )
2026-01-14 20:42:10 +08:00
if not code_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : code_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Get user by email
user = user_service . get_user_by_email ( email )
if not user :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "User not found" , "data" : None }), 404
2026-01-14 20:42:10 +08:00
# Update password
2026-04-09 14:30:51 +07:00
success = user_service . update_password ( user [ "id" ], new_password )
2026-01-14 20:42:10 +08:00
if not success :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Failed to reset password" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# Clear any existing login blocks for this account
2026-04-09 14:30:51 +07:00
security . clear_login_attempts ( user [ "username" ], "account" )
2026-01-14 20:42:10 +08:00
# Log password reset
2026-04-09 14:30:51 +07:00
security . log_security_event ( "password_reset" , user [ "id" ], ip_address , user_agent )
return jsonify ({ "code" : 1 , "msg" : "Password reset successful" , "data" : None })
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "reset_password error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Password reset failed" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/change-password" , methods = [ "POST" ])
2026-01-14 20:42:10 +08:00
@login_required
def change_password ():
"""
Change password with email verification (for logged-in users).
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
Request body:
code: str (verification code sent to user's email)
new_password: str
"""
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
user_id = g . user_id
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
from app.services.email_service import get_email_service
2026-04-09 14:30:51 +07:00
from app.services.security_service import get_security_service
2026-01-14 20:42:10 +08:00
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
security = get_security_service ()
email_service = get_email_service ()
user_service = get_user_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
data = request . get_json ()
if not data :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "No data provided" , "data" : None }), 400
code = data . get ( "code" , "" ) . strip ()
new_password = data . get ( "new_password" , "" )
2026-01-14 20:42:10 +08:00
if not code or not new_password :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Missing required fields" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Validate password strength
pwd_valid , pwd_msg = security . validate_password_strength ( new_password )
if not pwd_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : pwd_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Get user
user = user_service . get_user_by_id ( user_id )
2026-04-09 14:30:51 +07:00
if not user or not user . get ( "email" ):
return jsonify ({ "code" : 0 , "msg" : "User email not found" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Verify email code
2026-04-09 14:30:51 +07:00
code_valid , code_msg = email_service . verify_code ( user [ "email" ], code , "change_password" )
2026-01-14 20:42:10 +08:00
if not code_valid :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : code_msg , "data" : None }), 400
2026-01-14 20:42:10 +08:00
# Update password
success = user_service . update_password ( user_id , new_password )
if not success :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Failed to change password" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# Log password change
2026-04-09 14:30:51 +07:00
security . log_security_event ( "password_changed" , user_id , ip_address , user_agent )
return jsonify ({ "code" : 1 , "msg" : "Password changed successfully" , "data" : None })
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "change_password error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Password change failed" , "data" : None }), 500
2026-01-14 20:42:10 +08:00
# =============================================================================
# OAuth Endpoints
# =============================================================================
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/oauth/google" , methods = [ "GET" ])
2026-01-14 20:42:10 +08:00
def oauth_google ():
"""Redirect to Google OAuth authorization page"""
try :
from app.services.oauth_service import get_oauth_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
oauth = get_oauth_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not oauth . google_enabled :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "Google OAuth is not configured" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
auth_url , state = oauth . get_google_auth_url ()
return redirect ( auth_url )
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "oauth_google error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-14 20:42:10 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/oauth/google/callback" , methods = [ "GET" ])
2026-01-14 20:42:10 +08:00
def oauth_google_callback ():
"""Handle Google OAuth callback"""
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
from app.services.oauth_service import get_oauth_service
from app.services.security_service import get_security_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
oauth = get_oauth_service ()
security = get_security_service ()
2026-04-09 14:30:51 +07:00
code = request . args . get ( "code" )
state = request . args . get ( "state" )
error = request . args . get ( "error" )
2026-01-14 20:42:10 +08:00
frontend_url = oauth . frontend_url
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if error :
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = error ))
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not code or not state :
2026-04-09 14:30:51 +07:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = "missing_params" ))
2026-01-14 20:42:10 +08:00
# Handle callback
success , result = oauth . handle_google_callback ( code , state )
if not success :
2026-04-09 14:30:51 +07:00
error_msg = result . get ( "error" , "unknown_error" )
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = error_msg ))
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Get or create user
user_success , user_result = oauth . get_or_create_user_from_oauth ( result )
if not user_success :
2026-04-09 14:30:51 +07:00
error_msg = user_result . get ( "error" , "user_creation_failed" )
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = error_msg ))
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# Increment token_version (invalidates old sessions for single-client login)
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
user_service = get_user_service ()
try :
2026-04-09 14:30:51 +07:00
new_token_version = user_service . increment_token_version ( user_result [ "id" ])
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . warning ( f "Failed to increment token_version: { e } " )
new_token_version = 1
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# Generate token with new token_version
2026-01-14 20:42:10 +08:00
token = generate_token (
2026-04-09 14:30:51 +07:00
user_id = user_result [ "id" ],
username = user_result [ "username" ],
role = user_result . get ( "role" , "user" ),
token_version = new_token_version ,
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Log OAuth login
2026-04-09 14:30:51 +07:00
security . log_security_event ( "oauth_login" , user_result [ "id" ], ip_address , user_agent , { "provider" : "google" })
2026-01-14 20:42:10 +08:00
# Redirect to frontend with token
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_token = token ))
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "oauth_google_callback error: { e } " )
from app.services.oauth_service import get_oauth_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
frontend_url = get_oauth_service () . frontend_url
2026-04-09 14:30:51 +07:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = "server_error" ))
2026-01-14 20:42:10 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/oauth/github" , methods = [ "GET" ])
2026-01-14 20:42:10 +08:00
def oauth_github ():
"""Redirect to GitHub OAuth authorization page"""
try :
from app.services.oauth_service import get_oauth_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
oauth = get_oauth_service ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not oauth . github_enabled :
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : "GitHub OAuth is not configured" , "data" : None }), 400
2026-01-14 20:42:10 +08:00
auth_url , state = oauth . get_github_auth_url ()
return redirect ( auth_url )
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "oauth_github error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 0 , "msg" : str ( e ), "data" : None }), 500
2026-01-14 20:42:10 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/oauth/github/callback" , methods = [ "GET" ])
2026-01-14 20:42:10 +08:00
def oauth_github_callback ():
"""Handle GitHub OAuth callback"""
ip_address = _get_client_ip ()
user_agent = _get_user_agent ()
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
try :
from app.services.oauth_service import get_oauth_service
from app.services.security_service import get_security_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
oauth = get_oauth_service ()
security = get_security_service ()
2026-04-09 14:30:51 +07:00
code = request . args . get ( "code" )
state = request . args . get ( "state" )
error = request . args . get ( "error" )
2026-01-14 20:42:10 +08:00
frontend_url = oauth . frontend_url
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if error :
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = error ))
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
if not code or not state :
2026-04-09 14:30:51 +07:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = "missing_params" ))
2026-01-14 20:42:10 +08:00
# Handle callback
success , result = oauth . handle_github_callback ( code , state )
if not success :
2026-04-09 14:30:51 +07:00
error_msg = result . get ( "error" , "unknown_error" )
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = error_msg ))
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Get or create user
user_success , user_result = oauth . get_or_create_user_from_oauth ( result )
if not user_success :
2026-04-09 14:30:51 +07:00
error_msg = user_result . get ( "error" , "user_creation_failed" )
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = error_msg ))
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# Increment token_version (invalidates old sessions for single-client login)
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
user_service = get_user_service ()
try :
2026-04-09 14:30:51 +07:00
new_token_version = user_service . increment_token_version ( user_result [ "id" ])
2026-01-31 02:59:49 +08:00
except Exception as e :
logger . warning ( f "Failed to increment token_version: { e } " )
new_token_version = 1
2026-04-09 14:30:51 +07:00
2026-01-31 02:59:49 +08:00
# Generate token with new token_version
2026-01-14 20:42:10 +08:00
token = generate_token (
2026-04-09 14:30:51 +07:00
user_id = user_result [ "id" ],
username = user_result [ "username" ],
role = user_result . get ( "role" , "user" ),
token_version = new_token_version ,
2026-01-14 20:42:10 +08:00
)
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
# Log OAuth login
2026-04-09 14:30:51 +07:00
security . log_security_event ( "oauth_login" , user_result [ "id" ], ip_address , user_agent , { "provider" : "github" })
2026-01-14 20:42:10 +08:00
# Redirect to frontend with token
2026-01-14 23:50:31 +08:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_token = token ))
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
except Exception as e :
logger . error ( f "oauth_github_callback error: { e } " )
from app.services.oauth_service import get_oauth_service
2026-04-09 14:30:51 +07:00
2026-01-14 20:42:10 +08:00
frontend_url = get_oauth_service () . frontend_url
2026-04-09 14:30:51 +07:00
return redirect ( _build_frontend_login_redirect ( frontend_url , oauth_error = "server_error" ))
2026-01-14 20:42:10 +08:00
# =============================================================================
# Other Endpoints
# =============================================================================
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/logout" , methods = [ "POST" ])
2025-12-29 03:06:49 +08:00
def logout ():
"""Logout (client removes token; server is stateless)."""
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 1 , "msg" : "Logout successful" , "data" : None })
2025-12-29 03:06:49 +08:00
2026-01-14 05:29:55 +08:00
2026-04-09 14:30:51 +07:00
@auth_bp.route ( "/info" , methods = [ "GET" ])
2026-01-14 05:29:55 +08:00
@login_required
2025-12-29 03:06:49 +08:00
def get_user_info ():
2026-01-14 05:29:55 +08:00
"""Get current user info."""
try :
2026-04-09 14:30:51 +07:00
user_id = getattr ( g , "user_id" , 1 )
username = getattr ( g , "user" , Config . ADMIN_USER )
role = getattr ( g , "user_role" , "admin" )
2026-01-14 05:29:55 +08:00
# Try to get full user info from database
user_data = None
if not _is_single_user_mode ():
try :
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
user_data = get_user_service () . get_user_by_id ( user_id )
except Exception as e :
logger . warning ( f "Failed to get user from database: { e } " )
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
if user_data :
2026-04-09 14:30:51 +07:00
return jsonify (
{
"code" : 1 ,
"msg" : "Success" ,
"data" : {
"id" : user_data . get ( "id" ),
"username" : user_data . get ( "username" ),
"nickname" : user_data . get ( "nickname" , "User" ),
"email" : user_data . get ( "email" ),
"avatar" : user_data . get ( "avatar" , "/avatar2.jpg" ),
"timezone" : str ( user_data . get ( "timezone" ) or "" ) . strip (),
"role" : {
"id" : user_data . get ( "role" , "user" ),
"permissions" : _get_permissions ( user_data . get ( "role" , "user" )),
},
},
2026-01-14 05:29:55 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-14 05:29:55 +08:00
# Fallback for legacy mode
2026-04-09 14:30:51 +07:00
return jsonify (
{
"code" : 1 ,
"msg" : "Success" ,
"data" : {
"id" : user_id ,
"username" : username ,
"nickname" : "Admin" ,
"avatar" : "/avatar2.jpg" ,
"timezone" : "" ,
"role" : { "id" : role , "permissions" : _get_permissions ( role )},
},
2026-01-14 05:29:55 +08:00
}
2026-04-09 14:30:51 +07:00
)
2026-01-14 05:29:55 +08:00
except Exception as e :
logger . error ( f "get_user_info error: { e } " )
2026-04-09 14:30:51 +07:00
return jsonify ({ "code" : 500 , "msg" : str ( e ), "data" : None }), 500
2026-01-14 05:29:55 +08:00
2025-12-29 03:06:49 +08:00
2026-01-14 05:29:55 +08:00
def _get_permissions ( role : str ) -> list :
"""Get permissions list for a role"""
try :
from app.services.user_service import get_user_service
2026-04-09 14:30:51 +07:00
2026-01-14 05:29:55 +08:00
return get_user_service () . get_user_permissions ( role )
except Exception :
# Default permissions for admin
2026-04-09 14:30:51 +07:00
if role == "admin" :
return [
"dashboard" ,
"view" ,
"indicator" ,
"backtest" ,
"strategy" ,
"portfolio" ,
"settings" ,
"user_manage" ,
"credentials" ,
]
return [ "dashboard" , "view" , "indicator" , "backtest" , "strategy" , "portfolio" ]