Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
This commit is contained in:
commit
c177d7981d
43 files changed
+18124
No files matched your search
@@ -0,0 +1,363 @@
|
||||
"""Account API routes for AnchorHost."""
|
||||
|
||||
|
||||
from database_service import database_connection, dict_from_row
|
||||
from datetime import datetime, timedelta
|
||||
from email_service import get_email_service
|
||||
from flask import Blueprint, jsonify, request
|
||||
from runtime_config import (
|
||||
logger,
|
||||
)
|
||||
from server_lifecycle import delete_server
|
||||
from user_service import authenticate_user, get_bearer_token, get_user_by_username, hash_password, verify_password
|
||||
import uuid
|
||||
|
||||
__all__ = ['change_user_email', 'change_user_password', 'delete_user_account', 'export_user_data', 'get_user_profile', 'resend_verification', 'update_user_profile']
|
||||
|
||||
|
||||
|
||||
bp = Blueprint("account", __name__)
|
||||
|
||||
|
||||
@bp.route('/api/user/profile', methods=['GET'])
|
||||
def get_user_profile():
|
||||
"""Get user profile data"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get full user data including profile fields
|
||||
cursor.execute('''
|
||||
SELECT id, username, email, role, credits, created_at, updated_at,
|
||||
display_name, first_name, last_name, address, city,
|
||||
country, postal_code, phone, email_verified,
|
||||
email_verification_token, password_changed_at,
|
||||
two_factor_enabled, last_login, login_count, google_id, apple_id
|
||||
FROM users WHERE id = ?
|
||||
''', (user['id'],))
|
||||
|
||||
user_data = dict_from_row(cursor.fetchone())
|
||||
|
||||
if user_data:
|
||||
# Remove sensitive fields
|
||||
if 'email_verification_token' in user_data:
|
||||
user_data.pop('email_verification_token')
|
||||
if 'two_factor_secret' in user_data:
|
||||
user_data.pop('two_factor_secret', None)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'user': user_data
|
||||
}), 200
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'User not found'}), 404
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting user profile: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/user/profile', methods=['PUT'])
|
||||
def update_user_profile():
|
||||
"""Update user profile"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Build update query dynamically based on provided fields
|
||||
update_fields = []
|
||||
params = []
|
||||
|
||||
allowed_fields = ['display_name', 'first_name', 'last_name', 'address',
|
||||
'city', 'country', 'postal_code', 'phone']
|
||||
|
||||
for field in allowed_fields:
|
||||
if field in data:
|
||||
update_fields.append(f"{field} = ?")
|
||||
params.append(data[field])
|
||||
|
||||
if not update_fields:
|
||||
return jsonify({'success': False, 'error': 'No fields to update'}), 400
|
||||
|
||||
# Add updated_at
|
||||
update_fields.append("updated_at = CURRENT_TIMESTAMP")
|
||||
params.append(user['id'])
|
||||
|
||||
query = f"UPDATE users SET {', '.join(update_fields)} WHERE id = ?"
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"✓ Profile updated for user: {user['username']}")
|
||||
return jsonify({'success': True, 'message': 'Profile updated successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating profile: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/user/email', methods=['PUT'])
|
||||
def change_user_email():
|
||||
"""Change user email address"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
new_email = data.get('email')
|
||||
|
||||
if not new_email:
|
||||
return jsonify({'success': False, 'error': 'Email required'}), 400
|
||||
|
||||
# Validate email format
|
||||
import re
|
||||
if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', new_email):
|
||||
return jsonify({'success': False, 'error': 'Invalid email format'}), 400
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if email is already in use
|
||||
cursor.execute('SELECT id FROM users WHERE email = ? AND id != ?', (new_email, user['id']))
|
||||
if cursor.fetchone():
|
||||
return jsonify({'success': False, 'error': 'Email already in use'}), 400
|
||||
|
||||
# Generate new verification token
|
||||
verification_token = str(uuid.uuid4())
|
||||
|
||||
# Update user email and set as unverified
|
||||
cursor.execute('''
|
||||
UPDATE users
|
||||
SET email = ?, email_verified = FALSE, email_verification_token = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (new_email, verification_token, user['id']))
|
||||
|
||||
# Create verification token record
|
||||
token_id = f"token-{str(uuid.uuid4())[:8]}"
|
||||
expires_at = (datetime.now() + timedelta(hours=24)).isoformat()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO email_verification_tokens (id, user_id, token, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (token_id, user['id'], verification_token, expires_at))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Send verification email to new address
|
||||
email_svc = get_email_service()
|
||||
if email_svc:
|
||||
email_svc.send_verification_email(new_email, user['username'], verification_token)
|
||||
|
||||
logger.info(f"✓ Email changed for user: {user['username']} -> {new_email}")
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'Email changed. Please check your new email for verification link.'
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error changing email: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/user/password', methods=['PUT'])
|
||||
def change_user_password():
|
||||
"""Change user password"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
current_password = data.get('currentPassword')
|
||||
new_password = data.get('newPassword')
|
||||
|
||||
if not current_password or not new_password:
|
||||
return jsonify({'success': False, 'error': 'Current and new passwords required'}), 400
|
||||
|
||||
# Validate new password length
|
||||
if len(new_password) < 6:
|
||||
return jsonify({'success': False, 'error': 'Password must be at least 6 characters'}), 400
|
||||
|
||||
# Verify current password
|
||||
user_full = get_user_by_username(user['username'])
|
||||
if not verify_password(current_password, user_full['password_hash']):
|
||||
return jsonify({'success': False, 'error': 'Current password is incorrect'}), 401
|
||||
|
||||
# Hash new password
|
||||
new_password_hash = hash_password(new_password)
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
UPDATE users
|
||||
SET password_hash = ?, password_changed_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (new_password_hash, user['id']))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"✓ Password changed for user: {user['username']}")
|
||||
return jsonify({'success': True, 'message': 'Password changed successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error changing password: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/user/export-data', methods=['GET'])
|
||||
def export_user_data():
|
||||
"""Export all user data"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Get user data
|
||||
cursor.execute('SELECT * FROM users WHERE id = ?', (user['id'],))
|
||||
user_data = dict_from_row(cursor.fetchone())
|
||||
|
||||
# Remove sensitive fields
|
||||
if user_data:
|
||||
user_data.pop('password_hash', None)
|
||||
user_data.pop('email_verification_token', None)
|
||||
user_data.pop('two_factor_secret', None)
|
||||
|
||||
# Get servers
|
||||
cursor.execute('SELECT * FROM servers WHERE owner_id = ?', (user['id'],))
|
||||
servers = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
# Get domains
|
||||
cursor.execute('SELECT * FROM user_domains WHERE user_id = ?', (user['id'],))
|
||||
domains = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
# Get tickets
|
||||
cursor.execute('SELECT * FROM tickets WHERE user_id = ?', (user['id'],))
|
||||
tickets = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
# Get credit transactions
|
||||
cursor.execute('SELECT * FROM credit_transactions WHERE user_id = ?', (user['id'],))
|
||||
transactions = [dict_from_row(row) for row in cursor.fetchall()]
|
||||
|
||||
export_data = {
|
||||
'user': user_data,
|
||||
'servers': servers,
|
||||
'domains': domains,
|
||||
'tickets': tickets,
|
||||
'credit_transactions': transactions,
|
||||
'exported_at': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
logger.info(f"✓ Data exported for user: {user['username']}")
|
||||
return jsonify({'success': True, 'data': export_data}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error exporting data: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/user/account', methods=['DELETE'])
|
||||
def delete_user_account():
|
||||
"""Delete user account and all associated data"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
data = request.get_json()
|
||||
confirmation = data.get('confirmation')
|
||||
|
||||
if confirmation != 'DELETE MY ACCOUNT':
|
||||
return jsonify({'success': False, 'error': 'Invalid confirmation text'}), 400
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Delete all user's servers (cascade will handle related data)
|
||||
cursor.execute('SELECT id FROM servers WHERE owner_id = ?', (user['id'],))
|
||||
server_ids = [row['id'] for row in cursor.fetchall()]
|
||||
|
||||
# Delete servers from Kubernetes
|
||||
for server_id in server_ids:
|
||||
try:
|
||||
delete_server(server_id, user['id'])
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting server {server_id}: {e}")
|
||||
|
||||
# Delete user (cascade will handle most related data)
|
||||
cursor.execute('DELETE FROM users WHERE id = ?', (user['id'],))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"✓ Account deleted for user: {user['username']}")
|
||||
return jsonify({'success': True, 'message': 'Account deleted successfully'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting account: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
|
||||
@bp.route('/api/user/resend-verification', methods=['POST'])
|
||||
def resend_verification():
|
||||
"""Resend verification email"""
|
||||
try:
|
||||
token = get_bearer_token()
|
||||
user = authenticate_user(token)
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Unauthorized'}), 401
|
||||
|
||||
# Check if already verified
|
||||
if user.get('email_verified'):
|
||||
return jsonify({'success': False, 'error': 'Email already verified'}), 400
|
||||
|
||||
# Generate new token
|
||||
new_token = str(uuid.uuid4())
|
||||
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Delete old tokens
|
||||
cursor.execute('DELETE FROM email_verification_tokens WHERE user_id = ?', (user['id'],))
|
||||
|
||||
# Create new token
|
||||
token_id = f"token-{str(uuid.uuid4())[:8]}"
|
||||
expires_at = (datetime.now() + timedelta(hours=24)).isoformat()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO email_verification_tokens (id, user_id, token, expires_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (token_id, user['id'], new_token, expires_at))
|
||||
|
||||
# Update user
|
||||
cursor.execute('''
|
||||
UPDATE users
|
||||
SET email_verification_token = ?
|
||||
WHERE id = ?
|
||||
''', (new_token, user['id']))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Send email
|
||||
email_svc = get_email_service()
|
||||
if email_svc:
|
||||
success = email_svc.send_verification_email(user['email'], user['username'], new_token)
|
||||
if success:
|
||||
return jsonify({'success': True, 'message': 'Verification email sent'}), 200
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Failed to send email'}), 500
|
||||
else:
|
||||
return jsonify({'success': False, 'error': 'Email service unavailable'}), 503
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error resending verification: {e}")
|
||||
return jsonify({'success': False, 'error': str(e)}), 500
|
||||
Reference in new issue
Block a user