feat: bootstrap admin auth, DB schema, and encrypted API keys

- Add Prisma schema for AdminUser, Source, Article, SearchRule, SearchRun,
  and encrypted ApiKey storage.
- Implement JWT session middleware and admin login/logout.
- Add AES-256-GCM encryption helpers for API keys.
- Seed default news sources and admin credentials.
- Create admin shell layout and login page.
This commit is contained in:
hermes
2026-06-17 04:30:56 +00:00
parent b77d0c73cd
commit e086141620
18 changed files with 1785 additions and 8 deletions

127
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,127 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model AdminUser {
id String @id @default(cuid())
username String @unique
passwordHash String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Source {
id String @id @default(cuid())
name String
baseUrl String?
apiCode String @unique
defaultBias Bias?
credibilityScore Float @default(5)
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
articles Article[]
}
model Article {
id String @id @default(cuid())
title String
summary String?
url String @unique
sourceId String
source Source @relation(fields: [sourceId], references: [id])
imageUrl String?
publishedAt DateTime?
fetchedAt DateTime @default(now())
primaryBias Bias
biasConfidence Float @default(0)
politicalTags String @default("[]")
rankScore Float @default(0)
credibilityScore Float @default(5)
status ArticleStatus @default(DRAFT)
topics String @default("[]")
rawData String @default("{}")
aiExplanation String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([primaryBias, status])
@@index([status, publishedAt])
@@index([rankScore])
}
model ApiKey {
id String @id @default(cuid())
provider ApiProvider
encryptedKey String
label String?
isActive Boolean @default(true)
lastUsedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([provider, isActive])
}
model SearchRule {
id String @id @default(cuid())
name String
query String
sources String @default("[]")
schedule String @default("0 */6 * * *")
maxResults Int @default(20)
autoPublish Boolean @default(false)
minRankScore Float @default(5)
isActive Boolean @default(true)
aiProvider ApiProvider @default(KIMI)
aiModel String?
lastRunAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
runs SearchRun[]
}
model SearchRun {
id String @id @default(cuid())
ruleId String
rule SearchRule @relation(fields: [ruleId], references: [id], onDelete: Cascade)
status RunStatus @default(RUNNING)
stage String? // e.g. "FETCHING", "ANALYZING", "SAVING"
progress String? // human-readable progress, e.g. "3 of 12"
articlesFound Int @default(0)
error String?
startedAt DateTime @default(now())
endedAt DateTime?
}
enum Bias {
LEFT
CENTER
RIGHT
}
enum ArticleStatus {
DRAFT
PUBLISHED
ARCHIVED
}
enum ApiProvider {
NEWSAPI
GNEWS
KIMI
WEBSEARCH
NEWSDATA
BRAVE
}
enum RunStatus {
RUNNING
SUCCESS
ERROR
}