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:
2025-09-11 11:00:34 +09:00
parent 24cf84fda3
commit fd8c5cbb81
3 changed files with 597 additions and 1 deletions

View File

@@ -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):