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

fix(Workbench): ensure user has library access before book creation #167

Merged
merged 2 commits into from
Jan 11, 2024
Merged
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
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 @@ -3219,6 +3219,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