From fd8c5cbb816707543e66b2c10f313a6b6b21c601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=8B=9C=EA=B3=A8=EC=95=BD=EC=82=AC?= Date: Thu, 11 Sep 2025 11:00:34 +0900 Subject: [PATCH] =?UTF-8?q?Headscale=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B8=B0=EB=8A=A5=20=EC=99=84=EC=A0=84=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Headscale CLI 기반 사용자 생성/삭제 API 엔드포인트 추가 - 사용자-약국 매칭 정보 실시간 표시 및 관리 - 완전한 사용자 관리 웹 인터페이스 구현 - 통계 대시보드: 총 사용자, 약국 연결, 미연결, 노드 수 - 사용자별 노드 연결 상태 및 약국 정보 매칭 표시 - 새 사용자 생성 모달 (display_name, email 지원) - 안전한 사용자 삭제 확인 기능 - 네비게이션 메뉴에 사용자 관리 추가 - Headplane과 동일한 기능 + 약국 매칭 정보 추가 제공 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- farmq-admin/app.py | 153 +++++++++ farmq-admin/templates/base.html | 2 +- farmq-admin/templates/users/list.html | 443 ++++++++++++++++++++++++++ 3 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 farmq-admin/templates/users/list.html diff --git a/farmq-admin/app.py b/farmq-admin/app.py index c232a88..b4d9c91 100644 --- a/farmq-admin/app.py +++ b/farmq-admin/app.py @@ -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//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): diff --git a/farmq-admin/templates/base.html b/farmq-admin/templates/base.html index cbc4042..295e002 100644 --- a/farmq-admin/templates/base.html +++ b/farmq-admin/templates/base.html @@ -193,7 +193,7 @@ diff --git a/farmq-admin/templates/users/list.html b/farmq-admin/templates/users/list.html new file mode 100644 index 0000000..cc48677 --- /dev/null +++ b/farmq-admin/templates/users/list.html @@ -0,0 +1,443 @@ +{% extends "base.html" %} + +{% block title %}사용자 관리 - 팜큐 약국 관리 시스템{% endblock %} + +{% block breadcrumb %} + +{% endblock %} + +{% block content %} +
+
+
+
+

+ + 사용자 관리 +

+

Headscale 네트워크 사용자 및 약국 매칭 관리

+
+
+ + +
+
+
+
+ + +
+
+
+
+
0
+
+ 총 사용자 수 +
+
+
+
+ +
+
+
+
0
+
+ 약국 연결됨 +
+
+
+
+ +
+
+
+
0
+
+ 연결 필요 +
+
+
+
+ +
+
+
+
0
+
+ 총 연결 노드 +
+
+
+
+
+ + +
+
+
+
+
+ Headscale 사용자 목록 +
+
+
+
+
+ 로딩 중... +
+

사용자 목록을 로드하고 있습니다...

+
+
+
+
+
+ + + + + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} \ No newline at end of file