chore: add Docker deployment files, README, and Prisma migrations

- Add Dockerfile and docker-compose.yml for VPS/LXC deployment.
- Update README with setup, environment variables, and deployment notes.
- Commit initial Prisma migration for SQLite schema.
- Configure Next.js for standalone output and dev origin allowlist.
This commit is contained in:
hermes
2026-06-17 04:34:39 +00:00
parent 1b23f57d6c
commit 6d3cb9d678
6 changed files with 275 additions and 21 deletions

40
Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
FROM node:22-alpine AS base
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npx prisma generate
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.bin/prisma ./node_modules/.bin/prisma
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
RUN mkdir -p /app/data && chown nextjs:nodejs /app/data
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

131
README.md
View File

@@ -1,36 +1,127 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# BiasNews Aggregator
## Getting Started
A full-stack news aggregation website that collects articles, uses AI to classify them by political bias (Left / Center / Right), and presents them in side-by-side columns. Includes a protected admin console for discovering news, managing API keys, and configuring automated searches.
First, run the development server:
## Features
- **Public site**
- Three-column layout: Left, Center, Right
- Search and filter by bias, topic, date, source
- Article detail pages with AI summary and bias explanation
- Mobile-responsive design
- **Admin console**
- Secure credential-based login
- Discover news on demand using NewsAPI, GNews, or Serper web search
- AI processing via Kimi (OpenAI-compatible API) for:
- Summarization
- Bias classification (LEFT / CENTER / RIGHT)
- Political tags (liberal / moderate / conservative)
- Importance ranking and credibility scoring
- Topic extraction
- Manage published/draft/archived articles
- Encrypted API key storage with easy rotation
- Automated search rules with cron scheduling
- Source credibility and active/inactive toggles
## Tech stack
- Next.js 16 (App Router, TypeScript)
- Tailwind CSS 4
- Prisma ORM + SQLite
- `jose` for session cookies
- `bcryptjs` for password hashing
- `node-cron` for scheduled rules
- `openai` SDK for Kimi-compatible APIs
## Local development
```bash
npm install
# Copy environment file and fill in secrets + API keys
cp .env.example .env.local
# Generate admin password hash
npx tsx scripts/generate-admin-hash.ts your-password
# Update ADMIN_PASSWORD_HASH in .env.local
# Apply database migrations and seed default sources
npx prisma migrate dev --name init
npx prisma db seed
# Run dev server
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
Open:
- Public site: http://localhost:3000
- Admin console: http://localhost:3000/admin
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
## API keys
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
Add keys in **Admin > API Keys**:
## Learn More
| Provider | Purpose | How to get |
|----------|---------|------------|
| KIMI | AI summarization / bias / ranking | https://platform.moonshot.cn |
| NEWSAPI | News articles | https://newsapi.org |
| GNEWS | News articles | https://gnews.io |
| WEBSEARCH | Web search via Serper | https://serper.dev |
To learn more about Next.js, take a look at the following resources:
API keys are encrypted at rest with `API_KEY_ENCRYPTION_SECRET`.
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
## Deployment (Docker)
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
```bash
# Build image
docker build -t biasnews .
## Deploy on Vercel
# Run container
docker run -d \
-p 3000:3000 \
-e DATABASE_URL="file:./data/dev.db" \
-e ADMIN_USERNAME=admin \
-e ADMIN_PASSWORD_HASH="$2b$10$..." \
-e NEXTAUTH_SECRET="..." \
-e API_KEY_ENCRYPTION_SECRET="..." \
-e KIMI_API_URL="https://api.moonshot.cn/v1" \
-e KIMI_MODEL="moonshot-v1-8k" \
-v $(pwd)/data:/app/data \
biasnews
```
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Or use Docker Compose:
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```bash
docker compose up -d
```
## Automated searches
Create a rule in **Admin > Auto Rules**. The scheduler loads active rules when the app starts and reschedules whenever rules change. Each rule specifies:
- Search query
- Sources to query
- Cron schedule
- Max results
- Minimum rank score threshold
- Whether to auto-publish results
## Architecture notes
- The admin console is part of the same Next.js app but protected by authentication. It can be accessed from a separate machine (e.g., local laptop) by browsing to the public URL and logging in.
- If you want a fully separate admin deployment later, the admin API routes are isolated under `/api/admin/*` and admin pages under `/admin/*`.
- SQLite is used for single-node simplicity. For production scale, switch to PostgreSQL by changing the Prisma datasource URL.
## Scripts
```bash
npm run dev # Start dev server
npm run build # Production build
npm run start # Start production server
npm run db:migrate # Run Prisma migrations
npm run db:seed # Seed default sources and admin user
npm run db:studio # Open Prisma Studio
```

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
services:
app:
build: .
ports:
- "3000:3000"
environment:
DATABASE_URL: "file:./data/dev.db"
ADMIN_USERNAME: "${ADMIN_USERNAME:-admin}"
ADMIN_PASSWORD_HASH: "${ADMIN_PASSWORD_HASH}"
NEXTAUTH_SECRET: "${NEXTAUTH_SECRET}"
API_KEY_ENCRYPTION_SECRET: "${API_KEY_ENCRYPTION_SECRET}"
KIMI_API_URL: "${KIMI_API_URL:-https://api.moonshot.cn/v1}"
KIMI_MODEL: "${KIMI_MODEL:-moonshot-v1-8k}"
volumes:
- ./data:/app/data
restart: unless-stopped

View File

@@ -1,7 +1,8 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: 'standalone',
allowedDevOrigins: ['192.168.2.197'],
};
export default nextConfig;

View File

@@ -0,0 +1,103 @@
-- CreateTable
CREATE TABLE "AdminUser" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Source" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"baseUrl" TEXT,
"apiCode" TEXT NOT NULL,
"defaultBias" TEXT,
"credibilityScore" REAL NOT NULL DEFAULT 5,
"active" BOOLEAN NOT NULL DEFAULT true,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "Article" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"summary" TEXT,
"url" TEXT NOT NULL,
"sourceId" TEXT NOT NULL,
"imageUrl" TEXT,
"publishedAt" DATETIME,
"fetchedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"primaryBias" TEXT NOT NULL,
"biasConfidence" REAL NOT NULL DEFAULT 0,
"politicalTags" TEXT NOT NULL DEFAULT '[]',
"rankScore" REAL NOT NULL DEFAULT 0,
"credibilityScore" REAL NOT NULL DEFAULT 5,
"status" TEXT NOT NULL DEFAULT 'DRAFT',
"topics" TEXT NOT NULL DEFAULT '[]',
"rawData" TEXT NOT NULL DEFAULT '{}',
"aiExplanation" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "Article_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "Source" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "ApiKey" (
"id" TEXT NOT NULL PRIMARY KEY,
"provider" TEXT NOT NULL,
"encryptedKey" TEXT NOT NULL,
"label" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"lastUsedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "SearchRule" (
"id" TEXT NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL,
"query" TEXT NOT NULL,
"sources" TEXT NOT NULL DEFAULT '[]',
"schedule" TEXT NOT NULL DEFAULT '0 */6 * * *',
"maxResults" INTEGER NOT NULL DEFAULT 20,
"autoPublish" BOOLEAN NOT NULL DEFAULT false,
"minRankScore" REAL NOT NULL DEFAULT 5,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"lastRunAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
-- CreateTable
CREATE TABLE "SearchRun" (
"id" TEXT NOT NULL PRIMARY KEY,
"ruleId" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'RUNNING',
"articlesFound" INTEGER NOT NULL DEFAULT 0,
"error" TEXT,
"startedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"endedAt" DATETIME,
CONSTRAINT "SearchRun_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "SearchRule" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "AdminUser_username_key" ON "AdminUser"("username");
-- CreateIndex
CREATE UNIQUE INDEX "Source_apiCode_key" ON "Source"("apiCode");
-- CreateIndex
CREATE INDEX "Article_primaryBias_status_idx" ON "Article"("primaryBias", "status");
-- CreateIndex
CREATE INDEX "Article_status_publishedAt_idx" ON "Article"("status", "publishedAt");
-- CreateIndex
CREATE INDEX "Article_rankScore_idx" ON "Article"("rankScore");
-- CreateIndex
CREATE UNIQUE INDEX "ApiKey_provider_isActive_key" ON "ApiKey"("provider", "isActive");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "sqlite"