Skip to content
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
8 changes: 8 additions & 0 deletions server/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,14 @@ router
orgsAPI.getCampusAdmins
);

router
.route("/org/:orgID/custom-cover-config")
.get(
orgsAPI.validate("getCustomCoverConfig"),
middleware.checkValidationErrors,
orgsAPI.getCustomCoverConfig
);

/* Asset Tag Frameworks */
router
.route("/assettagframeworks")
Expand Down
68 changes: 56 additions & 12 deletions server/api/organizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

'use strict';
import logger from "../logger.js";
import express, {Request, Response, NextFunction} from 'express';
import express, { Request, Response, NextFunction } from 'express';
import { body, param, query } from 'express-validator';
import multer from 'multer';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
Expand Down Expand Up @@ -71,7 +71,7 @@ async function lookupOrganization(orgID: string) {
{ orgID },
{ _id: 0, defaultProjectLead: 0 },
).lean();
if(org?.commonsModules){
if (org?.commonsModules) {
// Remove _id and __v fields from commonsModules subdocument
// @ts-ignore
delete org.commonsModules._id;
Expand Down Expand Up @@ -225,6 +225,45 @@ async function getLibreGridOrganizations(_req: Request, res: Response) {
}
}

async function getCustomCoverConfig(req: Request, res: Response) {
try {
const { orgID } = req.params;

const orgData = await Organization.findOne(
{
orgID: { $eq: orgID }
},
{
orgID: 1,
name: 1,
customCoverConfig: 1,
}
).lean();

if (!orgData) {
return res.status(404).send({
err: true,
errMsg: conductorErrors.err11,
});
}

return res.send({
err: false,
org: {
orgID: orgData.orgID,
name: orgData.name,
},
customCoverConfig: orgData.customCoverConfig || null,
});
Comment thread
jakeaturner marked this conversation as resolved.
} catch (e) {
logger.error({ err: e }, "getCustomCoverConfig failed");
return res.status(500).send({
err: true,
errMsg: conductorErrors.err6,
});
}
}

/**
* Updates an Organization's information.
*
Expand All @@ -248,11 +287,11 @@ async function updateOrganizationInfo(req: Request, res: Response) {
}
};

if(req.body.primaryColor) {
if (req.body.primaryColor) {
updateObj.primaryColor = sanitizeCustomColor(req.body.primaryColor)
}

if(req.body.footerColor) {
if (req.body.footerColor) {
updateObj.footerColor = sanitizeCustomColor(req.body.footerColor)
}

Expand All @@ -274,7 +313,7 @@ async function updateOrganizationInfo(req: Request, res: Response) {
);
}

if(Object.hasOwn(updateObj, 'collectionsDisplayLabel') && isEmptyString(updateObj.collectionsDisplayLabel ?? '')){
if (Object.hasOwn(updateObj, 'collectionsDisplayLabel') && isEmptyString(updateObj.collectionsDisplayLabel ?? '')) {
// Reset label to 'Collections' if empty string was passed
updateObj.collectionsDisplayLabel = 'Collections'
}
Expand Down Expand Up @@ -355,9 +394,9 @@ async function updateBrandingImageAsset(req: Request, res: Response) {
errMsg: conductorErrors.err2,
});
}

let assetVersion = 1;

// @ts-ignore
if (org[assetName].includes(process.env.AWS_ORGDATA_DOMAIN)) {
//@ts-ignore
Expand Down Expand Up @@ -472,7 +511,7 @@ function validateCommonsModules(commonsModules: any) {
return false;
}
for (const module in commonsModules) {
if(module === "_id" || module === "__v"){
if (module === "_id" || module === "__v") {
// ignore these fields (mongodb adds them because commonsModules is technically a subdocument of the Organization model)
continue;
}
Expand Down Expand Up @@ -512,16 +551,20 @@ function validate(method: string) {
return [
param('orgID', conductorErrors.err1).exists().isLength({ min: 2, max: 50 }),
];
case 'getCustomCoverConfig':
return [
param('orgID', conductorErrors.err1).exists().isLength({ min: 2, max: 50 }),
];
case 'updateinfo':
return [
param('orgID', conductorErrors.err1).exists().isLength({ min: 2, max: 50 }),
body('aboutLink', conductorErrors.err1).optional({ checkFalsy: true }).isURL(),
body('commonsHeader', conductorErrors.err1).optional({ checkFalsy: true }).isLength({ max: 200 }),
body('commonsMessage', conductorErrors.err1).optional({ checkFalsy: true }).isLength({ max: 500 }),
body('collectionsDisplayLabel', conductorErrors.err1).optional({checkFalsy: true}).isLength({ max: 200 }),
body('collectionsMessage', conductorErrors.err1).optional({checkFalsy: true}).isLength({max: 500}),
body('primaryColor', conductorErrors.err1).optional({ checkFalsy: true }).isLength({min: 7, max: 7}).isHexColor(),
body('footerColor', conductorErrors.err1).optional({ checkFalsy: true }).isLength({min: 7, max: 7}).isHexColor(),
body('collectionsDisplayLabel', conductorErrors.err1).optional({ checkFalsy: true }).isLength({ max: 200 }),
body('collectionsMessage', conductorErrors.err1).optional({ checkFalsy: true }).isLength({ max: 500 }),
body('primaryColor', conductorErrors.err1).optional({ checkFalsy: true }).isLength({ min: 7, max: 7 }).isHexColor(),
body('footerColor', conductorErrors.err1).optional({ checkFalsy: true }).isLength({ min: 7, max: 7 }).isHexColor(),
body('addToLibreGridList', conductorErrors.err1).optional({ checkFalsy: true }).isBoolean().toBoolean(),
body('supportTicketNotifiers', conductorErrors.err1).optional({ checkFalsy: true }).isArray().isEmail(),
body('defaultAssetTagFrameworkUUID', conductorErrors.err1).optional({ checkFalsy: true }).isUUID(),
Expand Down Expand Up @@ -549,6 +592,7 @@ export default {
getOrganizationInfo,
getCampusAdmins,
getCurrentOrganization,
getCustomCoverConfig,
getAllOrganizations,
getLibreGridOrganizations,
updateOrganizationInfo,
Expand Down
2 changes: 2 additions & 0 deletions server/api/services/shapeshift-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export default class ShapeshiftService {
bookID,
contentPageCount,
timestamp,
customCoverOrg,
}: WebhookParams): Promise<boolean> {
const result = await Book.updateOne(
{
Expand All @@ -174,6 +175,7 @@ export default class ShapeshiftService {
...(contentPageCount !== undefined
? { "exportInfo.contentPageCount": contentPageCount }
: {}),
...(customCoverOrg !== undefined ? { "exportInfo.customCoverOrg": customCoverOrg } : {}),
},
}
);
Expand Down
1 change: 1 addition & 0 deletions server/api/validators/shapeshift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ export const WebhookValidator = z.object({
bookID: bookIDSchema,
contentPageCount: z.number().int().nonnegative().optional(),
timestamp: z.number().int().nonnegative(), // Unix timestamp in milliseconds
customCoverOrg: z.string().trim().min(1).max(255).optional(), // Only set if the book has a custom cover by the producing org
}),
});
5 changes: 5 additions & 0 deletions server/models/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface BookInterface extends Document {
lastCompiled?: number;
compiledBy?: string;
contentPageCount?: number;
customCoverOrg?: string; // The name of the org producing the book if a custom cover is used. Should be used for display/informational purposes only, not for load-bearing logic or validation.
lastJobID?: string;
lastJobSubmittedAt?: Date;
lastJobSubmittedBy?: string;
Expand Down Expand Up @@ -131,6 +132,10 @@ const BookSchema = new Schema<BookInterface>(
* The number of pages in the Book's content PDF, as reported by Shapeshift during compilation.
*/
contentPageCount: Number,
/**
* The name of the org producing the book if a custom cover is used. Should be used for display/informational purposes only, not for load-bearing logic or validation.
*/
customCoverOrg: String,
/**
* The Shapeshift job ID of the most recent compile submitted from Conductor.
*
Expand Down
43 changes: 43 additions & 0 deletions server/models/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ export type CommonsModuleSettings = {
minirepos: CommonsModuleConfig;
}

export type CustomCoverConfig = {
enabled: boolean;
casewrapCoverFrontTemplateURL: string;
casewrapCoverBackTemplateURL: string;
perfectboundCoverFrontTemplateURL: string;
perfectboundCoverBackTemplateURL: string;
spineHexColor: string;
spineImageURL?: string;
matchingPaths: string[];
}
Comment on lines +24 to +33

export interface OrganizationInterface extends Document {
orgID: string;
active: boolean;
Expand Down Expand Up @@ -51,6 +62,7 @@ export interface OrganizationInterface extends Document {
showCollections?: boolean;
assetFilterExclusions?: string[];
autoCatalogMatchingDisabled?: boolean;
customCoverConfig?: CustomCoverConfig;
listenerPriority: number;
cpuUnitsOverride: number;
memoryValueOverride: number;
Expand All @@ -60,6 +72,30 @@ export interface OrganizationInterface extends Document {
FEAT_RecordSearchQueries?: boolean;
}

/**
* Custom cover configuration is optional as a whole, but every field except
* `spineImageURL` is required when the object is present. Declaring it as a
* single nested subdocument (rather than a nested object literal) means these
* validators only run when the subdocument actually exists.
*/
const CustomCoverConfigSchema = new Schema<CustomCoverConfig>(
{
enabled: { type: Boolean, required: true },
casewrapCoverFrontTemplateURL: { type: String, required: true },
casewrapCoverBackTemplateURL: { type: String, required: true },
perfectboundCoverFrontTemplateURL: { type: String, required: true },
perfectboundCoverBackTemplateURL: { type: String, required: true },
spineHexColor: { type: String, required: true },
spineImageURL: String,
matchingPaths: {
type: [String],
required: true,
default: undefined,
},
},
{ _id: false }
);

const OrganizationSchema = new Schema<OrganizationInterface>(
{
/**
Expand Down Expand Up @@ -234,6 +270,13 @@ const OrganizationSchema = new Schema<OrganizationInterface>(
type: Boolean,
default: false,
},
/**
* Configuration for custom cover templates and spine colors for the Organization.
*/
customCoverConfig: {
type: CustomCoverConfigSchema,
required: false,
},
/**
* Used for deterministic routing in a load-balanced Conductor deployment.
*/
Expand Down
Loading