0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

Merge pull request #1973 from logto-io/gao-log-264-machine-to-machine-tech-support

feat(core): machine to machine apps
This commit is contained in:
Gao Sun 2022-09-22 22:58:08 +08:00 committed by GitHub
commit 665b0f479b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 251 additions and 139 deletions

View file

@ -7,12 +7,6 @@ import SinglePageApp from '@/assets/images/single-page-app.svg';
import TraditionalWebAppDark from '@/assets/images/traditional-web-app-dark.svg';
import TraditionalWebApp from '@/assets/images/traditional-web-app.svg';
export const applicationTypeI18nKey = Object.freeze({
[ApplicationType.Native]: 'applications.type.native',
[ApplicationType.SPA]: 'applications.type.spa',
[ApplicationType.Traditional]: 'applications.type.traditional',
} as const);
type ApplicationIconMap = {
[key in ApplicationType]: SvgComponent;
};
@ -21,10 +15,12 @@ export const lightModeApplicationIconMap: ApplicationIconMap = Object.freeze({
[ApplicationType.Native]: NativeApp,
[ApplicationType.SPA]: SinglePageApp,
[ApplicationType.Traditional]: TraditionalWebApp,
[ApplicationType.MachineToMachine]: TraditionalWebApp,
} as const);
export const darkModeApplicationIconMap: ApplicationIconMap = Object.freeze({
[ApplicationType.Native]: NativeAppDark,
[ApplicationType.SPA]: SinglePageAppDark,
[ApplicationType.Traditional]: TraditionalWebAppDark,
[ApplicationType.MachineToMachine]: TraditionalWebAppDark,
} as const);

View file

@ -30,6 +30,17 @@ const AdvancedSettings = ({ oidcConfig, defaultData, isDeleted }: Props) => {
return (
<>
<FormField
title="application_details.authorization_endpoint"
className={styles.textField}
tooltip="application_details.authorization_endpoint_tip"
>
<CopyToClipboard
className={styles.textField}
value={oidcConfig.authorization_endpoint}
variant="border"
/>
</FormField>
<FormField title="application_details.token_endpoint">
<CopyToClipboard
className={styles.textField}

View file

@ -64,7 +64,12 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
placeholder={t('application_details.description_placeholder')}
/>
</FormField>
{applicationType === ApplicationType.Traditional && (
<FormField title="application_details.application_id" className={styles.textField}>
<CopyToClipboard className={styles.textField} value={defaultData.id} variant="border" />
</FormField>
{[ApplicationType.Traditional, ApplicationType.MachineToMachine].includes(
applicationType
) && (
<FormField title="application_details.application_secret" className={styles.textField}>
<CopyToClipboard
hasVisibilityToggle
@ -74,99 +79,94 @@ const Settings = ({ applicationType, oidcConfig, defaultData, isDeleted }: Props
/>
</FormField>
)}
<FormField
title="application_details.authorization_endpoint"
className={styles.textField}
tooltip="application_details.authorization_endpoint_tip"
>
<CopyToClipboard
{applicationType !== ApplicationType.MachineToMachine && (
<FormField
isRequired
title="application_details.redirect_uris"
className={styles.textField}
value={oidcConfig.authorization_endpoint}
variant="border"
/>
</FormField>
<FormField
isRequired
title="application_details.redirect_uris"
className={styles.textField}
tooltip="application_details.redirect_uri_tip"
>
<Controller
name="oidcClientMetadata.redirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
...uriPatternRules,
required: t('application_details.redirect_uri_required'),
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
title="application_details.redirect_uris"
value={value}
error={convertRhfErrorMessage(error?.message)}
placeholder={
applicationType === ApplicationType.Native
? t('application_details.redirect_uri_placeholder_native')
: t('application_details.redirect_uri_placeholder')
}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField
title="application_details.post_sign_out_redirect_uris"
className={styles.textField}
tooltip="application_details.post_sign_out_redirect_uri_tip"
>
<Controller
name="oidcClientMetadata.postLogoutRedirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf(uriPatternRules),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
title="application_details.post_sign_out_redirect_uris"
value={value}
error={convertRhfErrorMessage(error?.message)}
placeholder={t('application_details.post_sign_out_redirect_uri_placeholder')}
onChange={onChange}
/>
)}
/>
</FormField>
<FormField
title="application_details.cors_allowed_origins"
className={styles.textField}
tooltip="application_details.cors_allowed_origins_tip"
>
<Controller
name="customClientMetadata.corsAllowedOrigins"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
pattern: {
verify: (value) => !value || uriOriginValidator(value),
message: t('errors.invalid_origin_format'),
},
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
title="application_details.cors_allowed_origins"
value={value}
error={convertRhfErrorMessage(error?.message)}
placeholder={t('application_details.cors_allowed_origins_placeholder')}
onChange={onChange}
/>
)}
/>
</FormField>
tooltip="application_details.redirect_uri_tip"
>
<Controller
name="oidcClientMetadata.redirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
...uriPatternRules,
required: t('application_details.redirect_uri_required'),
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
title="application_details.redirect_uris"
value={value}
error={convertRhfErrorMessage(error?.message)}
placeholder={
applicationType === ApplicationType.Native
? t('application_details.redirect_uri_placeholder_native')
: t('application_details.redirect_uri_placeholder')
}
onChange={onChange}
/>
)}
/>
</FormField>
)}
{applicationType !== ApplicationType.MachineToMachine && (
<FormField
title="application_details.post_sign_out_redirect_uris"
className={styles.textField}
tooltip="application_details.post_sign_out_redirect_uri_tip"
>
<Controller
name="oidcClientMetadata.postLogoutRedirectUris"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf(uriPatternRules),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
title="application_details.post_sign_out_redirect_uris"
value={value}
error={convertRhfErrorMessage(error?.message)}
placeholder={t('application_details.post_sign_out_redirect_uri_placeholder')}
onChange={onChange}
/>
)}
/>
</FormField>
)}
{applicationType !== ApplicationType.MachineToMachine && (
<FormField
title="application_details.cors_allowed_origins"
className={styles.textField}
tooltip="application_details.cors_allowed_origins_tip"
>
<Controller
name="customClientMetadata.corsAllowedOrigins"
control={control}
defaultValue={[]}
rules={{
validate: createValidatorForRhf({
pattern: {
verify: (value) => !value || uriOriginValidator(value),
message: t('errors.invalid_origin_format'),
},
}),
}}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MultiTextInput
title="application_details.cors_allowed_origins"
value={value}
error={convertRhfErrorMessage(error?.message)}
placeholder={t('application_details.cors_allowed_origins_placeholder')}
onChange={onChange}
/>
)}
/>
</FormField>
)}
<UnsavedChangesAlertModal hasUnsavedChanges={!isDeleted && isDirty} />
</>
);

View file

@ -1,4 +1,4 @@
import { Application, SnakeCaseOidcConfig } from '@logto/schemas';
import { Application, ApplicationType, SnakeCaseOidcConfig } from '@logto/schemas';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
@ -144,13 +144,16 @@ const ApplicationDetails = () => {
</div>
</div>
<div className={styles.operations}>
<Button
title="application_details.check_guide"
size="large"
onClick={() => {
setIsReadmeOpen(true);
}}
/>
{/* TODO: @Charles figure out a better way to check guide availability */}
{data.type !== ApplicationType.MachineToMachine && (
<Button
title="application_details.check_guide"
size="large"
onClick={() => {
setIsReadmeOpen(true);
}}
/>
)}
<Drawer isOpen={isReadmeOpen} onClose={onCloseDrawer}>
<Guide isCompact app={data} onClose={onCloseDrawer} />
</Drawer>

View file

@ -1,8 +1,9 @@
import { Application } from '@logto/schemas';
import { MDXProvider } from '@mdx-js/react';
import { Optional } from '@silverhand/essentials';
import i18next from 'i18next';
import { MDXProps } from 'mdx/types';
import { cloneElement, lazy, LazyExoticComponent, Suspense, useState } from 'react';
import { cloneElement, lazy, LazyExoticComponent, Suspense, useEffect, useState } from 'react';
import CodeEditor from '@/components/CodeEditor';
import DetailsSummary from '@/mdx-components/DetailsSummary';
@ -47,9 +48,20 @@ const Guides: Record<string, LazyExoticComponent<(props: MDXProps) => JSX.Elemen
const Guide = ({ app, isCompact, onClose }: Props) => {
const { id: appId, secret: appSecret, name: appName, type: appType, oidcClientMetadata } = app;
const sdks = applicationTypeAndSdkTypeMappings[appType];
const [selectedSdk, setSelectedSdk] = useState<SupportedSdk>(sdks[0]);
const [selectedSdk, setSelectedSdk] = useState<Optional<SupportedSdk>>(sdks[0]);
const [activeStepIndex, setActiveStepIndex] = useState(-1);
// Directly close guide if no SDK available
useEffect(() => {
if (!selectedSdk) {
onClose();
}
}, [onClose, selectedSdk]);
if (!selectedSdk) {
return null;
}
const locale = i18next.language;
const guideI18nKey = `${selectedSdk}_${locale}`.toLowerCase();
const GuideComponent = Guides[guideI18nKey] ?? Guides[selectedSdk.toLowerCase()];

View file

@ -4,6 +4,7 @@ export const applicationTypeI18nKey = Object.freeze({
[ApplicationType.Native]: 'applications.type.native',
[ApplicationType.SPA]: 'applications.type.spa',
[ApplicationType.Traditional]: 'applications.type.traditional',
[ApplicationType.MachineToMachine]: 'applications.type.machine_to_machine',
} as const);
export enum SupportedSdk {
@ -21,4 +22,5 @@ export const applicationTypeAndSdkTypeMappings = Object.freeze({
[ApplicationType.Native]: [SupportedSdk.iOS, SupportedSdk.Android],
[ApplicationType.SPA]: [SupportedSdk.React, SupportedSdk.Vue, SupportedSdk.Vanilla],
[ApplicationType.Traditional]: [SupportedSdk.Next, SupportedSdk.Express, SupportedSdk.GoWeb],
[ApplicationType.MachineToMachine]: [],
} as const);

View file

@ -27,6 +27,7 @@ export const mockApplication: Application = {
idTokenTtl: 5000,
refreshTokenTtl: 6_000_000,
},
roleNames: [],
createdAt: 1_645_334_775_356,
};

View file

@ -12,7 +12,7 @@ import {
} from '@/queries/oidc-model-instance';
import postgresAdapter from './adapter';
import { getApplicationTypeString } from './utils';
import { getConstantClientMetadata } from './utils';
jest.mock('@/queries/application', () => ({
findApplicationById: jest.fn(async (): Promise<Application> => mockApplication),
@ -69,9 +69,7 @@ describe('postgres Adapter', () => {
client_id,
client_name,
client_secret,
application_type: getApplicationTypeString(type),
grant_types: ['authorization_code', 'refresh_token'],
token_endpoint_auth_method: 'none',
...getConstantClientMetadata(type),
...snakecaseKeys(oidcClientMetadata),
...customClientMetadata,
});

View file

@ -1,4 +1,4 @@
import { ApplicationType, CreateApplication, GrantType, OidcClientMetadata } from '@logto/schemas';
import { ApplicationType, CreateApplication, OidcClientMetadata } from '@logto/schemas';
import { adminConsoleApplicationId, demoAppApplicationId } from '@logto/schemas/lib/seeds';
import dayjs from 'dayjs';
import { AdapterFactory, AllClientMetadata } from 'oidc-provider';
@ -16,7 +16,7 @@ import {
} from '@/queries/oidc-model-instance';
import { appendPath } from '@/utils/url';
import { getApplicationTypeString } from './utils';
import { getConstantClientMetadata } from './utils';
const buildAdminConsoleClientMetadata = (): AllClientMetadata => {
const { localhostUrl, adminConsoleUrl } = envSet.values;
@ -25,11 +25,9 @@ const buildAdminConsoleClientMetadata = (): AllClientMetadata => {
];
return {
...getConstantClientMetadata(ApplicationType.SPA),
client_id: adminConsoleApplicationId,
client_name: 'Admin Console',
application_type: getApplicationTypeString(ApplicationType.SPA),
grant_types: Object.values(GrantType),
token_endpoint_auth_method: 'none',
redirect_uris: urls.map((url) => appendPath(url, '/callback').toString()),
post_logout_redirect_uris: urls,
};
@ -68,10 +66,7 @@ export default function postgresAdapter(modelName: string): ReturnType<AdapterFa
client_id,
client_secret,
client_name,
application_type: getApplicationTypeString(type),
grant_types: Object.values(GrantType),
token_endpoint_auth_method:
type === ApplicationType.Traditional ? 'client_secret_basic' : 'none',
...getConstantClientMetadata(type),
...snakecaseKeys(oidcClientMetadata),
...(client_id === demoAppApplicationId &&
snakecaseKeys(buildDemoAppUris(oidcClientMetadata))),

View file

@ -12,9 +12,11 @@ import snakecaseKeys from 'snakecase-keys';
import envSet from '@/env-set';
import postgresAdapter from '@/oidc/adapter';
import { isOriginAllowed, validateCustomClientMetadata } from '@/oidc/utils';
import { findApplicationById } from '@/queries/application';
import { findResourceByIndicator } from '@/queries/resource';
import { findUserById } from '@/queries/user';
import { routes } from '@/routes/consts';
import assertThat from '@/utils/assert-that';
import { addOidcEventListeners } from '@/utils/oidc-provider-event-listener';
import { claimToUserKey, getUserClaims } from './scope';
@ -48,6 +50,7 @@ export default async function initOidc(app: Koa): Promise<Provider> {
userinfo: { enabled: true },
revocation: { enabled: true },
devInteractions: { enabled: false },
clientCredentials: { enabled: true },
rpInitiatedLogout: {
logoutSource: (ctx, form) => {
// eslint-disable-next-line no-template-curly-in-string
@ -157,17 +160,21 @@ export default async function initOidc(app: Koa): Promise<Provider> {
Grant: 1_209_600 /* 14 days in seconds */,
},
extraTokenClaims: async (_ctx, token) => {
// AccessToken type is not exported by default, need to asset token is AccessToken
if (token.kind === 'AccessToken') {
const { accountId } = token;
const { roleNames } = await findUserById(accountId);
// Add User Roles to the AccessToken claims. Should be removed once we have RBAC implemented.
// User Roles should be hidden and determined by the AccessToken scope only.
return snakecaseKeys({
roleNames,
});
}
// `token.kind === 'ClientCredentials'`
const { clientId } = token;
assertThat(clientId, 'oidc.invalid_grant');
const { roleNames } = await findApplicationById(clientId);
return snakecaseKeys({ roleNames });
},
});

View file

@ -1,19 +1,37 @@
import { ApplicationType, CustomClientMetadataKey } from '@logto/schemas';
import { ApplicationType, CustomClientMetadataKey, GrantType } from '@logto/schemas';
import {
isOriginAllowed,
buildOidcClientMetadata,
getApplicationTypeString,
getConstantClientMetadata,
validateCustomClientMetadata,
} from './utils';
it('getApplicationTypeString', () => {
expect(getApplicationTypeString(ApplicationType.SPA)).toEqual('web');
expect(getApplicationTypeString(ApplicationType.Native)).toEqual('native');
expect(getApplicationTypeString(ApplicationType.Traditional)).toEqual('web');
describe('getConstantClientMetadata()', () => {
expect(getConstantClientMetadata(ApplicationType.SPA)).toEqual({
application_type: 'web',
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
token_endpoint_auth_method: 'none',
});
expect(getConstantClientMetadata(ApplicationType.Native)).toEqual({
application_type: 'native',
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
token_endpoint_auth_method: 'none',
});
expect(getConstantClientMetadata(ApplicationType.Traditional)).toEqual({
application_type: 'web',
grant_types: [GrantType.AuthorizationCode, GrantType.RefreshToken],
token_endpoint_auth_method: 'client_secret_basic',
});
expect(getConstantClientMetadata(ApplicationType.MachineToMachine)).toEqual({
application_type: 'web',
grant_types: [GrantType.ClientCredentials],
token_endpoint_auth_method: 'client_secret_basic',
response_types: [],
});
});
it('buildOidcClientMetadata', () => {
describe('buildOidcClientMetadata()', () => {
const metadata = {
redirectUris: ['logto.dev'],
postLogoutRedirectUris: ['logto.dev'],

View file

@ -2,12 +2,38 @@ import {
ApplicationType,
CustomClientMetadata,
customClientMetadataGuard,
GrantType,
OidcClientMetadata,
} from '@logto/schemas';
import { errors } from 'oidc-provider';
import { conditional } from '@silverhand/essentials';
import { AllClientMetadata, ClientAuthMethod, errors } from 'oidc-provider';
export const getApplicationTypeString = (type: ApplicationType) =>
type === ApplicationType.Native ? 'native' : 'web';
export const getConstantClientMetadata = (
type: ApplicationType
): Pick<
AllClientMetadata,
'application_type' | 'grant_types' | 'token_endpoint_auth_method' | 'response_types'
> => {
const getTokenEndpointAuthMethod = (): ClientAuthMethod => {
switch (type) {
case ApplicationType.Native:
case ApplicationType.SPA:
return 'none';
default:
return 'client_secret_basic';
}
};
return {
application_type: type === ApplicationType.Native ? 'native' : 'web',
grant_types:
type === ApplicationType.MachineToMachine
? [GrantType.ClientCredentials]
: [GrantType.AuthorizationCode, GrantType.RefreshToken],
token_endpoint_auth_method: getTokenEndpointAuthMethod(),
response_types: conditional(type === ApplicationType.MachineToMachine && []),
};
};
export const buildOidcClientMetadata = (metadata?: OidcClientMetadata): OidcClientMetadata => ({
redirectUris: [],

View file

@ -9,6 +9,7 @@ const application_details = {
authorization_endpoint: 'Authorization endpoint',
authorization_endpoint_tip:
"The endpoint to perform authentication and authorization. It's used for OpenID Connect Authentication.",
application_id: 'App ID',
application_secret: 'App Secret',
redirect_uri: 'Redirect URI',
redirect_uris: 'Redirect URIs',
@ -28,7 +29,7 @@ const application_details = {
add_another: 'Add Another',
id_token_expiration: 'ID Token expiration',
refresh_token_expiration: 'Refresh Token expiration',
token_endpoint: 'Token endpoint',
token_endpoint: 'Token Endpoint',
user_info_endpoint: 'Userinfo endpoint',
delete_description:
'This action cannot be undone. It will permanently delete the application. Please enter the application name <span>{{name}}</span> to confirm.',

View file

@ -28,6 +28,11 @@ const applications = {
subtitle: 'An app that renders and updates pages by the web server alone',
description: 'E.g., Next.js, PHP',
},
machine_to_machine: {
title: 'Machine to Machine',
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
},
guide: {
get_sample_file: 'Get Sample',

View file

@ -9,6 +9,7 @@ const application_details = {
authorization_endpoint: 'Authorization endpoint',
authorization_endpoint_tip:
"Le point de terminaison pour effectuer l'authentification et l'autorisation. Il est utilisé pour l'authentification OpenID Connect.",
application_id: 'App ID',
application_secret: 'App Secret',
redirect_uri: 'Redirect URI',
redirect_uris: 'Redirect URIs',
@ -28,7 +29,7 @@ const application_details = {
add_another: 'Ajouter un autre',
id_token_expiration: "Expiration du jeton d'identification",
refresh_token_expiration: "Rafraîchir l'expiration du jeton",
token_endpoint: 'Token endpoint',
token_endpoint: 'Token Endpoint',
user_info_endpoint: 'Userinfo endpoint',
delete_description:
"Cette action ne peut être annulée. Elle supprimera définitivement l'application. Veuillez entrer le nom de l'application <span>{{nom}}</span> pour confirmer.",

View file

@ -29,6 +29,12 @@ const applications = {
subtitle: 'Une application qui met à jour les pages par le seul serveur web.',
description: 'Exemple: Next.js, PHP',
},
// UNTRANSLATED
machine_to_machine: {
title: 'Machine to Machine',
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
},
guide: {
get_sample_file: 'Obtenir un exemple',

View file

@ -9,6 +9,7 @@ const application_details = {
authorization_endpoint: '인증 End-Point',
authorization_endpoint_tip:
'인증 및 권한 부여를 진행할 End-Point예요. OpenID Connect 인증에서 사용되던 값 이에요.',
application_id: 'App ID',
application_secret: 'App Secret',
redirect_uri: 'Redirect URI',
redirect_uris: 'Redirect URIs',

View file

@ -27,6 +27,12 @@ const applications = {
subtitle: '서버를 통하여 웹 페이지가 업데이트 되는 앱',
description: '예) JSP, PHP',
},
// UNTRANSLATED
machine_to_machine: {
title: 'Machine to Machine',
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
},
guide: {
get_sample_file: '예제 찾기',

View file

@ -9,6 +9,7 @@ const application_details = {
authorization_endpoint: 'Endpoint de autorização',
authorization_endpoint_tip:
'O endpoint para realizar autenticação e autorização. É usado para autenticação OpenID Connect.',
application_id: 'ID da aplicação',
application_secret: 'Segredo da aplicação',
redirect_uri: 'URI de redirecionamento',
redirect_uris: 'URIs de redirecionamento',

View file

@ -28,6 +28,12 @@ const applications = {
subtitle: 'Uma aplicação que renderiza e atualiza páginas apenas pelo servidor web',
description: 'Ex., Next.js, PHP',
},
// UNTRANSLATED
machine_to_machine: {
title: 'Machine to Machine',
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
},
guide: {
get_sample_file: 'Obter amostra',

View file

@ -9,6 +9,7 @@ const application_details = {
authorization_endpoint: 'Yetkilendirme bitiş noktası',
authorization_endpoint_tip:
'Kimlik doğrulama ve yetkilendirme gerçekleştirmek için bitiş noktası. OpenID Connect Authentication için kullanılır.',
application_id: 'Uygulama IDsi',
application_secret: 'Uygulama Sırrı',
redirect_uri: 'Yönlendirme URIı',
redirect_uris: 'Yönlendirme URIları',

View file

@ -29,6 +29,12 @@ const applications = {
subtitle: 'Sayfaları yalnızca web sunucusu tarafından işleyen ve güncelleyen bir uygulama',
description: 'Örneğin, JSP, PHP',
},
// UNTRANSLATED
machine_to_machine: {
title: 'Machine to Machine',
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
},
guide: {
get_sample_file: 'Örnek Gör',

View file

@ -8,6 +8,7 @@ const application_details = {
description_placeholder: '请输入应用描述',
authorization_endpoint: 'Authorization Endpoint',
authorization_endpoint_tip: '进行鉴权与授权的端点 endpoint。用于 OpenID Connect 中的鉴权流程。',
application_id: 'App ID',
application_secret: 'App Secret',
redirect_uri: 'Redirect URI',
redirect_uris: 'Redirect URIs',
@ -27,7 +28,7 @@ const application_details = {
add_another: '新增',
id_token_expiration: 'ID Token 过期时间',
refresh_token_expiration: 'Refresh Token 过期时间',
token_endpoint: 'Token endpoint',
token_endpoint: 'Token Endpoint',
user_info_endpoint: 'UserInfo endpoint',
delete_description: '本操作会永久性地删除该应用,且不可撤销。输入 <span>{{name}}</span> 确认。',
enter_your_application_name: '输入你的应用名称',

View file

@ -26,6 +26,12 @@ const applications = {
subtitle: '仅由 Web 服务器渲染和更新的应用程序',
description: '例如 Next.js, PHP',
},
// UNTRANSLATED
machine_to_machine: {
title: 'Machine to Machine',
subtitle: 'An app (usually a service) that directly talks to resources',
description: 'E.g., Backend service',
},
},
guide: {
get_sample_file: '获取示例',

View file

@ -11,4 +11,5 @@ export type OidcConfig = KeysToCamelCase<SnakeCaseOidcConfig>;
export enum GrantType {
AuthorizationCode = 'authorization_code',
RefreshToken = 'refresh_token',
ClientCredentials = 'client_credentials',
}

View file

@ -1,4 +1,4 @@
create type application_type as enum ('Native', 'SPA', 'Traditional');
create type application_type as enum ('Native', 'SPA', 'Traditional', 'MachineToMachine');
create table applications (
id varchar(21) not null,
@ -8,6 +8,7 @@ create table applications (
type application_type not null,
oidc_client_metadata jsonb /* @use OidcClientMetadata */ not null,
custom_client_metadata jsonb /* @use CustomClientMetadata */ not null default '{}'::jsonb,
role_names jsonb /* @use RoleNames */ not null default '[]'::jsonb,
created_at timestamptz not null default(now()),
primary key (id)
);