新增工具部署: - imgcompress (端口18087) - 图片压缩、格式转换、AI抠图 - Lama Cleaner (端口18088) - AI图像擦除/去水印 - webp2jpg-online (端口18089) - 图片格式批量互转 - Overleaf (端口18090) - 在线LaTeX论文编辑器(docker-compose + MongoDB 8.0) - LaTeX公式编辑器 (端口18091) - 纯前端KaTeX公式编辑 应用广场优化: - 去掉tab切换,所有分类平铺展示 - CSS Grid自适应布局,一行可排3-4个卡片 - 重新分为4个分类:文档处理、图片处理、创作绘图、科研写作 其他: - 更新 CLAUDE.md 项目配置文档 - PPTist AI后端优化prompt和流式输出格式 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
129 lines
4.8 KiB
Python
129 lines
4.8 KiB
Python
"""
|
|
Overleaf 自助注册服务
|
|
提供简单的注册页面,用户输入邮箱和密码即可注册
|
|
内部通过管理员账号调用 Overleaf API 创建用户
|
|
"""
|
|
from fastapi import FastAPI, Request, Form
|
|
from fastapi.responses import HTMLResponse
|
|
import requests
|
|
import os
|
|
|
|
app = FastAPI()
|
|
|
|
OL_URL = os.getenv("OL_INSTANCE", "http://overleaf")
|
|
OL_ADMIN_EMAIL = os.getenv("OL_ADMIN_EMAIL", "")
|
|
OL_ADMIN_PASSWORD = os.getenv("OL_ADMIN_PASSWORD", "")
|
|
|
|
REGISTER_HTML = """
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>注册 - LaTeX 论文编辑器</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
|
.card { background: #fff; border-radius: 12px; padding: 40px; width: 400px; box-shadow: 0 4px 20px rgba(0,0,0,0.1); }
|
|
h2 { text-align: center; margin-bottom: 8px; color: #333; }
|
|
.sub { text-align: center; color: #999; font-size: 14px; margin-bottom: 30px; }
|
|
label { display: block; margin-bottom: 6px; color: #555; font-size: 14px; }
|
|
input { width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; margin-bottom: 16px; }
|
|
input:focus { outline: none; border-color: #004EA0; }
|
|
button { width: 100%; padding: 12px; background: #004EA0; color: #fff; border: none; border-radius: 8px; font-size: 16px; cursor: pointer; }
|
|
button:hover { background: #003a7a; }
|
|
.msg { text-align: center; margin-top: 16px; font-size: 14px; }
|
|
.msg.ok { color: #16a34a; }
|
|
.msg.err { color: #dc2626; }
|
|
a { color: #004EA0; text-decoration: none; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="card">
|
|
<h2>注册账号</h2>
|
|
<p class="sub">LaTeX 论文编辑器</p>
|
|
<form method="POST" action="/register">
|
|
<label>邮箱</label>
|
|
<input type="email" name="email" required placeholder="请输入邮箱">
|
|
<button type="submit">注册</button>
|
|
</form>
|
|
<p class="sub" style="margin-top:20px">已有账号?<a href="OLURL">去登录</a></p>
|
|
MSG_PLACEHOLDER
|
|
</div>
|
|
</body>
|
|
</html>
|
|
""".replace("OLURL", OL_URL)
|
|
|
|
|
|
class OverleafAdmin:
|
|
def __init__(self):
|
|
self.session = requests.Session()
|
|
self.logged_in = False
|
|
|
|
def _get_csrf(self, path="/login"):
|
|
resp = self.session.get(f"{OL_URL}{path}")
|
|
# 从页面中提取 CSRF token
|
|
import re
|
|
match = re.search(r'name="_csrf"[^>]*value="([^"]+)"', resp.text)
|
|
if not match:
|
|
match = re.search(r'ol-csrfToken["\s]*content="([^"]+)"', resp.text)
|
|
if not match:
|
|
match = re.search(r'csrfToken["\s]*:\s*"([^"]+)"', resp.text)
|
|
return match.group(1) if match else ""
|
|
|
|
def login(self):
|
|
csrf = self._get_csrf("/login")
|
|
resp = self.session.post(f"{OL_URL}/login", data={
|
|
"_csrf": csrf,
|
|
"email": OL_ADMIN_EMAIL,
|
|
"password": OL_ADMIN_PASSWORD,
|
|
}, allow_redirects=False)
|
|
self.logged_in = resp.status_code in (200, 302)
|
|
return self.logged_in
|
|
|
|
def register_user(self, email):
|
|
if not self.logged_in:
|
|
self.login()
|
|
csrf = self._get_csrf("/admin/register")
|
|
resp = self.session.post(f"{OL_URL}/admin/register", data={
|
|
"_csrf": csrf,
|
|
"email": email,
|
|
})
|
|
# 从响应中提取设置密码的链接
|
|
import re
|
|
set_password_url = ""
|
|
match = re.search(r'(https?://[^\s"<>]*user/password/set\?[^\s"<>]+)', resp.text)
|
|
if match:
|
|
set_password_url = match.group(1)
|
|
return resp.status_code == 200, set_password_url
|
|
|
|
|
|
admin = OverleafAdmin()
|
|
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def index():
|
|
return REGISTER_HTML.replace("MSG_PLACEHOLDER", "")
|
|
|
|
|
|
@app.post("/register", response_class=HTMLResponse)
|
|
async def register(email: str = Form(...)):
|
|
try:
|
|
ok, set_url = admin.register_user(email)
|
|
if ok and set_url:
|
|
# 把链接中的 localhost 替换为实际访问地址
|
|
import re
|
|
set_url = re.sub(r'https?://[^/]+', OL_URL, set_url)
|
|
msg = f'<p class="msg ok">注册成功!请点击下方链接设置密码:</p><p style="text-align:center;margin-top:10px"><a href="{set_url}" target="_blank" style="background:#004EA0;color:#fff;padding:10px 24px;border-radius:8px;text-decoration:none;font-size:14px">设置密码</a></p>'
|
|
elif ok:
|
|
msg = f'<p class="msg ok">注册成功!该邮箱可能已注册。请前往 <a href="{OL_URL}/login">登录页面</a> 登录。</p>'
|
|
else:
|
|
msg = f'<p class="msg err">注册失败,请稍后重试。</p>'
|
|
except Exception as e:
|
|
msg = f'<p class="msg err">注册失败:{str(e)[:100]}</p>'
|
|
return REGISTER_HTML.replace("MSG_PLACEHOLDER", msg)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=18091)
|