Docker hardening + first-run fixes
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
@@ -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(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
|
||||
interface RuntimeConfig {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user