diff --git a/api/src/app/services/art.service.ts b/api/src/app/services/art.service.ts index 5b8649c..7a3d648 100644 --- a/api/src/app/services/art.service.ts +++ b/api/src/app/services/art.service.ts @@ -6,12 +6,68 @@ import { Art, CreateArtDto, UpdateArtDto } from "@library/shared-types"; import { prisma } from "../lib/prisma"; +import { + validateUrl, + validateStringLength, + MAX_LENGTHS, +} from "../utils/validation"; export class ArtService { private prisma = prisma; 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. */ @@ -56,6 +112,9 @@ export class ArtService { * Create new art piece. */ async createArt(data: CreateArtDto): Promise { + // Validate input + this.validateArtData(data); + const art = await this.prisma.art.create({ data, }); @@ -75,6 +134,9 @@ export class ArtService { * Update art by ID. */ async updateArt(id: string, data: UpdateArtDto): Promise { + // Validate input + this.validateArtData(data); + const art = await this.prisma.art.update({ where: { id }, data, diff --git a/api/src/app/services/manga.service.ts b/api/src/app/services/manga.service.ts index f63dcd0..e164622 100644 --- a/api/src/app/services/manga.service.ts +++ b/api/src/app/services/manga.service.ts @@ -6,12 +6,71 @@ import { Manga, MangaStatus, CreateMangaDto, UpdateMangaDto } from "@library/shared-types"; import { prisma } from "../lib/prisma"; +import { + validateUrl, + validateRating, + validateStringLength, + MAX_LENGTHS, +} from "../utils/validation"; export class MangaService { private prisma = prisma; 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 { const manga = await this.prisma.manga.findMany({ orderBy: { updatedAt: "desc" }, @@ -53,6 +112,9 @@ export class MangaService { } async createManga(data: CreateMangaDto): Promise { + // Validate input + this.validateMangaData(data); + const manga = await this.prisma.manga.create({ data: { ...data, @@ -75,6 +137,9 @@ export class MangaService { } async updateManga(id: string, data: UpdateMangaDto): Promise { + // Validate input + this.validateMangaData(data); + const updateData = { ...data }; if (updateData.status) { updateData.status = updateData.status.toUpperCase() as any; diff --git a/api/src/app/services/music.service.ts b/api/src/app/services/music.service.ts index 405d8f0..34890f6 100644 --- a/api/src/app/services/music.service.ts +++ b/api/src/app/services/music.service.ts @@ -6,12 +6,71 @@ import { Music, MusicStatus, MusicType, CreateMusicDto, UpdateMusicDto } from "@library/shared-types"; import { prisma } from "../lib/prisma"; +import { + validateUrl, + validateRating, + validateStringLength, + MAX_LENGTHS, +} from "../utils/validation"; export class MusicService { private prisma = prisma; 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. */ @@ -64,6 +123,9 @@ export class MusicService { * Create new music. */ async createMusic(data: CreateMusicDto): Promise { + // Validate input + this.validateMusicData(data); + const music = await this.prisma.music.create({ data: { ...data, @@ -91,6 +153,9 @@ export class MusicService { * Update music by ID. */ async updateMusic(id: string, data: UpdateMusicDto): Promise { + // Validate input + this.validateMusicData(data); + const updateData = { ...data }; if (updateData.type) { updateData.type = updateData.type.toUpperCase() as any; diff --git a/api/src/app/services/show.service.ts b/api/src/app/services/show.service.ts index 7d87652..51bcd40 100644 --- a/api/src/app/services/show.service.ts +++ b/api/src/app/services/show.service.ts @@ -6,12 +6,68 @@ import { Show, ShowStatus, ShowType, CreateShowDto, UpdateShowDto } from "@library/shared-types"; import { prisma } from "../lib/prisma"; +import { + validateUrl, + validateRating, + validateStringLength, + MAX_LENGTHS, +} from "../utils/validation"; export class ShowService { private prisma = prisma; 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 { const shows = await this.prisma.show.findMany({ orderBy: { updatedAt: "desc" }, @@ -55,6 +111,9 @@ export class ShowService { } async createShow(data: CreateShowDto): Promise { + // Validate input + this.validateShowData(data); + const show = await this.prisma.show.create({ data: { ...data, @@ -79,6 +138,9 @@ export class ShowService { } async updateShow(id: string, data: UpdateShowDto): Promise { + // Validate input + this.validateShowData(data); + const updateData = { ...data }; if (updateData.type) { updateData.type = updateData.type.toUpperCase() as any;