From 06d0098a43a128fd3b3760a66a6cc3d75cf16246 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, 7 May 2026 08:57:55 +0900 Subject: [PATCH] =?UTF-8?q?=EC=B4=88=EA=B8=B0=20=EC=BB=A4=EB=B0=8B:=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EC=84=9C=EB=B2=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 0bin-label-app 프로젝트의 auth 폴더에서 별도 리포지토리로 분리. Flask 기반 로그인 인증 서버: - POST /api/login: 클라이언트 로그인 API - GET /api/health: 서버 상태 확인 - /admin: 관리자 웹 페이지 - SQLite 기반 사용자 및 로그인 기록 저장 Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 27 ++++++ README.md | 99 +++++++++++++++++++ app.py | 197 ++++++++++++++++++++++++++++++++++++++ models.py | 48 ++++++++++ requirements.txt | 3 + templates/dashboard.html | 199 +++++++++++++++++++++++++++++++++++++++ templates/login.html | 91 ++++++++++++++++++ 7 files changed, 664 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 models.py create mode 100644 requirements.txt create mode 100644 templates/dashboard.html create mode 100644 templates/login.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc217c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +*.egg-info/ +.venv/ +venv/ +env/ + +# Flask +instance/ +*.sqlite +*.db + +# IDE +.vscode/ +.idea/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..24f671c --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# 0bin Label App - 인증 서버 + +Flask 기반 인증 서버 (포트: 8898) + +## 설치 + +```bash +cd auth +pip install -r requirements.txt +``` + +## 실행 + +```bash +python app.py +``` + +서버가 `http://0.0.0.0:8898`에서 시작됩니다. + +## 기능 + +### 1. 클라이언트 인증 API + +- **엔드포인트**: `POST /api/login` +- **요청**: + ```json + { + "username": "test", + "password": "test" + } + ``` +- **응답 (성공)**: + ```json + { + "success": true, + "message": "로그인 성공", + "username": "test" + } + ``` +- **응답 (실패)**: + ```json + { + "success": false, + "message": "아이디 또는 비밀번호가 올바르지 않습니다" + } + ``` + +### 2. Admin 웹 페이지 + +- **URL**: `http://localhost:8898/admin` +- **계정**: `admin` / `admin1234` +- **기능**: + - 사용자 목록 조회 + - 로그인 기록 조회 (최근 50개) + - 통계 확인 + +## 기본 계정 + +### 클라이언트 계정 +- 아이디: `test` +- 비밀번호: `test` + +### Admin 계정 +- 아이디: `admin` +- 비밀번호: `admin1234` + +## 리버스 프록시 설정 + +외부에서 `login.0bin.in`으로 접근하려면 Nginx 등을 사용하여 리버스 프록시를 설정하세요. + +```nginx +server { + listen 80; + server_name login.0bin.in; + + location / { + proxy_pass http://localhost:8898; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + +## 데이터베이스 + +- SQLite (`auth_db.sqlite`) +- 자동으로 생성됨 +- 테이블: + - `users`: 사용자 정보 + - `login_logs`: 로그인 기록 + +## 보안 + +**주의**: 프로덕션 환경에서는 다음을 변경하세요: +1. `app.config['SECRET_KEY']` 변경 +2. Admin 계정 비밀번호 변경 +3. HTTPS 사용 +4. 환경 변수로 민감 정보 관리 diff --git a/app.py b/app.py new file mode 100644 index 0000000..456cb7f --- /dev/null +++ b/app.py @@ -0,0 +1,197 @@ +# app.py +# 0bin Label App 인증 서버 (Flask) + +from flask import Flask, request, jsonify, render_template, redirect, url_for, session +from models import db, User, LoginLog +from datetime import datetime +import os + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'your-secret-key-change-this-in-production' +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///auth_db.sqlite' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +# DB 초기화 +db.init_app(app) + + +# ============================================================ +# API 엔드포인트 (클라이언트용) +# ============================================================ + +@app.route('/api/login', methods=['POST']) +def api_login(): + """클라이언트 로그인 API""" + try: + data = request.get_json() + username = data.get('username', '').strip() + password = data.get('password', '').strip() + + if not username or not password: + return jsonify({ + 'success': False, + 'message': '아이디와 비밀번호를 입력하세요' + }), 400 + + # 사용자 조회 + user = User.query.filter_by(username=username).first() + + # 로그인 시도 기록 + ip_address = request.remote_addr + user_agent = request.headers.get('User-Agent', '') + + if user and user.is_active and user.check_password(password): + # 로그인 성공 + log = LoginLog( + user_id=user.id, + ip_address=ip_address, + user_agent=user_agent, + success=True + ) + db.session.add(log) + db.session.commit() + + return jsonify({ + 'success': True, + 'message': '로그인 성공', + 'username': user.username + }), 200 + else: + # 로그인 실패 + if user: + log = LoginLog( + user_id=user.id, + ip_address=ip_address, + user_agent=user_agent, + success=False + ) + db.session.add(log) + db.session.commit() + + return jsonify({ + 'success': False, + 'message': '아이디 또는 비밀번호가 올바르지 않습니다' + }), 401 + + except Exception as e: + return jsonify({ + 'success': False, + 'message': f'서버 오류: {str(e)}' + }), 500 + + +@app.route('/api/health', methods=['GET']) +def api_health(): + """서버 상태 확인""" + return jsonify({ + 'status': 'ok', + 'timestamp': datetime.utcnow().isoformat() + }), 200 + + +# ============================================================ +# Admin 페이지 (웹 UI) +# ============================================================ + +@app.route('/') +def index(): + """메인 페이지 - Admin 로그인으로 리다이렉트""" + return redirect(url_for('admin_login')) + + +@app.route('/admin', methods=['GET']) +def admin_login(): + """Admin 로그인 페이지""" + if 'admin_logged_in' in session: + return redirect(url_for('admin_dashboard')) + return render_template('login.html') + + +@app.route('/admin/login', methods=['POST']) +def admin_login_post(): + """Admin 로그인 처리""" + username = request.form.get('username', '').strip() + password = request.form.get('password', '').strip() + + # 간단한 admin 계정 (하드코딩) + # 실제 운영 시에는 별도 Admin 테이블 사용 권장 + if username == 'admin' and password == 'admin1234': + session['admin_logged_in'] = True + session['admin_username'] = username + return redirect(url_for('admin_dashboard')) + else: + return render_template('login.html', error='잘못된 관리자 계정입니다') + + +@app.route('/admin/logout') +def admin_logout(): + """Admin 로그아웃""" + session.pop('admin_logged_in', None) + session.pop('admin_username', None) + return redirect(url_for('admin_login')) + + +@app.route('/admin/dashboard') +def admin_dashboard(): + """Admin 대시보드""" + if 'admin_logged_in' not in session: + return redirect(url_for('admin_login')) + + # 사용자 목록 + users = User.query.all() + + # 최근 로그인 기록 (최대 50개) + recent_logs = LoginLog.query.order_by( + LoginLog.login_time.desc() + ).limit(50).all() + + # 통계 + total_users = User.query.count() + total_logins = LoginLog.query.filter_by(success=True).count() + failed_logins = LoginLog.query.filter_by(success=False).count() + + return render_template( + 'dashboard.html', + users=users, + recent_logs=recent_logs, + total_users=total_users, + total_logins=total_logins, + failed_logins=failed_logins + ) + + +# ============================================================ +# DB 초기화 +# ============================================================ + +def init_db(): + """데이터베이스 초기화 및 test 계정 생성""" + with app.app_context(): + # 테이블 생성 + db.create_all() + + # test 계정이 없으면 생성 + test_user = User.query.filter_by(username='test').first() + if not test_user: + test_user = User(username='test') + test_user.set_password('test') + db.session.add(test_user) + db.session.commit() + print('[DB] test/test 계정 생성 완료') + else: + print('[DB] test 계정 이미 존재') + + +if __name__ == '__main__': + # DB 초기화 + if not os.path.exists('auth_db.sqlite'): + print('[DB] 데이터베이스 생성 중...') + init_db() + else: + print('[DB] 기존 데이터베이스 사용') + + # Flask 서버 실행 (8898 포트) + print('[서버] 인증 서버 시작: http://0.0.0.0:8898') + print('[서버] Admin 페이지: http://localhost:8898/admin') + print('[서버] API 엔드포인트: http://localhost:8898/api/login') + app.run(host='0.0.0.0', port=8898, debug=True) diff --git a/models.py b/models.py new file mode 100644 index 0000000..fde3df8 --- /dev/null +++ b/models.py @@ -0,0 +1,48 @@ +# models.py +# 인증 서버 데이터베이스 모델 + +from flask_sqlalchemy import SQLAlchemy +from datetime import datetime +from werkzeug.security import generate_password_hash, check_password_hash + +db = SQLAlchemy() + + +class User(db.Model): + """사용자 모델""" + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(50), unique=True, nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + is_active = db.Column(db.Boolean, default=True) + + # 관계: 로그인 기록 + login_logs = db.relationship('LoginLog', backref='user', lazy=True) + + def set_password(self, password): + """비밀번호 해시 설정""" + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + """비밀번호 확인""" + return check_password_hash(self.password_hash, password) + + def __repr__(self): + return f'' + + +class LoginLog(db.Model): + """로그인 기록 모델""" + __tablename__ = 'login_logs' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + login_time = db.Column(db.DateTime, default=datetime.utcnow) + ip_address = db.Column(db.String(50)) + user_agent = db.Column(db.String(255)) + success = db.Column(db.Boolean, default=True) + + def __repr__(self): + return f'' diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3d29f5e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +Flask==3.0.0 +Flask-SQLAlchemy==3.1.1 +Werkzeug==3.0.1 diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 0000000..5ffd6ef --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,199 @@ + + + + + + 0bin Label App - Admin 대시보드 + + + + + + + + +
+ +
+
+
+
+
+
+
총 사용자
+

{{ total_users }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
성공한 로그인
+

{{ total_logins }}

+
+
+ +
+
+
+
+
+
+
+
+
+
+
실패한 로그인
+

{{ failed_logins }}

+
+
+ +
+
+
+
+
+
+ + +
+

+ 사용자 목록 +

+
+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
ID아이디생성일활성 상태로그인 횟수
{{ user.id }}{{ user.username }}{{ user.created_at.strftime('%Y-%m-%d %H:%M') }} + {% if user.is_active %} + 활성 + {% else %} + 비활성 + {% endif %} + + + {{ user.login_logs|selectattr('success')|list|length }}회 + +
+
+
+ + +
+

+ 최근 로그인 기록 (최대 50개) +

+
+ + + + + + + + + + + + {% for log in recent_logs %} + + + + + + + + {% endfor %} + +
시간사용자IP 주소User Agent결과
{{ log.login_time.strftime('%Y-%m-%d %H:%M:%S') }}{{ log.user.username }}{{ log.ip_address }} + + {{ log.user_agent[:50] }}{% if log.user_agent|length > 50 %}...{% endif %} + + + {% if log.success %} + 성공 + {% else %} + 실패 + {% endif %} +
+
+
+
+ + + + diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..7b13146 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,91 @@ + + + + + + 0bin Label App - Admin 로그인 + + + + + + + + +