232 lines
7.0 KiB
Python
232 lines
7.0 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
相册查看器后端(带登录保护)—— 双模式认证(本地 Session + SSO Cookie)
|
||
"""
|
||
from flask import Flask, jsonify, send_from_directory, request, make_response
|
||
from flask_cors import CORS
|
||
from flask_login import LoginManager, login_required, current_user
|
||
from pathlib import Path
|
||
from datetime import timedelta
|
||
import os
|
||
import sys
|
||
|
||
# 添加当前目录到路径
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from models import db, User, init_default_users
|
||
from auth import auth_bp, api_login_required
|
||
|
||
app = Flask(__name__)
|
||
app.config['SECRET_KEY'] = 'album-viewer-secret-key-change-in-production'
|
||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///album_auth.db'
|
||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||
|
||
# Session cookie 配置(支持跨域)
|
||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # HTTP环境用Lax
|
||
app.config['SESSION_COOKIE_SECURE'] = False # HTTP环境
|
||
app.config['SESSION_COOKIE_DOMAIN'] = None
|
||
|
||
# 初始化扩展
|
||
db.init_app(app)
|
||
# CORS 精确白名单(不能用 *,因为需要 credentials)
|
||
CORS(app, supports_credentials=True, origins=[
|
||
"http://129.211.173.87:8892",
|
||
"http://129.211.173.87:9000",
|
||
"http://localhost:8892",
|
||
])
|
||
|
||
# Flask-Login
|
||
login_manager = LoginManager()
|
||
login_manager.init_app(app)
|
||
|
||
@login_manager.user_loader
|
||
def load_user(user_id):
|
||
return User.query.get(int(user_id))
|
||
|
||
# 注册蓝图
|
||
app.register_blueprint(auth_bp)
|
||
|
||
# 配置
|
||
ALBUMS_ROOT = Path('/home/ubuntu/albums')
|
||
THUMB_WIDTH = 320
|
||
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
|
||
|
||
# SSO Cookie 配置(与 SSO 中心一致,用于清除 Cookie)
|
||
SSO_COOKIE_DOMAIN = "129.211.173.87"
|
||
|
||
|
||
def is_image(filename):
|
||
"""检查是否为图片文件"""
|
||
return Path(filename).suffix.lower() in IMAGE_EXTENSIONS
|
||
|
||
|
||
def get_directory_contents(path):
|
||
"""获取目录内容(相册和图片)"""
|
||
items = {'albums': [], 'images': []}
|
||
|
||
if not path.exists():
|
||
return items
|
||
|
||
for item in sorted(path.iterdir(), key=lambda x: x.name.lower()):
|
||
if item.is_dir() and item.name != 'thumbnails':
|
||
has_content = False
|
||
for child in item.iterdir():
|
||
if child.is_file() and is_image(child.name):
|
||
has_content = True
|
||
break
|
||
if child.is_dir() and child.name != 'thumbnails':
|
||
has_content = True
|
||
break
|
||
|
||
cover = None
|
||
thumb_dir = item / 'thumbnails'
|
||
if thumb_dir.exists():
|
||
for thumb in sorted(thumb_dir.iterdir()):
|
||
if is_image(thumb.name):
|
||
cover = f"/thumbs/{item.relative_to(ALBUMS_ROOT)}/thumbnails/{thumb.name}"
|
||
break
|
||
|
||
items['albums'].append({
|
||
'name': item.name,
|
||
'path': str(item.relative_to(ALBUMS_ROOT)),
|
||
'cover': cover,
|
||
'type': 'album'
|
||
})
|
||
|
||
elif item.is_file() and is_image(item.name):
|
||
rel_path = item.relative_to(ALBUMS_ROOT)
|
||
thumb_path = item.parent / 'thumbnails' / item.name
|
||
has_thumbnail = thumb_path.exists()
|
||
|
||
items['images'].append({
|
||
'name': item.name,
|
||
'path': str(rel_path),
|
||
'thumbnail': f"/thumbs/{rel_path.parent}/thumbnails/{item.name}" if has_thumbnail else None,
|
||
'size': item.stat().st_size,
|
||
'type': 'image'
|
||
})
|
||
|
||
return items
|
||
|
||
|
||
# ========== 受保护路由(需要登录)==========
|
||
|
||
@app.route('/api/albums', strict_slashes=False)
|
||
@api_login_required
|
||
def list_albums():
|
||
"""列出相册 - 需要登录"""
|
||
contents = get_directory_contents(ALBUMS_ROOT)
|
||
return jsonify({
|
||
'path': '',
|
||
'name': '相册首页',
|
||
'albums': contents['albums'],
|
||
'images': contents['images']
|
||
})
|
||
|
||
|
||
@app.route('/api/albums/<path:album_path>', strict_slashes=False)
|
||
@api_login_required
|
||
def get_album(album_path):
|
||
"""获取相册内容 - 需要登录"""
|
||
album_full_path = ALBUMS_ROOT / album_path
|
||
|
||
try:
|
||
album_full_path.relative_to(ALBUMS_ROOT)
|
||
except ValueError:
|
||
return jsonify({'error': 'Invalid path'}), 403
|
||
|
||
if not album_full_path.exists():
|
||
return jsonify({'error': 'Album not found'}), 404
|
||
|
||
contents = get_directory_contents(album_full_path)
|
||
|
||
return jsonify({
|
||
'path': album_path,
|
||
'name': album_full_path.name,
|
||
'albums': contents['albums'],
|
||
'images': contents['images']
|
||
})
|
||
|
||
|
||
@app.route('/api/image/<path:image_path>')
|
||
@api_login_required
|
||
def get_image(image_path):
|
||
"""获取原图 - 需要登录 ⭐ 关键保护 """
|
||
full_path = ALBUMS_ROOT / image_path
|
||
|
||
try:
|
||
full_path.relative_to(ALBUMS_ROOT)
|
||
except ValueError:
|
||
return jsonify({'error': 'Invalid path'}), 403
|
||
|
||
if not full_path.exists() or not is_image(full_path.name):
|
||
return jsonify({'error': 'Image not found'}), 404
|
||
|
||
return send_from_directory(full_path.parent, full_path.name)
|
||
|
||
|
||
@app.route('/thumbs/<path:thumb_path>')
|
||
@api_login_required
|
||
def serve_thumbnail(thumb_path):
|
||
"""服务缩略图 - 需要登录 ⭐ 关键保护 """
|
||
full_path = ALBUMS_ROOT / thumb_path
|
||
|
||
try:
|
||
full_path.relative_to(ALBUMS_ROOT)
|
||
except ValueError:
|
||
return jsonify({'error': 'Invalid path'}), 403
|
||
|
||
if not full_path.exists():
|
||
return jsonify({'error': 'Thumbnail not found'}), 404
|
||
|
||
return send_from_directory(full_path.parent, full_path.name)
|
||
|
||
|
||
# ========== SSO 退出路由 ==========
|
||
|
||
@app.route('/api/auth/sso-logout', methods=['POST'])
|
||
def sso_logout():
|
||
"""退出登录 —— 同时清除本地 Session 和 SSO Cookie"""
|
||
from flask_login import logout_user
|
||
if current_user.is_authenticated:
|
||
logout_user()
|
||
# 清除 SSO Cookie
|
||
resp = make_response(jsonify({'success': True, 'message': '已登出'}))
|
||
resp.delete_cookie('access_token', domain=SSO_COOKIE_DOMAIN, path='/')
|
||
return resp
|
||
|
||
|
||
# ========== 前端路由 ==========
|
||
|
||
@app.route('/')
|
||
def index():
|
||
"""首页 - 公开,但前端会检查登录状态"""
|
||
return send_from_directory('../frontend', 'index.html')
|
||
|
||
|
||
@app.route('/<path:path>')
|
||
def static_files(path):
|
||
"""静态文件 - 公开(CSS/JS/HTML)"""
|
||
# 排除 API 路径
|
||
if path.startswith('api/'):
|
||
return jsonify({'error': 'Not found'}), 404
|
||
return send_from_directory('../frontend', path)
|
||
|
||
|
||
# ========== 初始化 ==========
|
||
|
||
with app.app_context():
|
||
db.create_all()
|
||
init_default_users()
|
||
print("✅ 数据库初始化完成")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
print("🖼️ 相册查看器(带登录保护)启动...")
|
||
print(f" 相册根目录: {ALBUMS_ROOT}")
|
||
print(f" 默认账号: admin / abcd1234")
|
||
print(f" 访问: http://localhost:8892")
|
||
|
||
app.run(host='0.0.0.0', port=8892, debug=False)
|