diff --git a/backend_api_python/app/routes/auth.py b/backend_api_python/app/routes/auth.py index 186d7d4..f25c195 100644 --- a/backend_api_python/app/routes/auth.py +++ b/backend_api_python/app/routes/auth.py @@ -6,6 +6,7 @@ Supports both multi-user (database) and single-user (legacy) modes. """ import os from flask import Blueprint, request, jsonify, g, redirect +from urllib.parse import urlencode from app.config.settings import Config from app.utils.auth import generate_token, login_required, authenticate_legacy from app.utils.logger import get_logger @@ -14,6 +15,28 @@ logger = get_logger(__name__) auth_bp = Blueprint('auth', __name__) +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`. + """ + base = (frontend_url or '').strip().rstrip('/') + if not base: + base = 'http://localhost:8080' + + if '/#/' in base: + origin = base.split('/#/', 1)[0].rstrip('/') + elif '#' in base: + origin = base.split('#', 1)[0].rstrip('/') + else: + origin = base + + login_url = f"{origin}/#/user/login" + qs = urlencode({k: v for k, v in params.items() if v is not None and v != ''}) + return f"{login_url}?{qs}" if qs else login_url + def _is_single_user_mode() -> bool: """Check if system is in single-user (legacy) mode""" @@ -813,22 +836,22 @@ def oauth_google_callback(): frontend_url = oauth.frontend_url if error: - return redirect(f"{frontend_url}/user/login?oauth_error={error}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error)) if not code or not state: - return redirect(f"{frontend_url}/user/login?oauth_error=missing_params") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error='missing_params')) # Handle callback success, result = oauth.handle_google_callback(code, state) if not success: error_msg = result.get('error', 'unknown_error') - return redirect(f"{frontend_url}/user/login?oauth_error={error_msg}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg)) # Get or create user user_success, user_result = oauth.get_or_create_user_from_oauth(result) if not user_success: error_msg = user_result.get('error', 'user_creation_failed') - return redirect(f"{frontend_url}/user/login?oauth_error={error_msg}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg)) # Generate token token = generate_token( @@ -842,13 +865,13 @@ def oauth_google_callback(): {'provider': 'google'}) # Redirect to frontend with token - return redirect(f"{frontend_url}/user/login?oauth_token={token}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_token=token)) except Exception as e: logger.error(f"oauth_google_callback error: {e}") from app.services.oauth_service import get_oauth_service frontend_url = get_oauth_service().frontend_url - return redirect(f"{frontend_url}/user/login?oauth_error=server_error") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error='server_error')) @auth_bp.route('/oauth/github', methods=['GET']) @@ -889,22 +912,22 @@ def oauth_github_callback(): frontend_url = oauth.frontend_url if error: - return redirect(f"{frontend_url}/user/login?oauth_error={error}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error)) if not code or not state: - return redirect(f"{frontend_url}/user/login?oauth_error=missing_params") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error='missing_params')) # Handle callback success, result = oauth.handle_github_callback(code, state) if not success: error_msg = result.get('error', 'unknown_error') - return redirect(f"{frontend_url}/user/login?oauth_error={error_msg}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg)) # Get or create user user_success, user_result = oauth.get_or_create_user_from_oauth(result) if not user_success: error_msg = user_result.get('error', 'user_creation_failed') - return redirect(f"{frontend_url}/user/login?oauth_error={error_msg}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error=error_msg)) # Generate token token = generate_token( @@ -918,13 +941,13 @@ def oauth_github_callback(): {'provider': 'github'}) # Redirect to frontend with token - return redirect(f"{frontend_url}/user/login?oauth_token={token}") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_token=token)) except Exception as e: logger.error(f"oauth_github_callback error: {e}") from app.services.oauth_service import get_oauth_service frontend_url = get_oauth_service().frontend_url - return redirect(f"{frontend_url}/user/login?oauth_error=server_error") + return redirect(_build_frontend_login_redirect(frontend_url, oauth_error='server_error')) # ============================================================================= diff --git a/backend_api_python/app/services/oauth_service.py b/backend_api_python/app/services/oauth_service.py index 2d134f3..931c4ee 100644 --- a/backend_api_python/app/services/oauth_service.py +++ b/backend_api_python/app/services/oauth_service.py @@ -431,6 +431,24 @@ class OAuthService: db.commit() cur.close() + + # Grant registration bonus credits for OAuth-created users + # Keep consistent with email/password registration flows (auth.py). + try: + register_bonus = int(os.getenv('CREDITS_REGISTER_BONUS', '0')) + except (ValueError, TypeError): + register_bonus = 0 + if register_bonus > 0: + try: + from app.services.billing_service import get_billing_service + get_billing_service().add_credits( + user_id=user_id, + amount=register_bonus, + action='register_bonus', + remark=f'Registration bonus (OAuth:{provider})' + ) + except Exception as e: + logger.warning(f"Failed to grant OAuth registration bonus: {e}") return True, { 'id': user_id, diff --git a/quantdinger_vue/src/api/auth.js b/quantdinger_vue/src/api/auth.js index bb388b3..5212a6f 100644 --- a/quantdinger_vue/src/api/auth.js +++ b/quantdinger_vue/src/api/auth.js @@ -1,5 +1,18 @@ import request from '@/utils/request' +function joinApiBase (path) { + const base = (process.env.VUE_APP_API_BASE_URL || '').trim() + const p = path.startsWith('/') ? path : `/${path}` + if (!base) return p + + const b = base.replace(/\/+$/, '') + // Avoid duplicate "/api/api/*" when base is "/api" or ends with "/api" + if (b.endsWith('/api') && p.startsWith('/api/')) { + return b + p.slice('/api'.length) + } + return b + p +} + /** * Get security configuration (Turnstile, OAuth settings) */ @@ -107,12 +120,12 @@ export function changePassword (data) { * Get Google OAuth URL */ export function getGoogleOAuthUrl () { - return `${process.env.VUE_APP_API_BASE_URL || ''}/api/auth/oauth/google` + return joinApiBase('/api/auth/oauth/google') } /** * Get GitHub OAuth URL */ export function getGitHubOAuthUrl () { - return `${process.env.VUE_APP_API_BASE_URL || ''}/api/auth/oauth/github` + return joinApiBase('/api/auth/oauth/github') } diff --git a/quantdinger_vue/src/views/user/Login.vue b/quantdinger_vue/src/views/user/Login.vue index ee4f2c1..b12b64e 100644 --- a/quantdinger_vue/src/views/user/Login.vue +++ b/quantdinger_vue/src/views/user/Login.vue @@ -794,7 +794,9 @@ export default { if (oauthToken) { this.oauthProcessing = true - storage.set(ACCESS_TOKEN, oauthToken, 7 * 24 * 60 * 60 * 1000) + // NOTE: storage expire plugin expects an absolute timestamp (ms since epoch), + // not a duration. Use "now + 7 days" to avoid immediate expiration. + storage.set(ACCESS_TOKEN, oauthToken, new Date().getTime() + 7 * 24 * 60 * 60 * 1000) window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split('?')[0]) this.$store.dispatch('GetInfo').then(() => { this.$router.push({ path: '/' })