From 6d3cb9d67844617c4c2f08d937e4e51546cbdf7e Mon Sep 17 00:00:00 2001 From: hermes Date: Wed, 17 Jun 2026 04:34:39 +0000 Subject: [PATCH] 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. --- Dockerfile | 40 ++++++ README.md | 131 +++++++++++++++--- docker-compose.yml | 16 +++ next.config.ts | 3 +- .../20260617003114_init/migration.sql | 103 ++++++++++++++ prisma/migrations/migration_lock.toml | 3 + 6 files changed, 275 insertions(+), 21 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 prisma/migrations/20260617003114_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fe16c63 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index e215bc4..e65f1a5 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..10ede7e --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/next.config.ts b/next.config.ts index e9ffa30..8afdd5d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/prisma/migrations/20260617003114_init/migration.sql b/prisma/migrations/20260617003114_init/migration.sql new file mode 100644 index 0000000..4543307 --- /dev/null +++ b/prisma/migrations/20260617003114_init/migration.sql @@ -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"); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -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"