AutoPercenty3/admin/main.py

468 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import sys
import time
import os
import requests
from PySide6.QtWidgets import (
QApplication, QDialog, QMainWindow, QVBoxLayout, QHBoxLayout, QWidget,
QLabel, QLineEdit, QPushButton, QTextEdit, QTableWidget, QTableWidgetItem,
QTabWidget, QMessageBox, QComboBox
)
from PySide6.QtCore import QTimer, Slot, Qt
from supabase import create_client, Client
##############################################
# 1. Supabase 클라이언트 초기화
##############################################
# SUPABASE_URL = os.environ.get("SUPABASE_URL", "https://sp1.cckb9998.synology.me")
# SUPABASE_KEY = os.environ.get("SUPABASE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q")
SUPABASE_URL = os.environ.get("SUPABASE_URL", "http://oci1ckh08045.duckdns.org:8000")
SUPABASE_KEY = os.environ.get("SUPABASE_KEY", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJzZXJ2aWNlX3JvbGUiLAogICAgImlzcyI6ICJzdXBhYmFzZS1kZW1vIiwKICAgICJpYXQiOiAxNjQxNzY5MjAwLAogICAgImV4cCI6IDE3OTk1MzU2MDAKfQ.DaYlNEoUrrEn2Ig7tqibS-PHK5vgusbcbo7X36XVt4Q")
supabase: Client = create_client(SUPABASE_URL, SUPABASE_KEY)
##############################################
# 2. Supabase API 연동 함수들
##############################################
def admin_sign_in(email: str, password: str):
"""Supabase Auth를 이용한 관리자 로그인 (실패 시 error 객체 반환)"""
# result = supabase.auth.sign_in_with_password(email=email, password=password)
result = supabase.auth.sign_in_with_password({"email": email, "password": password})
print(f"result : {result}")
user_id = result.user.id
# if result.get("error"):
# return None, result["error"]
return user_id, None
def fetch_pending_signups():
"""pending_signups 테이블의 대기 중인 가입 요청을 반환"""
response = supabase.table("pending_signups").select("*").execute()
if response.error:
print("fetch_pending_signups error:", response.error)
return []
return response.data
def approve_signup(user_id: str):
"""
가입 승인 처리
1. pending_signups에서 해당 사용자의 레코드를 조회
2. users 테이블에 신규 사용자 추가 (멤버십은 기본 'basic')
3. pending_signups에서 해당 레코드 삭제
"""
resp = supabase.table("pending_signups").select("*").eq("id", user_id).execute()
if resp.error or not resp.data:
return False
signup = resp.data[0]
# 사용자 추가 추가 컬럼은 실제 스키마에 맞게 수정 필요
ins = supabase.table("users").insert({
"id": signup["id"],
"email": signup["email"],
"nickname": signup.get("nickname", "New User"),
"membership_level": "basic"
}).execute()
if ins.error:
print("approve_signup insert error:", ins.error)
return False
# pending_signups에서 삭제
del_resp = supabase.table("pending_signups").delete().eq("id", user_id).execute()
return del_resp.error is None
def fetch_common_banned_words():
"""common_banned_words 테이블의 금지어 목록을 반환"""
response = supabase.table("common_banned_words").select("*").execute()
if response.error:
print("fetch_common_banned_words error:", response.error)
return []
return response.data
def add_common_banned_word(word: str, grade: str, request_reason: str):
"""common_banned_words 테이블에 금지어 추가"""
ins = supabase.table("common_banned_words").insert({
"banned_word": word,
"grade": grade,
"request_reason": request_reason
}).execute()
if ins.error:
print("add_common_banned_word error:", ins.error)
return ins.error is None
def update_common_banned_word(word: str, new_grade: str, request_reason: str):
"""common_banned_words 테이블에서 금지어 수정"""
upd = supabase.table("common_banned_words").update({
"grade": new_grade,
"request_reason": request_reason
}).eq("banned_word", word).execute()
if upd.error:
print("update_common_banned_word error:", upd.error)
return upd.error is None
def delete_common_banned_word(word: str):
"""common_banned_words 테이블에서 금지어 삭제"""
del_resp = supabase.table("common_banned_words").delete().eq("banned_word", word).execute()
if del_resp.error:
print("delete_common_banned_word error:", del_resp.error)
return del_resp.error is None
def fetch_users():
"""users 테이블의 사용자 목록을 반환"""
response = supabase.table("users").select("*").execute()
if response.error:
print("fetch_users error:", response.error)
return []
return response.data
def update_membership(user_id: str, new_level: str):
"""users 테이블에서 특정 사용자의 멤버십 레벨 업데이트"""
upd = supabase.table("users").update({
"membership_level": new_level
}).eq("id", user_id).execute()
if upd.error:
print("update_membership error:", upd.error)
return upd.error is None
##############################################
# 3. Notification Manager (텔레그램 알림)
##############################################
def send_telegram_notification(bot_token: str, chat_id: str, message: str) -> dict:
"""
텔레그램 봇을 통해 메시지를 전송합니다.
:param bot_token: 텔레그램 봇 토큰
:param chat_id: 메시지를 받을 채팅 ID
:param message: 전송할 메시지
:return: API 응답 (JSON)
"""
url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
payload = {"chat_id": chat_id, "text": message}
try:
response = requests.post(url, data=payload)
return response.json()
except Exception as e:
print("Telegram notification error:", e)
return {"error": str(e)}
##############################################
# 4. 관리자 로그인 다이얼로그
##############################################
class AdminLoginDialog(QDialog):
def __init__(self):
super().__init__()
self.setWindowTitle("Admin Login")
self.setFixedSize(300, 150)
layout = QVBoxLayout()
self.label_id = QLabel("Email:")
self.edit_id = QLineEdit()
layout.addWidget(self.label_id)
layout.addWidget(self.edit_id)
self.label_pw = QLabel("Password:")
self.edit_pw = QLineEdit()
self.edit_pw.setEchoMode(QLineEdit.Password)
layout.addWidget(self.label_pw)
layout.addWidget(self.edit_pw)
self.btn_login = QPushButton("Login")
self.btn_login.clicked.connect(self.handle_login)
layout.addWidget(self.btn_login)
self.setLayout(layout)
@Slot()
def handle_login(self):
email = self.edit_id.text().strip()
password = self.edit_pw.text().strip()
user, error = admin_sign_in(email, password)
if error or user is None:
QMessageBox.warning(self, "Login Failed", f"Invalid credentials.\nError: {error}")
else:
self.accept()
##############################################
# 5. PendingSignupsTab (회원가입 승인 탭)
##############################################
class PendingSignupsTab(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.refresh_data()
def init_ui(self):
layout = QVBoxLayout()
self.table = QTableWidget(0, 5)
self.table.setHorizontalHeaderLabels(["ID", "Email", "Request Reason", "Submitted At", "Attachments"])
layout.addWidget(self.table)
self.btn_approve = QPushButton("Approve Selected Signup")
self.btn_approve.clicked.connect(self.approve_selected)
layout.addWidget(self.btn_approve)
self.setLayout(layout)
def refresh_data(self):
signups = fetch_pending_signups()
self.table.setRowCount(len(signups))
for row, signup in enumerate(signups):
self.table.setItem(row, 0, QTableWidgetItem(str(signup.get("id", ""))))
self.table.setItem(row, 1, QTableWidgetItem(signup.get("email", "")))
self.table.setItem(row, 2, QTableWidgetItem(signup.get("request_reason", "")))
self.table.setItem(row, 3, QTableWidgetItem(signup.get("submitted_at", "")))
self.table.setItem(row, 4, QTableWidgetItem(str(signup.get("attachments", ""))))
@Slot()
def approve_selected(self):
selected = self.table.currentRow()
if selected < 0:
QMessageBox.warning(self, "No selection", "Select a signup to approve.")
return
user_id = self.table.item(selected, 0).text()
if approve_signup(user_id):
QMessageBox.information(self, "Approved", f"User {user_id} approved.")
self.refresh_data()
else:
QMessageBox.warning(self, "Error", "Failed to approve signup.")
##############################################
# 6. BannedWordsTab (Common Banned Words 관리 탭)
##############################################
class BannedWordsTab(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.refresh_data()
def init_ui(self):
layout = QVBoxLayout()
self.table = QTableWidget(0, 4)
self.table.setHorizontalHeaderLabels(["Word", "Grade", "Request Reason", "Created At"])
layout.addWidget(self.table)
form_layout = QHBoxLayout()
# 금지어 입력 (자유 텍스트)
self.edit_word = QLineEdit()
self.edit_word.setPlaceholderText("Word")
form_layout.addWidget(self.edit_word)
# 드롭다운: 금지어 레벨 선택 ("비허용", "금지")
self.combo_grade = QComboBox()
self.combo_grade.addItems(["비허용", "금지"])
form_layout.addWidget(self.combo_grade)
# 요청 사유 입력 (멀티라인)
self.edit_request_reason = QTextEdit()
self.edit_request_reason.setPlaceholderText("Request Reason (텍스트, 링크, 사진, 문서 등)")
self.edit_request_reason.setFixedHeight(50)
form_layout.addWidget(self.edit_request_reason)
self.btn_add = QPushButton("Add Word")
self.btn_add.clicked.connect(self.add_word)
self.btn_update = QPushButton("Update")
self.btn_update.clicked.connect(self.update_word)
self.btn_delete = QPushButton("Delete")
self.btn_delete.clicked.connect(self.delete_word)
form_layout.addWidget(self.btn_add)
form_layout.addWidget(self.btn_update)
form_layout.addWidget(self.btn_delete)
layout.addLayout(form_layout)
self.setLayout(layout)
def refresh_data(self):
words = fetch_common_banned_words()
self.table.setRowCount(len(words))
for row, info in enumerate(words):
self.table.setItem(row, 0, QTableWidgetItem(info.get("banned_word", "")))
self.table.setItem(row, 1, QTableWidgetItem(info.get("grade", "")))
self.table.setItem(row, 2, QTableWidgetItem(info.get("request_reason", "")))
self.table.setItem(row, 3, QTableWidgetItem(info.get("created_at", "")))
@Slot()
def add_word(self):
word = self.edit_word.text().strip()
grade = self.combo_grade.currentText()
request_reason = self.edit_request_reason.toPlainText().strip()
if not word or not grade:
QMessageBox.warning(self, "Input Error", "Please provide word and select a grade.")
return
if add_common_banned_word(word, grade, request_reason):
QMessageBox.information(self, "Added", f"Word '{word}' added.")
self.refresh_data()
else:
QMessageBox.warning(self, "Error", "Failed to add word (it may already exist).")
@Slot()
def update_word(self):
selected = self.table.currentRow()
if selected < 0:
QMessageBox.warning(self, "No selection", "Select a word to update.")
return
word = self.table.item(selected, 0).text()
new_grade = self.combo_grade.currentText()
request_reason = self.edit_request_reason.toPlainText().strip()
if update_common_banned_word(word, new_grade, request_reason):
QMessageBox.information(self, "Updated", f"Word '{word}' updated.")
self.refresh_data()
else:
QMessageBox.warning(self, "Error", "Failed to update word.")
@Slot()
def delete_word(self):
selected = self.table.currentRow()
if selected < 0:
QMessageBox.warning(self, "No selection", "Select a word to delete.")
return
word = self.table.item(selected, 0).text()
if delete_common_banned_word(word):
QMessageBox.information(self, "Deleted", f"Word '{word}' deleted.")
self.refresh_data()
else:
QMessageBox.warning(self, "Error", "Failed to delete word.")
##############################################
# 7. UserInfoTab (유저 정보 관리 탭)
##############################################
class UserInfoTab(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.refresh_data()
def init_ui(self):
layout = QVBoxLayout()
self.table = QTableWidget(0, 4)
self.table.setHorizontalHeaderLabels(["ID", "Email", "Nickname", "Membership Level"])
layout.addWidget(self.table)
form_layout = QHBoxLayout()
# 드롭다운: 멤버십 레벨 선택 ("basic", "premium", "vip")
self.combo_membership = QComboBox()
self.combo_membership.addItems(["basic", "premium", "vip"])
form_layout.addWidget(QLabel("New Membership Level:"))
form_layout.addWidget(self.combo_membership)
self.btn_update_membership = QPushButton("Update Membership")
self.btn_update_membership.clicked.connect(self.update_membership)
form_layout.addWidget(self.btn_update_membership)
layout.addLayout(form_layout)
self.setLayout(layout)
def refresh_data(self):
users = fetch_users()
self.table.setRowCount(len(users))
for row, user in enumerate(users):
self.table.setItem(row, 0, QTableWidgetItem(str(user.get("id", ""))))
self.table.setItem(row, 1, QTableWidgetItem(user.get("email", "")))
self.table.setItem(row, 2, QTableWidgetItem(user.get("nickname", "")))
self.table.setItem(row, 3, QTableWidgetItem(user.get("membership_level", "")))
@Slot()
def update_membership(self):
selected = self.table.currentRow()
if selected < 0:
QMessageBox.warning(self, "No selection", "Select a user to update membership.")
return
new_level = self.combo_membership.currentText()
user_id = self.table.item(selected, 0).text()
if update_membership(user_id, new_level):
QMessageBox.information(self, "Updated", f"User {user_id} membership updated to {new_level}.")
self.refresh_data()
else:
QMessageBox.warning(self, "Error", "Failed to update membership.")
##############################################
# 8. AdminMainWindow (관리자 메인 윈도우)
##############################################
# 알림 응답 타임아웃 (예: 10초)
ALERT_RESPONSE_TIMEOUT = 10000
# 텔레그램 알림용 자격증명 (실제 값 입력)
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "your_bot_token_here")
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "your_chat_id_here")
class AdminMainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Admin Client")
self.setGeometry(100, 100, 900, 700)
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
# 탭 위젯: 회원가입 승인, 금지어 관리, 유저 정보 관리
self.tabs = QTabWidget()
self.pending_signups_tab = PendingSignupsTab()
self.banned_words_tab = BannedWordsTab()
self.user_info_tab = UserInfoTab()
self.tabs.addTab(self.pending_signups_tab, "회원가입 승인")
self.tabs.addTab(self.banned_words_tab, "Common Banned Words 관리")
self.tabs.addTab(self.user_info_tab, "유저 정보 관리")
layout.addWidget(self.tabs)
# 실시간 알림 로그 영역
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
layout.addWidget(self.log_area)
# 알림 응답 버튼
self.btn_respond = QPushButton("Respond to Alert", self)
self.btn_respond.clicked.connect(self.user_response)
layout.addWidget(self.btn_respond)
# 실시간 알림 타이머 (예: 5초마다 알림 시뮬레이션)
self.alert_timer = QTimer(self)
self.alert_timer.timeout.connect(self.receive_alert)
self.alert_timer.start(5000)
# 응답 타이머 (알림 후 지정 시간 내 응답 없으면 외부 알림 발송)
self.response_timer = QTimer(self)
self.response_timer.setSingleShot(True)
self.response_timer.timeout.connect(self.handle_no_response)
self.current_alert = None
@Slot()
def receive_alert(self):
alert_message = f"Alert received at {time.strftime('%H:%M:%S')}"
self.log_area.append(alert_message)
self.current_alert = alert_message
self.response_timer.start(ALERT_RESPONSE_TIMEOUT)
@Slot()
def handle_no_response(self):
if self.current_alert:
notification_message = f"No response to alert: {self.current_alert}"
self.log_area.append("No response detected, sending push notification...")
result = send_telegram_notification(TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, notification_message)
self.log_area.append(f"Telegram notification sent: {result}")
self.current_alert = None
@Slot()
def user_response(self):
if self.response_timer.isActive():
self.response_timer.stop()
self.log_area.append("Admin responded to the alert.")
self.current_alert = None
##############################################
# 9. main() 함수: 실행 진입점
##############################################
def main():
app = QApplication(sys.argv)
# 관리자 로그인 다이얼로그
login_dialog = AdminLoginDialog()
if login_dialog.exec() != QDialog.Accepted:
sys.exit("Login canceled or failed.")
window = AdminMainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()