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:
Mavis
2026-06-07 21:51:01 +08:00
commit 60b062daf2
81 changed files with 5540 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
import { defineStore } from 'pinia'
import { http } from '@/api/client'
interface User {
id: number
username: string
role: 'owner' | 'member'
email?: string | null
}
const ACCESS_KEY = 'dn.access'
const REFRESH_KEY = 'dn.refresh'
const USER_KEY = 'dn.user'
export const useAuthStore = defineStore('auth', {
state: () => ({
accessToken: '' as string,
refreshToken: '' as string,
user: null as User | null,
}),
getters: {
isLogged: (s) => !!s.accessToken,
isOwner: (s) => s.user?.role === 'owner',
},
actions: {
async login(username: string, password: string) {
const { data } = await http.post('/auth/login', { username, password })
this.accessToken = data.access_token
this.refreshToken = data.refresh_token
localStorage.setItem(ACCESS_KEY, this.accessToken)
localStorage.setItem(REFRESH_KEY, this.refreshToken)
await this.fetchMe()
},
async refresh() {
const { data } = await http.post('/auth/refresh', { refresh_token: this.refreshToken })
this.accessToken = data.access_token
this.refreshToken = data.refresh_token
localStorage.setItem(ACCESS_KEY, this.accessToken)
localStorage.setItem(REFRESH_KEY, this.refreshToken)
},
async fetchMe() {
const { data } = await http.get('/me')
this.user = data
localStorage.setItem(USER_KEY, JSON.stringify(data))
},
restore() {
this.accessToken = localStorage.getItem(ACCESS_KEY) || ''
this.refreshToken = localStorage.getItem(REFRESH_KEY) || ''
const u = localStorage.getItem(USER_KEY)
if (u) this.user = JSON.parse(u)
},
logout() {
this.accessToken = ''
this.refreshToken = ''
this.user = null
localStorage.removeItem(ACCESS_KEY)
localStorage.removeItem(REFRESH_KEY)
localStorage.removeItem(USER_KEY)
},
},
})