Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose API key on profile page. #2391

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 79 additions & 1 deletion imports/client/components/OwnProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@ import FormControl from "react-bootstrap/FormControl";
import FormGroup from "react-bootstrap/FormGroup";
import FormLabel from "react-bootstrap/FormLabel";
import FormText from "react-bootstrap/FormText";
import InputGroup from "react-bootstrap/InputGroup";
import CopyToClipboard from "react-copy-to-clipboard";
import Flags from "../../Flags";
import { formatDiscordName } from "../../lib/discord";
import linkUserDiscordAccount from "../../methods/linkUserDiscordAccount";
import linkUserGoogleAccount from "../../methods/linkUserGoogleAccount";
import rollAPIKey from "../../methods/rollAPIKey";
import unlinkUserDiscordAccount from "../../methods/unlinkUserDiscordAccount";
import unlinkUserGoogleAccount from "../../methods/unlinkUserGoogleAccount";
import updateProfile from "../../methods/updateProfile";
Expand Down Expand Up @@ -292,7 +295,13 @@ enum OwnProfilePageSubmitState {
ERROR = "error",
}

const OwnProfilePage = ({ initialUser }: { initialUser: Meteor.User }) => {
const OwnProfilePage = ({
initialUser,
initialAPIKey,
}: {
initialUser: Meteor.User;
initialAPIKey?: string;
}) => {
const [displayName, setDisplayName] = useState<string>(
initialUser.displayName ?? "",
);
Expand All @@ -306,6 +315,9 @@ const OwnProfilePage = ({ initialUser }: { initialUser: Meteor.User }) => {
OwnProfilePageSubmitState.IDLE,
);
const [submitError, setSubmitError] = useState<string>("");
const [showAPIKey, setShowAPIKey] = useState<boolean>(false);
const [regeneratingAPIKey, setRegeneratingAPIKey] = useState<boolean>(false);
const [APIKeyError, setAPIKeyError] = useState<string>();

const handleDisplayNameFieldChange: NonNullable<
FormControlProps["onChange"]
Expand Down Expand Up @@ -358,6 +370,27 @@ const OwnProfilePage = ({ initialUser }: { initialUser: Meteor.User }) => {
setSubmitState(OwnProfilePageSubmitState.IDLE);
}, []);

const toggleShowAPIKey = useCallback(() => {
setShowAPIKey(!showAPIKey);
}, [showAPIKey]);

const regenerateAPIKey = useCallback(() => {
setRegeneratingAPIKey(true);
setAPIKeyError("");
rollAPIKey.call({}, (error) => {
if (error) {
setAPIKeyError(error.message);
} else {
setAPIKeyError("");
}
setRegeneratingAPIKey(false);
});
}, []);

const dismissAPIKeyAlert = useCallback(() => {
setAPIKeyError("");
}, []);

const shouldDisableForm = submitState === "submitting";
return (
<Container>
Expand Down Expand Up @@ -452,6 +485,51 @@ const OwnProfilePage = ({ initialUser }: { initialUser: Meteor.User }) => {
</ActionButtonRow>

<AudioConfig />

<section className="advanced-section mt-3">
<h2>Advanced</h2>
<FormGroup className="mb-3">
<FormLabel htmlFor="jr-profile-api-key">API key</FormLabel>
<InputGroup>
<FormControl
id="jr-profile-api-key"
type={showAPIKey ? "text" : "password"}
readOnly
disabled
value={initialAPIKey}
/>
<CopyToClipboard text={initialAPIKey ?? ""}>
<Button variant="outline-secondary" disabled={regeneratingAPIKey}>
Copy
</Button>
</CopyToClipboard>
<Button
variant="outline-secondary"
onClick={toggleShowAPIKey}
disabled={regeneratingAPIKey}
>
{showAPIKey ? "Hide" : "Show"}
</Button>
<Button
variant="outline-secondary"
onClick={regenerateAPIKey}
disabled={regeneratingAPIKey}
>
{regeneratingAPIKey || (initialAPIKey?.length ?? 0) > 0
? "Regenerate"
: "Generate"}
</Button>
</InputGroup>
<FormText>
Authorization credential used to make API calls. Keep this secret!
</FormText>
{APIKeyError ? (
<Alert variant="danger" dismissible onClose={dismissAPIKeyAlert}>
Key generation failed: {APIKeyError}
</Alert>
) : null}
</FormGroup>
</section>
</Container>
);
};
Expand Down
14 changes: 12 additions & 2 deletions imports/client/components/ProfilePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { Meteor } from "meteor/meteor";
import { useSubscribe, useTracker } from "meteor/react-meteor-data";
import React from "react";
import { Navigate, useParams } from "react-router-dom";
import APIKeys from "../../lib/models/APIKeys";
import MeteorUsers from "../../lib/models/MeteorUsers";
import apiKeysForSelf from "../../lib/publications/apiKeysForSelf";
import { useBreadcrumb } from "../hooks/breadcrumb";
import useTypedSubscribe from "../hooks/useTypedSubscribe";
import OthersProfilePage from "./OthersProfilePage";
import OwnProfilePage from "./OwnProfilePage";

Expand All @@ -17,7 +20,14 @@ const ResolvedProfilePage = ({
const huntId = useParams<"huntId">().huntId;

const profileLoading = useSubscribe("profile", userId);
const loading = profileLoading();

const apiKeyLoading = useTypedSubscribe(apiKeysForSelf);

const apiKey = useTracker(() => {
return isSelf ? APIKeys.findOne()?.key : undefined;
}, [isSelf]);

const loading = profileLoading() || apiKeyLoading();

const user = useTracker(() => {
return loading ? undefined : MeteorUsers.findOne(userId);
Expand All @@ -33,7 +43,7 @@ const ResolvedProfilePage = ({
} else if (!user) {
return <div>{`No user ${userId} found.`}</div>;
} else if (isSelf) {
return <OwnProfilePage initialUser={user} />;
return <OwnProfilePage initialUser={user} initialAPIKey={apiKey} />;
}

return <OthersProfilePage user={user} />;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { z } from "zod";
import type { ModelType } from "../../lib/models/Model";
import SoftDeletedModel from "../../lib/models/SoftDeletedModel";
import { foreignKey } from "../../lib/models/customTypes";
import withCommon from "../../lib/models/withCommon";
import type { ModelType } from "./Model";
import SoftDeletedModel from "./SoftDeletedModel";
import { foreignKey } from "./customTypes";
import withCommon from "./withCommon";

const APIKey = withCommon(
z.object({
Expand Down
3 changes: 3 additions & 0 deletions imports/lib/publications/apiKeysForSelf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import TypedPublication from "./TypedPublication";

export default new TypedPublication<void>("APIKeys.publications.forSelf");
2 changes: 1 addition & 1 deletion imports/server/api/authenticator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type express from "express";
import APIKeys from "../../lib/models/APIKeys";
import expressAsyncWrapper from "../expressAsyncWrapper";
import APIKeys from "../models/APIKeys";

const authenticator: express.Handler = expressAsyncWrapper(
async (req, res, next) => {
Expand Down
2 changes: 1 addition & 1 deletion imports/server/ensureAPIKey.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Random } from "meteor/random";
import Logger from "../Logger";
import APIKeys from "./models/APIKeys";
import APIKeys from "../lib/models/APIKeys";
import userForKeyOperation from "./userForKeyOperation";
import withLock from "./withLock";

Expand Down
2 changes: 1 addition & 1 deletion imports/server/methods/rollAPIKey.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { check, Match } from "meteor/check";
import Logger from "../../Logger";
import APIKeys from "../../lib/models/APIKeys";
import rollAPIKey from "../../methods/rollAPIKey";
import ensureAPIKey from "../ensureAPIKey";
import APIKeys from "../models/APIKeys";
import userForKeyOperation from "../userForKeyOperation";
import defineMethod from "./defineMethod";

Expand Down
2 changes: 1 addition & 1 deletion imports/server/migrations/51-backfill-updated-at.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { z } from "zod";
import APIKeys from "../../lib/models/APIKeys";
import Announcements from "../../lib/models/Announcements";
import ChatMessages from "../../lib/models/ChatMessages";
import ChatNotifications from "../../lib/models/ChatNotifications";
Expand Down Expand Up @@ -31,7 +32,6 @@ import Routers from "../../lib/models/mediasoup/Routers";
import TransportRequests from "../../lib/models/mediasoup/TransportRequests";
import TransportStates from "../../lib/models/mediasoup/TransportStates";
import Transports from "../../lib/models/mediasoup/Transports";
import APIKeys from "../models/APIKeys";
import LatestDeploymentTimestamps from "../models/LatestDeploymentTimestamps";
import Subscribers from "../models/Subscribers";
import UploadTokens from "../models/UploadTokens";
Expand Down
13 changes: 13 additions & 0 deletions imports/server/publications/apiKeysForSelf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import APIKeys from "../../lib/models/APIKeys";
import apiKeysForSelf from "../../lib/publications/apiKeysForSelf";
import definePublication from "./definePublication";

definePublication(apiKeysForSelf, {
run() {
if (!this.userId) {
return [];
}

return APIKeys.find({ user: this.userId });
},
});
1 change: 1 addition & 0 deletions imports/server/publications/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./announcementsForAnnouncementsPage";
import "./apiKeysForSelf";
import "./blobMappingsAll";
import "./bookmarkNotificationsForSelf";
import "./chatMessagesForFirehose";
Expand Down
Loading