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
+3
View File
@@ -139,6 +139,7 @@ export async function registerUser(
displayName?: string,
storageProvider?: string,
storageEndpoint?: string | null,
storagePublicBaseUrl?: string | null,
storageRegion?: string,
storageBucket?: string,
storageAccessKey?: string,
@@ -201,6 +202,7 @@ export async function registerUser(
const avatarUrl = await generateAndUploadAvatarToUserStorage(
fullHandle,
storageEndpoint || undefined,
storagePublicBaseUrl || undefined,
storageRegion,
storageBucket,
storageAccessKey,
@@ -222,6 +224,7 @@ export async function registerUser(
privateKeyEncrypted: serializeEncryptedKey(encryptedPrivateKey),
storageProvider,
storageEndpoint: storageEndpoint || null,
storagePublicBaseUrl: storagePublicBaseUrl || null,
storageRegion,
storageBucket,
storageAccessKeyEncrypted: serializeEncryptedKey(encryptedAccessKey),
+33
View File
@@ -0,0 +1,33 @@
let cachedConfig: { domain: string } | null = null;
let configPromise: Promise<{ domain: string }> | null = null;
export async function getRuntimeConfig() {
if (cachedConfig) return cachedConfig;
if (configPromise) return configPromise;
configPromise = fetch('/api/config')
.then((res) => res.json())
.then((data) => {
cachedConfig = {
domain: data.domain || 'localhost:3000',
};
return cachedConfig;
})
.catch(() => {
cachedConfig = {
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000',
};
return cachedConfig;
});
return configPromise;
}
export function getDomain(): string {
return cachedConfig?.domain || process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
}
export function clearCachedConfig() {
cachedConfig = null;
configPromise = null;
}
+63
View File
@@ -0,0 +1,63 @@
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
interface RuntimeConfig {
domain: string;
}
interface ConfigContextType {
config: RuntimeConfig | null;
isLoading: boolean;
}
const ConfigContext = createContext<ConfigContextType>({
config: null,
isLoading: true,
});
export function ConfigProvider({ children }: { children: ReactNode }) {
const [config, setConfig] = useState<RuntimeConfig | null>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Fetch runtime config on mount
fetch('/api/config')
.then((res) => res.json())
.then((data) => {
setConfig({
domain: data.domain || 'localhost:3000',
});
})
.catch(() => {
// Fallback to build-time value if fetch fails
setConfig({
domain: process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000',
});
})
.finally(() => {
setIsLoading(false);
});
}, []);
return (
<ConfigContext.Provider value={{ config, isLoading }}>
{children}
</ConfigContext.Provider>
);
}
export function useRuntimeConfig() {
const context = useContext(ConfigContext);
if (!context) {
throw new Error('useRuntimeConfig must be used within a ConfigProvider');
}
return context;
}
export function useDomain(): string {
const { config, isLoading } = useRuntimeConfig();
// Return runtime domain if loaded, otherwise fall back to build-time value
if (isLoading || !config) {
return process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
}
return config.domain;
}
+14 -4
View File
@@ -65,6 +65,7 @@ export async function uploadToUserStorage(
mimeType: string,
provider: StorageProvider,
endpoint: string | null,
publicBaseUrl: string | null,
region: string,
bucket: string,
encryptedAccessKey: string,
@@ -89,7 +90,7 @@ export async function uploadToUserStorage(
// Upload file
const key = `synapsis/${filename}`;
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
@@ -99,8 +100,12 @@ export async function uploadToUserStorage(
// Construct URL based on provider
let url: string;
if (endpoint) {
// Custom endpoint (R2, MinIO, etc)
// Priority: user's publicBaseUrl > construct from endpoint > AWS format
if (publicBaseUrl) {
url = `${publicBaseUrl.replace(/\/$/, '')}/${key}`;
} else if (endpoint) {
// Custom endpoint (MinIO, etc)
url = `${endpoint}/${bucket}/${key}`;
} else {
// AWS S3 standard URL
@@ -165,6 +170,7 @@ export async function testS3Credentials(
export async function generateAndUploadAvatarToUserStorage(
handle: string,
endpoint: string | undefined,
publicBaseUrl: string | undefined,
region: string,
bucket: string,
accessKey: string,
@@ -193,7 +199,7 @@ export async function generateAndUploadAvatarToUserStorage(
});
const key = `synapsis/avatars/${handle.replace(/[^a-zA-Z0-9]/g, '')}-avatar.png`;
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
@@ -202,6 +208,10 @@ export async function generateAndUploadAvatarToUserStorage(
}));
// 3. Return URL
// Priority: user's publicBaseUrl > construct from endpoint > AWS format
if (publicBaseUrl) {
return `${publicBaseUrl.replace(/\/$/, '')}/${key}`;
}
if (endpoint) {
return `${endpoint}/${bucket}/${key}`;
}
+28
View File
@@ -1,3 +1,6 @@
import { useDomain } from '@/lib/contexts/ConfigContext';
// Build-time domain fallback (for SSR/non-React contexts)
export const NODE_DOMAIN = process.env.NEXT_PUBLIC_NODE_DOMAIN || 'localhost:3000';
/**
@@ -23,3 +26,28 @@ export function formatFullHandle(handle: string, nodeDomain?: string | null): st
const domain = nodeDomain || NODE_DOMAIN;
return `@${cleanHandle}@${domain}`;
}
/**
* React hook that formats a handle using the runtime domain config.
* Use this in client components instead of formatFullHandle for local handles.
*
* @param handle - The user handle (with or without domain)
* @param nodeDomain - Optional domain override for swarm posts
*/
export function useFormattedHandle(handle: string, nodeDomain?: string | null): string {
const runtimeDomain = useDomain();
if (!handle) return '';
// Remove leading @ if present for processing
const cleanHandle = handle.startsWith('@') ? handle.slice(1) : handle;
// Check if it already has a domain (contains @)
if (cleanHandle.includes('@')) {
return `@${cleanHandle}`;
}
// Use provided domain (for remote posts) or fall back to runtime domain
const domain = nodeDomain || runtimeDomain;
return `@${cleanHandle}@${domain}`;
}