Skip to content

Commit 9f73f58

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: add page with oauth application form (#34453)
Sends a request to register an oauth application to our support channel. GitOrigin-RevId: 974d10858d6dba103b27bad00ee6f6a73ad738dd
1 parent b0a7aa5 commit 9f73f58

File tree

4 files changed

+256
-2
lines changed

4 files changed

+256
-2
lines changed
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import { Button } from "dashboard-common/elements/Button";
2+
import { TextInput } from "dashboard-common/elements/TextInput";
3+
import { useFormik } from "formik";
4+
import { Spinner } from "dashboard-common/elements/Spinner";
5+
import { Sheet } from "dashboard-common/elements/Sheet";
6+
import { useAuthHeader } from "hooks/fetching";
7+
import { toast } from "dashboard-common/lib/utils";
8+
import * as Yup from "yup";
9+
import { useState } from "react";
10+
import { useProfile } from "api/profile";
11+
import { CheckIcon } from "@radix-ui/react-icons";
12+
13+
export function RegisterApplication() {
14+
const [done, setDone] = useState(false);
15+
const authHeader = useAuthHeader();
16+
const profile = useProfile();
17+
18+
const formState = useFormik({
19+
initialValues: {
20+
applicationName: "",
21+
domain: "",
22+
redirectUris: "",
23+
description: "",
24+
contactEmail: profile?.email ?? "",
25+
},
26+
validationSchema: Yup.object({
27+
applicationName: Yup.string()
28+
.max(128)
29+
.required("Application name is required"),
30+
domain: Yup.string()
31+
.matches(
32+
/^https?:\/\/.+/,
33+
"Domain must be a valid URL starting with http:// or https://",
34+
)
35+
.required("Domain is required"),
36+
redirectUris: Yup.string()
37+
.required("Redirect URIs are required")
38+
.test(
39+
"valid-uris",
40+
"Invalid URIs. Each line should be a valid URL",
41+
(value) => {
42+
if (!value) return false;
43+
return value.split("\n").every((uri) => {
44+
try {
45+
const _ = new URL(uri.trim());
46+
return true;
47+
} catch {
48+
return false;
49+
}
50+
});
51+
},
52+
),
53+
description: Yup.string().max(2500).required("Description is required"),
54+
contactEmail: Yup.string()
55+
.email("Invalid email address")
56+
.required("Contact email is required"),
57+
}),
58+
onSubmit: async (values) => {
59+
try {
60+
const resp = await fetch("/api/contact-form", {
61+
method: "POST",
62+
body: JSON.stringify({
63+
teamId: 0,
64+
subject: `OAuth Registration Request: ${values.applicationName}`,
65+
message: `Application Name: ${values.applicationName}
66+
Domain: ${values.domain}
67+
Contact Email: ${values.contactEmail}
68+
Redirect URIs:
69+
${values.redirectUris}
70+
71+
Description:
72+
${values.description}
73+
74+
This is an OAuth Application registration request.`,
75+
}),
76+
headers: {
77+
"Content-Type": "application/json",
78+
Authorization: authHeader,
79+
},
80+
});
81+
82+
if (!resp.ok) {
83+
toast(
84+
"error",
85+
"Failed to send registration request. Please try again or email us at [email protected]",
86+
undefined,
87+
false,
88+
);
89+
return;
90+
}
91+
92+
setDone(true);
93+
formState.resetForm();
94+
} catch (error) {
95+
toast(
96+
"error",
97+
"Failed to send registration request. Please try again or email us at [email protected]",
98+
undefined,
99+
false,
100+
);
101+
}
102+
},
103+
});
104+
105+
if (done) {
106+
return (
107+
<Sheet className="flex max-w-prose animate-fadeInFromLoading flex-col gap-4">
108+
<h3 className="flex items-center gap-1">
109+
<CheckIcon className="size-6" /> Registration request sent!
110+
</h3>
111+
<p>
112+
We'll review your request and get back to you soon at{" "}
113+
{formState.values.contactEmail}.
114+
</p>
115+
<p>
116+
Please contact us at{" "}
117+
<a href="mailto:[email protected]">[email protected]</a> if you have
118+
any questions.
119+
</p>
120+
</Sheet>
121+
);
122+
}
123+
124+
return (
125+
<Sheet className="flex max-w-prose animate-fadeInFromLoading flex-col gap-4">
126+
<h3>Register a Convex OAuth Application</h3>
127+
<p>
128+
Once approved, your application will be able to request access to Convex
129+
projects through OAuth.
130+
</p>
131+
<form className="flex flex-col gap-4" onSubmit={formState.handleSubmit}>
132+
<TextInput
133+
label="Application Name"
134+
id="applicationName"
135+
required
136+
onChange={formState.handleChange}
137+
onBlur={formState.handleBlur}
138+
value={formState.values.applicationName}
139+
error={
140+
formState.touched.applicationName
141+
? formState.errors.applicationName
142+
: undefined
143+
}
144+
/>
145+
<TextInput
146+
label="Domain"
147+
id="domain"
148+
required
149+
placeholder="https://your-domain.com"
150+
onChange={formState.handleChange}
151+
onBlur={formState.handleBlur}
152+
value={formState.values.domain}
153+
error={formState.touched.domain ? formState.errors.domain : undefined}
154+
/>
155+
<TextInput
156+
label="Contact Email"
157+
id="contactEmail"
158+
type="email"
159+
required
160+
onChange={formState.handleChange}
161+
onBlur={formState.handleBlur}
162+
value={formState.values.contactEmail}
163+
error={
164+
formState.touched.contactEmail
165+
? formState.errors.contactEmail
166+
: undefined
167+
}
168+
/>
169+
<label
170+
htmlFor="redirectUris"
171+
className="flex flex-col gap-1 text-sm text-content-primary"
172+
>
173+
Redirect URIs
174+
<textarea
175+
id="redirectUris"
176+
name="redirectUris"
177+
className="h-24 resize-y rounded border bg-background-secondary px-4 py-2 text-content-primary placeholder:text-content-tertiary focus:border-border-selected focus:outline-none"
178+
required
179+
onChange={formState.handleChange}
180+
onBlur={formState.handleBlur}
181+
value={formState.values.redirectUris}
182+
placeholder="https://your-domain.com/oauth/callback"
183+
/>
184+
{formState.touched.redirectUris && formState.errors.redirectUris && (
185+
<p
186+
className="flex max-w-prose gap-1 text-xs text-content-errorSecondary"
187+
role="alert"
188+
>
189+
{formState.errors.redirectUris}
190+
</p>
191+
)}
192+
<p className="text-xs text-content-secondary">
193+
Enter one redirect URI per line. These are the allowed callback URLs
194+
for your OAuth flow. You'll be able to modify these or add more
195+
later.
196+
</p>
197+
</label>
198+
<label
199+
htmlFor="description"
200+
className="flex flex-col gap-1 text-sm text-content-primary"
201+
>
202+
Description
203+
<textarea
204+
id="description"
205+
name="description"
206+
className="h-32 resize-y rounded border bg-background-secondary px-4 py-2 text-content-primary placeholder:text-content-tertiary focus:border-border-selected focus:outline-none"
207+
required
208+
onChange={formState.handleChange}
209+
onBlur={formState.handleBlur}
210+
value={formState.values.description}
211+
placeholder="Describe your application and how it will use Convex..."
212+
/>
213+
{formState.touched.description && formState.errors.description && (
214+
<p
215+
className="flex max-w-prose gap-1 text-xs text-content-errorSecondary"
216+
role="alert"
217+
>
218+
{formState.errors.description}
219+
</p>
220+
)}
221+
</label>
222+
<Button
223+
type="submit"
224+
className="ml-auto mt-4"
225+
disabled={formState.isSubmitting || !formState.isValid}
226+
icon={formState.isSubmitting && <Spinner />}
227+
>
228+
{formState.isSubmitting ? "Sending..." : "Submit Registration"}
229+
</Button>
230+
</form>
231+
</Sheet>
232+
);
233+
}

npm-packages/dashboard/src/components/header/DashboardHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const NO_TEAM_ROUTES = [
4444
"/verify",
4545
];
4646

47-
const NO_HEADER_ROUTES = ["/oauth/authorize/project"];
47+
const NO_HEADER_ROUTES = ["/oauth/authorize/project", "/oauth/register"];
4848

4949
function DashboardHeaderWhenLoggedIn() {
5050
const { user } = useAuth0();

npm-packages/dashboard/src/elements/SupportWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ function SupportForm() {
208208
<textarea
209209
id="message"
210210
name="message"
211-
className="h-48 resize-y rounded border bg-background-secondary px-4 py-2 text-content-primary focus:border-border-selected focus:outline-none"
211+
className="h-48 resize-y rounded border bg-background-secondary px-4 py-2 text-content-primary placeholder:text-content-tertiary focus:border-border-selected focus:outline-none"
212212
required
213213
onChange={formState.handleChange}
214214
value={formState.values.message}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { withAuthenticatedPage } from "lib/withAuthenticatedPage";
2+
import Head from "next/head";
3+
import { LoginLayout } from "layouts/LoginLayout";
4+
import { RegisterApplication } from "components/RegisterApplication";
5+
6+
export { getServerSideProps } from "lib/ssr";
7+
8+
function OAuthProviderRegistration() {
9+
return (
10+
<div className="h-screen">
11+
<Head>
12+
<title>Register Convex OAuth Application</title>
13+
</Head>
14+
<LoginLayout>
15+
<RegisterApplication />
16+
</LoginLayout>
17+
</div>
18+
);
19+
}
20+
21+
export default withAuthenticatedPage(OAuthProviderRegistration);

0 commit comments

Comments
 (0)