mirror of
https://github.com/rustfs/console.git
synced 2026-08-30 17:14:47 +08:00
273 lines
7.1 KiB
TypeScript
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,
|
|
}
|
|
}
|