Skip to content

Commit

Permalink
feat: allow app secret edit
Browse files Browse the repository at this point in the history
  • Loading branch information
gao-sun committed Jul 27, 2024
1 parent c1d9d71 commit e53d727
Show file tree
Hide file tree
Showing 9 changed files with 200 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function CreateSecretModal({ appId, isOpen, onClose }: Props) {
})
.json<ApplicationSecret>();
toast.success(
t('organization_template.roles.create_modal.created', { name: createdData.name })
t('application_details.secrets.create_modal.created', { name: createdData.name })
);
onCloseHandler(createdData);
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { type ApplicationSecret } from '@logto/schemas';
import { useCallback } from 'react';
import { useForm } from 'react-hook-form';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import ReactModal from 'react-modal';

import Button from '@/ds-components/Button';
import FormField from '@/ds-components/FormField';
import ModalLayout from '@/ds-components/ModalLayout';
import TextInput from '@/ds-components/TextInput';
import useApi from '@/hooks/use-api';
import modalStyles from '@/scss/modal.module.scss';
import { trySubmitSafe } from '@/utils/form';

import { type ApplicationSecretRow } from './EndpointsAndCredentials/use-secret-table-columns';

type FormData = { name: string; expiration: string };

type Props = {
readonly appId: string;
readonly secret: ApplicationSecretRow;
readonly isOpen: boolean;
readonly onClose: (updated: boolean) => void;
};

function EditSecretModal({ appId, secret, isOpen, onClose }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const {
register,
formState: { errors, isSubmitting },
handleSubmit,
reset,
} = useForm<FormData>({ defaultValues: { name: secret.name } });
const onCloseHandler = useCallback(
(updated?: boolean) => {
reset();
onClose(updated ?? false);
},
[onClose, reset]
);
const api = useApi();

const submit = handleSubmit(
trySubmitSafe(async (data) => {
const createdData = await api
.patch(`api/applications/${appId}/secrets/${encodeURIComponent(secret.name)}`, {
json: data,
})
.json<ApplicationSecret>();
toast.success(t('application_details.secrets.edit_modal.edited', { name: createdData.name }));
onCloseHandler(true);
})
);

return (
<ReactModal
isOpen={isOpen}
className={modalStyles.content}
overlayClassName={modalStyles.overlay}
onRequestClose={() => {
onCloseHandler();
}}
>
<ModalLayout
title="application_details.secrets.edit_modal.title"
footer={
<Button type="primary" title="general.save" isLoading={isSubmitting} onClick={submit} />
}
onClose={onCloseHandler}
>
<FormField isRequired title="general.name">
<TextInput
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
placeholder="My secret"
error={Boolean(errors.name)}
{...register('name', { required: true })}
/>
</FormField>
</ModalLayout>
</ReactModal>
);
}

export default EditSecretModal;
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { type RequestError } from '@/hooks/use-api';
import useCustomDomain from '@/hooks/use-custom-domain';

import CreateSecretModal from '../CreateSecretModal';
import EditSecretModal from '../EditSecretModal';

import styles from './index.module.scss';
import { type ApplicationSecretRow, useSecretTableColumns } from './use-secret-table-columns';
Expand All @@ -51,6 +52,7 @@ function EndpointsAndCredentials({
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { data: customDomain, applyDomain: applyCustomDomain } = useCustomDomain();
const [showCreateSecretModal, setShowCreateSecretModal] = useState(false);
const [editSecret, setEditSecret] = useState<ApplicationSecretRow>();
const secrets = useSWR<ApplicationSecretRow[], RequestError>(`api/applications/${id}/secrets`);
const shouldShowAppSecrets = hasSecrets(type);

Expand Down Expand Up @@ -87,9 +89,13 @@ function EndpointsAndCredentials({
},
[onApplicationUpdated, secrets]
);
const onEditSecret = useCallback((secret: ApplicationSecretRow) => {
setEditSecret(secret);
}, []);
const tableColumns = useSecretTableColumns({
appId: id,
onUpdated,
onEdit: onEditSecret,
});
return (
<FormCard
Expand Down Expand Up @@ -242,11 +248,26 @@ function EndpointsAndCredentials({
<CreateSecretModal
appId={id}
isOpen={showCreateSecretModal}
onClose={() => {
onClose={(created) => {
if (created) {
void secrets.mutate();
}
setShowCreateSecretModal(false);
void secrets.mutate();
}}
/>
{editSecret && (
<EditSecretModal
isOpen
appId={id}
secret={editSecret}
onClose={(updated) => {
if (updated) {
void secrets.mutate();
}
setEditSecret(undefined);
}}
/>
)}
</FormField>
)}
</FormCard>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export type ApplicationSecretRow = Pick<ApplicationSecret, 'name' | 'value' | 'e
isLegacy?: boolean;
};

const isExpired = (expiresAt: Date | number) => compareDesc(expiresAt, new Date()) === 1;

function Expired({ expiresAt }: { readonly expiresAt: Date }) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
return (
Expand All @@ -30,10 +32,11 @@ function Expired({ expiresAt }: { readonly expiresAt: Date }) {

type UseSecretTableColumns = {
appId: string;
onEdit: (secret: ApplicationSecretRow) => void;
onUpdated: (isLegacy: boolean) => void;
};

export const useSecretTableColumns = ({ appId, onUpdated }: UseSecretTableColumns) => {
export const useSecretTableColumns = ({ appId, onUpdated, onEdit }: UseSecretTableColumns) => {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const api = useApi();
const tableColumns: Array<Column<ApplicationSecretRow>> = useMemo(
Expand Down Expand Up @@ -65,7 +68,7 @@ export const useSecretTableColumns = ({ appId, onUpdated }: UseSecretTableColumn
render: ({ expiresAt }) => (
<span>
{expiresAt ? (
compareDesc(expiresAt, new Date()) === 1 ? (
isExpired(expiresAt) ? (
<Expired expiresAt={new Date(expiresAt)} />
) : (
new Date(expiresAt).toLocaleString()
Expand All @@ -79,21 +82,31 @@ export const useSecretTableColumns = ({ appId, onUpdated }: UseSecretTableColumn
{
title: '',
dataIndex: 'actions',
render: ({ name, isLegacy }) => (
<ActionsButton
fieldName="application_details.application_secret"
deleteConfirmation="application_details.secrets.delete_confirmation"
onDelete={async () => {
await (isLegacy
? api.delete(`api/applications/${appId}/legacy-secret`)
: api.delete(`api/applications/${appId}/secrets/${encodeURIComponent(name)}`));
onUpdated(isLegacy ?? false);
}}
/>
),
render: (secret) => {
const { expiresAt, isLegacy, name } = secret;
return (
<ActionsButton
fieldName="application_details.application_secret"
deleteConfirmation="application_details.secrets.delete_confirmation"
onEdit={
(isLegacy ?? false) || (expiresAt && isExpired(expiresAt))
? undefined
: () => {
onEdit(secret);
}
}
onDelete={async () => {
await (isLegacy
? api.delete(`api/applications/${appId}/legacy-secret`)
: api.delete(`api/applications/${appId}/secrets/${encodeURIComponent(name)}`));
onUpdated(isLegacy ?? false);
}}
/>
);
},
},
],
[api, appId, onUpdated, t]
[api, appId, onEdit, onUpdated, t]
);

return tableColumns;
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/middleware/koa-security-headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
const coreOrigins = urlSet.origins;
const developmentOrigins = isProduction
? []
: ['ws:', ...['6001', '6002', '6003'].map((port) => `ws://localhost:${port}`)];
: ['ws:', ...['6001', '6002', '6003'].flatMap((port) => [`ws://localhost:${port}`, `http://localhost:${port}`])];
const logtoOrigin = 'https://*.logto.io';
/** Google Sign-In (GSI) origin for Google One Tap. */
const gsiOrigin = 'https://accounts.google.com/gsi/';
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/queries/application-secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { buildInsertIntoWithPool } from '#src/database/insert-into.js';
import { DeletionError } from '#src/errors/SlonikError/index.js';
import { convertToIdentifiers } from '#src/utils/sql.js';

import { buildUpdateWhereWithPool } from '../database/update-where.js';

type ApplicationCredentials = ApplicationSecret & {
/** The original application secret that stored in the `applications` table. */
originalSecret: string;
Expand All @@ -17,6 +19,8 @@ export class ApplicationSecretQueries {
returning: true,
});

public readonly update = buildUpdateWhereWithPool(this.pool)(ApplicationSecrets, true);

constructor(public readonly pool: CommonQueryMethods) {}

async findByCredentials(appId: string, appSecret: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@
"description": "The secret was deleted successfully."
}
}
},
"patch": {
"summary": "Update application secret",
"description": "Update a secret for the application by name.",
"parameters": [
{
"name": "name",
"in": "path",
"description": "The name of the secret."
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"name": {
"description": "The secret name to update. Must be unique within the application."
}
}
}
}
}
},
"responses": {
"204": {
"description": "The secret was updated successfully."
}
}
}
}
}
Expand Down
23 changes: 23 additions & 0 deletions packages/core/src/routes/applications/application-secret.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,27 @@ export default function applicationSecretRoutes<T extends ManagementApiRouter>(
return next();
}
);

router.patch(
'/applications/:id/secrets/:name',
koaGuard({
params: z.object({ id: z.string(), name: z.string() }),
body: ApplicationSecrets.updateGuard.pick({ name: true }).required(),
response: ApplicationSecrets.guard,
status: [200, 400, 404],
}),
async (ctx, next) => {
const {
params: { id: appId, name },
body,
} = ctx.guard;

ctx.body = await queries.applicationSecrets.update({
where: { applicationId: appId, name },
set: body,
jsonbMode: 'replace',
});
return next();
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ const application_details = {
'The secret will never expire. We recommend setting an expiration date for better security.',
days: '{{count}} day',
days_other: '{{count}} days',
created: 'The secret {{name}} has been successfully created.',
},
edit_modal: {
title: 'Edit application secret',
edited: 'The secret {{name}} has been successfully edited.',
},
},
};
Expand Down

0 comments on commit e53d727

Please sign in to comment.