Skip to content

Commit

Permalink
Apikeys (#313)
Browse files Browse the repository at this point in the history
* Apikeys

* CodeQL fixes

---------

Co-authored-by: Rajat Saxena <[email protected]>
  • Loading branch information
rajat1saxena and Rajat Saxena authored Feb 24, 2024
1 parent 35699c2 commit ec038f0
Show file tree
Hide file tree
Showing 21 changed files with 568 additions and 30 deletions.
151 changes: 151 additions & 0 deletions apps/web/components/admin/settings/apikey/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import { Address, AppMessage } from "@courselit/common-models";
import {
Breadcrumbs,
Button,
Form,
FormField,
IconButton,
} from "@courselit/components-library";
import { AppDispatch, AppState } from "@courselit/state-management";
import { FetchBuilder } from "@courselit/utils";
import {
APIKEY_NEW_BTN_CAPTION,
APIKEY_NEW_GENERATED_KEY_COPIED,
APIKEY_NEW_GENERATED_KEY_DESC,
APIKEY_NEW_GENERATED_KEY_HEADER,
APIKEY_NEW_HEADER,
APIKEY_NEW_LABEL,
BUTTON_CANCEL_TEXT,
BUTTON_DONE_TEXT,
} from "@ui-config/strings";
import Link from "next/link";
import { FormEvent, useState } from "react";
import { connect } from "react-redux";
import {
networkAction,
setAppMessage,
} from "@courselit/state-management/dist/action-creators";
import { Clipboard } from "@courselit/icons";

interface NewApikeyProps {
address: Address;
dispatch: AppDispatch;
networkAction: boolean;
}

function NewApikey({
address,
dispatch,
networkAction: loading,
}: NewApikeyProps) {
const [name, setName] = useState("");
const [apikey, setApikey] = useState("");

const copyApikey = (e: FormEvent) => {
e.preventDefault();

if (window.isSecureContext && navigator.clipboard) {
navigator.clipboard.writeText(apikey);
dispatch(
setAppMessage(new AppMessage(APIKEY_NEW_GENERATED_KEY_COPIED)),
);
}
};

const createApikey = async (e: FormEvent) => {
e.preventDefault();

const query = `
mutation {
apikey: addApikey(
name: "${name}"
) {
keyId,
key,
name
}
}
`;
const fetch = new FetchBuilder()
.setUrl(`${address.backend}/api/graph`)
.setPayload(query)
.setIsGraphQLEndpoint(true)
.build();
try {
dispatch(networkAction(true));
const response = await fetch.exec();
if (response.apikey) {
setApikey(response.apikey.key);
}
} catch (err: any) {
dispatch(setAppMessage(new AppMessage(err.message)));
} finally {
dispatch(networkAction(false));
}
};

return (
<div className="flex flex-col gap-4">
<Breadcrumbs aria-label="new-apikey-breadcrumbs">
<Link href="/dashboard/settings">Apikeys</Link>
</Breadcrumbs>
<h1 className="text-4xl font-semibold mb-4">{APIKEY_NEW_HEADER}</h1>
<Form
method="post"
onSubmit={createApikey}
className="flex flex-col gap-4"
>
<FormField
required
label={APIKEY_NEW_LABEL}
name="name"
value={name}
onChange={(e) => setName(e.target.value)}
disabled={!!apikey}
/>
{apikey && (
<div className="flex flex-col gap-2">
<h2 className="text-lg font-medium">
{APIKEY_NEW_GENERATED_KEY_HEADER}
</h2>
<p className="text-slate-500">
{APIKEY_NEW_GENERATED_KEY_DESC}
</p>
<div className="flex gap-2 mb-4">
<FormField name="apikey" value={apikey} disabled />
<IconButton
className="px-3"
onClick={copyApikey}
variant="soft"
>
<Clipboard fontSize="small" />
</IconButton>
</div>
<Link href={`/dashboard/settings`}>
<Button>{BUTTON_DONE_TEXT}</Button>
</Link>
</div>
)}
{!apikey && (
<div className="flex gap-2">
<Button disabled={!name || loading} sx={{ mr: 1 }}>
{APIKEY_NEW_BTN_CAPTION}
</Button>
<Link href={`/dashboard/products`}>
<Button variant="soft">{BUTTON_CANCEL_TEXT}</Button>
</Link>
</div>
)}
</Form>
</div>
);
}

const mapStateToProps = (state: AppState) => ({
address: state.address,
networkAction: state.networkAction,
});

const mapDispatchToProps = (dispatch: AppDispatch) => ({ dispatch });

export default connect(mapStateToProps, mapDispatchToProps)(NewApikey);
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
PAYMENT_METHOD_STRIPE,
PAYMENT_METHOD_NONE,
MIMETYPE_IMAGE,
} from "../../ui-config/constants";
} from "../../../ui-config/constants";
import {
SITE_SETTINGS_TITLE,
SITE_SETTINGS_SUBTITLE,
Expand All @@ -29,14 +29,22 @@ import {
SITE_SETTINGS_PAYMENT_METHOD_NONE_LABEL,
SITE_CUSTOMISATIONS_SETTING_CODEINJECTION_BODY,
BTN_EDIT_SITE,
} from "../../ui-config/strings";
SITE_APIKEYS_SETTING_HEADER,
APIKEY_NEW_BUTTON,
APIKEY_EXISTING_HEADER,
APIKEY_EXISTING_TABLE_HEADER_CREATED,
APIKEY_EXISTING_TABLE_HEADER_NAME,
APIKEY_REMOVE_BTN,
APIKEY_REMOVE_DIALOG_HEADER,
APIKYE_REMOVE_DIALOG_DESC,
} from "../../../ui-config/strings";
import { FetchBuilder, capitalize } from "@courselit/utils";
import { decode, encode } from "base-64";
import { AppMessage, Profile } from "@courselit/common-models";
import type { SiteInfo, Address, Auth } from "@courselit/common-models";
import type { AppDispatch, AppState } from "@courselit/state-management";
import { actionCreators } from "@courselit/state-management";
import currencies from "../../data/iso4217.json";
import currencies from "../../../data/iso4217.json";
import {
Select as SingleSelect,
MediaSelector,
Expand All @@ -45,6 +53,11 @@ import {
FormField,
Button,
Link,
Table,
TableHead,
TableBody,
TableRow,
Dialog2,
} from "@courselit/components-library";

const { networkAction, newSiteInfoAvailable, setAppMessage } = actionCreators;
Expand All @@ -56,11 +69,14 @@ interface SettingsProps {
dispatch: (...args: any[]) => void;
address: Address;
networkAction: boolean;
loading: boolean;
}

const Settings = (props: SettingsProps) => {
const [settings, setSettings] = useState<Partial<SiteInfo>>({});
const [newSettings, setNewSettings] = useState<Partial<SiteInfo>>({});
const [apikeyPage, setApikeyPage] = useState(1);
const [apikeys, setApikeys] = useState([]);

const fetch = new FetchBuilder()
.setUrl(`${props.address.backend}/api/graph`)
Expand Down Expand Up @@ -112,6 +128,11 @@ const Settings = (props: SettingsProps) => {
codeInjectionHead,
codeInjectionBody
}
},
apikeys: getApikeys {
name,
keyId,
createdAt
}
}`;
try {
Expand All @@ -120,6 +141,9 @@ const Settings = (props: SettingsProps) => {
if (response.settings.settings) {
setSettingsState(response.settings.settings);
}
if (response.apikeys) {
setApikeys(response.apikeys);
}
} catch (e) {}
};

Expand Down Expand Up @@ -364,6 +388,28 @@ const Settings = (props: SettingsProps) => {
: settings.paytmSecret,
});

const removeApikey = async (keyId: string) => {
const query = `
mutation {
removed: removeApikey(keyId: "${keyId}")
}
`;
try {
const fetchRequest = fetch.setPayload(query).build();
props.dispatch(networkAction(true));
await fetchRequest.exec();
setApikeys(
apikeys.filter(
(item: Record<string, unknown>) => item.keyId !== keyId,
),
);
} catch (e: any) {
props.dispatch(setAppMessage(new AppMessage(e.message)));
} finally {
props.dispatch(networkAction(false));
}
};

return (
<div>
<div className="flex justify-between items-baseline">
Expand All @@ -384,6 +430,7 @@ const Settings = (props: SettingsProps) => {
SITE_SETTINGS_SECTION_GENERAL,
SITE_SETTINGS_SECTION_PAYMENT,
SITE_CUSTOMISATIONS_SETTING_HEADER,
SITE_APIKEYS_SETTING_HEADER,
]}
>
<Form
Expand Down Expand Up @@ -606,6 +653,72 @@ const Settings = (props: SettingsProps) => {
</Button>
</div>
</Form>
<div className="flex flex-col gap-4 pt-4">
<div className="flex justify-between">
<h2 className="text-lg font-semibold">
{APIKEY_EXISTING_HEADER}
</h2>
<Link href="/dashboard/settings/apikeys/new">
<Button>{APIKEY_NEW_BUTTON}</Button>
</Link>
</div>
<Table aria-label="API keys" className="mb-4">
<TableHead className="border-0 border-b border-slate-200">
<td>{APIKEY_EXISTING_TABLE_HEADER_NAME}</td>
<td>{APIKEY_EXISTING_TABLE_HEADER_CREATED}</td>
<td align="right"> </td>
</TableHead>
<TableBody
loading={props.loading}
endReached={true}
page={apikeyPage}
onPageChange={(value: number) => {
setApikeyPage(value);
}}
>
{apikeys.map(
(
item: Record<string, unknown>,
index: number,
) => (
<TableRow key={item.name as string}>
<td className="py-4">{item.name}</td>
<td>
{new Date(
item.createdAt as number,
).toLocaleDateString()}
</td>
<td align="right">
<Dialog2
title={
APIKEY_REMOVE_DIALOG_HEADER
}
trigger={
<Button variant="soft">
{APIKEY_REMOVE_BTN}
</Button>
}
okButton={
<Button
onClick={() =>
removeApikey(
item.keyId,
)
}
>
{APIKEY_REMOVE_BTN}
</Button>
}
>
{APIKYE_REMOVE_DIALOG_DESC}
</Dialog2>
</td>
</TableRow>
),
)}
</TableBody>
</Table>
</div>
</Tabs>
</div>
);
Expand All @@ -617,6 +730,7 @@ const mapStateToProps = (state: AppState) => ({
address: state.address,
networkAction: state.networkAction,
profile: state.profile,
loading: state.networkAction,
});

const mapDispatchToProps = (dispatch: AppDispatch) => ({
Expand Down
14 changes: 9 additions & 5 deletions apps/web/components/admin/users/details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
USER_BASIC_DETAILS_HEADER,
USER_EMAIL_SUBHEADER,
USER_NAME_SUBHEADER,
USER_TAGS_SUBHEADER,
} from "../../../ui-config/strings";
import { FetchBuilder } from "@courselit/utils";
import { AppMessage } from "@courselit/common-models";
Expand Down Expand Up @@ -248,11 +249,14 @@ const Details = ({ userId, address, dispatch }: DetailsProps) => {
onChange={(value) => toggleActiveState(value)}
/>
</div>
<ComboBox
options={tags}
selectedOptions={new Set(userData.tags)}
onChange={updateTags}
/>
<div className="flex flex-col gap-2">
<p>{USER_TAGS_SUBHEADER}</p>
<ComboBox
options={tags}
selectedOptions={new Set(userData.tags)}
onChange={updateTags}
/>
</div>
</Section>
<PermissionsEditor user={userData} />
</div>
Expand Down
2 changes: 0 additions & 2 deletions apps/web/components/admin/users/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => {
offset: ${page},
rowsPerPage: ${rowsPerPage}
}) {
id,
name,
userId,
email,
Expand All @@ -101,7 +100,6 @@ const UsersManager = ({ address, dispatch, loading }: UserManagerProps) => {
offset: ${page},
rowsPerPage: ${rowsPerPage}
}) {
id,
name,
userId,
email,
Expand Down
Loading

0 comments on commit ec038f0

Please sign in to comment.