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