feat: Introduce runtime configuration for domain and enhance S3 storage settings with public base URL and Contabo support.

This commit is contained in:
Christomatt
2026-02-03 01:27:46 +01:00
parent 91d122ca3a
commit 38ddbf8bc1
20 changed files with 244 additions and 48 deletions
+2
View File
@@ -15,6 +15,7 @@ const registerSchema = z.object({
// S3-compatible storage credentials
storageProvider: z.string().min(1),
storageEndpoint: z.string().nullable().optional(),
storagePublicBaseUrl: z.string().nullable().optional(),
storageRegion: z.string().min(1),
storageBucket: z.string().min(1),
storageAccessKey: z.string().min(10),
@@ -71,6 +72,7 @@ export async function POST(request: Request) {
data.displayName,
data.storageProvider,
data.storageEndpoint || null,
data.storagePublicBaseUrl || null,
data.storageRegion,
data.storageBucket,
data.storageAccessKey,
+1
View File
@@ -78,6 +78,7 @@ export async function POST(request: Request) {
botAvatarUrl = await generateAndUploadAvatarToUserStorage(
botHandle,
user.storageEndpoint || undefined,
user.storagePublicBaseUrl || undefined,
user.storageRegion || 'auto',
user.storageBucket,
accessKeyId,
+7
View File
@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || process.env.NODE_DOMAIN || 'localhost:3000',
});
}
+1
View File
@@ -64,6 +64,7 @@ export async function POST(req: NextRequest) {
file.type,
user.storageProvider as any,
user.storageEndpoint,
user.storagePublicBaseUrl,
user.storageRegion || 'us-east-1',
user.storageBucket || '',
user.storageAccessKeyEncrypted,
+3 -2
View File
@@ -4,7 +4,7 @@ import { useState, useEffect, useRef } from 'react';
import { useAuth } from '@/lib/contexts/AuthContext';
import { signedAPI } from '@/lib/api/signed-fetch';
import { ArrowLeft, Send, Loader2, MessageCircle, Search, Plus, Trash2, MoreVertical } from 'lucide-react';
import { formatFullHandle } from '@/lib/utils/handle';
import { useFormattedHandle } from '@/lib/utils/handle';
import { useRouter, useSearchParams } from 'next/navigation';
interface Conversation {
@@ -43,6 +43,7 @@ export default function ChatPage() {
// Chat Data State
const [conversations, setConversations] = useState<Conversation[]>([]);
const [selectedConversation, setSelectedConversation] = useState<Conversation | null>(null);
const selectedHandle = selectedConversation ? useFormattedHandle(selectedConversation.participant2.handle) : '';
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState('');
const [loading, setLoading] = useState(true);
@@ -389,7 +390,7 @@ export default function ChatPage() {
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontWeight: 600, fontSize: '15px' }}>{selectedConversation.participant2.displayName}</div>
<div style={{ fontSize: '12px', color: 'var(--foreground-tertiary)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{formatFullHandle(selectedConversation.participant2.handle)}
{selectedHandle}
</div>
</div>
<button
+3 -2
View File
@@ -5,7 +5,7 @@ import Link from 'next/link';
import { SearchIcon, TrendingIcon, UsersIcon } from '@/components/Icons';
import { PostCard } from '@/components/PostCard';
import { Post } from '@/lib/types';
import { formatFullHandle } from '@/lib/utils/handle';
import { useFormattedHandle } from '@/lib/utils/handle';
import { Bot, Network, Server, EyeOff } from 'lucide-react';
import { useAuth } from '@/lib/contexts/AuthContext';
@@ -21,6 +21,7 @@ interface User {
}
function UserCard({ user }: { user: User }) {
const fullHandle = useFormattedHandle(user.handle);
return (
<Link href={`/u/${user.handle}`} className="user-card">
<div className="avatar">
@@ -52,7 +53,7 @@ function UserCard({ user }: { user: User }) {
</span>
)}
</div>
<div className="user-card-handle">{formatFullHandle(user.handle)}</div>
<div className="user-card-handle">{fullHandle}</div>
{user.bio && <div className="user-card-bio">{user.bio}</div>}
</div>
</Link>
+12 -9
View File
@@ -57,6 +57,7 @@ export const dynamic = 'force-dynamic';
import { AuthProvider } from '@/lib/contexts/AuthContext';
import { ToastProvider } from '@/lib/contexts/ToastContext';
import { AccentColorProvider } from '@/lib/contexts/AccentColorContext';
import { ConfigProvider } from '@/lib/contexts/ConfigContext';
import { LayoutWrapper } from '@/components/LayoutWrapper';
export default function RootLayout({
@@ -67,15 +68,17 @@ export default function RootLayout({
return (
<html lang="en" className={`${inter.variable} ${sairaCondensed.variable}`}>
<body>
<AuthProvider>
<AccentColorProvider>
<ToastProvider>
<LayoutWrapper>
{children}
</LayoutWrapper>
</ToastProvider>
</AccentColorProvider>
</AuthProvider>
<ConfigProvider>
<AuthProvider>
<AccentColorProvider>
<ToastProvider>
<LayoutWrapper>
{children}
</LayoutWrapper>
</ToastProvider>
</AccentColorProvider>
</AuthProvider>
</ConfigProvider>
</body>
</html>
);
+45 -15
View File
@@ -34,6 +34,7 @@ export default function LoginPage() {
const [displayName, setDisplayName] = useState('');
const [storageProvider, setStorageProvider] = useState('r2');
const [storageEndpoint, setStorageEndpoint] = useState('');
const [storagePublicBaseUrl, setStoragePublicBaseUrl] = useState('');
const [storageRegion, setStorageRegion] = useState('auto');
const [storageBucket, setStorageBucket] = useState('');
const [storageAccessKey, setStorageAccessKey] = useState('');
@@ -252,6 +253,7 @@ export default function LoginPage() {
displayName,
storageProvider,
storageEndpoint: storageEndpoint || null,
storagePublicBaseUrl: storagePublicBaseUrl || null,
storageRegion,
storageBucket,
storageAccessKey,
@@ -573,6 +575,7 @@ export default function LoginPage() {
<option value="r2">Cloudflare R2 (10GB free)</option>
<option value="b2">Backblaze B2 (10GB free)</option>
<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>
@@ -611,22 +614,49 @@ export default function LoginPage() {
</div>
</div>
{/* S3 Credentials - Full Width */}
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Endpoint URL (optional for AWS)
</label>
<input
type="text"
className="input"
value={storageEndpoint}
onChange={(e) => setStorageEndpoint(e.target.value)}
placeholder="https://<account>.r2.cloudflarestorage.com"
/>
<div style={{ fontSize: '11px', color: 'var(--foreground-tertiary)', marginTop: '4px' }}>
Leave empty for AWS S3. Required for R2, B2, MinIO.
{/* Endpoint URL - only show for providers that need it (R2, B2, Contabo, MinIO, Other) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'minio' || storageProvider === 'other') && (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Endpoint URL
</label>
<input
type="text"
className="input"
value={storageEndpoint}
onChange={(e) => setStorageEndpoint(e.target.value)}
placeholder={storageProvider === 'contabo' ? 'https://s3.eu2.contabo.com' : 'https://<account>.r2.cloudflarestorage.com'}
required
/>
<div style={{ fontSize: '11px', color: 'var(--foreground-tertiary)', marginTop: '4px' }}>
{storageProvider === 'contabo' ? 'S3 API endpoint from Contabo dashboard (e.g., https://s3.eu2.contabo.com)' :
'S3-compatible endpoint URL for uploads.'}
</div>
</div>
</div>
)}
{/* Public Base URL - only show for providers that need it (R2, B2, Contabo, Other) */}
{(storageProvider === 'r2' || storageProvider === 'b2' || storageProvider === 'contabo' || storageProvider === 'other') && (
<div style={{ marginBottom: '16px' }}>
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: 500 }}>
Public Base URL
</label>
<input
type="text"
className="input"
value={storagePublicBaseUrl}
onChange={(e) => setStoragePublicBaseUrl(e.target.value)}
placeholder={storageProvider === 'contabo' ? 'https://usc1.contabostorage.com/d5bec82ae49b444d8314dcb13654dd1d:your-bucket' : 'https://pub-xxx.r2.dev'}
required
/>
<div style={{ fontSize: '11px', color: 'var(--foreground-tertiary)', marginTop: '4px' }}>
{storageProvider === 'contabo' ? 'Public URL from Contabo (format: https://region.contabostorage.com/account-id:bucket)' :
storageProvider === 'r2' ? 'Public bucket URL from R2 dashboard (e.g., https://pub-xxx.r2.dev)' :
storageProvider === 'b2' ? 'Public bucket URL from B2 dashboard (e.g., https://f000.backblazeb2.com/file/bucket)' :
'Public URL where files are accessible (without trailing slash)'}
</div>
</div>
)}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '16px', marginBottom: '16px' }}>
<div>
+3 -2
View File
@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback } from 'react';
import Link from 'next/link';
import { useSearchParams, useRouter } from 'next/navigation';
import { formatFullHandle } from '@/lib/utils/handle';
import { useFormattedHandle } from '@/lib/utils/handle';
import { PostCard } from '@/components/PostCard';
import { Post } from '@/lib/types';
import { Bot } from 'lucide-react';
@@ -65,6 +65,7 @@ const FlagIcon = () => (
);
function UserCard({ user }: { user: User }) {
const fullHandle = useFormattedHandle(user.handle);
return (
<Link
href={`/@${user.handle}`}
@@ -107,7 +108,7 @@ function UserCard({ user }: { user: User }) {
</span>
)}
</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '14px' }}>{formatFullHandle(user.handle)}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '14px' }}>{fullHandle}</div>
{user.bio && (
<div style={{
color: 'var(--foreground-secondary)',
+5 -3
View File
@@ -8,7 +8,7 @@ import { PostCard } from '@/components/PostCard';
import { User, Post } from '@/lib/types';
import AutoTextarea from '@/components/AutoTextarea';
import { Rocket, MoreHorizontal, Mail, Camera } from 'lucide-react';
import { formatFullHandle } from '@/lib/utils/handle';
import { useFormattedHandle } from '@/lib/utils/handle';
import { Bot } from 'lucide-react';
import { useAuth } from '@/lib/contexts/AuthContext';
@@ -35,6 +35,7 @@ const stripHtml = (html: string | null | undefined): string | null => {
};
function UserRow({ user }: { user: UserSummary }) {
const fullHandle = useFormattedHandle(user.handle);
return (
<Link href={`/u/${user.handle}`} className="user-row">
<div className="avatar">
@@ -66,7 +67,7 @@ function UserRow({ user }: { user: UserSummary }) {
</span>
)}
</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>{formatFullHandle(user.handle)}</div>
<div style={{ color: 'var(--foreground-tertiary)', fontSize: '13px' }}>{fullHandle}</div>
{user.bio && stripHtml(user.bio) && (
<div className="user-row-bio">{stripHtml(user.bio)}</div>
)}
@@ -82,6 +83,7 @@ export default function ProfilePage() {
const { isIdentityUnlocked, signUserAction } = useAuth();
const [user, setUser] = useState<User | null>(null);
const userFullHandle = user ? useFormattedHandle(user.handle) : '';
const [posts, setPosts] = useState<Post[]>([]);
const [likedPosts, setLikedPosts] = useState<Post[]>([]);
const [currentUser, setCurrentUser] = useState<{ id: string; handle: string } | null>(null);
@@ -719,7 +721,7 @@ export default function ProfilePage() {
{/* User Info */}
<div style={{ padding: '12px 0' }}>
<h2 style={{ fontSize: '20px', fontWeight: 700 }}>{user.displayName || user.handle}</h2>
<p style={{ color: 'var(--foreground-tertiary)' }}>{formatFullHandle(user.handle)}</p>
<p style={{ color: 'var(--foreground-tertiary)' }}>{userFullHandle}</p>
{user.bio && (
<p style={{ marginTop: '12px', lineHeight: 1.5 }}>{user.bio}</p>