mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 12:22:14 +03:00
C1: sanitize opts.name and backupId against path traversal (replace /\\ with _)
C2: read backup files locally as base64 instead of sending local path to cloud API
C3: implement markOAuthDone/markOAuthFailed via module-level callbacks registered
in useEffect — TUI now transitions to DONE/FAILED when polling signals completion
I1: fix matchesGlob — equality-only for non-glob patterns (removes false-positive
startsWith), and support multi-wildcard patterns (e.g. **.json) by iterating parts
I3: replace two hardcoded English strings in tunnel.mjs with t() calls; add
tunnel.notAvailable and tunnel.noTunnels to en.json and pt-BR.json
I4: log HTTP 4xx errors in runKeysAddCommand and return 1 instead of falling
through silently to DB write on client errors
I5: truncate err.message to 100 chars in ProvidersTestAll.jsx and test-provider.mjs
m1: fix EvalWatch StatusBadge — completed state now uses status="ok" instead of "running"
m2: replace hardcoded ~/.omniroute/backup-schedule.json with resolveDataDir()-based path
m4: remove ...opts spread from all apiFetch calls in keys.mjs — pass only explicit options
(method, body, retry, acceptNotOk) to avoid leaking apiKey/baseUrl/yes/etc.
194 lines
4.8 KiB
JavaScript
194 lines
4.8 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import { render, Box, Text, useInput } from "ink";
|
|
import Spinner from "ink-spinner";
|
|
import { StatusBadge } from "../tui-components/StatusBadge.jsx";
|
|
import { ConfirmDialog } from "../tui-components/ConfirmDialog.jsx";
|
|
|
|
const PHASE = {
|
|
WAITING: "waiting",
|
|
POLLING: "polling",
|
|
DONE: "done",
|
|
FAILED: "failed",
|
|
CANCELLED: "cancelled",
|
|
};
|
|
|
|
let _globalDone = null;
|
|
let _globalFail = null;
|
|
|
|
function OAuthFlowApp({ provider, url, deviceCode, onCancel, onDone, onFail }) {
|
|
const [phase, setPhase] = useState(PHASE.WAITING);
|
|
const [result, setResult] = useState(null);
|
|
const [error, setError] = useState(null);
|
|
const [elapsed, setElapsed] = useState(0);
|
|
const [confirmCancel, setConfirmCancel] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const id = setInterval(() => setElapsed((e) => e + 1), 1000);
|
|
return () => clearInterval(id);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
_globalDone = (res) => {
|
|
setResult(res);
|
|
setPhase(PHASE.DONE);
|
|
onDone?.(res);
|
|
};
|
|
_globalFail = (err) => {
|
|
setError(typeof err === "string" ? err : (err?.message ?? String(err)));
|
|
setPhase(PHASE.FAILED);
|
|
onFail?.(err);
|
|
};
|
|
setPhase(PHASE.POLLING);
|
|
return () => {
|
|
_globalDone = null;
|
|
_globalFail = null;
|
|
};
|
|
}, []);
|
|
|
|
useInput((input, key) => {
|
|
if (phase === PHASE.DONE || phase === PHASE.FAILED || phase === PHASE.CANCELLED) return;
|
|
if (input === "q" || (key.ctrl && input === "c")) {
|
|
setConfirmCancel(true);
|
|
}
|
|
});
|
|
|
|
function handleCancelConfirm(yes) {
|
|
setConfirmCancel(false);
|
|
if (yes) {
|
|
setPhase(PHASE.CANCELLED);
|
|
onCancel?.();
|
|
}
|
|
}
|
|
|
|
const elapsed_str = `${Math.floor(elapsed / 60)}:${String(elapsed % 60).padStart(2, "0")}`;
|
|
|
|
if (confirmCancel) {
|
|
return (
|
|
<Box flexDirection="column" padding={1}>
|
|
<ConfirmDialog
|
|
message="Cancel OAuth authorization?"
|
|
onConfirm={handleCancelConfirm}
|
|
defaultNo
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={2} paddingY={1}>
|
|
<Box marginBottom={1}>
|
|
<Text bold color="cyan">
|
|
OmniRoute OAuth — {provider}
|
|
</Text>
|
|
</Box>
|
|
|
|
{url && (
|
|
<Box flexDirection="column" marginBottom={1}>
|
|
<Text>Open this URL in your browser to authorize:</Text>
|
|
<Box marginTop={0}>
|
|
<Text bold color="yellow">
|
|
{url}
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
)}
|
|
|
|
{deviceCode && (
|
|
<Box flexDirection="column" marginBottom={1}>
|
|
<Text>
|
|
Device code:{" "}
|
|
<Text bold color="yellow">
|
|
{deviceCode}
|
|
</Text>
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
|
|
<Box marginTop={1}>
|
|
{phase === PHASE.POLLING || phase === PHASE.WAITING ? (
|
|
<Box>
|
|
<Text color="green">
|
|
<Spinner type="dots" />
|
|
</Text>
|
|
<Text> Waiting for authorization... </Text>
|
|
<Text dimColor>({elapsed_str})</Text>
|
|
</Box>
|
|
) : phase === PHASE.DONE ? (
|
|
<Box>
|
|
<StatusBadge status="ok" />
|
|
<Text> Authorized: {result?.email ?? result?.account ?? "connected"}</Text>
|
|
</Box>
|
|
) : phase === PHASE.FAILED ? (
|
|
<Box>
|
|
<StatusBadge status="error" />
|
|
<Text> Failed: {error}</Text>
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<StatusBadge status="warn" />
|
|
<Text> Cancelled.</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{(phase === PHASE.POLLING || phase === PHASE.WAITING) && (
|
|
<Box marginTop={1}>
|
|
<Text dimColor>[q] cancel</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export async function startOAuthTui({ provider, url, deviceCode }) {
|
|
return new Promise((resolve, reject) => {
|
|
let resolved = false;
|
|
|
|
function onDone(result) {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
unmount();
|
|
resolve({ status: "authorized", result });
|
|
}
|
|
|
|
function onFail(err) {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
unmount();
|
|
resolve({ status: "failed", error: err });
|
|
}
|
|
|
|
function onCancel() {
|
|
if (resolved) return;
|
|
resolved = true;
|
|
unmount();
|
|
resolve({ status: "cancelled" });
|
|
}
|
|
|
|
const { unmount, waitUntilExit } = render(
|
|
<OAuthFlowApp
|
|
provider={provider}
|
|
url={url}
|
|
deviceCode={deviceCode}
|
|
onDone={onDone}
|
|
onFail={onFail}
|
|
onCancel={onCancel}
|
|
/>
|
|
);
|
|
|
|
waitUntilExit()
|
|
.then(() => {
|
|
if (!resolved) resolve({ status: "exited" });
|
|
})
|
|
.catch(reject);
|
|
});
|
|
}
|
|
|
|
export function markOAuthDone(result) {
|
|
_globalDone?.(result);
|
|
}
|
|
|
|
export function markOAuthFailed(error) {
|
|
_globalFail?.(error);
|
|
}
|