@@ -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'))
|
||||
|
||||
|
||||
# =============================================================================
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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: '/' })
|
||||
|
||||
Reference in New Issue
Block a user