Skip to content

Commit

Permalink
feat(Support): generate ADAPT access code
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner committed Aug 21, 2024
1 parent d5923b7 commit 475e24d
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 19 deletions.
10 changes: 5 additions & 5 deletions client/src/Platform.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,12 @@ const Platform = () => {
}
}, []);

const ApplicationTree = ()=> {
const ApplicationTree = () => {
return (
<ErrorBoundary FallbackComponent={ErrorScreen}>
<div className="App">
<ModalsProvider>
<NotificationsProvider>
<NotificationsProvider>
<ModalsProvider>
<Switch>
{/* Commons Render Tree */}
{/* @ts-expect-error */}
Expand All @@ -92,8 +92,8 @@ const Platform = () => {
{/* Conductor and fallback Render Tree */}
<Route component={Conductor} />
</Switch>
</NotificationsProvider>
</ModalsProvider>
</ModalsProvider>
</NotificationsProvider>
<ErrorModal />
</div>
</ErrorBoundary>
Expand Down
7 changes: 7 additions & 0 deletions client/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,13 @@ class API {
return res;
}

async generateADAPTAccessCode() {
const res = await axios.get<{
access_code: string;
} & ConductorBaseResponse>("/central-identity/adapt-access-code");
return res;
}

async getCentralIdentityLicenses() {
const res = await axios.get<
{
Expand Down
92 changes: 92 additions & 0 deletions client/src/components/support/ADAPTAccessCodeModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
Modal,
Button,
ModalProps,
Icon,
} from "semantic-ui-react";
import useGlobalError from "../error/ErrorHooks";
import { useQuery } from "@tanstack/react-query";
import api from "../../api";
import CopyButton from "../util/CopyButton";
import { useNotifications } from "../../context/NotificationContext";
import LoadingSpinner from "../LoadingSpinner";

interface ADAPTAccessCodeModalProps extends ModalProps {
open: boolean;
onClose: () => void;
}

const ADAPTAccessCodeModal: React.FC<ADAPTAccessCodeModalProps> = ({
open,
onClose,
...rest
}) => {
const { handleGlobalError } = useGlobalError();
const { addNotification } = useNotifications();

const { data, isFetching } = useQuery<string | null>({
queryKey: ['ADAPTAccessCode'],
queryFn: () => getADAPTAccessCode(),
enabled: open,
staleTime: Infinity,
refetchOnMount: false,
refetchOnWindowFocus: false,
})

async function getADAPTAccessCode() {
try {
const res = await api.generateADAPTAccessCode();

if (res.data.err) {
throw new Error(res.data.errMsg);
}

return res.data.access_code;
} catch (err) {
handleGlobalError(err);
return null;
}
}

return (
<Modal open={open} onClose={onClose} {...rest}>
<Modal.Header>Generate ADAPT Access Code</Modal.Header>
<Modal.Content>
{isFetching && <LoadingSpinner />}
<div className="flex flex-row text-center justify-center">
<p className="text-xl text-center">
<span className="font-semibold">Access Code: </span>
{data ? data : "Failed to generate access code"}
</p>
</div>
</Modal.Content>
<Modal.Actions>
<Button onClick={onClose} loading={isFetching}>
Close
</Button>
{data && (
<CopyButton val={data ?? 'unknown'} >
{({ copy }) => (
<Button
color="green"
onClick={() => {
copy();
addNotification({
message: "Access code copied to clipboard!",
type: "success",
});
onClose();
}}
disabled={!data}
>
Copy & Close
</Button>
)}
</CopyButton>
)}
</Modal.Actions>
</Modal>
);
};

export default ADAPTAccessCodeModal;
20 changes: 19 additions & 1 deletion client/src/screens/conductor/support/Ticket.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ import TicketAttachments from "../../../components/support/TicketAttachments";
import ConfirmDeleteTicketModal from "../../../components/support/ConfirmDeleteTicketModal";
import api from "../../../api";
import { capitalizeFirstLetter } from "../../../components/util/HelperFunctions";
import { ca, hi } from "date-fns/locale";
import TicketAutoCloseWarning from "../../../components/support/TicketAutoCloseWarning";
import { SupportTicketPriority } from "../../../types/support";
import { useModals } from "../../../context/ModalContext";
import ADAPTAccessCodeModal from "../../../components/support/ADAPTAccessCodeModal";

const AssignTicketModal = lazy(
() => import("../../../components/support/AssignTicketModal")
);
Expand All @@ -41,6 +43,7 @@ const SupportTicketView = () => {
const queryClient = useQueryClient();
const location = useLocation();
const { handleGlobalError } = useGlobalError();
const { openModal, closeAllModals } = useModals();
const user = useTypedSelector((state) => state.user);

const [id, setId] = useState<string>("");
Expand Down Expand Up @@ -195,6 +198,15 @@ const SupportTicketView = () => {
deleteTicketMutation.mutateAsync();
}

function handleOpenADAPTAccessCodeModal() {
openModal(
<ADAPTAccessCodeModal
open
onClose={() => closeAllModals()}
/>
)
}

const changePriorityOptions = useMemo(() => {
const allOpts = ["high", "medium", "low", "severe"];
const currentPriority = ticket?.priority ?? "medium";
Expand Down Expand Up @@ -230,6 +242,12 @@ const SupportTicketView = () => {
)}
{["open", "in_progress"].includes(ticket?.status ?? "") && (
<>
<Button
color="teal"
onClick={handleOpenADAPTAccessCodeModal}>
<Icon name="key" />
ADAPT Access Code
</Button>
<Button
onClick={() =>
toggleAutoCloseMutation.mutateAsync(!ticket?.autoCloseSilenced)
Expand Down
9 changes: 8 additions & 1 deletion server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,18 @@ router.route('/central-identity/orgs').get(
);

router.route('/central-identity/adapt-orgs').get(
centralIdentityAPI.validate('getADAPTOrgs'), // Don't need checkCentralIdentityConfig here because it's does not require a valid API key
centralIdentityAPI.validate('getADAPTOrgs'), // Don't need checkCentralIdentityConfig here because it does not require a valid API key
middleware.checkValidationErrors,
centralIdentityAPI.getADAPTOrgs,
);

router.route('/central-identity/adapt-access-code').get(
authAPI.verifyRequest, // Don't need checkCentralIdentityConfig here because it does not require a valid API key
authAPI.getUserAttributes,
authAPI.checkHasRoleMiddleware('libretexts', 'support'),
centralIdentityAPI.generateADAPTAccessCode
);

router.route('/central-identity/systems').get(
middleware.checkCentralIdentityConfig,
authAPI.verifyRequest,
Expand Down
39 changes: 34 additions & 5 deletions server/api/central-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
CentralIdentityVerificationRequestStatus,
} from "../types";
import {
ADAPTAccessCodeResponse,
CentralIdentityLicense,
CentralIdentityUpdateVerificationRequestBody,
} from "../types/CentralIdentity.js";
Expand All @@ -46,6 +47,8 @@ import Project, { ProjectInterface } from "../models/project.js";
import { getSubdomainFromLibrary } from "../util/librariesclient.js";
import { updateTeamWorkbenchPermissions } from "../util/projectutils.js";
import fse from "fs-extra";
import axios from "axios";
import { SignJWT } from "jose";

async function getUsers(
req: TypedReqQuery<{
Expand Down Expand Up @@ -332,8 +335,7 @@ async function deleteUserApplication(
) {
try {
const appRes = await useCentralIdentityAxios(false).delete(
`/users/${
req.params.id
`/users/${req.params.id
}/applications/${req.params.applicationId?.toString()}`
);

Expand Down Expand Up @@ -743,7 +745,7 @@ async function getVerificationRequests(
const page = parseInt(req.query.page?.toString()) || 1;
const limit = parseInt(req.query.limit?.toString()) || 10;
const status = req.query.status || 'open';

const offset = getPaginationOffset(page, limit);

const requestsRes = await useCentralIdentityAxios(false).get(
Expand Down Expand Up @@ -860,10 +862,36 @@ async function updateVerificationRequest(
}
}

function validateVerificationRequestStatus(raw: string): boolean {
return isCentralIdentityVerificationRequestStatus(raw);
async function generateADAPTAccessCode(req: Request, res: Response) {
try {
if (!process.env.ADAPT_ACCESS_TOKEN) {
throw new Error("Missing required environment variable.");
}

const encoded = new TextEncoder().encode(process.env.ADAPT_ACCESS_TOKEN);
const jwtToSend = await new SignJWT({}).setProtectedHeader({ alg: 'HS256', typ: 'JWT' }).setIssuedAt().setExpirationTime('1h').sign(encoded);

const adaptRes = await axios.get<ADAPTAccessCodeResponse>("https://adapt.libretexts.org/api/access-code/instructor", {
headers: {
Authorization: `Bearer ${jwtToSend}`
}
});

if (adaptRes.data.type === 'error') {
throw new Error(adaptRes.data.message);
}

return res.send({
err: false,
access_code: adaptRes.data.access_code
});
} catch (err) {
debugError(err);
return conductor500Err(res);
}
}


async function processNewUserWebhookEvent(
req: z.infer<typeof NewUserWebhookValidator>,
res: Response
Expand Down Expand Up @@ -1101,6 +1129,7 @@ export default {
getVerificationRequest,
updateVerificationRequest,
updateUser,
generateADAPTAccessCode,
processNewUserWebhookEvent,
processLibraryAccessWebhookEvent,
processVerificationStatusUpdateWebook,
Expand Down
2 changes: 1 addition & 1 deletion server/api/validators/central-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export const GetVerificationRequestsSchema = z.object({
query: z.object({
status: z.enum(["open", "closed"]).optional()
}).merge(PaginationSchema),
})
});
20 changes: 14 additions & 6 deletions server/types/CentralIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,11 @@ export type CentralIdentityUser = {
student_id?: string;
uuid: string;
verify_status:
| "not_attempted"
| "pending"
| "needs_review"
| "denied"
| "verified";
| "not_attempted"
| "pending"
| "needs_review"
| "denied"
| "verified";
};

export type CentralIdentityVerificationRequest = {
Expand All @@ -78,7 +78,7 @@ export type CentralIdentityVerificationRequest = {
user: Pick<CentralIdentityUser, "first_name" | "last_name" | "email" | "uuid">;
};

export type CentralIdentityVerificationRequestStatus = 'approved' | 'denied' | 'needs_change' | 'open';
export type CentralIdentityVerificationRequestStatus = 'approved' | 'denied' | 'needs_change' | 'open';

export type CentralIdentityAccessRequestChangeEffect = 'deny' | 'approve' | 'request_change';

Expand All @@ -94,4 +94,12 @@ export type CentralIdentityLicense = {
url?: string;
created_at: Date;
updated_at: Date;
}

export type ADAPTAccessCodeResponse = {
type: 'success',
access_code: string,
} | {
type: 'error',
message: string,
}

0 comments on commit 475e24d

Please sign in to comment.