Skip to content

Commit fe3162f

Browse files
committed
feat(engine): ask for token when unauth
1 parent f6fa143 commit fe3162f

File tree

12 files changed

+370
-16
lines changed

12 files changed

+370
-16
lines changed

frontend/src/app.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ declare module "@tanstack/react-router" {
2222
}
2323
}
2424

25+
declare module "@tanstack/react-query" {
26+
interface Register {
27+
queryMeta: {
28+
mightRequireAuth?: boolean;
29+
};
30+
}
31+
}
32+
2533
export const router = createRouter({
2634
basepath: import.meta.env.BASE_URL,
2735
routeTree,
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as EngineCredentialsForm from "@/app/forms/engine-credentials-form";
2+
import {
3+
DialogDescription,
4+
DialogFooter,
5+
DialogHeader,
6+
DialogTitle,
7+
Flex,
8+
getConfig,
9+
ls,
10+
toast,
11+
} from "@/components";
12+
import { queryClient } from "@/queries/global";
13+
import { createClient } from "@/queries/manager-engine";
14+
15+
export default function ProvideEngineCredentialsDialogContent() {
16+
return (
17+
<EngineCredentialsForm.Form
18+
defaultValues={{ token: "" }}
19+
errors={
20+
ls.engineCredentials.get(getConfig().apiUrl)
21+
? { token: { message: "Invalid token.", type: "manual" } }
22+
: {}
23+
}
24+
onSubmit={async (values, form) => {
25+
const client = createClient({
26+
token: values.token,
27+
});
28+
29+
try {
30+
await client.namespaces.list();
31+
32+
await queryClient.invalidateQueries({
33+
refetchType: "active",
34+
});
35+
36+
ls.engineCredentials.set(getConfig().apiUrl, values.token);
37+
38+
toast.success(
39+
"Successfully authenticated with Rivet Engine",
40+
);
41+
} catch (e) {
42+
if (e && typeof e === "object" && "statusCode" in e) {
43+
if (e.statusCode === 403) {
44+
form.setError("token", {
45+
message: "Invalid token.",
46+
});
47+
return;
48+
}
49+
}
50+
51+
form.setError("token", {
52+
message: "Failed to connect. Please try again.",
53+
});
54+
return;
55+
}
56+
}}
57+
>
58+
<DialogHeader>
59+
<DialogTitle>Missing Rivet Engine credentials</DialogTitle>
60+
<DialogDescription>
61+
It looks like the instance of Rivet Engine that you're
62+
connected to requires additional credentials, please provide
63+
them below.
64+
</DialogDescription>
65+
</DialogHeader>
66+
<Flex gap="4" direction="col">
67+
<EngineCredentialsForm.Token />
68+
</Flex>
69+
<DialogFooter>
70+
<EngineCredentialsForm.Submit type="submit" allowPristine>
71+
Save
72+
</EngineCredentialsForm.Submit>
73+
</DialogFooter>
74+
</EngineCredentialsForm.Form>
75+
);
76+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { type UseFormReturn, useFormContext } from "react-hook-form";
2+
import z from "zod";
3+
import {
4+
createSchemaForm,
5+
FormControl,
6+
FormField,
7+
FormItem,
8+
FormLabel,
9+
FormMessage,
10+
Input,
11+
} from "@/components";
12+
13+
export const formSchema = z.object({
14+
token: z.string().nonempty("Token is required"),
15+
});
16+
17+
export type FormValues = z.infer<typeof formSchema>;
18+
export type SubmitHandler = (
19+
values: FormValues,
20+
form: UseFormReturn<FormValues>,
21+
) => Promise<void>;
22+
23+
const { Form, Submit, SetValue } = createSchemaForm(formSchema);
24+
export { Form, Submit, SetValue };
25+
26+
export const Token = ({ className }: { className?: string }) => {
27+
const { control } = useFormContext<FormValues>();
28+
return (
29+
<FormField
30+
control={control}
31+
name="token"
32+
render={({ field }) => (
33+
<FormItem className={className}>
34+
<FormLabel className="col-span-1">Token</FormLabel>
35+
<FormControl className="row-start-2">
36+
<Input
37+
placeholder="Enter a token..."
38+
type="password"
39+
{...field}
40+
/>
41+
</FormControl>
42+
<FormMessage className="col-span-1" />
43+
</FormItem>
44+
)}
45+
/>
46+
);
47+
};

frontend/src/app/use-dialog.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import { createDialogHook, useDialog } from "@/components/actors";
1+
import {
2+
useDialog as baseUseDialog,
3+
createDialogHook,
4+
} from "@/components/actors";
25

3-
const d = useDialog as typeof useDialog &
4-
Record<string, ReturnType<typeof createDialogHook>>;
5-
d.CreateNamespace = createDialogHook(
6-
import("@/app/dialogs/create-namespace-dialog"),
7-
);
8-
9-
export { d as useDialog };
6+
export const useDialog = {
7+
...baseUseDialog,
8+
CreateNamespace: createDialogHook(
9+
import("@/app/dialogs/create-namespace-dialog"),
10+
),
11+
ProvideEngineCredentials: createDialogHook(
12+
import("@/app/dialogs/provide-engine-credentials-dialog"),
13+
),
14+
};

frontend/src/components/lib/create-schema-form.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type FieldPath,
77
type FieldValues,
88
type PathValue,
9+
type UseFormProps,
910
type UseFormReturn,
1011
useForm,
1112
useFormContext,
@@ -18,6 +19,7 @@ interface FormProps<FormValues extends FieldValues>
1819
extends Omit<ComponentProps<"form">, "onSubmit"> {
1920
onSubmit: SubmitHandler<FormValues>;
2021
defaultValues: DefaultValues<FormValues>;
22+
errors?: UseFormProps<FormValues>["errors"];
2123
values?: FormValues;
2224
children: ReactNode;
2325
}
@@ -36,13 +38,15 @@ export const createSchemaForm = <Schema extends z.ZodSchema>(
3638
values,
3739
children,
3840
onSubmit,
41+
errors,
3942
...props
4043
}: FormProps<z.TypeOf<Schema>>) => {
4144
const form = useForm<z.TypeOf<Schema>>({
4245
reValidateMode: "onSubmit",
4346
resolver: zodResolver(schema),
4447
defaultValues,
4548
values,
49+
errors,
4650
});
4751
return (
4852
<Form {...form}>

frontend/src/components/lib/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type ClassValue, clsx } from "clsx";
2+
import { set } from "lodash";
23
import { twMerge } from "tailwind-merge";
34

45
export function cn(...inputs: ClassValue[]) {
@@ -23,6 +24,21 @@ export const ls = {
2324
clear: () => {
2425
localStorage.clear();
2526
},
27+
engineCredentials: {
28+
set: (url: string, token: string) => {
29+
ls.set(
30+
`engine-credentials-${JSON.stringify(url)}`,
31+
JSON.stringify({ token }),
32+
);
33+
},
34+
get: (url: string) => {
35+
const value = ls.get(`engine-credentials-${JSON.stringify(url)}`);
36+
if (value && typeof value === "object" && "token" in value) {
37+
return (value as { token: string }).token;
38+
}
39+
return null;
40+
},
41+
},
2642
actorsList: {
2743
set: (width: number, folded: boolean) => {
2844
ls.set("actors-list-preview-width", width);
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"use client";
2+
import { useDialog } from "@/app/use-dialog";
3+
import { modalActions, useOpenModal } from "@/stores/modal-store";
4+
5+
export function ModalRenderer() {
6+
const openModal = useOpenModal();
7+
8+
if (!openModal) {
9+
return null;
10+
}
11+
12+
const DialogComponent = getDialogComponent(openModal.dialogKey);
13+
if (!DialogComponent) {
14+
console.warn(
15+
`Dialog component not found for key: ${openModal.dialogKey}`,
16+
);
17+
return null;
18+
}
19+
20+
return (
21+
<DialogComponent
22+
{...(openModal.props || {})}
23+
dialogProps={{
24+
open: true,
25+
onOpenChange: (open: boolean) => {
26+
if (!open) {
27+
modalActions.closeModal();
28+
}
29+
},
30+
}}
31+
/>
32+
);
33+
}
34+
35+
function getDialogComponent(dialogKey: string) {
36+
const dialogs = useDialog;
37+
const dialog = dialogs[dialogKey];
38+
39+
if (!dialog || typeof dialog !== "function") {
40+
return null;
41+
}
42+
43+
// Access the Dialog component from the hook
44+
return dialog.Dialog;
45+
}

frontend/src/queries/global.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
11
import { MutationCache, QueryCache, QueryClient } from "@tanstack/react-query";
22
import { toast } from "@/components";
3+
import { modal } from "@/utils/modal-utils";
34

4-
const queryCache = new QueryCache();
5+
const queryCache = new QueryCache({
6+
onError(error, query) {
7+
if (
8+
query.meta?.mightRequireAuth &&
9+
"statusCode" in error &&
10+
error.statusCode === 403
11+
) {
12+
modal.open("ProvideEngineCredentials");
13+
return;
14+
}
15+
},
16+
});
517

618
const mutationCache = new MutationCache({
719
onError(error, variables, context, mutation) {

0 commit comments

Comments
 (0)