468 lines
19 KiB
Python
468 lines
19 KiB
Python
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()
|