Docker hardening + first-run fixes

This commit is contained in:
Christomatt
2026-02-05 02:49:34 +01:00
parent 38ddbf8bc1
commit a686db38b0
19 changed files with 346 additions and 53 deletions
+23 -4
View File
@@ -1,9 +1,16 @@
import { NextRequest, NextResponse } from 'next/server';
import { db, users } from '@/db';
import { db, users, isDbAvailable } from '@/db';
import { eq } from 'drizzle-orm';
export async function GET(req: NextRequest) {
try {
if (!isDbAvailable()) {
return NextResponse.json(
{ available: false, error: 'Database not configured' },
{ status: 503 }
);
}
const { searchParams } = new URL(req.url);
const handle = searchParams.get('handle')?.toLowerCase().trim();
@@ -15,9 +22,21 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ available: false, error: 'Invalid characters' });
}
const existingUser = await db.query.users.findFirst({
where: eq(users.handle, handle),
});
let existingUser = null;
try {
existingUser = await db.query.users.findFirst({
where: eq(users.handle, handle),
});
} catch (err: any) {
// Handle fresh installs where the users table isn't created yet.
if (err?.code === '42P01' || /relation .*users.* does not exist/i.test(err?.message || '')) {
return NextResponse.json(
{ available: true, handle, warning: 'Database not initialized' },
{ status: 503 }
);
}
throw err;
}
return NextResponse.json({
available: !existingUser,
+29 -6
View File
@@ -1,12 +1,29 @@
import { NextResponse } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { db, nodes } from '@/db';
import { eq } from 'drizzle-orm';
export async function GET() {
function getRequestBaseUrl(req: NextRequest, fallbackDomain: string): string {
const forwardedHost = req.headers.get('x-forwarded-host');
const forwardedProto = req.headers.get('x-forwarded-proto');
const host = forwardedHost?.split(',')[0]?.trim() || req.headers.get('host');
const protocol =
forwardedProto?.split(',')[0]?.trim() ||
(host && host.includes('localhost') ? 'http' : 'https');
if (host) {
return `${protocol}://${host}`;
}
return `https://${fallbackDomain}`;
}
export async function GET(req: NextRequest) {
try {
if (!db) {
// Redirect to default favicon
return NextResponse.redirect(new URL('/favicon.png', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'));
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const baseUrl = getRequestBaseUrl(req, domain);
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
}
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
@@ -17,14 +34,20 @@ export async function GET() {
if (node?.faviconUrl) {
// Redirect to custom favicon
return NextResponse.redirect(node.faviconUrl);
const baseUrl = getRequestBaseUrl(req, domain);
const target = node.faviconUrl.startsWith('/')
? new URL(node.faviconUrl, baseUrl)
: node.faviconUrl;
return NextResponse.redirect(target);
}
// Redirect to default favicon
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || `https://${domain}`;
const baseUrl = getRequestBaseUrl(req, domain);
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
} catch (error) {
console.error('Favicon error:', error);
return NextResponse.redirect(new URL('/favicon.png', process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000'));
const domain = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
const baseUrl = getRequestBaseUrl(req, domain);
return NextResponse.redirect(new URL('/favicon.png', baseUrl));
}
}
+1 -1
View File
@@ -22,7 +22,7 @@ export async function POST(req: NextRequest) {
accessKeyId: process.env.STORAGE_ACCESS_KEY || '',
secretAccessKey: process.env.STORAGE_SECRET_KEY || '',
},
forcePathStyle: true, // Needed for many S3-compatible providers (MinIO, etc.)
forcePathStyle: true, // Needed for many S3-compatible providers
});
const bucket = process.env.STORAGE_BUCKET || 'synapsis';
+4 -6
View File
@@ -577,8 +577,6 @@ export default function LoginPage() {
<option value="wasabi">Wasabi</option>
<option value="contabo">Contabo S3</option>
<option value="s3">AWS S3</option>
<option value="minio">MinIO / Self-hosted</option>
<option value="other">Other S3-compatible</option>
</select>
</div>
</div>
@@ -614,8 +612,8 @@ export default function LoginPage() {
</div>
</div>
{/* Endpoint URL - only show for providers that need it (R2, B2, Contabo, MinIO, Other) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'minio' || storageProvider === 'other') && (
{/* Endpoint URL - only show for providers that need it (R2, B2, Contabo) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo') && (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Endpoint URL
@@ -635,8 +633,8 @@ export default function LoginPage() {
</div>
)}
{/* Public Base URL - only show for providers that need it (R2, B2, Contabo, Other) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'other') && (
{/* Public Base URL - only show for providers that need it (R2, B2, Contabo) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo') && (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Public Base URL
+3 -3
View File
@@ -65,7 +65,7 @@ export const users = pgTable('users', {
movedFrom: text('moved_from'), // Old actor URL if this account migrated here
migratedAt: timestamp('migrated_at'), // When the migration occurred
// User-owned S3-compatible storage - required for new users
storageProvider: text('storage_provider'), // 's3', 'r2', 'b2', 'wasabi', 'contabo', etc
storageProvider: text('storage_provider'), // 's3', 'r2', 'b2', 'wasabi', 'contabo'
storageEndpoint: text('storage_endpoint'), // S3 endpoint URL (optional for AWS)
storagePublicBaseUrl: text('storage_public_base_url'), // Public URL for viewing files (required for R2, B2, Contabo)
storageRegion: text('storage_region'), // Region (e.g., 'us-east-1')
@@ -919,8 +919,8 @@ export const chatConversationsRelations = relations(chatConversations, ({ one, m
/**
* Individual chat messages within conversations.
* Messages are encrypted end-to-end using recipient's public key.
* Sender also gets a copy encrypted with their own public key.
* Messages are stored as plain text on the server.
* Both sender and recipient can view the message content.
*/
export const chatMessages = pgTable('chat_messages', {
id: uuid('id').primaryKey().defaultRandom(),
+2
View File
@@ -1,3 +1,5 @@
'use client';
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
interface RuntimeConfig {
+3 -3
View File
@@ -1,13 +1,13 @@
/**
* User-Owned S3-Compatible Storage Utilities
*
* Supports AWS S3, Cloudflare R2, Backblaze B2, Wasabi, MinIO, etc.
* Supports AWS S3, Cloudflare R2, Backblaze B2, Wasabi, and Contabo.
*/
import { S3Client, PutObjectCommand, HeadBucketCommand } from '@aws-sdk/client-s3';
import { decryptPrivateKey, deserializeEncryptedKey } from '@/lib/crypto/private-key';
export type StorageProvider = 's3' | 'r2' | 'b2' | 'wasabi' | 'minio' | 'other';
export type StorageProvider = 's3' | 'r2' | 'b2' | 'wasabi' | 'contabo';
interface S3Credentials {
endpoint?: string;
@@ -105,7 +105,7 @@ export async function uploadToUserStorage(
if (publicBaseUrl) {
url = `${publicBaseUrl.replace(/\/$/, '')}/${key}`;
} else if (endpoint) {
// Custom endpoint (MinIO, etc)
// Custom endpoint (R2, B2, Contabo)
url = `${endpoint}/${bucket}/${key}`;
} else {
// AWS S3 standard URL