generated from nhcarrigan/template
fix: handle base64 uploads correctly (#64)
### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Reviewed-on: #64
This commit was merged in pull request #64.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
validateUrl,
|
validateUrl,
|
||||||
validateRating,
|
validateRating,
|
||||||
validateStringLength,
|
validateStringLength,
|
||||||
|
validateDataUrl,
|
||||||
MAX_LENGTHS,
|
MAX_LENGTHS,
|
||||||
} from "../utils/validation";
|
} from "../utils/validation";
|
||||||
|
|
||||||
@@ -44,10 +45,24 @@ export class BookService {
|
|||||||
throw new Error("Rating must be an integer between 0 and 10.");
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate cover image URL
|
if (data.coverImage) {
|
||||||
if (data.coverImage && !validateUrl(data.coverImage)) {
|
if (data.coverImage.startsWith("data:")) {
|
||||||
|
const sizeInBytes = data.coverImage.length * 0.75;
|
||||||
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
|
throw new Error("Cover image must be under 5MB.");
|
||||||
|
}
|
||||||
|
if (!validateDataUrl(data.coverImage)) {
|
||||||
|
throw new Error("Invalid image data URL.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateUrl(data.coverImage)) {
|
||||||
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate tags
|
// Validate tags
|
||||||
if (data.tags) {
|
if (data.tags) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
validateUrl,
|
validateUrl,
|
||||||
validateRating,
|
validateRating,
|
||||||
validateStringLength,
|
validateStringLength,
|
||||||
|
validateDataUrl,
|
||||||
MAX_LENGTHS,
|
MAX_LENGTHS,
|
||||||
} from "../utils/validation";
|
} from "../utils/validation";
|
||||||
|
|
||||||
@@ -42,9 +43,24 @@ export class GameService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate cover image URL
|
// Validate cover image URL
|
||||||
if (data.coverImage && !validateUrl(data.coverImage)) {
|
if (data.coverImage) {
|
||||||
|
if (data.coverImage.startsWith("data:")) {
|
||||||
|
const sizeInBytes = data.coverImage.length * 0.75;
|
||||||
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
|
throw new Error("Cover image must be under 5MB.");
|
||||||
|
}
|
||||||
|
if (!validateDataUrl(data.coverImage)) {
|
||||||
|
throw new Error("Invalid image data URL.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateUrl(data.coverImage)) {
|
||||||
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate tags
|
// Validate tags
|
||||||
if (data.tags) {
|
if (data.tags) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
validateUrl,
|
validateUrl,
|
||||||
validateRating,
|
validateRating,
|
||||||
validateStringLength,
|
validateStringLength,
|
||||||
|
validateDataUrl,
|
||||||
MAX_LENGTHS,
|
MAX_LENGTHS,
|
||||||
} from "../utils/validation";
|
} from "../utils/validation";
|
||||||
|
|
||||||
@@ -42,9 +43,24 @@ export class MangaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate cover image URL
|
// Validate cover image URL
|
||||||
if (data.coverImage && !validateUrl(data.coverImage)) {
|
if (data.coverImage) {
|
||||||
|
if (data.coverImage.startsWith("data:")) {
|
||||||
|
const sizeInBytes = data.coverImage.length * 0.75;
|
||||||
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
|
throw new Error("Cover image must be under 5MB.");
|
||||||
|
}
|
||||||
|
if (!validateDataUrl(data.coverImage)) {
|
||||||
|
throw new Error("Invalid image data URL.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateUrl(data.coverImage)) {
|
||||||
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate tags
|
// Validate tags
|
||||||
if (data.tags) {
|
if (data.tags) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
validateUrl,
|
validateUrl,
|
||||||
validateRating,
|
validateRating,
|
||||||
validateStringLength,
|
validateStringLength,
|
||||||
|
validateDataUrl,
|
||||||
MAX_LENGTHS,
|
MAX_LENGTHS,
|
||||||
} from "../utils/validation";
|
} from "../utils/validation";
|
||||||
|
|
||||||
@@ -42,8 +43,23 @@ export class MusicService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate cover art URL
|
// Validate cover art URL
|
||||||
if (data.coverArt && !validateUrl(data.coverArt)) {
|
if (data.coverArt) {
|
||||||
throw new Error("Invalid cover art URL. Only http and https URLs are allowed.");
|
if (data.coverArt.startsWith("data:")) {
|
||||||
|
const sizeInBytes = data.coverArt.length * 0.75;
|
||||||
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
|
throw new Error("Cover image must be under 5MB.");
|
||||||
|
}
|
||||||
|
if (!validateDataUrl(data.coverArt)) {
|
||||||
|
throw new Error("Invalid image data URL.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!validateStringLength(data.coverArt, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateUrl(data.coverArt)) {
|
||||||
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tags
|
// Validate tags
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
validateUrl,
|
validateUrl,
|
||||||
validateRating,
|
validateRating,
|
||||||
validateStringLength,
|
validateStringLength,
|
||||||
|
validateDataUrl,
|
||||||
MAX_LENGTHS,
|
MAX_LENGTHS,
|
||||||
} from "../utils/validation";
|
} from "../utils/validation";
|
||||||
|
|
||||||
@@ -39,9 +40,24 @@ export class ShowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate cover image URL
|
// Validate cover image URL
|
||||||
if (data.coverImage && !validateUrl(data.coverImage)) {
|
if (data.coverImage) {
|
||||||
|
if (data.coverImage.startsWith("data:")) {
|
||||||
|
const sizeInBytes = data.coverImage.length * 0.75;
|
||||||
|
if (sizeInBytes > MAX_LENGTHS.DATA_URL) {
|
||||||
|
throw new Error("Cover image must be under 5MB.");
|
||||||
|
}
|
||||||
|
if (!validateDataUrl(data.coverImage)) {
|
||||||
|
throw new Error("Invalid image data URL.");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateUrl(data.coverImage)) {
|
||||||
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate tags
|
// Validate tags
|
||||||
if (data.tags) {
|
if (data.tags) {
|
||||||
|
|||||||
@@ -4,6 +4,13 @@
|
|||||||
* @author Naomi Carrigan
|
* @author Naomi Carrigan
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates that a URL is a proper base64 data string.
|
||||||
|
*/
|
||||||
|
export function validateDataUrl(url: string): boolean {
|
||||||
|
return /^data:image\/(jpeg|png|gif|webp|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates that a URL is safe and points to an allowed protocol.
|
* Validates that a URL is safe and points to an allowed protocol.
|
||||||
* Prevents javascript:, data:, vbscript:, and file: URLs.
|
* Prevents javascript:, data:, vbscript:, and file: URLs.
|
||||||
@@ -83,4 +90,5 @@ export const MAX_LENGTHS = {
|
|||||||
NOTES: 5000,
|
NOTES: 5000,
|
||||||
TAGS: 50, // per tag
|
TAGS: 50, // per tag
|
||||||
ISBN: 50,
|
ISBN: 50,
|
||||||
|
DATA_URL: 5 * 1024 * 1024, // 5MB in bytes (not chars)
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user