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