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:
127
prisma/schema.prisma
Normal file
127
prisma/schema.prisma
Normal 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
|
||||
}
|
||||
44
prisma/seed.ts
Normal file
44
prisma/seed.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
const sources = [
|
||||
{ name: 'NewsAPI', apiCode: 'newsapi', baseUrl: 'https://newsapi.org', credibilityScore: 7 },
|
||||
{ name: 'GNews', apiCode: 'gnews', baseUrl: 'https://gnews.io', credibilityScore: 7 },
|
||||
{ name: 'NewsData', apiCode: 'newsdata', baseUrl: 'https://newsdata.io', credibilityScore: 7 },
|
||||
{ name: 'Web Search', apiCode: 'websearch', baseUrl: null, credibilityScore: 5 },
|
||||
{ name: 'Brave Search', apiCode: 'brave', baseUrl: 'https://search.brave.com', credibilityScore: 6 },
|
||||
];
|
||||
|
||||
for (const source of sources) {
|
||||
await prisma.source.upsert({
|
||||
where: { apiCode: source.apiCode },
|
||||
update: {},
|
||||
create: source,
|
||||
});
|
||||
}
|
||||
|
||||
const adminUsername = process.env.ADMIN_USERNAME || 'admin';
|
||||
const adminPasswordHash = process.env.ADMIN_PASSWORD_HASH;
|
||||
|
||||
if (adminPasswordHash) {
|
||||
await prisma.adminUser.upsert({
|
||||
where: { username: adminUsername },
|
||||
update: { passwordHash: adminPasswordHash },
|
||||
create: { username: adminUsername, passwordHash: adminPasswordHash },
|
||||
});
|
||||
console.log(`Seeded admin user: ${adminUsername}`);
|
||||
}
|
||||
|
||||
console.log('Seeded default sources.');
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user