Skip to content

Commit

Permalink
Add /edit-config on portal
Browse files Browse the repository at this point in the history
ref DEV-1419
  • Loading branch information
louischan-oursky committed Aug 20, 2024
2 parents 93cde71 + 4972316 commit d62eb3b
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 10 deletions.
4 changes: 1 addition & 3 deletions pkg/portal/graphql/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,7 @@ var nodeApp = node(
if argPaths, ok := p.Args["paths"]; ok {
for _, path := range argPaths.([]interface{}) {
path := path.(string)
if path == configsource.AuthgearYAML {
return nil, errors.New("direct access on authgear.yaml is disallowed")
}
// Note we do not block direct access to authgear.yaml
if path == configsource.AuthgearSecretYAML {
return nil, errors.New("direct access on authgear.secrets.yaml is disallowed")
}
Expand Down
3 changes: 0 additions & 3 deletions pkg/portal/graphql/app_mutation.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,6 @@ var _ = registerMutationField(
for _, f := range updates {
f := f.(map[string]interface{})
path := f["path"].(string)
if path == configsource.AuthgearYAML {
return nil, errors.New("direct update on authgear.yaml is disallowed")
}
if path == configsource.AuthgearSecretYAML {
return nil, errors.New("direct update on authgear.secrets.yaml is disallowed")
}
Expand Down
13 changes: 13 additions & 0 deletions portal/src/AppRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ScreenLayout from "./ScreenLayout";
import ShowLoading from "./ShowLoading";
import CookieLifetimeConfigurationScreen from "./graphql/portal/CookieLifetimeConfigurationScreen";
import { useUnauthenticatedDialogContext } from "./components/auth/UnauthenticatedDialogContext";
import EditConfigurationScreen from "./graphql/portal/EditConfigurationScreen";

const RolesScreen = lazy(async () => import("./graphql/adminapi/RolesScreen"));
const AddRoleScreen = lazy(
Expand Down Expand Up @@ -790,6 +791,18 @@ const AppRoot: React.VFC = function AppRoot() {
}
/>
</Route>

{/* This screen is not shown in nav bar, which is intentional to prevent normal users from accessing it */}
<Route path="edit-config">
<Route
index={true}
element={
<Suspense fallback={<ShowLoading />}>
<EditConfigurationScreen />
</Suspense>
}
/>
</Route>
</Routes>
</ScreenLayout>
</ApolloProvider>
Expand Down
7 changes: 7 additions & 0 deletions portal/src/graphql/portal/EditConfigurationScreen.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.widget {
@apply col-span-8 tablet:col-span-full;
}

.codeEditor {
height: 70vh; /* 680 / 960 ref https://www.figma.com/design/msiE4O5imHONAG5EjhZeiZ/Authgear-UI?node-id=10346-12112&t=ubpO59nWLEfmakyL-0 */
}
138 changes: 138 additions & 0 deletions portal/src/graphql/portal/EditConfigurationScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React, { useCallback, useMemo } from "react";
import ScreenContent from "../../ScreenContent";
import ScreenTitle from "../../ScreenTitle";
import { FormattedMessage } from "@oursky/react-messageformat";
import EditTemplatesWidget, {
EditTemplatesWidgetSection,
} from "./EditTemplatesWidget";

import styles from "./EditConfigurationScreen.module.css";
import { useParams } from "react-router-dom";
import ShowLoading from "../../ShowLoading";
import ShowError from "../../ShowError";
import FormContainer from "../../FormContainer";
import {
ResourcesFormState,
useResourceForm,
} from "../../hook/useResourceForm";
import {
expandSpecifier,
Resource,
ResourceSpecifier,
specifierId,
} from "../../util/resource";
import { RESOURCE_AUTHGEAR_YAML } from "../../resources";
import { useAppAndSecretConfigQuery } from "./query/appAndSecretConfigQuery";

interface FormModel {
isLoading: boolean;
isUpdating: boolean;
isDirty: boolean;
loadError: unknown;
updateError: unknown;
state: FormState;
setState: (fn: (state: FormState) => FormState) => void;
reload: () => void;
reset: () => void;
save: () => Promise<void>;
}

interface FormState extends ResourcesFormState {}
const AUTHGEAR_YAML_RESOURCE_SPECIFIER: ResourceSpecifier = {
def: RESOURCE_AUTHGEAR_YAML,
locale: null,
extension: null,
};

const EditConfigurationScreen: React.VFC = function EditConfigurationScreen() {
const { appID } = useParams() as { appID: string };
const specifiers = [AUTHGEAR_YAML_RESOURCE_SPECIFIER];
const resourceForm = useResourceForm(appID, specifiers);
const { refetch } = useAppAndSecretConfigQuery(appID);

const form: FormModel = useMemo(
() => ({
...resourceForm,
save: async (...args: Parameters<(typeof resourceForm)["save"]>) => {
await resourceForm.save(...args);
await refetch();
},
}),
[refetch, resourceForm]
);

const rawAuthgearYAML = useMemo(() => {
const resource =
form.state.resources[specifierId(AUTHGEAR_YAML_RESOURCE_SPECIFIER)];
if (resource == null) {
return null;
}
if (resource.nullableValue == null) {
return null;
}
return resource.nullableValue;
}, [form.state.resources]);

const onChange = useCallback(
(value: string | undefined, _e: unknown) => {
const resource: Resource = {
specifier: AUTHGEAR_YAML_RESOURCE_SPECIFIER,
path: expandSpecifier(AUTHGEAR_YAML_RESOURCE_SPECIFIER),
nullableValue: value,
effectiveData: value,
};
const updatedResources = {
[specifierId(AUTHGEAR_YAML_RESOURCE_SPECIFIER)]: resource,
};
form.setState(() => {
return {
resources: updatedResources,
};
});
},
[form]
);

if (form.isLoading) {
return <ShowLoading />;
}

if (form.loadError) {
return <ShowError error={form.loadError} onRetry={form.reload} />;
}

const authgearYAMLSections: [EditTemplatesWidgetSection] = [
{
key: "authgear.yaml",
title: null,
items: [
{
key: "authgear.yaml",
title: null,
editor: "code",
language: "yaml",

value: rawAuthgearYAML ?? "",
onChange,
},
],
},
];

return (
<FormContainer form={form}>
<ScreenContent>
<ScreenTitle className={styles.widget}>
<FormattedMessage id="EditConfigurationScreen.title" />
</ScreenTitle>
<EditTemplatesWidget
className={styles.widget}
codeEditorClassname={styles.codeEditor}
sections={authgearYAMLSections}
/>
</ScreenContent>
</FormContainer>
);
};

export default EditConfigurationScreen;
11 changes: 7 additions & 4 deletions portal/src/graphql/portal/EditTemplatesWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const TextFieldWidgetItem: React.VFC<TextFieldWidgetIteProps> =
export interface EditTemplatesWidgetItem {
key: string;
title: React.ReactNode;
language: "html" | "plaintext" | "json" | "css";
language: "html" | "plaintext" | "json" | "css" | "yaml";
editor: "code" | "textfield";
value: string;
onChange: (value: string | undefined, e: unknown) => void;
Expand All @@ -55,27 +55,30 @@ export interface EditTemplatesWidgetSection {

export interface EditTemplatesWidgetProps {
className?: string;
codeEditorClassname?: string;
sections: EditTemplatesWidgetSection[];
}

const EditTemplatesWidget: React.VFC<EditTemplatesWidgetProps> =
function EditTemplatesWidget(props: EditTemplatesWidgetProps) {
const { className, sections } = props;
const { className, codeEditorClassname, sections } = props;

return (
<div className={cn(styles.form, className)}>
{sections.map((section) => {
return (
<Fragment key={section.key}>
<Label className={styles.boldLabel}>{section.title}</Label>
{section.title != null ? (
<Label className={styles.boldLabel}>{section.title}</Label>
) : null}
{section.items.map((item) => {
return item.editor === "code" ? (
<Fragment key={item.key}>
<Text className={styles.label} block={true}>
{item.title}
</Text>
<CodeEditor
className={styles.codeEditor}
className={cn(styles.codeEditor, codeEditorClassname)}
language={item.language}
value={item.value}
onChange={item.onChange}
Expand Down
2 changes: 2 additions & 0 deletions portal/src/locale-data/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1922,6 +1922,8 @@
"LoginMethodConfigurationScreen.passkey.title": "Enable passkey support for compatible devices. {DocLink, react, href{https://docs.authgear.com/strategies/passkeys} children{Learn more about passkey}}",
"LoginMethodConfigurationScreen.combineLoginSignup.title": "Automatically signup a new user if a login ID is not found during login",

"EditConfigurationScreen.title": "Edit Project Configurations",

"LoginIDKeyType.email": "Email address",
"LoginIDKeyType.phone": "Phone number",
"LoginIDKeyType.username": "Username",
Expand Down
7 changes: 7 additions & 0 deletions portal/src/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,13 @@ export const RESOURCE_USERNAME_EXCLUDED_KEYWORDS_TXT: ResourceDefinition = {
optional: true,
};

export const RESOURCE_AUTHGEAR_YAML: ResourceDefinition = {
resourcePath: resourcePath`authgear.yaml`,
type: "text",
extensions: [],
optional: false,
};

export const TRANSLATION_JSON_KEY_EMAIL_FORGOT_PASSWORD_LINK_SUBJECT =
"email.forgot-password.subject";
export const TRANSLATION_JSON_KEY_EMAIL_FORGOT_PASSWORD_CODE_SUBJECT =
Expand Down

0 comments on commit d62eb3b

Please sign in to comment.