XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEServerManager

【Java】我的世界XFE服务器管理器

公开
关注 0 Fork 0 Star 0
UTF-8
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type FormEvent,
  type ReactNode,
} from 'react';
import { ApiClient, ApiError, api as defaultApi } from '../lib/api';
import { ModalPortal } from '../components/modal-portal';
import type {
  ConnectionState,
  FeatureFlags,
  OperationStatus,
  ServerStatus,
  SessionInfo,
  StreamEvent,
} from '../types';

interface ServerContextValue {
  api: ApiClient;
  connection: ConnectionState;
  status?: ServerStatus;
  session?: SessionInfo;
  features?: FeatureFlags;
  lastError?: string;
  lastEventAt?: Date;
  events: StreamEvent[];
  operations: OperationStatus[];
  revisions: Readonly<Record<string, number>>;
  refresh: () => Promise<void>;
  setup: (bootstrapToken: string, username: string, password: string) => Promise<void>;
  login: (username: string, password: string, secondFactor?: string, recovery?: boolean) => Promise<void>;
  logout: () => Promise<void>;
}

const ServerContext = createContext<ServerContextValue | null>(null);

function saveCsrf(session?: SessionInfo): void {
  if (session?.csrfToken) sessionStorage.setItem('xfesm.csrf', session.csrfToken);
  else sessionStorage.removeItem('xfesm.csrf');
}

export function ServerProvider({ children, client = defaultApi }: { children: ReactNode; client?: ApiClient }) {
  const [connection, setConnection] = useState<ConnectionState>('connecting');
  const [status, setStatus] = useState<ServerStatus>();
  const [session, setSession] = useState<SessionInfo>();
  const [features, setFeatures] = useState<FeatureFlags>();
  const [lastError, setLastError] = useState<string>();
  const [lastEventAt, setLastEventAt] = useState<Date>();
  const [events, setEvents] = useState<StreamEvent[]>([]);
  const [operations, setOperations] = useState<OperationStatus[]>([]);
  const [revisions, setRevisions] = useState<Record<string, number>>({});
  const mounted = useRef(true);
  const [reauthenticationOpen, setReauthenticationOpen] = useState(false);
  const [reauthenticationCode, setReauthenticationCode] = useState('');
  const [reauthenticationError, setReauthenticationError] = useState<string>();
  const [reauthenticationBusy, setReauthenticationBusy] = useState(false);
  const reauthenticationPromise = useRef<Promise<void> | undefined>(undefined);
  const reauthenticationResolve = useRef<(() => void) | undefined>(undefined);
  const reauthenticationReject = useRef<((error: Error) => void) | undefined>(undefined);

  const refresh = useCallback(async () => {
    setConnection((current) => (current === 'offline' ? 'connecting' : current));
    try {
      let nextSession: SessionInfo;
      try {
        nextSession = await client.session();
      } catch (error) {
        if (!(error instanceof ApiError) || error.status !== 401) throw error;
        const meta = await client.meta();
        nextSession = { authenticated: false, bootstrapRequired: meta.setupRequired };
      }
      if (!mounted.current) return;
      setSession(nextSession);
      saveCsrf(nextSession);
      if (!nextSession.authenticated) {
        setStatus(undefined);
        setFeatures(undefined);
        setConnection('online');
        setLastError(undefined);
        return;
      }

      const [nextStatus, nextFeatures] = await Promise.all([client.status(), client.features()]);
      if (!mounted.current) return;
      setStatus(nextStatus);
      setFeatures(nextFeatures);
      setConnection('online');
      setLastError(undefined);
    } catch (error) {
      if (!mounted.current) return;
      const apiError = error instanceof ApiError ? error : undefined;
      setConnection(apiError?.status === 401 || apiError?.status === 403 ? 'degraded' : 'offline');
      setLastError(error instanceof Error ? error.message : String(error));
      if (apiError?.status === 401) {
        const unauthenticated: SessionInfo = { authenticated: false, bootstrapRequired: false };
        setSession(unauthenticated);
        saveCsrf(unauthenticated);
      }
    }
  }, [client]);

  useEffect(() => {
    mounted.current = true;
    void refresh();
    const timer = globalThis.setInterval(() => void refresh(), 10_000);
    return () => {
      mounted.current = false;
      globalThis.clearInterval(timer);
    };
  }, [refresh]);

  useEffect(() => {
    client.setReauthenticationHandler(() => {
      if (reauthenticationPromise.current) return reauthenticationPromise.current;
      setReauthenticationCode('');
      setReauthenticationError(undefined);
      setReauthenticationOpen(true);
      const pending = new Promise<void>((resolve, reject) => {
        reauthenticationResolve.current = resolve;
        reauthenticationReject.current = reject;
      });
      reauthenticationPromise.current = pending;
      return pending;
    });
    return () => {
      client.setReauthenticationHandler(undefined);
      reauthenticationReject.current?.(new Error('Reauthentication was cancelled.'));
      reauthenticationPromise.current = undefined;
    };
  }, [client]);

  useEffect(() => {
    if (!session?.authenticated) return undefined;
    return client.subscribe((event) => {
    setLastEventAt(new Date());
    setEvents((current) => [event, ...current].slice(0, 40));
    setRevisions((current) => ({ ...current, [event.type]: (current[event.type] ?? 0) + 1 }));
    if (event.type === 'server-status') setStatus(event.data as ServerStatus);
    if (event.type === 'operation') {
      const operation = event.data as OperationStatus;
      setOperations((current) => [operation, ...current.filter((item) => item.id !== operation.id)].slice(0, 12));
    }
  }, (streamState) => {
    if (streamState === 'open') {
      setConnection('online');
      setLastError(undefined);
    } else {
      setConnection((current) => (current === 'offline' ? current : 'degraded'));
    }
    });
  }, [client, session?.authenticated]);

  const setup = useCallback(async (bootstrapToken: string, username: string, password: string) => {
    const nextSession = await client.setup(bootstrapToken, username, password);
    setSession(nextSession);
    saveCsrf(nextSession);
    await refresh();
  }, [client, refresh]);

  const login = useCallback(async (username: string, password: string, secondFactor?: string, recovery = false) => {
    const nextSession = await client.login(username, password, secondFactor, recovery);
    setSession(nextSession);
    saveCsrf(nextSession);
    await refresh();
  }, [client, refresh]);

  const logout = useCallback(async () => {
    await client.logout();
    const nextSession: SessionInfo = { authenticated: false };
    setSession(nextSession);
    saveCsrf(nextSession);
  }, [client]);

  const value = useMemo<ServerContextValue>(() => ({
    api: client,
    connection,
    status,
    session,
    features,
    lastError,
    lastEventAt,
    events,
    operations,
    revisions,
    refresh,
    setup,
    login,
    logout,
  }), [client, connection, events, features, lastError, lastEventAt, login, logout, operations, refresh, revisions, session, setup, status]);

  const cancelReauthentication = () => {
    reauthenticationReject.current?.(new Error('Reauthentication was cancelled.'));
    reauthenticationResolve.current = undefined;
    reauthenticationReject.current = undefined;
    reauthenticationPromise.current = undefined;
    setReauthenticationOpen(false);
  };

  const submitReauthentication = async (event: FormEvent) => {
    event.preventDefault();
    if (reauthenticationCode.length !== 6) return;
    setReauthenticationBusy(true);
    setReauthenticationError(undefined);
    try {
      await client.reauthenticateWithTotp(reauthenticationCode);
      const nextSession = await client.session();
      setSession(nextSession);
      saveCsrf(nextSession);
      reauthenticationResolve.current?.();
      reauthenticationResolve.current = undefined;
      reauthenticationReject.current = undefined;
      reauthenticationPromise.current = undefined;
      setReauthenticationOpen(false);
      setReauthenticationCode('');
    } catch (error) {
      setReauthenticationError(error instanceof Error ? error.message : String(error));
    } finally {
      setReauthenticationBusy(false);
    }
  };

  return <ServerContext.Provider value={value}>
    {children}
    {reauthenticationOpen && <ModalPortal><div className="modal-backdrop" role="presentation">
      <section className="modal" role="dialog" aria-modal="true" aria-labelledby="reauthentication-title">
        <h2 id="reauthentication-title">双因素认证 / Two-factor authentication</h2>
        <p>此操作需要新的动态验证码。验证成功后将自动继续刚才的操作。</p>
        <form className="form-stack" onSubmit={submitReauthentication}>
          <label className="field">
            <span className="field__label">六位验证码 / 6-digit code</span>
            <input className="input" autoFocus inputMode="numeric" autoComplete="one-time-code"
              pattern="[0-9]{6}" value={reauthenticationCode}
              onChange={(event) => setReauthenticationCode(event.target.value.replace(/\D/g, '').slice(0, 6))} />
          </label>
          {reauthenticationError && <div className="form-error" role="alert">{reauthenticationError}</div>}
          <div className="modal__actions">
            <button type="button" className="button button--secondary" onClick={cancelReauthentication}
              disabled={reauthenticationBusy}>取消 / Cancel</button>
            <button type="submit" className="button button--primary"
              disabled={reauthenticationBusy || reauthenticationCode.length !== 6}>
              {reauthenticationBusy ? '验证中…' : '验证并继续 / Verify & continue'}
            </button>
          </div>
        </form>
      </section>
    </div></ModalPortal>}
  </ServerContext.Provider>;
}

export function useServer(): ServerContextValue {
  const context = useContext(ServerContext);
  if (!context) throw new Error('useServer must be used inside ServerProvider');
  return context;
}

interface ResourceState<T> {
  data?: T;
  error?: string;
  loading: boolean;
  reload: () => void;
}

export function useApiResource<T>(
  loader: () => Promise<T>,
  dependencies: readonly unknown[] = [],
  eventTypes: readonly string[] = [],
  enabled = true,
): ResourceState<T> {
  const { connection, revisions } = useServer();
  const [data, setData] = useState<T>();
  const [error, setError] = useState<string>();
  const [loading, setLoading] = useState(false);
  const [reloadToken, setReloadToken] = useState(0);
  const loaderRef = useRef(loader);
  loaderRef.current = loader;
  const eventRevision = eventTypes.reduce((total, type) => total + (revisions[type] ?? 0), 0);

  useEffect(() => {
    if (!enabled || connection === 'offline' || connection === 'connecting') {
      setLoading(false);
      if (!enabled) {
        setData(undefined);
        setError(undefined);
      }
      return;
    }
    let active = true;
    setLoading(true);
    loaderRef.current()
      .then((result) => {
        if (!active) return;
        setData(result);
        setError(undefined);
      })
      .catch((cause: unknown) => {
        if (!active) return;
        setError(cause instanceof Error ? cause.message : String(cause));
      })
      .finally(() => {
        if (active) setLoading(false);
      });
    return () => {
      active = false;
    };
    // Callers supply stable primitive dependencies. revision refreshes resources after SSE activity.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [connection, enabled, reloadToken, eventRevision, ...dependencies]);

  return { data, error, loading, reload: () => setReloadToken((current) => current + 1) };
}