generated from nhcarrigan/template
feat: apply comprehensive validation to all remaining services
Extended the comprehensive input validation pattern to Music, Art, Show, and Manga services, completing security coverage across all media types in the library. Services Updated: 1. Music Service - Title validation (max 500 characters) - Artist validation (max 200 characters) - Notes validation (max 5000 characters) - Cover art URL validation (max 2048 characters, http/https only) - Rating validation (0-10 integers) - Tags validation (each max 50 characters) - Links validation (valid URLs, max lengths) 2. Art Service - Title validation (max 500 characters) - Artist validation (max 200 characters) - Description validation (max 5000 characters) - Image URL validation (required, valid URL) - Links validation (valid URLs, max lengths) 3. Show Service - Title validation (max 500 characters) - Notes validation (max 5000 characters) - Cover image URL validation (max 2048 characters, http/https only) - Rating validation (0-10 integers) - Tags validation (each max 50 characters) - Links validation (valid URLs, max lengths) 4. Manga Service - Title validation (max 500 characters) - Author validation (max 200 characters) - Notes validation (max 5000 characters) - Cover image URL validation (max 2048 characters, http/https only) - Rating validation (0-10 integers) - Tags validation (each max 50 characters) - Links validation (valid URLs, max lengths) Security Improvements: All services now protect against: - XSS attacks via malicious URLs (javascript:, data:, vbscript:, file:) - Buffer overflow via excessively long strings - Invalid data formats - DoS attacks via massive input Validation Pattern: Each service includes: - Private validateData() method with comprehensive checks - Validation calls at the start of create() and update() methods - Descriptive error messages for all validation failures - Consistent use of MAX_LENGTHS constants Files Modified: - api/src/app/services/music.service.ts - api/src/app/services/art.service.ts - api/src/app/services/show.service.ts - api/src/app/services/manga.service.ts The entire application now has consistent, comprehensive input validation across all user-facing services!
This commit is contained in:
@@ -6,12 +6,68 @@
|
|||||||
|
|
||||||
import { Art, CreateArtDto, UpdateArtDto } from "@library/shared-types";
|
import { Art, CreateArtDto, UpdateArtDto } from "@library/shared-types";
|
||||||
import { prisma } from "../lib/prisma";
|
import { prisma } from "../lib/prisma";
|
||||||
|
import {
|
||||||
|
validateUrl,
|
||||||
|
validateStringLength,
|
||||||
|
MAX_LENGTHS,
|
||||||
|
} from "../utils/validation";
|
||||||
|
|
||||||
export class ArtService {
|
export class ArtService {
|
||||||
private prisma = prisma;
|
private prisma = prisma;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate art data for security.
|
||||||
|
*/
|
||||||
|
private validateArtData(data: CreateArtDto | UpdateArtDto): void {
|
||||||
|
// Validate string lengths
|
||||||
|
if (!validateStringLength(data.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.artist, MAX_LENGTHS.AUTHOR)) {
|
||||||
|
throw new Error(`Artist must be ${MAX_LENGTHS.AUTHOR} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.description, MAX_LENGTHS.DESCRIPTION)) {
|
||||||
|
throw new Error(`Description must be ${MAX_LENGTHS.DESCRIPTION} characters or less.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate image URL (required)
|
||||||
|
if (!data.imageUrl) {
|
||||||
|
throw new Error("Image URL is required.");
|
||||||
|
}
|
||||||
|
if (!validateUrl(data.imageUrl)) {
|
||||||
|
throw new Error("Invalid image URL. Only http and https URLs are allowed.");
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.imageUrl, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tags
|
||||||
|
if (data.tags) {
|
||||||
|
for (const tag of data.tags) {
|
||||||
|
if (!validateStringLength(tag, MAX_LENGTHS.TAGS)) {
|
||||||
|
throw new Error(`Each tag must be ${MAX_LENGTHS.TAGS} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate link URLs
|
||||||
|
if (data.links) {
|
||||||
|
for (const link of data.links) {
|
||||||
|
if (!validateUrl(link.url)) {
|
||||||
|
throw new Error(`Invalid link URL: ${link.title}. Only http and https URLs are allowed.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.title, MAX_LENGTHS.AUTHOR)) {
|
||||||
|
throw new Error(`Link title must be ${MAX_LENGTHS.AUTHOR} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.url, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Link URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all art pieces.
|
* Get all art pieces.
|
||||||
*/
|
*/
|
||||||
@@ -56,6 +112,9 @@ export class ArtService {
|
|||||||
* Create new art piece.
|
* Create new art piece.
|
||||||
*/
|
*/
|
||||||
async createArt(data: CreateArtDto): Promise<Art> {
|
async createArt(data: CreateArtDto): Promise<Art> {
|
||||||
|
// Validate input
|
||||||
|
this.validateArtData(data);
|
||||||
|
|
||||||
const art = await this.prisma.art.create({
|
const art = await this.prisma.art.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
@@ -75,6 +134,9 @@ export class ArtService {
|
|||||||
* Update art by ID.
|
* Update art by ID.
|
||||||
*/
|
*/
|
||||||
async updateArt(id: string, data: UpdateArtDto): Promise<Art> {
|
async updateArt(id: string, data: UpdateArtDto): Promise<Art> {
|
||||||
|
// Validate input
|
||||||
|
this.validateArtData(data);
|
||||||
|
|
||||||
const art = await this.prisma.art.update({
|
const art = await this.prisma.art.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data,
|
data,
|
||||||
|
|||||||
@@ -6,12 +6,71 @@
|
|||||||
|
|
||||||
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto } from "@library/shared-types";
|
import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto } from "@library/shared-types";
|
||||||
import { prisma } from "../lib/prisma";
|
import { prisma } from "../lib/prisma";
|
||||||
|
import {
|
||||||
|
validateUrl,
|
||||||
|
validateRating,
|
||||||
|
validateStringLength,
|
||||||
|
MAX_LENGTHS,
|
||||||
|
} from "../utils/validation";
|
||||||
|
|
||||||
export class MangaService {
|
export class MangaService {
|
||||||
private prisma = prisma;
|
private prisma = prisma;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate manga data for security.
|
||||||
|
*/
|
||||||
|
private validateMangaData(data: CreateMangaDto | UpdateMangaDto): void {
|
||||||
|
// Validate string lengths
|
||||||
|
if (!validateStringLength(data.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.author, MAX_LENGTHS.AUTHOR)) {
|
||||||
|
throw new Error(`Author must be ${MAX_LENGTHS.AUTHOR} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
|
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rating
|
||||||
|
if (!validateRating(data.rating)) {
|
||||||
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cover image URL
|
||||||
|
if (data.coverImage && !validateUrl(data.coverImage)) {
|
||||||
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tags
|
||||||
|
if (data.tags) {
|
||||||
|
for (const tag of data.tags) {
|
||||||
|
if (!validateStringLength(tag, MAX_LENGTHS.TAGS)) {
|
||||||
|
throw new Error(`Each tag must be ${MAX_LENGTHS.TAGS} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate link URLs
|
||||||
|
if (data.links) {
|
||||||
|
for (const link of data.links) {
|
||||||
|
if (!validateUrl(link.url)) {
|
||||||
|
throw new Error(`Invalid link URL: ${link.title}. Only http and https URLs are allowed.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Link title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.url, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Link URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAllManga(): Promise<Manga[]> {
|
async getAllManga(): Promise<Manga[]> {
|
||||||
const manga = await this.prisma.manga.findMany({
|
const manga = await this.prisma.manga.findMany({
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
@@ -53,6 +112,9 @@ export class MangaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createManga(data: CreateMangaDto): Promise<Manga> {
|
async createManga(data: CreateMangaDto): Promise<Manga> {
|
||||||
|
// Validate input
|
||||||
|
this.validateMangaData(data);
|
||||||
|
|
||||||
const manga = await this.prisma.manga.create({
|
const manga = await this.prisma.manga.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
@@ -75,6 +137,9 @@ export class MangaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateManga(id: string, data: UpdateMangaDto): Promise<Manga> {
|
async updateManga(id: string, data: UpdateMangaDto): Promise<Manga> {
|
||||||
|
// Validate input
|
||||||
|
this.validateMangaData(data);
|
||||||
|
|
||||||
const updateData = { ...data };
|
const updateData = { ...data };
|
||||||
if (updateData.status) {
|
if (updateData.status) {
|
||||||
updateData.status = updateData.status.toUpperCase() as any;
|
updateData.status = updateData.status.toUpperCase() as any;
|
||||||
|
|||||||
@@ -6,12 +6,71 @@
|
|||||||
|
|
||||||
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto } from "@library/shared-types";
|
import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto } from "@library/shared-types";
|
||||||
import { prisma } from "../lib/prisma";
|
import { prisma } from "../lib/prisma";
|
||||||
|
import {
|
||||||
|
validateUrl,
|
||||||
|
validateRating,
|
||||||
|
validateStringLength,
|
||||||
|
MAX_LENGTHS,
|
||||||
|
} from "../utils/validation";
|
||||||
|
|
||||||
export class MusicService {
|
export class MusicService {
|
||||||
private prisma = prisma;
|
private prisma = prisma;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate music data for security.
|
||||||
|
*/
|
||||||
|
private validateMusicData(data: CreateMusicDto | UpdateMusicDto): void {
|
||||||
|
// Validate string lengths
|
||||||
|
if (!validateStringLength(data.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.artist, MAX_LENGTHS.AUTHOR)) {
|
||||||
|
throw new Error(`Artist must be ${MAX_LENGTHS.AUTHOR} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
|
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.coverArt, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover art URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rating
|
||||||
|
if (data.rating !== undefined && !validateRating(data.rating)) {
|
||||||
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cover art URL
|
||||||
|
if (data.coverArt && !validateUrl(data.coverArt)) {
|
||||||
|
throw new Error("Invalid cover art URL. Only http and https URLs are allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tags
|
||||||
|
if (data.tags) {
|
||||||
|
for (const tag of data.tags) {
|
||||||
|
if (!validateStringLength(tag, MAX_LENGTHS.TAGS)) {
|
||||||
|
throw new Error(`Each tag must be ${MAX_LENGTHS.TAGS} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate link URLs
|
||||||
|
if (data.links) {
|
||||||
|
for (const link of data.links) {
|
||||||
|
if (!validateUrl(link.url)) {
|
||||||
|
throw new Error(`Invalid link URL: ${link.title}. Only http and https URLs are allowed.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Link title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.url, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Link URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all music.
|
* Get all music.
|
||||||
*/
|
*/
|
||||||
@@ -64,6 +123,9 @@ export class MusicService {
|
|||||||
* Create new music.
|
* Create new music.
|
||||||
*/
|
*/
|
||||||
async createMusic(data: CreateMusicDto): Promise<Music> {
|
async createMusic(data: CreateMusicDto): Promise<Music> {
|
||||||
|
// Validate input
|
||||||
|
this.validateMusicData(data);
|
||||||
|
|
||||||
const music = await this.prisma.music.create({
|
const music = await this.prisma.music.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
@@ -91,6 +153,9 @@ export class MusicService {
|
|||||||
* Update music by ID.
|
* Update music by ID.
|
||||||
*/
|
*/
|
||||||
async updateMusic(id: string, data: UpdateMusicDto): Promise<Music> {
|
async updateMusic(id: string, data: UpdateMusicDto): Promise<Music> {
|
||||||
|
// Validate input
|
||||||
|
this.validateMusicData(data);
|
||||||
|
|
||||||
const updateData = { ...data };
|
const updateData = { ...data };
|
||||||
if (updateData.type) {
|
if (updateData.type) {
|
||||||
updateData.type = updateData.type.toUpperCase() as any;
|
updateData.type = updateData.type.toUpperCase() as any;
|
||||||
|
|||||||
@@ -6,12 +6,68 @@
|
|||||||
|
|
||||||
import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto } from "@library/shared-types";
|
import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto } from "@library/shared-types";
|
||||||
import { prisma } from "../lib/prisma";
|
import { prisma } from "../lib/prisma";
|
||||||
|
import {
|
||||||
|
validateUrl,
|
||||||
|
validateRating,
|
||||||
|
validateStringLength,
|
||||||
|
MAX_LENGTHS,
|
||||||
|
} from "../utils/validation";
|
||||||
|
|
||||||
export class ShowService {
|
export class ShowService {
|
||||||
private prisma = prisma;
|
private prisma = prisma;
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate show data for security.
|
||||||
|
*/
|
||||||
|
private validateShowData(data: CreateShowDto | UpdateShowDto): void {
|
||||||
|
// Validate string lengths
|
||||||
|
if (!validateStringLength(data.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.notes, MAX_LENGTHS.NOTES)) {
|
||||||
|
throw new Error(`Notes must be ${MAX_LENGTHS.NOTES} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(data.coverImage, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Cover image URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate rating
|
||||||
|
if (!validateRating(data.rating)) {
|
||||||
|
throw new Error("Rating must be an integer between 0 and 10.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate cover image URL
|
||||||
|
if (data.coverImage && !validateUrl(data.coverImage)) {
|
||||||
|
throw new Error("Invalid cover image URL. Only http and https URLs are allowed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tags
|
||||||
|
if (data.tags) {
|
||||||
|
for (const tag of data.tags) {
|
||||||
|
if (!validateStringLength(tag, MAX_LENGTHS.TAGS)) {
|
||||||
|
throw new Error(`Each tag must be ${MAX_LENGTHS.TAGS} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate link URLs
|
||||||
|
if (data.links) {
|
||||||
|
for (const link of data.links) {
|
||||||
|
if (!validateUrl(link.url)) {
|
||||||
|
throw new Error(`Invalid link URL: ${link.title}. Only http and https URLs are allowed.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.title, MAX_LENGTHS.TITLE)) {
|
||||||
|
throw new Error(`Link title must be ${MAX_LENGTHS.TITLE} characters or less.`);
|
||||||
|
}
|
||||||
|
if (!validateStringLength(link.url, MAX_LENGTHS.URL)) {
|
||||||
|
throw new Error(`Link URL must be ${MAX_LENGTHS.URL} characters or less.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getAllShows(): Promise<Show[]> {
|
async getAllShows(): Promise<Show[]> {
|
||||||
const shows = await this.prisma.show.findMany({
|
const shows = await this.prisma.show.findMany({
|
||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
@@ -55,6 +111,9 @@ export class ShowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createShow(data: CreateShowDto): Promise<Show> {
|
async createShow(data: CreateShowDto): Promise<Show> {
|
||||||
|
// Validate input
|
||||||
|
this.validateShowData(data);
|
||||||
|
|
||||||
const show = await this.prisma.show.create({
|
const show = await this.prisma.show.create({
|
||||||
data: {
|
data: {
|
||||||
...data,
|
...data,
|
||||||
@@ -79,6 +138,9 @@ export class ShowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateShow(id: string, data: UpdateShowDto): Promise<Show> {
|
async updateShow(id: string, data: UpdateShowDto): Promise<Show> {
|
||||||
|
// Validate input
|
||||||
|
this.validateShowData(data);
|
||||||
|
|
||||||
const updateData = { ...data };
|
const updateData = { ...data };
|
||||||
if (updateData.type) {
|
if (updateData.type) {
|
||||||
updateData.type = updateData.type.toUpperCase() as any;
|
updateData.type = updateData.type.toUpperCase() as any;
|
||||||
|
|||||||
Reference in New Issue
Block a user