124 lines
5.5 KiB
Python
124 lines
5.5 KiB
Python
"""Nancode Portal 后端 — SQLite 持久化链接 + SSO Cookie 认证"""
|
|
import os, sqlite3
|
|
from contextlib import contextmanager
|
|
from fastapi import FastAPI, Request, HTTPException, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
from jose import jwt, JWTError
|
|
|
|
# ── 配置 ─────────────────────────────────
|
|
DB_PATH = os.path.join(os.path.dirname(__file__), "portal.db")
|
|
SSO_SECRET_KEY = "your-secret-key-change-in-production" # 与 SSO 中心一致
|
|
ALGORITHM = "HS256"
|
|
|
|
# ── 数据库 ───────────────────────────────
|
|
@contextmanager
|
|
def get_db():
|
|
conn = sqlite3.connect(DB_PATH)
|
|
conn.row_factory = sqlite3.Row
|
|
try:
|
|
yield conn
|
|
finally:
|
|
conn.close()
|
|
|
|
def init_db():
|
|
with get_db() as conn:
|
|
conn.execute("""CREATE TABLE IF NOT EXISTS links (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
desc TEXT DEFAULT '',
|
|
icon TEXT DEFAULT '',
|
|
sort_order INTEGER DEFAULT 0
|
|
)""")
|
|
# 首次启动:如果表为空则写入默认数据
|
|
count = conn.execute("SELECT COUNT(*) FROM links").fetchone()[0]
|
|
if count == 0:
|
|
defaults = [
|
|
('1','个人博客','http://129.211.173.87:8888','Next.js技术博客,个人技术文章与生活记录','book-open',1),
|
|
('2','相册查看器','http://129.211.173.87:8892','图片相册浏览与管理工具','image',2),
|
|
('3','快乐游戏','http://129.211.173.87:8895','Flask小游戏集合,休闲益智','gamepad-2',3),
|
|
('4','网游引擎','http://129.211.173.87:8900','网络游戏引擎服务平台','server',4),
|
|
('5','有声书播放器','http://129.211.173.87:8899','在线有声书播放与TTS服务','headphones',5),
|
|
('6','WanAndroid','http://129.211.173.87:8893','Material Design风格的Web应用','smartphone',6),
|
|
('7','FumaMX CRM','http://129.211.173.87:10000','客户关系管理系统','briefcase',7),
|
|
('8','AI聊天服务','http://129.211.173.87:8890','AI对话机器人服务','bot',8),
|
|
('9','新媒体平台','http://129.211.173.87:18888','CMS内容管理系统与网站后台','layout-dashboard',9),
|
|
('10','微信小程序桥接','http://129.211.173.87:5000','微信-Nanobot消息桥接服务','message-circle',10),
|
|
('11','音频录制工具','http://129.211.173.87:8901','在线音频录制与处理工具','mic',11),
|
|
('12','粒子实验室','http://129.211.173.87:8902','粒子特效与图形渲染实验','sparkles',12),
|
|
('13','WebGPU演示','http://129.211.173.87:8902','WebGPU与PlayCanvas 3D演示','globe',13),
|
|
('14','Hermes网关','http://129.211.173.87:8894','AI网关与代理服务','hexagon',14),
|
|
]
|
|
conn.executemany("INSERT INTO links (id,name,url,desc,icon,sort_order) VALUES (?,?,?,?,?,?)", defaults)
|
|
conn.commit()
|
|
|
|
init_db()
|
|
|
|
# ── SSO 认证 ─────────────────────────────
|
|
def require_sso(request: Request) -> str:
|
|
"""从 Cookie 验签,返回 username;失败抛 401"""
|
|
token = request.cookies.get("access_token")
|
|
if not token:
|
|
raise HTTPException(401, "未登录")
|
|
try:
|
|
payload = jwt.decode(token, SSO_SECRET_KEY, algorithms=[ALGORITHM])
|
|
username = payload.get("sub")
|
|
if not username:
|
|
raise HTTPException(401, "无效令牌")
|
|
return username
|
|
except JWTError:
|
|
raise HTTPException(401, "令牌已过期或无效")
|
|
|
|
# ── FastAPI ──────────────────────────────
|
|
app = FastAPI(title="Nancode Portal API")
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=[
|
|
"http://129.211.173.87",
|
|
"http://129.211.173.87:8887",
|
|
"http://129.211.173.87:8888",
|
|
"http://129.211.173.87:9000",
|
|
],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
class LinkCreate(BaseModel):
|
|
name: str
|
|
url: str
|
|
desc: str = ""
|
|
icon: str = ""
|
|
|
|
@app.get("/api/links")
|
|
def list_links():
|
|
with get_db() as conn:
|
|
rows = conn.execute("SELECT id,name,url,desc,icon FROM links ORDER BY sort_order, id").fetchall()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.post("/api/links")
|
|
def add_link(link: LinkCreate, username: str = Depends(require_sso)):
|
|
import time, random
|
|
lid = f"{int(time.time()):x}{random.randint(0,0xffffff):06x}"
|
|
with get_db() as conn:
|
|
max_sort = conn.execute("SELECT COALESCE(MAX(sort_order),0) FROM links").fetchone()[0]
|
|
conn.execute("INSERT INTO links (id,name,url,desc,icon,sort_order) VALUES (?,?,?,?,?,?)",
|
|
(lid, link.name, link.url, link.desc, link.icon, max_sort + 1))
|
|
conn.commit()
|
|
return {"id": lid, "name": link.name, "url": link.url, "desc": link.desc, "icon": link.icon}
|
|
|
|
@app.delete("/api/links/{link_id}")
|
|
def delete_link(link_id: str, username: str = Depends(require_sso)):
|
|
with get_db() as conn:
|
|
cur = conn.execute("DELETE FROM links WHERE id=?", (link_id,))
|
|
conn.commit()
|
|
if cur.rowcount == 0:
|
|
raise HTTPException(404, "链接不存在")
|
|
return {"success": True}
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8887)
|