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:
40
Dockerfile
Normal file
40
Dockerfile
Normal 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
131
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
|
```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
|
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.
|
## Deployment (Docker)
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
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
16
docker-compose.yml
Normal 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
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
output: 'standalone',
|
||||||
|
allowedDevOrigins: ['192.168.2.197'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
103
prisma/migrations/20260617003114_init/migration.sql
Normal file
103
prisma/migrations/20260617003114_init/migration.sql
Normal 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");
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal 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"
|
||||||
Reference in New Issue
Block a user