File size: 9,028 Bytes
2eb87e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d22337b
2eb87e3
 
 
 
d22337b
2eb87e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d22337b
 
2eb87e3
 
 
 
 
 
 
 
d22337b
 
2eb87e3
 
 
d22337b
 
2eb87e3
 
 
 
 
 
 
 
 
 
d22337b
2eb87e3
 
 
 
 
 
d22337b
2eb87e3
 
 
d22337b
2eb87e3
d22337b
2eb87e3
 
 
 
d22337b
2eb87e3
 
 
 
 
 
 
d22337b
2eb87e3
 
 
 
 
 
d22337b
2eb87e3
 
 
 
 
 
d22337b
2eb87e3
d22337b
2eb87e3
 
 
 
d22337b
2eb87e3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { ArrowUp, CircleAlert, Github, LoaderCircle } from "lucide-react";
import { ApiError, api, navigateToLogin } from "../shared/api/client";
import { useMe } from "../features/auth/useAuth";
import { Button } from "./Button";

function mapError(err: unknown): string {
  if (!(err instanceof ApiError)) {
    return "Something went wrong. Please try again.";
  }
  const reason = String(err.context?.reason ?? "");
  const message = String(err.context?.message ?? "");
  if (err.status === 401) {
    return "Please sign in with GitHub to submit a repository.";
  }
  if (err.status === 403) {
    return (
      message ||
      "Only accounts with admin or maintain permission on this repository can submit it."
    );
  }
  if (reason === "ref_not_found" || reason === "invalid_ref") {
    return "That branch/tag/commit was not found in this repository.";
  }
  if (reason === "invalid_github_url") {
    return "That doesn't look like a GitHub repository URL. Expected: https://github.com/owner/repo";
  }
  if (reason === "private_repo") {
    return "This repository is private or does not exist. OpenVuln scans public projects only.";
  }
  if (reason === "cooldown") {
    const days = err.context?.retry_after_days;
    return `This project was scanned recently. You can resubmit after ${days ?? "a few"} day(s).`;
  }
  if (reason === "duplicate" || err.status === 409) {
    return message || "This project is already on OpenVuln.";
  }
  if (err.status === 404) {
    return "This repository is private or does not exist. OpenVuln scans public projects only.";
  }
  if (err.status >= 500) {
    return "OpenVuln is temporarily unavailable. Please try again in a moment.";
  }
  return message || err.message || "Submission failed.";
}

export function RepoSubmitForm({
  size = "default",
  appearance = "default",
  className = "",
  align = "center",
}: {
  size?: "default" | "hero";
  appearance?: "default" | "dark";
  className?: string;
  align?: "center" | "left";
}) {
  const nav = useNavigate();
  const meQ = useMe();
  const [url, setUrl] = useState("");
  const [ref, setRef] = useState("");
  const [showRef, setShowRef] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [pending, setPending] = useState(false);

  const hero = size === "hero";
  const dark = appearance === "dark";

  const onSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    setError(null);
    const git_url = url.trim();
    if (!git_url) {
      setError("Please paste a GitHub repository URL.");
      return;
    }
    // 未登录 → 整页跳 GitHub OAuth,登录后回到部署首页
    if (meQ.data && !meQ.data.authenticated) {
      navigateToLogin();
      window.dispatchEvent(new Event("ov-oauth-popup-opened"));
      return;
    }
    setPending(true);
    try {
      const res = await api.submitProject({ git_url, ...(ref.trim() ? { ref: ref.trim() } : {}) });
      nav(`/p/${res.project.owner_login}/${res.project.name}`, {
        state: { justSubmitted: true },
      });
    } catch (err) {
      setError(mapError(err));
    } finally {
      setPending(false);
    }
  };

  if (dark) {
    // hero composer(暗色控制台);appearance prop 名保留为调用方兼容
    const centered = align === "center";
    return (
      <form
        onSubmit={(e) => void onSubmit(e)}
        className={className}
        aria-label="Submit a GitHub repository"
        aria-busy={pending}
      >
        <div
          className={`openvuln-composer group flex min-h-[58px] items-center gap-3 rounded-xl border bg-surface-raised p-2 pl-4 shadow-[0_20px_48px_-20px_rgba(0,0,0,0.65)] transition focus-within:bg-surface-sunken/60 ${
            error ? "border-danger/60" : "border-line focus-within:border-line-strong"
          }`}
        >
          <Github
            size={18}
            className="shrink-0 text-ink-tertiary transition group-focus-within:text-ink-secondary"
          />
          <input
            type="url"
            value={url}
            onChange={(e) => {
              setUrl(e.target.value);
              setError(null);
            }}
            placeholder="Paste a public GitHub repository URL"
            spellCheck={false}
            className="min-w-0 flex-1 bg-transparent py-3 font-mono text-[13px] text-ink outline-none placeholder:text-ink-tertiary sm:text-[14px]"
            aria-label="GitHub repository URL"
            aria-invalid={!!error}
          />
          <button
            type="submit"
            aria-label={pending ? "Submitting repository" : "Analyze repository"}
            className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-ink text-surface shadow-sm transition hover:bg-white active:scale-95 focus-ring disabled:cursor-not-allowed disabled:bg-surface-sunken disabled:text-ink-tertiary disabled:opacity-100"
            disabled={!url.trim() || pending}
          >
            {pending ? (
              <LoaderCircle size={19} className="animate-spin motion-reduce:animate-none" />
            ) : (
              <ArrowUp size={19} strokeWidth={2.2} />
            )}
          </button>
        </div>

        <div className={`mt-2.5 flex ${centered ? "justify-center" : "justify-start"}`}>
          {showRef ? (
            <input
              type="text"
              value={ref}
              onChange={(e) => setRef(e.target.value)}
              placeholder="Branch, tag, or commit SHA (optional)"
              spellCheck={false}
              className="h-8 w-72 rounded-lg border border-line bg-surface-raised px-3 font-mono text-xs text-ink outline-none placeholder:text-ink-tertiary focus:border-line-strong"
              aria-label="Version to scan (branch, tag, or commit SHA)"
            />
          ) : (
            <button
              type="button"
              onClick={() => setShowRef(true)}
              className="font-mono text-[11px] text-ink-tertiary underline decoration-line underline-offset-2 transition hover:text-ink-secondary"
            >
              Scan a specific version
            </button>
          )}
        </div>

        <div className={`mt-2.5 flex min-h-5 items-start gap-1.5 text-xs text-ink-tertiary ${centered ? "justify-center text-center" : "justify-start text-left"}`}>
          {error ? (
            <span className="inline-flex items-start gap-1.5 text-danger" role="alert">
              <CircleAlert size={14} className="mt-px shrink-0" />
              <span>{error}</span>
            </span>
          ) : pending ? (
            <span className="text-ink-secondary" role="status">Submitting repository…</span>
          ) : (
            <span className="inline-flex items-center gap-1.5">
              <CircleAlert size={13} strokeWidth={1.8} className="shrink-0" aria-hidden />
              <span>Public repositories · Repository maintainers only</span>
            </span>
          )}
        </div>
      </form>
    );
  }

  return (
    <form onSubmit={(e) => void onSubmit(e)} className={className}>
      <div className="flex flex-col gap-2 sm:flex-row">
        <input
          type="text"
          value={url}
          onChange={(e) => setUrl(e.target.value)}
          placeholder="https://github.com/owner/repo"
          spellCheck={false}
          className={`h-12 flex-1 rounded-lg border bg-surface-raised px-3.5 font-mono text-sm text-ink placeholder:text-ink-tertiary focus-ring ${
            error ? "border-danger" : "border-line"
          }`}
          aria-invalid={!!error}
        />
        <Button type="submit" size="lg" disabled={pending} className="rounded-lg shrink-0">
          {pending ? "Submitting…" : "Submit"}
        </Button>
      </div>
      <div className="mt-2">
        {showRef ? (
          <input
            type="text"
            value={ref}
            onChange={(e) => setRef(e.target.value)}
            placeholder="Branch, tag, or commit SHA (optional — default: default branch HEAD)"
            spellCheck={false}
            className="h-9 w-full rounded-md border border-line bg-surface-raised px-3 font-mono text-xs text-ink placeholder:text-ink-tertiary focus-ring"
            aria-label="Version to scan (branch, tag, or commit SHA)"
          />
        ) : (
          <button
            type="button"
            onClick={() => setShowRef(true)}
            className="text-[12px] text-ink-tertiary underline decoration-line underline-offset-2 transition-colors hover:text-ink-secondary"
          >
            Scan a specific version
          </button>
        )}
      </div>
      {error && (
        <p
          className={`mt-2 flex items-start gap-1.5 text-sm text-danger ${hero ? "justify-center text-left" : ""}`}
          role="alert"
        >
          <CircleAlert size={16} className="mt-0.5 shrink-0" />
          <span>{error}</span>
        </p>
      )}
    </form>
  );
}