Update app callback server
This commit is contained in:
@@ -0,0 +1,368 @@
|
||||
"""
|
||||
QBO Excel Sync - OAuth Callback Server
|
||||
|
||||
This is a simple Flask server that handles OAuth callbacks from QuickBooks.
|
||||
Host this on your own server (e.g., https://yourcompany.com/oauth/callback)
|
||||
|
||||
Usage:
|
||||
1. Deploy this to your server
|
||||
2. Set up SSL (required for QuickBooks production)
|
||||
3. Register the callback URL in your QuickBooks app settings
|
||||
4. Update the desktop app's redirect_uri to match
|
||||
|
||||
Environment Variables:
|
||||
- PORT: Server port (default: 5000)
|
||||
- SECRET_KEY: Flask secret key for sessions
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, render_template_string, jsonify
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = os.environ.get('SECRET_KEY', 'your-secret-key-change-in-production')
|
||||
|
||||
# HTML template for displaying the authorization code
|
||||
CALLBACK_TEMPLATE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>QBO Excel Sync - Authorization</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
padding: 40px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
.success-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #10b981;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
.success-icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
fill: white;
|
||||
}
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background: #ef4444;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
.error-icon svg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
fill: white;
|
||||
}
|
||||
h1 {
|
||||
color: #1f2937;
|
||||
font-size: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.subtitle {
|
||||
color: #6b7280;
|
||||
font-size: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.code-section {
|
||||
background: #f3f4f6;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.code-label {
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.code-box {
|
||||
background: white;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
word-break: break-all;
|
||||
margin-bottom: 16px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.copy-btn {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 12px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
}
|
||||
.copy-btn:hover {
|
||||
background: #2563eb;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.copy-btn:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
.copy-btn.copied {
|
||||
background: #10b981;
|
||||
}
|
||||
.realm-info {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #f59e0b;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
text-align: left;
|
||||
}
|
||||
.realm-info strong {
|
||||
color: #92400e;
|
||||
}
|
||||
.instructions {
|
||||
text-align: left;
|
||||
color: #4b5563;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.instructions ol {
|
||||
margin-left: 20px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.instructions li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #ef4444;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
color: #991b1b;
|
||||
text-align: left;
|
||||
}
|
||||
.footer {
|
||||
margin-top: 32px;
|
||||
color: #9ca3af;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% if error %}
|
||||
<div class="error-icon">
|
||||
<svg viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
|
||||
</div>
|
||||
<h1>Authorization Failed</h1>
|
||||
<div class="error-message">
|
||||
<strong>Error:</strong> {{ error }}<br>
|
||||
{% if error_description %}
|
||||
<strong>Details:</strong> {{ error_description }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="success-icon">
|
||||
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
||||
</div>
|
||||
<h1>Authorization Successful!</h1>
|
||||
<p class="subtitle">Copy the information below and paste it into the QBO Excel Sync app.</p>
|
||||
|
||||
<div class="realm-info">
|
||||
<strong>Company ID (Realm ID):</strong> {{ realm_id }}
|
||||
</div>
|
||||
|
||||
<div class="code-section">
|
||||
<div class="code-label">Authorization Code</div>
|
||||
<div class="code-box" id="authCode">{{ code }}</div>
|
||||
<button class="copy-btn" onclick="copyCode()">
|
||||
<span id="btnText">Copy Authorization Code</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="instructions">
|
||||
<strong>Next Steps:</strong>
|
||||
<ol>
|
||||
<li>Go back to the <strong>QBO Excel Sync</strong> desktop application</li>
|
||||
<li>Click <strong>"Enter Code Manually"</strong> button</li>
|
||||
<li>Paste the <strong>Authorization Code</strong> and <strong>Company ID</strong></li>
|
||||
<li>Click <strong>Connect</strong></li>
|
||||
</ol>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="footer">
|
||||
QBO Excel Sync © {{ year }} | You can close this window after copying the code.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyCode() {
|
||||
const code = document.getElementById('authCode').textContent;
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
const btn = document.querySelector('.copy-btn');
|
||||
const btnText = document.getElementById('btnText');
|
||||
btn.classList.add('copied');
|
||||
btnText.textContent = '✓ Copied!';
|
||||
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('copied');
|
||||
btnText.textContent = 'Copy Authorization Code';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""Health check endpoint."""
|
||||
return jsonify({
|
||||
'status': 'ok',
|
||||
'service': 'QBO Excel Sync OAuth Callback Server',
|
||||
'timestamp': datetime.now().isoformat()
|
||||
})
|
||||
|
||||
|
||||
@app.route('/oauth/callback')
|
||||
def oauth_callback():
|
||||
"""
|
||||
Handle OAuth callback from QuickBooks.
|
||||
|
||||
QuickBooks redirects here with:
|
||||
- code: Authorization code (on success)
|
||||
- realmId: Company ID
|
||||
- state: State parameter we sent
|
||||
- error: Error code (on failure)
|
||||
- error_description: Error details (on failure)
|
||||
"""
|
||||
# Get parameters
|
||||
code = request.args.get('code')
|
||||
realm_id = request.args.get('realmId')
|
||||
state = request.args.get('state')
|
||||
error = request.args.get('error')
|
||||
error_description = request.args.get('error_description')
|
||||
|
||||
# Log the callback
|
||||
logger.info(f"OAuth callback received - realm_id: {realm_id}, state: {state}, error: {error}")
|
||||
|
||||
if error:
|
||||
logger.error(f"OAuth error: {error} - {error_description}")
|
||||
return render_template_string(
|
||||
CALLBACK_TEMPLATE,
|
||||
error=error,
|
||||
error_description=error_description,
|
||||
year=datetime.now().year
|
||||
)
|
||||
|
||||
if not code:
|
||||
logger.error("No authorization code received")
|
||||
return render_template_string(
|
||||
CALLBACK_TEMPLATE,
|
||||
error="No authorization code received",
|
||||
error_description="The authorization server did not return a code.",
|
||||
year=datetime.now().year
|
||||
)
|
||||
|
||||
# Success - display the code to the user
|
||||
logger.info(f"Authorization successful for realm {realm_id}")
|
||||
|
||||
return render_template_string(
|
||||
CALLBACK_TEMPLATE,
|
||||
code=code,
|
||||
realm_id=realm_id,
|
||||
state=state,
|
||||
error=None,
|
||||
year=datetime.now().year
|
||||
)
|
||||
|
||||
|
||||
@app.route('/oauth/callback/json')
|
||||
def oauth_callback_json():
|
||||
"""
|
||||
JSON endpoint for programmatic access.
|
||||
Can be used if you want to implement automatic code relay.
|
||||
"""
|
||||
code = request.args.get('code')
|
||||
realm_id = request.args.get('realmId')
|
||||
state = request.args.get('state')
|
||||
error = request.args.get('error')
|
||||
error_description = request.args.get('error_description')
|
||||
|
||||
if error:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': error,
|
||||
'error_description': error_description
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'code': code,
|
||||
'realm_id': realm_id,
|
||||
'state': state
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.environ.get('PORT', 5000))
|
||||
debug = os.environ.get('DEBUG', 'false').lower() == 'true'
|
||||
|
||||
print(f"""
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ QBO Excel Sync - OAuth Callback Server ║
|
||||
╠═══════════════════════════════════════════════════════════════╣
|
||||
║ Server running on port {port} ║
|
||||
║ Callback URL: http://localhost:{port}/oauth/callback ║
|
||||
║ ║
|
||||
║ For production, deploy with HTTPS (required by QuickBooks) ║
|
||||
║ Example: https://yourcompany.com/oauth/callback ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
""")
|
||||
|
||||
app.run(host='0.0.0.0', port=port, debug=debug)
|
||||
Reference in New Issue
Block a user