Headscale 사용자 관리 기능 완전 구현
- Headscale CLI 기반 사용자 생성/삭제 API 엔드포인트 추가 - 사용자-약국 매칭 정보 실시간 표시 및 관리 - 완전한 사용자 관리 웹 인터페이스 구현 - 통계 대시보드: 총 사용자, 약국 연결, 미연결, 노드 수 - 사용자별 노드 연결 상태 및 약국 정보 매칭 표시 - 새 사용자 생성 모달 (display_name, email 지원) - 안전한 사용자 삭제 확인 기능 - 네비게이션 메뉴에 사용자 관리 추가 - Headplane과 동일한 기능 + 약국 매칭 정보 추가 제공 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ Headscale + Headplane 고도화 관리자 페이지
|
||||
|
||||
from flask import Flask, render_template, jsonify, request, redirect, url_for
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
from config import config
|
||||
@@ -15,6 +16,7 @@ from utils.database_new import (
|
||||
get_machine_detail, get_pharmacy_detail, get_active_alerts,
|
||||
sync_machines_from_headscale, sync_users_from_headscale
|
||||
)
|
||||
from models.farmq_models import PharmacyInfo
|
||||
import subprocess
|
||||
from utils.proxmox_client import ProxmoxClient
|
||||
|
||||
@@ -437,6 +439,157 @@ def create_app(config_name=None):
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 사용자 관리 라우트
|
||||
@app.route('/users')
|
||||
def users_list():
|
||||
"""사용자 관리 페이지"""
|
||||
return render_template('users/list.html')
|
||||
|
||||
# 사용자 관리 API
|
||||
@app.route('/api/users', methods=['GET'])
|
||||
def api_get_users():
|
||||
"""Headscale 사용자 목록 조회"""
|
||||
try:
|
||||
# Headscale CLI를 통해 사용자 목록 조회
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', 'headscale', 'headscale', 'users', 'list', '-o', 'json'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
users_data = json.loads(result.stdout)
|
||||
|
||||
# FARMQ 약국 정보와 매칭
|
||||
farmq_session = get_farmq_session()
|
||||
try:
|
||||
pharmacies = farmq_session.query(PharmacyInfo).all()
|
||||
pharmacy_map = {p.headscale_user_name: p for p in pharmacies if p.headscale_user_name}
|
||||
|
||||
# 사용자별 노드 수 조회
|
||||
for user in users_data:
|
||||
user_name = user.get('name', '')
|
||||
|
||||
# 약국 정보 매칭
|
||||
pharmacy = pharmacy_map.get(user_name)
|
||||
user['pharmacy'] = {
|
||||
'id': pharmacy.id,
|
||||
'name': pharmacy.pharmacy_name,
|
||||
'manager': pharmacy.manager_name
|
||||
} if pharmacy else None
|
||||
|
||||
# 해당 사용자의 노드 수 조회
|
||||
node_result = subprocess.run(
|
||||
['docker', 'exec', 'headscale', 'headscale', 'nodes', 'list', '-o', 'json'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
nodes_data = json.loads(node_result.stdout)
|
||||
user['node_count'] = len([n for n in nodes_data if n.get('user', {}).get('name') == user_name])
|
||||
|
||||
return jsonify({'success': True, 'users': users_data})
|
||||
|
||||
finally:
|
||||
farmq_session.close()
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else e.stdout
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'사용자 목록 조회 실패: {error_msg}'
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 사용자 목록 조회 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users', methods=['POST'])
|
||||
def api_create_user():
|
||||
"""새 Headscale 사용자 생성"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
user_name = data.get('name', '').strip()
|
||||
display_name = data.get('display_name', '').strip()
|
||||
email = data.get('email', '').strip()
|
||||
|
||||
if not user_name:
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': '사용자 이름은 필수입니다.'
|
||||
}), 400
|
||||
|
||||
# Headscale CLI를 통해 사용자 생성
|
||||
cmd = ['docker', 'exec', 'headscale', 'headscale', 'users', 'create', user_name]
|
||||
|
||||
if display_name:
|
||||
cmd.extend(['-d', display_name])
|
||||
if email:
|
||||
cmd.extend(['-e', email])
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'사용자 "{user_name}"가 성공적으로 생성되었습니다.',
|
||||
'output': result.stdout
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else e.stdout
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'사용자 생성 실패: {error_msg}'
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 사용자 생성 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/users/<user_name>/delete', methods=['DELETE'])
|
||||
def api_delete_user(user_name):
|
||||
"""Headscale 사용자 삭제"""
|
||||
try:
|
||||
# Headscale CLI를 통해 사용자 삭제
|
||||
result = subprocess.run(
|
||||
['docker', 'exec', 'headscale', 'headscale', 'users', 'destroy',
|
||||
'--name', user_name, '--force'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'사용자 "{user_name}"가 성공적으로 삭제되었습니다.',
|
||||
'output': result.stdout
|
||||
})
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else e.stdout
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'사용자 삭제 실패: {error_msg}'
|
||||
}), 400
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 사용자 삭제 오류: {e}")
|
||||
return jsonify({
|
||||
'success': False,
|
||||
'error': f'서버 오류: {str(e)}'
|
||||
}), 500
|
||||
|
||||
# 에러 핸들러
|
||||
@app.errorhandler(404)
|
||||
def not_found_error(error):
|
||||
|
||||
Reference in New Issue
Block a user