feat: initial MVP - FastAPI backend + Vue3 frontend + docker-compose
- backend: FastAPI + SQLAlchemy 2.0(async) + asyncpg + Alembic - 7 API routes: auth/me/articles/sources/bookmarks/subscriptions/admin - models: User/Source/Article/Bookmark/Subscription/ApiToken - services: RSS fetcher (feedparser) + Tencent TMT translator with quota + cache + local NLLB fallback - workers: APScheduler + asyncio pipeline (fetch -> dedupe -> insert -> translate) - seed scripts: create_user, seed_sources (5 RSS: Reuters/BBC/Al Jazeera/NHK/DW) - frontend: Vue 3 + Vite + Naive UI + Pinia + vue-router - pages: Login, Feed (24h), ArticleDetail, Sources, Bookmarks, AdminSources - deploy: docker-compose (postgres/redis/api/worker/frontend/caddy) - docs: README, DEPLOY, architecture, acceptance
This commit is contained in:
56
backend/app/scripts/create_user.py
Normal file
56
backend/app/scripts/create_user.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""创建用户(默认 owner)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from getpass import getpass
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.security import hash_password
|
||||
from app.database import AsyncSessionLocal
|
||||
from app.models.user import User, UserRole
|
||||
|
||||
|
||||
async def main(username: str, password: str, email: str | None, role: UserRole) -> int:
|
||||
async with AsyncSessionLocal() as session:
|
||||
exists = (await session.execute(select(User).where(User.username == username))).scalar_one_or_none()
|
||||
if exists:
|
||||
print(f"user '{username}' already exists (id={exists.id})", file=sys.stderr)
|
||||
return 1
|
||||
u = User(
|
||||
username=username,
|
||||
email=email,
|
||||
password_hash=hash_password(password),
|
||||
role=role,
|
||||
is_active=True,
|
||||
)
|
||||
session.add(u)
|
||||
await session.commit()
|
||||
await session.refresh(u)
|
||||
print(f"created user id={u.id} username={u.username} role={u.role.value}")
|
||||
return 0
|
||||
|
||||
|
||||
def cli() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--username", required=True)
|
||||
p.add_argument("--password", default=None, help="缺省则交互输入")
|
||||
p.add_argument("--email", default=None)
|
||||
p.add_argument("--role", choices=["owner", "member"], default="member")
|
||||
args = p.parse_args()
|
||||
password = args.password
|
||||
if not password:
|
||||
pw1 = getpass("password: ")
|
||||
pw2 = getpass("password (again): ")
|
||||
if pw1 != pw2 or len(pw1) < 6:
|
||||
print("passwords differ or too short", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
password = pw1
|
||||
rc = asyncio.run(main(args.username, password, args.email, UserRole(args.role)))
|
||||
sys.exit(rc)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
Reference in New Issue
Block a user