feat: Introduce runtime configuration for domain and enhance S3 storage settings with public base URL and Contabo support.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,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)',
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user