Files
rustfs-console/lib/config-helpers.ts
T

273 lines
7.1 KiB
TypeScript

import type { SiteConfig } from "@/types/config"
import { AwsClient } from "@/lib/aws4fetch"
import { logger } from "./logger"
type ConfigSource = "browser" | "localStorage" | "server" | "default"
interface VersionConfigResponse {
version?: string
date?: string
versionInfo?: string
}
export interface ConfigResult {
config: SiteConfig | null
source: ConfigSource
error?: string
}
interface HostInfo {
protocol: string
host: string
serverHost: string
}
export interface ServerHealthCheckResult {
healthy: boolean
url?: string
status?: number
error?: string
}
const STORAGE_KEY = "rustfs-server-host"
const CREDENTIALS_KEY = "auth.credentials"
const PERMANENT_CREDENTIALS_KEY = "auth.permanent"
const DEFAULT_REGION = "us-east-1"
const API_PATH = "/rustfs/admin/v3"
const VERSION_PATH = "/rustfs/console/version"
const REQUEST_TIMEOUT = 5000
const HEALTH_REQUEST_TIMEOUT = 5000
const HEALTH_PATHS = ["/rustfs/console/health", "/health"] as const
const getApiPrefix = (): string => (process.env.NEXT_PUBLIC_API_PREFIX || "").replace(/\/$/, "")
const isBrowser = (): boolean => typeof window !== "undefined"
const getCurrentHostInfo = (): HostInfo | null => {
if (!isBrowser()) return null
const protocol = window.location.protocol.replace(":", "")
const host = window.location.host
const serverHost = `${protocol}://${host}`
return { protocol, host, serverHost }
}
export const createDefaultConfig = (serverHost: string): SiteConfig => {
const apiPrefix = getApiPrefix()
return {
serverHost,
api: {
baseURL: `${serverHost}${apiPrefix}${API_PATH}`,
},
s3: {
endpoint: serverHost,
region: DEFAULT_REGION,
accessKeyId: "",
secretAccessKey: "",
},
}
}
export const getStoredHostConfig = (): ConfigResult => {
if (!isBrowser()) {
return { config: null, source: "localStorage", error: "Not in browser environment" }
}
const savedHost = localStorage.getItem(STORAGE_KEY)
if (!savedHost) {
return { config: null, source: "localStorage", error: "No saved host found" }
}
try {
const url = new URL(savedHost)
const serverHost = `${url.protocol}//${url.host}`
const config = createDefaultConfig(serverHost)
return { config, source: "localStorage" }
} catch (error) {
const errorMessage = `Invalid saved host configuration: ${error instanceof Error ? error.message : "Unknown error"}`
logger.warn(errorMessage)
return { config: null, source: "localStorage", error: errorMessage }
}
}
export const getCurrentBrowserConfig = (): ConfigResult => {
const hostInfo = getCurrentHostInfo()
if (!hostInfo) {
return { config: null, source: "browser", error: "Not in browser environment" }
}
const config = createDefaultConfig(hostInfo.serverHost)
return { config, source: "browser" }
}
const parseStoredJson = <T>(value: string | null): T | null => {
if (!value) return null
try {
return JSON.parse(value) as T
} catch {
return null
}
}
const isExpired = (expiration?: string) => {
if (!expiration) return false
return new Date(expiration) < new Date()
}
const getStoredSigningCredentials = (): {
accessKeyId: string
secretAccessKey: string
sessionToken?: string
} | null => {
if (!isBrowser()) return null
const stored = parseStoredJson<{
AccessKeyId?: string
SecretAccessKey?: string
SessionToken?: string
Expiration?: string
}>(localStorage.getItem(CREDENTIALS_KEY))
if (stored?.AccessKeyId && stored?.SecretAccessKey && stored?.SessionToken && !isExpired(stored.Expiration)) {
return {
accessKeyId: stored.AccessKeyId,
secretAccessKey: stored.SecretAccessKey,
sessionToken: stored.SessionToken,
}
}
const permanent = parseStoredJson<{
AccessKeyId?: string
SecretAccessKey?: string
}>(localStorage.getItem(PERMANENT_CREDENTIALS_KEY))
if (permanent?.AccessKeyId && permanent?.SecretAccessKey) {
return {
accessKeyId: permanent.AccessKeyId,
secretAccessKey: permanent.SecretAccessKey,
}
}
return null
}
export const fetchVersionConfigFromServer = async (serverHost: string): Promise<VersionConfigResponse | null> => {
const configUrl = `${serverHost}${VERSION_PATH}`
try {
const credentials = getStoredSigningCredentials()
if (!credentials) {
logger.warn("Skip version config fetch: no signing credentials available")
return null
}
const client = new AwsClient({
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
sessionToken: credentials.sessionToken,
service: "s3",
region: DEFAULT_REGION,
})
const response = await client.fetch(configUrl, {
method: "GET",
headers: { "Content-Type": "application/json" },
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
})
if (!response.ok) {
logger.warn(`Failed to fetch version config from ${configUrl}: ${response.status} ${response.statusText}`)
return null
}
const data: VersionConfigResponse = await response.json()
if (!(typeof data === "object" && data !== null)) {
logger.warn("Invalid version config: not a valid object")
return null
}
return data
} catch (error) {
logger.warn(
`Error fetching version config from server: ${error instanceof Error ? error.message : "Unknown error"}`,
)
return null
}
}
const requestHealth = async (url: string): Promise<Response> => {
const headResponse = await fetch(url, {
method: "HEAD",
signal: AbortSignal.timeout(HEALTH_REQUEST_TIMEOUT),
})
if (headResponse.status !== 405) return headResponse
return fetch(url, {
method: "GET",
signal: AbortSignal.timeout(HEALTH_REQUEST_TIMEOUT),
})
}
export const checkServerHealth = async (serverHost: string): Promise<ServerHealthCheckResult> => {
const normalizedHost = serverHost.replace(/\/+$/, "")
let lastError = "Unknown health check error"
for (const path of HEALTH_PATHS) {
const healthUrl = `${normalizedHost}${path}`
try {
const response = await requestHealth(healthUrl)
if (response.ok) {
return {
healthy: true,
url: healthUrl,
status: response.status,
}
}
lastError = `${response.status} ${response.statusText || "Health check failed"}`
} catch (error) {
lastError = error instanceof Error ? error.message : "Health check request failed"
}
}
return {
healthy: false,
error: lastError,
}
}
export const getServerDefaultConfig = (): ConfigResult => {
const defaultServerHost = "http://localhost:9000"
const config = createDefaultConfig(defaultServerHost)
return { config, source: "default" }
}
export const validateConfig = (config: SiteConfig): { valid: boolean; errors: string[] } => {
const errors: string[] = []
if (!config.serverHost) {
errors.push("serverHost is required")
}
if (!config.api?.baseURL) {
errors.push("api.baseURL is required")
}
if (!config.s3?.endpoint) {
errors.push("s3.endpoint is required")
}
if (!config.s3?.region) {
errors.push("s3.region is required")
}
return {
valid: errors.length === 0,
errors,
}
}