Compare commits

...

6 Commits

Author SHA1 Message Date
Tom Moor c37bed2572 fix 2025-04-26 15:37:29 -04:00
Tom Moor df34cf2b76 fix: oauthAuthentications endpoint should not consider revoked
fix: Allow revoking from list
Combined API keys and Apps screens
2025-04-26 12:11:16 -04:00
Tom Moor 820c5ea65e Merge branch 'oauth-server' into oauth-server-authorizations 2025-04-26 09:54:47 -04:00
Tom Moor ba247c5ba8 wip 2025-04-21 22:47:42 -04:00
Tom Moor 2504b051ec wip 2025-04-21 19:45:58 -04:00
Tom Moor 5ce17b799b api 2025-04-13 17:06:28 -04:00
26 changed files with 477 additions and 117 deletions
@@ -3,7 +3,7 @@ import * as React from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { OAuthClientValidation } from "@shared/validations";
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import ImageInput from "~/scenes/Settings/components/ImageInput";
import Button from "~/components/Button";
import Flex from "~/components/Flex";
+6 -6
View File
@@ -30,7 +30,7 @@ import usePolicy from "./usePolicy";
const ApiKeys = lazy(() => import("~/scenes/Settings/ApiKeys"));
const Applications = lazy(() => import("~/scenes/Settings/Applications"));
const PersonalApiKeys = lazy(() => import("~/scenes/Settings/PersonalApiKeys"));
const APIAndApps = lazy(() => import("~/scenes/Settings/APIAndApps"));
const Details = lazy(() => import("~/scenes/Settings/Details"));
const Export = lazy(() => import("~/scenes/Settings/Export"));
const Features = lazy(() => import("~/scenes/Settings/Features"));
@@ -88,12 +88,12 @@ const useSettingsConfig = () => {
icon: EmailIcon,
},
{
name: t("API Keys"),
path: settingsPath("personal-api-keys"),
component: PersonalApiKeys,
enabled: can.createApiKey && !can.listApiKeys,
name: t("API & Apps"),
path: settingsPath("api-and-apps"),
component: APIAndApps,
enabled: true,
group: t("Account"),
icon: CodeIcon,
icon: PadlockIcon,
},
// Workspace
{
+57
View File
@@ -0,0 +1,57 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
import ConfirmationDialog from "~/components/ConfirmationDialog";
import ContextMenu from "~/components/ContextMenu";
import MenuItem from "~/components/ContextMenu/MenuItem";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
import useStores from "~/hooks/useStores";
type Props = {
/** The OAuthAuthentication to associate with the menu */
oauthAuthentication: OAuthAuthentication;
};
function OAuthAuthenticationMenu({ oauthAuthentication }: Props) {
const menu = useMenuState({
modal: true,
});
const { dialogs } = useStores();
const { t } = useTranslation();
const handleRevoke = React.useCallback(() => {
dialogs.openModal({
title: t("Revoke {{ appName }}", {
appName: oauthAuthentication.oauthClient.name,
}),
content: (
<ConfirmationDialog
onSubmit={async () => {
await oauthAuthentication.deleteAll();
dialogs.closeAllModals();
}}
submitText={t("Revoke")}
savingText={`${t("Revoking")}`}
danger
>
{t("Are you sure you want to revoke access?")}
</ConfirmationDialog>
),
});
}, [t, dialogs, oauthAuthentication]);
return (
<>
<OverflowMenuButton aria-label={t("Show menu")} {...menu} />
<ContextMenu {...menu}>
<MenuItem {...menu} onClick={handleRevoke} dangerous>
{t("Revoke")}
</MenuItem>
</ContextMenu>
</>
);
}
export default observer(OAuthAuthenticationMenu);
+1 -1
View File
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { useMenuState } from "reakit/Menu";
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import OAuthClientDeleteDialog from "~/scenes/Settings/components/OAuthClientDeleteDialog";
import ContextMenu from "~/components/ContextMenu";
import OverflowMenuButton from "~/components/ContextMenu/OverflowMenuButton";
+39
View File
@@ -0,0 +1,39 @@
import { action, observable } from "mobx";
import { client } from "~/utils/ApiClient";
import User from "../User";
import ParanoidModel from "../base/ParanoidModel";
import Field from "../decorators/Field";
import Relation from "../decorators/Relation";
import OAuthClient from "./OAuthClient";
class OAuthAuthentication extends ParanoidModel {
static modelName = "OAuthAuthentication";
/** A list of scopes that this authentication has access to */
@Field
@observable
scope: string[];
@Relation(() => User)
user: User;
userId: string;
oauthClient: Pick<OAuthClient, "id" | "name" | "clientId">;
oauthClientId: string;
lastActiveAt: string;
@action
public async deleteAll() {
await client.post(`/${this.store.apiEndpoint}.delete`, {
oauthClientId: this.oauthClientId,
scope: this.scope,
});
return this.store.remove(this.id);
}
}
export default OAuthAuthentication;
@@ -3,10 +3,10 @@ import { observable, runInAction } from "mobx";
import queryString from "query-string";
import env from "~/env";
import { client } from "~/utils/ApiClient";
import User from "./User";
import ParanoidModel from "./base/ParanoidModel";
import Field from "./decorators/Field";
import Relation from "./decorators/Relation";
import User from "../User";
import ParanoidModel from "../base/ParanoidModel";
import Field from "../decorators/Field";
import Relation from "../decorators/Relation";
class OAuthClient extends ParanoidModel {
static modelName = "OAuthClient";
@@ -73,6 +73,10 @@ class OAuthClient extends ParanoidModel {
});
}
public get initial() {
return this.name[0];
}
public get authorizationUrl(): string {
const params = {
client_id: this.clientId,
+107
View File
@@ -0,0 +1,107 @@
import { observer } from "mobx-react";
import { PadlockIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import ApiKey from "~/models/ApiKey";
import OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import { createApiKey } from "~/actions/definitions/apiKeys";
import env from "~/env";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import ApiKeyListItem from "./components/ApiKeyListItem";
import OAuthAuthenticationListItem from "./components/OAuthAuthenticationListItem";
function APIAndApps() {
const team = useCurrentTeam();
const user = useCurrentUser();
const { t } = useTranslation();
const { apiKeys, oauthAuthentications } = useStores();
const can = usePolicy(team);
const context = useActionContext();
const appName = env.APP_NAME;
return (
<Scene
title={t("API & Apps")}
icon={<PadlockIcon />}
actions={
<>
{can.createApiKey && (
<Action>
<Button
type="submit"
value={`${t("New API key")}`}
action={createApiKey}
context={context}
/>
</Action>
)}
</>
}
>
<Heading>{t("API & Apps")}</Heading>
<h2>{t("API keys")}</h2>
{can.createApiKey ? (
<Text as="p" type="secondary">
<Trans
defaults="Create personal API keys to authenticate with the API and programatically control
your workspace's data. For more details see the <em>developer documentation</em>."
components={{
em: (
<a
href="https://www.getoutline.com/developers"
target="_blank"
rel="noreferrer"
/>
),
}}
/>
</Text>
) : (
<Trans>
{t("API keys have been disabled by an admin for your account")}
</Trans>
)}
<PaginatedList<ApiKey>
fetch={apiKeys.fetchPage}
items={apiKeys.personalApiKeys}
options={{ userId: user.id }}
renderItem={(apiKey) => (
<ApiKeyListItem key={apiKey.id} apiKey={apiKey} />
)}
/>
<PaginatedList
fetch={oauthAuthentications.fetchPage}
items={oauthAuthentications.orderedData}
heading={
<>
<h2>{t("Application access")}</h2>
<Text as="p" type="secondary">
{t(
"Manage which third-party and internal applications have been granted access to your {{ appName }} account.",
{ appName }
)}
</Text>
</>
}
renderItem={(oauthAuthentication: OAuthAuthentication) => (
<OAuthAuthenticationListItem
key={oauthAuthentication.id}
oauthAuthentication={oauthAuthentication}
/>
)}
/>
</Scene>
);
}
export default observer(APIAndApps);
+1 -1
View File
@@ -6,7 +6,7 @@ import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
import { toast } from "sonner";
import { OAuthClientValidation } from "@shared/validations";
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import Breadcrumb from "~/components/Breadcrumb";
import Button from "~/components/Button";
import ConfirmationDialog from "~/components/ConfirmationDialog";
+3 -3
View File
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import { InternetIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
@@ -57,10 +57,10 @@ function Applications() {
}}
/>
</Text>
<PaginatedList
<PaginatedList<OAuthClient>
fetch={oauthClients.fetchPage}
items={oauthClients.orderedData}
renderItem={(oauthClient: OAuthClient) => (
renderItem={(oauthClient) => (
<OAuthClientListItem key={oauthClient.id} oauthClient={oauthClient} />
)}
/>
-77
View File
@@ -1,77 +0,0 @@
import { observer } from "mobx-react";
import { CodeIcon } from "outline-icons";
import * as React from "react";
import { useTranslation, Trans } from "react-i18next";
import ApiKey from "~/models/ApiKey";
import { Action } from "~/components/Actions";
import Button from "~/components/Button";
import Heading from "~/components/Heading";
import PaginatedList from "~/components/PaginatedList";
import Scene from "~/components/Scene";
import Text from "~/components/Text";
import { createApiKey } from "~/actions/definitions/apiKeys";
import useActionContext from "~/hooks/useActionContext";
import useCurrentTeam from "~/hooks/useCurrentTeam";
import useCurrentUser from "~/hooks/useCurrentUser";
import usePolicy from "~/hooks/usePolicy";
import useStores from "~/hooks/useStores";
import ApiKeyListItem from "./components/ApiKeyListItem";
function PersonalApiKeys() {
const team = useCurrentTeam();
const user = useCurrentUser();
const { t } = useTranslation();
const { apiKeys } = useStores();
const can = usePolicy(team);
const context = useActionContext();
return (
<Scene
title={t("API")}
icon={<CodeIcon />}
actions={
<>
{can.createApiKey && (
<Action>
<Button
type="submit"
value={`${t("New API key")}`}
action={createApiKey}
context={context}
/>
</Action>
)}
</>
}
>
<Heading>{t("API")}</Heading>
<Text as="p" type="secondary">
<Trans
defaults="Create personal API keys to authenticate with the API and programatically control
your workspace's data. API keys have the same permissions as your user account.
For more details see the <em>developer documentation</em>."
components={{
em: (
<a
href="https://www.getoutline.com/developers"
target="_blank"
rel="noreferrer"
/>
),
}}
/>
</Text>
<PaginatedList<ApiKey>
fetch={apiKeys.fetchPage}
items={apiKeys.personalApiKeys}
options={{ userId: user.id }}
heading={<h2>{t("Personal keys")}</h2>}
renderItem={(apiKey) => (
<ApiKeyListItem key={apiKey.id} apiKey={apiKey} />
)}
/>
</Scene>
);
}
export default observer(PersonalApiKeys);
@@ -0,0 +1,61 @@
import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
import { OAuthScopeHelper } from "~/scenes/Login/OAuthScopeHelper";
import { Avatar, AvatarSize } from "~/components/Avatar";
import { AvatarVariant } from "~/components/Avatar/Avatar";
import ListItem from "~/components/List/Item";
import Text from "~/components/Text";
import Time from "~/components/Time";
import OAuthAuthenticationMenu from "~/menus/OAuthAuthenticationMenu";
type Props = {
/** The OAuthAuthentication to display */
oauthAuthentication: OAuthAuthentication;
};
const OAuthAuthenticationListItem = ({ oauthAuthentication }: Props) => {
const { t } = useTranslation();
const subtitle = (
<>
<Text type="tertiary">
{oauthAuthentication.lastActiveAt ? (
<>
{t("Last active")}{" "}
<Time dateTime={oauthAuthentication.lastActiveAt} addSuffix />
</>
) : (
t("Never used")
)}{" "}
&middot;{" "}
</Text>
<Text type="tertiary" ellipsis>
{OAuthScopeHelper.normalizeScopes(oauthAuthentication.scope, t).join(
", "
)}
</Text>
</>
);
return (
<ListItem
key={oauthAuthentication.id}
image={
<Avatar
model={oauthAuthentication.oauthClient}
size={AvatarSize.Large}
variant={AvatarVariant.Square}
/>
}
title={oauthAuthentication.oauthClient.name}
subtitle={subtitle}
actions={
<OAuthAuthenticationMenu oauthAuthentication={oauthAuthentication} />
}
/>
);
};
export default observer(OAuthAuthenticationListItem);
@@ -1,6 +1,6 @@
import * as React from "react";
import { useTranslation } from "react-i18next";
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import ConfirmationDialog from "~/components/ConfirmationDialog";
type Props = {
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import * as React from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import { Avatar, AvatarSize } from "~/components/Avatar";
import { AvatarVariant } from "~/components/Avatar/Avatar";
import ListItem from "~/components/List/Item";
@@ -31,18 +31,12 @@ const OAuthClientListItem = ({ oauthClient }: Props) => {
</>
);
const avatarModel = {
id: oauthClient.id,
initial: oauthClient.name[0],
avatarUrl: oauthClient.avatarUrl,
};
return (
<ListItem
key={oauthClient.id}
image={
<Avatar
model={avatarModel}
model={oauthClient}
size={AvatarSize.Large}
variant={AvatarVariant.Square}
/>
+11
View File
@@ -0,0 +1,11 @@
import OAuthAuthentication from "~/models/oauth/OAuthAuthentication";
import RootStore from "./RootStore";
import Store from "./base/Store";
export default class OAuthAuthenticationsStore extends Store<OAuthAuthentication> {
apiEndpoint = "oauthAuthentications";
constructor(rootStore: RootStore) {
super(rootStore, OAuthAuthentication);
}
}
+3 -1
View File
@@ -1,8 +1,10 @@
import OAuthClient from "~/models/OAuthClient";
import OAuthClient from "~/models/oauth/OAuthClient";
import RootStore from "./RootStore";
import Store from "./base/Store";
export default class OAuthClientsStore extends Store<OAuthClient> {
apiEndpoint = "oauthClients";
constructor(rootStore: RootStore) {
super(rootStore, OAuthClient);
}
+20 -2
View File
@@ -18,6 +18,7 @@ import ImportsStore from "./ImportsStore";
import IntegrationsStore from "./IntegrationsStore";
import MembershipsStore from "./MembershipsStore";
import NotificationsStore from "./NotificationsStore";
import OAuthAuthenticationsStore from "./OAuthAuthenticationsStore";
import OAuthClientsStore from "./OAuthClientsStore";
import PinsStore from "./PinsStore";
import PoliciesStore from "./PoliciesStore";
@@ -50,6 +51,7 @@ export default class RootStore {
integrations: IntegrationsStore;
memberships: MembershipsStore;
notifications: NotificationsStore;
oauthAuthentications: OAuthAuthenticationsStore;
oauthClients: OAuthClientsStore;
presence: DocumentPresenceStore;
pins: PinsStore;
@@ -82,6 +84,7 @@ export default class RootStore {
this.registerStore(IntegrationsStore);
this.registerStore(MembershipsStore);
this.registerStore(NotificationsStore);
this.registerStore(OAuthAuthenticationsStore, "oauthAuthentications");
this.registerStore(OAuthClientsStore, "oauthClients");
this.registerStore(PinsStore);
this.registerStore(PoliciesStore);
@@ -113,8 +116,9 @@ export default class RootStore {
*/
public getStoreForModelName<K extends keyof RootStore>(modelName: string) {
const storeName = this.getStoreNameForModelName(modelName);
invariant(storeName, `No store found for model name "${modelName}"`);
const store = this[storeName];
invariant(store, `No store found for model name "${modelName}"`);
return store as RootStore[K];
}
@@ -142,10 +146,24 @@ export default class RootStore {
// @ts-expect-error TS thinks we are instantiating an abstract class.
const store = new StoreClass(this);
const storeName = name ?? this.getStoreNameForModelName(store.modelName);
invariant(storeName, `No store found for model name "${store.modelName}"`);
this[storeName] = store;
}
private getStoreNameForModelName(modelName: string) {
return pluralize(lowerFirst(modelName)) as keyof RootStore;
for (const key of Object.keys(this)) {
const store = this[key as keyof RootStore];
if ("modelName" in store && store.modelName === modelName) {
return key as keyof RootStore;
}
}
const storeName = pluralize(lowerFirst(modelName)) as keyof RootStore;
if (storeName) {
return storeName;
}
return undefined;
}
}
@@ -12,7 +12,6 @@ import {
BelongsTo,
ForeignKey,
Table,
BeforeCreate,
IsDate,
Unique,
} from "sequelize-typescript";
@@ -115,13 +114,6 @@ class OAuthAuthentication extends ParanoidModel<
return this.save({ silent: true });
};
// hooks
@BeforeCreate
static async setLastActiveAt(model: OAuthAuthentication) {
model.lastActiveAt = new Date();
}
// instance methods
/** Checks if the authentication has access to the given path */
+1
View File
@@ -13,6 +13,7 @@ import "./import";
import "./integration";
import "./notification";
import "./oauthClient";
import "./oauthAuthentication";
import "./pins";
import "./reaction";
import "./revision";
+14
View File
@@ -0,0 +1,14 @@
import { Team, User, OAuthAuthentication } from "@server/models";
import { allow } from "./cancan";
import { isTeamModel } from "./utils";
allow(User, "listOAuthAuthentications", Team, (actor, team) =>
isTeamModel(actor, team)
);
allow(
User,
["read", "delete"],
OAuthAuthentication,
(actor, oauthAuthentication) => actor?.id === oauthAuthentication?.userId
);
+16
View File
@@ -0,0 +1,16 @@
import { OAuthAuthentication } from "@server/models";
import { presentPublishedOAuthClient } from "./oauthClient";
export default function presentOAuthAuthentication(
oauthAuthentication: OAuthAuthentication
) {
return {
id: oauthAuthentication.id,
userId: oauthAuthentication.userId,
oauthClientId: oauthAuthentication.oauthClientId,
oauthClient: presentPublishedOAuthClient(oauthAuthentication.oauthClient),
scope: oauthAuthentication.scope,
lastActiveAt: oauthAuthentication.lastActiveAt,
createdAt: oauthAuthentication.createdAt,
};
}
+1
View File
@@ -31,6 +31,7 @@ export default function presentOAuthClient(oauthClient: OAuthClient) {
*/
export function presentPublishedOAuthClient(oauthClient: OAuthClient) {
return {
id: oauthClient.id,
name: oauthClient.name,
description: oauthClient.description,
developerName: oauthClient.developerName,
+2
View File
@@ -28,6 +28,7 @@ import apiErrorHandler from "./middlewares/apiErrorHandler";
import apiResponse from "./middlewares/apiResponse";
import editor from "./middlewares/editor";
import notifications from "./notifications";
import oauthAuthentications from "./oauthAuthentications";
import oauthClients from "./oauthClients";
import pins from "./pins";
import reactions from "./reactions";
@@ -91,6 +92,7 @@ router.use("/", suggestions.routes());
router.use("/", teams.routes());
router.use("/", integrations.routes());
router.use("/", notifications.routes());
router.use("/", oauthAuthentications.routes());
router.use("/", oauthClients.routes());
router.use("/", attachments.routes());
router.use("/", cron.routes());
@@ -0,0 +1 @@
export { default } from "./oauthAuthentications";
@@ -0,0 +1,89 @@
import Router from "koa-router";
import { QueryTypes } from "sequelize";
import auth from "@server/middlewares/authentication";
import { rateLimiter } from "@server/middlewares/rateLimiter";
import { transaction } from "@server/middlewares/transaction";
import validate from "@server/middlewares/validate";
import { OAuthAuthentication } from "@server/models";
import { authorize } from "@server/policies";
import { presentPolicies } from "@server/presenters";
import presentOAuthAuthentication from "@server/presenters/oauthAuthentication";
import { sequelize } from "@server/storage/database";
import { APIContext } from "@server/types";
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
import pagination from "../middlewares/pagination";
import * as T from "./schema";
const router = new Router();
router.post(
"oauthAuthentications.list",
auth(),
pagination(),
validate(T.OAuthAuthenticationsListSchema),
async (ctx: APIContext<T.OAuthAuthenticationsListReq>) => {
const { user } = ctx.state.auth;
const oauthAuthentications = await sequelize.query<OAuthAuthentication>(
`
SELECT DISTINCT ON (oa."oauthClientId", oa."scope")
oa.*,
oc.id AS "oauthClient.id",
oc.name AS "oauthClient.name",
oc."clientId" AS "oauthClient.clientId"
FROM oauth_authentications oa
INNER JOIN oauth_clients oc ON oc.id = oa."oauthClientId"
WHERE oa."userId" = :userId
AND oa."deletedAt" IS NULL
ORDER BY oa."oauthClientId", oa."scope", oa."lastActiveAt", oa."createdAt" DESC
LIMIT :limit OFFSET :offset
`,
{
replacements: {
userId: user.id,
limit: ctx.state.pagination.limit,
offset: ctx.state.pagination.offset,
},
type: QueryTypes.SELECT,
nest: true,
}
);
ctx.body = {
pagination: { ...ctx.state.pagination },
data: oauthAuthentications.map(presentOAuthAuthentication),
policies: presentPolicies(user, oauthAuthentications),
};
}
);
router.post(
"oauthAuthentications.delete",
rateLimiter(RateLimiterStrategy.TwentyFivePerMinute),
auth(),
validate(T.OAuthAuthenticationsDeleteSchema),
transaction(),
async (ctx: APIContext<T.OAuthAuthenticationsDeleteReq>) => {
const { user } = ctx.state.auth;
const { oauthClientId, scope } = ctx.request.body;
const oauthAuthentications = await OAuthAuthentication.findAll({
where: {
userId: user.id,
oauthClientId,
scope,
},
transaction: ctx.state.transaction,
});
for (const oauthAuthentication of oauthAuthentications) {
authorize(user, "delete", oauthAuthentication);
await oauthAuthentication.destroyWithCtx(ctx);
}
ctx.body = {
success: true,
};
}
);
export default router;
@@ -0,0 +1,21 @@
import { z } from "zod";
import { BaseSchema } from "@server/routes/api/schema";
export const OAuthAuthenticationsListSchema = BaseSchema.extend({
body: z.object({}),
});
export type OAuthAuthenticationsListReq = z.infer<
typeof OAuthAuthenticationsListSchema
>;
export const OAuthAuthenticationsDeleteSchema = BaseSchema.extend({
body: z.object({
oauthClientId: z.string(),
scope: z.array(z.string()).optional(),
}),
});
export type OAuthAuthenticationsDeleteReq = z.infer<
typeof OAuthAuthenticationsDeleteSchema
>;
+11 -4
View File
@@ -520,12 +520,13 @@
"Unsubscribed from document": "Unsubscribed from document",
"Unsubscribed from collection": "Unsubscribed from collection",
"Account": "Account",
"API Keys": "API Keys",
"API & Apps": "API & Apps",
"Details": "Details",
"Security": "Security",
"Features": "Features",
"Members": "Members",
"Groups": "Groups",
"API Keys": "API Keys",
"Applications": "Applications",
"Shared Links": "Shared Links",
"Import": "Import",
@@ -560,6 +561,9 @@
"New child document": "New child document",
"Save in workspace": "Save in workspace",
"Notification settings": "Notification settings",
"Revoke {{ appName }}": "Revoke {{ appName }}",
"Revoking": "Revoking",
"Are you sure you want to revoke access?": "Are you sure you want to revoke access?",
"Delete app": "Delete app",
"Revision options": "Revision options",
"Share link revoked": "Share link revoked",
@@ -874,6 +878,11 @@
"Something went wrong": "Something went wrong",
"Please try again or contact support if the problem persists": "Please try again or contact support if the problem persists",
"No documents found for your search filters.": "No documents found for your search filters.",
"Create personal API keys to authenticate with the API and programatically control\n your workspace's data. For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. For more details see the <em>developer documentation</em>.",
"{t(\"API keys have been disabled by an admin for your account\")}": "{t(\"API keys have been disabled by an admin for your account\")}",
"API keys have been disabled by an admin for your account": "API keys have been disabled by an admin for your account",
"Application access": "Application access",
"Manage which third-party and internal applications have been granted access to your {{ appName }} account.": "Manage which third-party and internal applications have been granted access to your {{ appName }} account.",
"API": "API",
"API keys can be used to authenticate with the API and programatically control\n your workspace's data. For more details see the <em>developer documentation</em>.": "API keys can be used to authenticate with the API and programatically control\n your workspace's data. For more details see the <em>developer documentation</em>.",
"Client secret rotated": "Client secret rotated",
@@ -899,7 +908,6 @@
"Restricted scope": "Restricted scope",
"API key copied to clipboard": "API key copied to clipboard",
"Copied": "Copied",
"Revoking": "Revoking",
"Are you sure you want to revoke the {{ tokenName }} token?": "Are you sure you want to revoke the {{ tokenName }} token?",
"Disconnect integration": "Disconnect integration",
"Connected": "Connected",
@@ -963,6 +971,7 @@
"Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload": "Drag and drop the zip file from the Markdown export option in {{appName}}, or click to upload",
"Last active": "Last active",
"Guest": "Guest",
"Never used": "Never used",
"Are you sure you want to delete the {{ appName }} application? This cannot be undone.": "Are you sure you want to delete the {{ appName }} application? This cannot be undone.",
"Shared by": "Shared by",
"Date shared": "Date shared",
@@ -1048,8 +1057,6 @@
"Unsubscription successful. Your notification settings were updated": "Unsubscription successful. Your notification settings were updated",
"Manage when and where you receive email notifications.": "Manage when and where you receive email notifications.",
"The email integration is currently disabled. Please set the associated environment variables and restart the server to enable notifications.": "The email integration is currently disabled. Please set the associated environment variables and restart the server to enable notifications.",
"Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.": "Create personal API keys to authenticate with the API and programatically control\n your workspace's data. API keys have the same permissions as your user account.\n For more details see the <em>developer documentation</em>.",
"Personal keys": "Personal keys",
"Preferences saved": "Preferences saved",
"Delete account": "Delete account",
"Manage settings that affect your personal experience.": "Manage settings that affect your personal experience.",