Signed-off-by: TIANHE <TIANHE@GMAIL.COM>
This commit is contained in:
TIANHE
2026-01-14 23:50:31 +08:00
parent 47efb7e024
commit 6011ba4ce3
4 changed files with 71 additions and 15 deletions
+35 -12
View File
@@ -6,6 +6,7 @@ Supports both multi-user (database) and single-user (legacy) modes.
""" """
import os import os
from flask import Blueprint, request, jsonify, g, redirect from flask import Blueprint, request, jsonify, g, redirect
from urllib.parse import urlencode
from app.config.settings import Config from app.config.settings import Config
from app.utils.auth import generate_token, login_required, authenticate_legacy from app.utils.auth import generate_token, login_required, authenticate_legacy
from app.utils.logger import get_logger from app.utils.logger import get_logger
@@ -14,6 +15,28 @@ logger = get_logger(__name__)
auth_bp = Blueprint('auth', __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: def _is_single_user_mode() -> bool:
"""Check if system is in single-user (legacy) mode""" """Check if system is in single-user (legacy) mode"""
@@ -813,22 +836,22 @@ def oauth_google_callback():
frontend_url = oauth.frontend_url frontend_url = oauth.frontend_url
if error: 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: 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 # Handle callback
success, result = oauth.handle_google_callback(code, state) success, result = oauth.handle_google_callback(code, state)
if not success: if not success:
error_msg = result.get('error', 'unknown_error') 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 # Get or create user
user_success, user_result = oauth.get_or_create_user_from_oauth(result) user_success, user_result = oauth.get_or_create_user_from_oauth(result)
if not user_success: if not user_success:
error_msg = user_result.get('error', 'user_creation_failed') 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 # Generate token
token = generate_token( token = generate_token(
@@ -842,13 +865,13 @@ def oauth_google_callback():
{'provider': 'google'}) {'provider': 'google'})
# Redirect to frontend with token # 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: except Exception as e:
logger.error(f"oauth_google_callback error: {e}") logger.error(f"oauth_google_callback error: {e}")
from app.services.oauth_service import get_oauth_service from app.services.oauth_service import get_oauth_service
frontend_url = get_oauth_service().frontend_url 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']) @auth_bp.route('/oauth/github', methods=['GET'])
@@ -889,22 +912,22 @@ def oauth_github_callback():
frontend_url = oauth.frontend_url frontend_url = oauth.frontend_url
if error: 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: 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 # Handle callback
success, result = oauth.handle_github_callback(code, state) success, result = oauth.handle_github_callback(code, state)
if not success: if not success:
error_msg = result.get('error', 'unknown_error') 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 # Get or create user
user_success, user_result = oauth.get_or_create_user_from_oauth(result) user_success, user_result = oauth.get_or_create_user_from_oauth(result)
if not user_success: if not user_success:
error_msg = user_result.get('error', 'user_creation_failed') 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 # Generate token
token = generate_token( token = generate_token(
@@ -918,13 +941,13 @@ def oauth_github_callback():
{'provider': 'github'}) {'provider': 'github'})
# Redirect to frontend with token # 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: except Exception as e:
logger.error(f"oauth_github_callback error: {e}") logger.error(f"oauth_github_callback error: {e}")
from app.services.oauth_service import get_oauth_service from app.services.oauth_service import get_oauth_service
frontend_url = get_oauth_service().frontend_url 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'))
# ============================================================================= # =============================================================================
@@ -431,6 +431,24 @@ class OAuthService:
db.commit() db.commit()
cur.close() 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, { return True, {
'id': user_id, 'id': user_id,
+15 -2
View File
@@ -1,5 +1,18 @@
import request from '@/utils/request' 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) * Get security configuration (Turnstile, OAuth settings)
*/ */
@@ -107,12 +120,12 @@ export function changePassword (data) {
* Get Google OAuth URL * Get Google OAuth URL
*/ */
export function getGoogleOAuthUrl () { 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 * Get GitHub OAuth URL
*/ */
export function getGitHubOAuthUrl () { export function getGitHubOAuthUrl () {
return `${process.env.VUE_APP_API_BASE_URL || ''}/api/auth/oauth/github` return joinApiBase('/api/auth/oauth/github')
} }
+3 -1
View File
@@ -794,7 +794,9 @@ export default {
if (oauthToken) { if (oauthToken) {
this.oauthProcessing = true 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]) window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split('?')[0])
this.$store.dispatch('GetInfo').then(() => { this.$store.dispatch('GetInfo').then(() => {
this.$router.push({ path: '/' }) this.$router.push({ path: '/' })