feat(api-keys): the expiration field support custom option (#2186)

* feat(api-keys): the expiration field support custom option

* feat: support never option

* feat: support if expiresIn = never the expiresIn will replace to 1000y

* fix: toggle datepicker

* feat: update syntax

* fix: option order

* docs: update

* fix: maskCloseable should be false

* refactor: performance and remove unused code

* feat: should not allow clear

* fix: decode maybe fail

* fix: i18n
This commit is contained in:
Dunqing 2023-07-06 16:33:28 +08:00 committed by GitHub
parent dc91d44ce6
commit 817646d68d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 128 additions and 22 deletions

View File

@ -32,7 +32,11 @@ export class JwtService {
} }
sign(payload: SignPayload, options?: SignOptions) { sign(payload: SignPayload, options?: SignOptions) {
return jwt.sign(payload, this.secret(), { expiresIn: this.expiresIn(), ...options }); const opt = { expiresIn: this.expiresIn(), ...options };
if (opt.expiresIn === 'never') {
opt.expiresIn = '1000y';
}
return jwt.sign(payload, this.secret(), opt);
} }
decode(token: string): Promise<any> { decode(token: string): Promise<any> {
@ -54,11 +58,14 @@ export class JwtService {
if (!this.blacklist) { if (!this.blacklist) {
return null; return null;
} }
try {
const { exp } = await this.decode(token); const { exp } = await this.decode(token);
return this.blacklist.add({ return this.blacklist.add({
token, token,
expiration: new Date(exp * 1000).toString(), expiration: new Date(exp * 1000).toString(),
}); });
} catch {
return null;
}
} }
} }

View File

@ -16,4 +16,4 @@ curl '{domain}/api/roles:check' -H 'Authorization: Bearer {API key}'
## Deleting an API key ## Deleting an API key
Currently, deleting an API key does not make it invalid. Please keep your API key safe. After deleting the API key, it will no longer be usable.

View File

@ -16,4 +16,4 @@ curl '{domain}/api/roles:check' -H 'Authorization: Bearer {API key}'
## 删除 API key ## 删除 API key
目前删除 API key 并不能使 Key 失效,请注意保管好你的 API key 删除 API key 后,该 Key 将无法继续使用

View File

@ -0,0 +1,80 @@
import { css } from '@emotion/css';
import { connect, mapProps, mapReadPretty } from '@formily/react';
import { useRecord } from '@nocobase/client';
import { useBoolean } from 'ahooks';
import { DatePicker, Select, Space, Typography } from 'antd';
import moment from 'moment';
import React, { useMemo } from 'react';
import { useTranslation } from '../locale';
const TOMORROW = moment().add(1, 'days');
const spaceCSS = css`
width: 100%;
& > .ant-space-item {
flex: 1;
}
`;
const InternalExpiresSelect = (props) => {
const { onChange } = props;
const [isCustom, { toggle: toggleShowDatePicker, setFalse }] = useBoolean();
const onSelectChange = (v) => {
if (v === 'custom') {
onChange('1d');
return toggleShowDatePicker();
} else {
setFalse();
onChange(v);
}
};
const onDatePickerChange = (v: moment.Moment) => {
v = v.milliseconds(0).second(0);
const NOW = moment().milliseconds(0).seconds(0);
const value = `${v.diff(NOW, 'd')}d`;
onChange(value);
};
return (
<Space className={spaceCSS}>
<Select {...props} value={isCustom ? 'custom' : props.value} onChange={onSelectChange}></Select>
{isCustom ? (
<DatePicker
disabledDate={(time) => {
return time.isSameOrBefore();
}}
defaultValue={TOMORROW}
onChange={onDatePickerChange}
showToday={false}
allowClear={false}
/>
) : null}
</Space>
);
};
const ReadPretty = () => {
const { expiresIn, createdAt } = useRecord();
const { t } = useTranslation();
const expiresDate = useMemo(() => {
if (expiresIn === 'never') return t('Never expires');
return moment(createdAt)
.add(expiresIn?.replace('d', '') || 0, 'days')
.format('YYYY-MM-DD HH:mm:ss');
}, [createdAt, expiresIn]);
return <Typography.Text>{expiresDate}</Typography.Text>;
};
const ExpiresSelect = connect(
InternalExpiresSelect,
mapProps({
dataSource: 'options',
}),
mapReadPretty(ReadPretty),
);
export { ExpiresSelect };

View File

@ -2,6 +2,7 @@ import { RecursionField } from '@formily/react';
import { CollectionManagerProvider, SchemaComponentOptions, useCurrentRoles } from '@nocobase/client'; import { CollectionManagerProvider, SchemaComponentOptions, useCurrentRoles } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { apiKeysCollection } from '../../collections'; import { apiKeysCollection } from '../../collections';
import { ExpiresSelect } from './ExpiresSelect';
import { configurationSchema } from './schema'; import { configurationSchema } from './schema';
export const Configuration = () => { export const Configuration = () => {
@ -9,7 +10,7 @@ export const Configuration = () => {
return ( return (
<CollectionManagerProvider collections={[apiKeysCollection]}> <CollectionManagerProvider collections={[apiKeysCollection]}>
<SchemaComponentOptions scope={{ currentRoles }}> <SchemaComponentOptions scope={{ currentRoles }} components={{ ExpiresSelect }}>
<RecursionField schema={configurationSchema} /> <RecursionField schema={configurationSchema} />
</SchemaComponentOptions> </SchemaComponentOptions>
</CollectionManagerProvider> </CollectionManagerProvider>

View File

@ -3,7 +3,8 @@ import { uid } from '@formily/shared';
import { useActionContext, useBlockRequestContext, useRecord } from '@nocobase/client'; import { useActionContext, useBlockRequestContext, useRecord } from '@nocobase/client';
import { Alert, Modal, Space, Typography } from 'antd'; import { Alert, Modal, Space, Typography } from 'antd';
import React from 'react'; import React from 'react';
import { generateNTemplate, useTranslation } from '../locale'; import { generateNTemplate } from '../../locale';
import { useTranslation } from '../locale';
const { useModal } = Modal; const { useModal } = Modal;
const useCreateAction = () => { const useCreateAction = () => {
@ -99,6 +100,7 @@ export const configurationSchema: ISchema = {
'x-decorator': 'Form', 'x-decorator': 'Form',
'x-component': 'Action.Modal', 'x-component': 'Action.Modal',
'x-component-props': { 'x-component-props': {
maskClosable: false,
style: { style: {
maxWidth: '520px', maxWidth: '520px',
width: '100%', width: '100%',

View File

@ -6,10 +6,6 @@ export function lang(key: string) {
return i18n.t(key, { ns: NAMESPACE }); return i18n.t(key, { ns: NAMESPACE });
} }
export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
}
export function useTranslation() { export function useTranslation() {
return useT([NAMESPACE, 'client'], { return useT([NAMESPACE, 'client'], {
nsMode: 'fallback', nsMode: 'fallback',

View File

@ -9,6 +9,13 @@ const locale = {
'Keys manager': '密钥管理', 'Keys manager': '密钥管理',
'Created at': '创建时间', 'Created at': '创建时间',
'Add API key': '添加 API key', 'Add API key': '添加 API key',
Never: '永不',
Custom: '自定义',
'Never expires': '永不过期',
'1 Day': '1 天',
'7 Days': '7 天',
'30 Days': '30 天',
'90 Days': '90 天',
}; };
export default locale; export default locale;

View File

@ -1,4 +1,5 @@
import type { CollectionOptions } from '@nocobase/database'; import type { CollectionOptions } from '@nocobase/database';
import { generateNTemplate } from '../locale';
export default { export default {
namespace: 'api-keys', namespace: 'api-keys',
@ -53,28 +54,35 @@ export default {
{ {
name: 'expiresIn', name: 'expiresIn',
type: 'string', type: 'string',
interface: 'select',
uiSchema: { uiSchema: {
type: 'string', type: 'string',
title: '{{t("Expires")}}', title: generateNTemplate('Expires'),
'x-component': 'Select', 'x-component': 'ExpiresSelect',
enum: [ enum: [
{ {
label: '{{t("1 day")}}', label: generateNTemplate('1 Day'),
value: '1d', value: '1d',
}, },
{ {
label: '{{t("7 days")}}', label: generateNTemplate('7 Days'),
value: '7d', value: '7d',
}, },
{ {
label: '{{t("30 days")}}', label: generateNTemplate('30 Days'),
value: '30d', value: '30d',
}, },
{ {
label: '{{t("90 days")}}', label: generateNTemplate('90 Days'),
value: '90d', value: '90d',
}, },
{
label: generateNTemplate('Custom'),
value: 'custom',
},
{
label: generateNTemplate('Never'),
value: 'never',
},
], ],
}, },
}, },

View File

@ -0,0 +1,5 @@
import { NAMESPACE } from './constants';
export function generateNTemplate(key: string) {
return `{{t('${key}', { ns: '${NAMESPACE}', nsMode: 'fallback' })}}`;
}