Skip to content

Commit

Permalink
fix(Workbench): ensure user has library access before book creation (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jakeaturner authored Jan 11, 2024
1 parent d67fead commit fbad678
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 26 deletions.
92 changes: 84 additions & 8 deletions client/src/components/projects/CreateWorkbenchModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,113 @@ import {
Dropdown,
Form,
Icon,
Message,
Modal,
ModalProps,
} from "semantic-ui-react";
import useGlobalError from "../error/ErrorHooks";
import axios from "axios";
import { Controller, useForm } from "react-hook-form";
import { Controller, get, useForm } from "react-hook-form";
import CtlTextInput from "../ControlledInputs/CtlTextInput";
import { libraryOptions } from "../util/LibraryOptions";
import { required } from "../../utils/formRules";
import { useEffect, useState } from "react";
import { useTypedSelector } from "../../state/hooks";
import { CentralIdentityApp } from "../../types";
import { getCentralAuthInstructorURL } from "../../utils/centralIdentityHelpers";

interface CreateWorkbenchModalProps extends ModalProps {
show: boolean;
projectID: string;
projectTitle: string;
onClose: () => void;
onSuccess: () => void;
}

interface CreateWorkbenchForm {
library: string;
library: number | string;
title: string;
}

const CreateWorkbenchModal: React.FC<CreateWorkbenchModalProps> = ({
show,
projectID,
projectTitle,
onClose,
onSuccess,
...rest
}) => {
const { handleGlobalError } = useGlobalError();
const user = useTypedSelector((state) => state.user);
const { control, getValues, setValue, reset, trigger, formState } =
const { control, getValues, setValue, reset, trigger, formState, watch } =
useForm<CreateWorkbenchForm>({
defaultValues: {
library: "",
title: "",
},
});
const [loading, setLoading] = useState(false);
const [libraryOptions, setLibraryOptions] = useState<CentralIdentityApp[]>(
[]
);
const [canAccessLibrary, setCanAccessLibrary] = useState(true);

useEffect(() => {
loadLibraries();
setValue("title", projectTitle);
}, []);

useEffect(() => {
if (show) {
reset(); // reset form on open
}
}, [show]);

async function loadLibraries() {
try {
setLoading(true);
const res = await axios.get("/central-identity/public/apps");
if (res.data.err) {
throw new Error(res.data.errMsg);
}
if (!res.data.applications) throw new Error("No libraries found");

const libraries = res.data.applications.filter(
(a: CentralIdentityApp) => a.app_type === "library"
);

if (!libraries.length) throw new Error("No libraries found");
setLibraryOptions(libraries);
} catch (err) {
handleGlobalError(err);
} finally {
setLoading(false);
}
}

useEffect(() => {
checkLibraryAccess();
}, [user, watch("library")]);

async function checkLibraryAccess() {
try {
if (!user.uuid || !getValues("library")) return;
const res = await axios.get(
`/central-identity/users/${user.uuid}/applications/${getValues(
"library"
)}`
);
if (res.data.err) {
throw new Error(res.data.errMsg);
}
setCanAccessLibrary(res.data.hasAccess ?? false);
} catch (err) {
handleGlobalError(err);
}
}

async function createWorkbench() {
try {
if(!canAccessLibrary) return;
setLoading(true);
if (!(await trigger())) return;
const res = await axios.post("/commons/book", {
Expand All @@ -69,6 +126,7 @@ const CreateWorkbenchModal: React.FC<CreateWorkbenchModalProps> = ({
setLoading(false);
}
}

return (
<Modal size="fullscreen" {...rest}>
<Modal.Header>Create Book</Modal.Header>
Expand All @@ -94,10 +152,14 @@ const CreateWorkbenchModal: React.FC<CreateWorkbenchModalProps> = ({
render={({ field }) => (
<Dropdown
id="projectStatus"
options={libraryOptions}
options={libraryOptions.map((l) => ({
key: l.id,
text: l.name,
value: l.id,
}))}
{...field}
onChange={(e, data) => {
field.onChange(data.value?.toString() ?? "text");
field.onChange(data.value);
}}
fluid
selection
Expand Down Expand Up @@ -129,10 +191,23 @@ const CreateWorkbenchModal: React.FC<CreateWorkbenchModalProps> = ({
/>
</div>
<p>
<strong>CAUTION:</strong> Library cannot be changed after Book
is created. Please check your selection before submitting.
<strong>CAUTION:</strong> Library cannot be changed after Book is
created. Please check your selection before submitting.
</p>
</Form>
{!canAccessLibrary && (
<Message warning>
<Message.Header>Cannot Access Library</Message.Header>
<p>
Oops, it looks like you do not have access to this library. If you need to request
access, please submit or update your instructor verification
request here:{" "}
<a href={getCentralAuthInstructorURL()} target="_blank">
{getCentralAuthInstructorURL()}
</a>
</p>
</Message>
)}
</Modal.Content>
<Modal.Actions>
<Button onClick={onClose} loading={loading}>
Expand All @@ -144,6 +219,7 @@ const CreateWorkbenchModal: React.FC<CreateWorkbenchModalProps> = ({
icon
color="green"
loading={loading}
disabled={!canAccessLibrary}
>
<Icon name="save" />
Create
Expand Down
1 change: 1 addition & 0 deletions client/src/components/projects/ProjectView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3201,6 +3201,7 @@ const ProjectView = (props) => {
<CreateWorkbenchModal
open={showCreateWorkbenchModal}
projectID={project.projectID}
projectTitle={project.title}
onClose={() => setShowCreateWorkbenchModal(false)}
onSuccess={() => window.location.reload()}
/>
Expand Down
8 changes: 7 additions & 1 deletion server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,13 @@ router.route('/central-identity/users/:id/applications').get(
centralIdentityAPI.addUserApplications
)

router.route('/central-identity/users/:id/applications/:applicationId').delete(
router.route('/central-identity/users/:id/applications/:applicationId').get(
middleware.checkCentralIdentityConfig,
authAPI.verifyRequest,
authAPI.getUserAttributes,
middleware.validateZod(centralIdentityValidators.CheckUserApplicationAccessValidator),
centralIdentityAPI.checkUserApplicationAccess
).delete(
middleware.checkCentralIdentityConfig,
authAPI.verifyRequest,
authAPI.getUserAttributes,
Expand Down
22 changes: 15 additions & 7 deletions server/api/books.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import PeerReview from "../models/peerreview.js";
import Tag from "../models/tag.js";
import CIDDescriptor from "../models/ciddescriptor.js";
import conductorErrors from "../conductor-errors.js";
import { isEmptyString, isValidDateObject, sleep } from "../util/helpers.js";
import { getSubdomainFromUrl, isEmptyString, isValidDateObject, sleep } from "../util/helpers.js";
import {
checkBookIDFormat,
extractLibFromID,
Expand Down Expand Up @@ -48,15 +48,13 @@ import {
CXOneFetch,
generateBookPathAndURL,
generateChapterOnePath,
getDeveloperGroup,
getLibUser,
getPageID,
getSubdomainFromLibrary,
} from "../util/librariesclient.js";
import MindTouch from "../util/CXOne/index.js";
import { conductor500Err } from "../util/errorutils.js";
import { ZodReqWithUser } from "../types/Express.js";
import User from "../models/user.js";
import centralIdentity from "./central-identity.js";
const defaultImagesURL = "https://cdn.libretexts.net/DefaultImages";

/**
Expand Down Expand Up @@ -1378,7 +1376,12 @@ async function createBook(
const user = await User.findOne({ uuid: userID }).orFail();
const project = await Project.findOne({ projectID }).orFail();

const subdomain = getSubdomainFromLibrary(library);
const libraryApp = await centralIdentity.getApplicationById(library);
if(!libraryApp) {
throw new Error("badlibrary");
}

const subdomain = getSubdomainFromUrl(libraryApp.main_url);
if(!subdomain) {
throw new Error("badlibrary");
}
Expand All @@ -1389,6 +1392,11 @@ async function createBook(
throw new Error(conductorErrors.err8);
}

const hasLibAccess = await centralIdentity.checkUserApplicationAccessInternal(user.centralID, libraryApp.id);
if(!hasLibAccess) {
throw new Error(conductorErrors.err8);
}

// Create book coverpage
const [bookPath, bookURL] = generateBookPathAndURL(subdomain, title);
const createBookRes = await CXOneFetch({
Expand Down Expand Up @@ -1515,7 +1523,7 @@ async function createBook(
});
}
debugError(err);
if(err.name === "CreateBookError") {
if(["CreateBookError", 'badlibrary'].includes(err.name)) {
return res.status(400).send({
err: true,
errMsg: err.message,
Expand Down Expand Up @@ -2225,7 +2233,7 @@ const retrieveKBExport = (_req: Request, res: Response) => {

const createBookSchema = z.object({
body: z.object({
library: z.string().min(1).max(255),
library: z.coerce.number().positive().int(),
title: z.string().min(1).max(255),
projectID: z.string().length(10),
}),
Expand Down
Loading

0 comments on commit fbad678

Please sign in to comment.