mirror of
https://github.com/Tony0410/quietthanks.git
synced 2026-05-24 13:21:38 +08:00
Enhance security: Rotate VAPID keys, encrypt LLM API keys, and use env vars
This commit is contained in:
34
docker-compose.example.yml
Normal file
34
docker-compose.example.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
services:
|
||||||
|
quietthanks:
|
||||||
|
build: .
|
||||||
|
container_name: quietthanks
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "6124:3000"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
environment:
|
||||||
|
- DATABASE_PATH=/app/data/quietthanks.db
|
||||||
|
- NEXT_PUBLIC_VAPID_PUBLIC_KEY=${NEXT_PUBLIC_VAPID_PUBLIC_KEY}
|
||||||
|
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||||
|
- VAPID_EMAIL=${VAPID_EMAIL}
|
||||||
|
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||||
|
- TZ=${TZ}
|
||||||
|
|
||||||
|
scheduler:
|
||||||
|
image: alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
quietthanks:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
- TZ=${TZ}
|
||||||
|
entrypoint: /bin/sh
|
||||||
|
command: >
|
||||||
|
-c "apk add --no-cache curl &&
|
||||||
|
while true; do
|
||||||
|
echo 'Checking for notifications...' &&
|
||||||
|
curl -s -X POST http://quietthanks:3000/api/notifications/send &&
|
||||||
|
echo '' &&
|
||||||
|
sleep 60;
|
||||||
|
done"
|
||||||
@@ -9,10 +9,11 @@ services:
|
|||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_PATH=/app/data/quietthanks.db
|
- DATABASE_PATH=/app/data/quietthanks.db
|
||||||
- NEXT_PUBLIC_VAPID_PUBLIC_KEY=BIKukAq5-KPwJAMpksxD7UNL8XfF-oJOI0CLGGZQAY93igZgf1PYa9MVvS8GaBv-vv9ckcXPCEKdzWDCtOyQpKg
|
- NEXT_PUBLIC_VAPID_PUBLIC_KEY=${NEXT_PUBLIC_VAPID_PUBLIC_KEY}
|
||||||
- VAPID_PRIVATE_KEY=IBkQ14BLKFCg2PmGOWheC7xfYHS5J49vXS8duHCeDBw
|
- VAPID_PRIVATE_KEY=${VAPID_PRIVATE_KEY}
|
||||||
- VAPID_EMAIL=mailto:admin@example.com
|
- VAPID_EMAIL=${VAPID_EMAIL}
|
||||||
- TZ=Australia/Perth
|
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
|
||||||
|
- TZ=${TZ}
|
||||||
|
|
||||||
scheduler:
|
scheduler:
|
||||||
image: alpine
|
image: alpine
|
||||||
@@ -21,7 +22,7 @@ services:
|
|||||||
quietthanks:
|
quietthanks:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
environment:
|
environment:
|
||||||
- TZ=Australia/Perth
|
- TZ=${TZ}
|
||||||
entrypoint: /bin/sh
|
entrypoint: /bin/sh
|
||||||
command: >
|
command: >
|
||||||
-c "apk add --no-cache curl &&
|
-c "apk add --no-cache curl &&
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { db, schema } from "@/lib/db";
|
import { db, schema } from "@/lib/db";
|
||||||
import { getSession } from "@/lib/auth";
|
import { getSession } from "@/lib/auth";
|
||||||
import { eq, and, gte, lte, desc } from "drizzle-orm";
|
import { eq, and, gte, lte, desc } from "drizzle-orm";
|
||||||
|
import { decrypt } from "@/lib/crypto";
|
||||||
|
|
||||||
// POST /api/reflections - Generate an LLM reflection for a time period
|
// POST /api/reflections - Generate an LLM reflection for a time period
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
@@ -31,7 +32,8 @@ export async function POST(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { llmProvider, llmApiKey, llmModel } = users[0];
|
const { llmProvider, llmModel } = users[0];
|
||||||
|
const llmApiKey = decrypt(users[0].llmApiKey);
|
||||||
|
|
||||||
// Calculate date range
|
// Calculate date range
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import { db, schema } from "@/lib/db";
|
import { db, schema } from "@/lib/db";
|
||||||
import { getSession } from "@/lib/auth";
|
import { getSession } from "@/lib/auth";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
import { encrypt } from "@/lib/crypto";
|
||||||
|
|
||||||
// GET /api/settings - Get current user settings
|
// GET /api/settings - Get current user settings
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
@@ -70,7 +71,8 @@ export async function PATCH(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body.llmApiKey === "string") {
|
if (typeof body.llmApiKey === "string") {
|
||||||
updates.llmApiKey = body.llmApiKey || null;
|
// Encrypt the API key before storing
|
||||||
|
updates.llmApiKey = body.llmApiKey ? encrypt(body.llmApiKey) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body.llmModel === "string") {
|
if (typeof body.llmModel === "string") {
|
||||||
|
|||||||
55
src/lib/crypto.ts
Normal file
55
src/lib/crypto.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto';
|
||||||
|
|
||||||
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '';
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
|
||||||
|
export function encrypt(text: string): string {
|
||||||
|
if (!ENCRYPTION_KEY) {
|
||||||
|
console.warn('ENCRYPTION_KEY not set, storing in plain text');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const iv = randomBytes(16);
|
||||||
|
const key = Buffer.from(ENCRYPTION_KEY, 'hex');
|
||||||
|
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
|
||||||
|
const authTag = cipher.getAuthTag().toString('hex');
|
||||||
|
|
||||||
|
// Format: iv:authTag:encrypted
|
||||||
|
return `${iv.toString('hex')}:${authTag}:${encrypted}`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Encryption failed:', error);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function decrypt(text: string): string {
|
||||||
|
if (!ENCRYPTION_KEY || !text) return text;
|
||||||
|
|
||||||
|
const parts = text.split(':');
|
||||||
|
// If not in encrypted format (legacy plain text), return as is
|
||||||
|
if (parts.length !== 3) return text;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [ivHex, authTagHex, encryptedHex] = parts;
|
||||||
|
const key = Buffer.from(ENCRYPTION_KEY, 'hex');
|
||||||
|
const iv = Buffer.from(ivHex, 'hex');
|
||||||
|
const authTag = Buffer.from(authTagHex, 'hex');
|
||||||
|
|
||||||
|
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||||||
|
decipher.setAuthTag(authTag);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encryptedHex, 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
} catch (error) {
|
||||||
|
// If decryption fails, assume it might be plain text or corrupted
|
||||||
|
console.warn('Decryption failed, returning original text');
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user