mirror of
https://github.com/outline/outline.git
synced 2026-06-14 03:45:00 +03:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c37bed2572 | |||
| df34cf2b76 | |||
| 820c5ea65e | |||
| ba247c5ba8 | |||
| 2504b051ec | |||
| 5ce17b799b |
@@ -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";
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
@@ -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);
|
||||
@@ -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";
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -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")
|
||||
)}{" "}
|
||||
·{" "}
|
||||
</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}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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 */
|
||||
|
||||
@@ -13,6 +13,7 @@ import "./import";
|
||||
import "./integration";
|
||||
import "./notification";
|
||||
import "./oauthClient";
|
||||
import "./oauthAuthentication";
|
||||
import "./pins";
|
||||
import "./reaction";
|
||||
import "./revision";
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user