generated from nhcarrigan/template
feat: Multiple Features, Accessibility, Security, and UX Improvements #59
@@ -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<Art> {
|
||||
// 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<Art> {
|
||||
// Validate input
|
||||
this.validateArtData(data);
|
||||
|
||||
const art = await this.prisma.art.update({
|
||||
where: { id },
|
||||
data,
|
||||
|
||||
@@ -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<Manga[]> {
|
||||
const manga = await this.prisma.manga.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
@@ -53,6 +112,9 @@ export class MangaService {
|
||||
}
|
||||
|
||||
async createManga(data: CreateMangaDto): Promise<Manga> {
|
||||
// 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<Manga> {
|
||||
// Validate input
|
||||
this.validateMangaData(data);
|
||||
|
||||
const updateData = { ...data };
|
||||
if (updateData.status) {
|
||||
updateData.status = updateData.status.toUpperCase() as any;
|
||||
|
||||
@@ -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<Music> {
|
||||
// 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<Music> {
|
||||
// Validate input
|
||||
this.validateMusicData(data);
|
||||
|
||||
const updateData = { ...data };
|
||||
if (updateData.type) {
|
||||
updateData.type = updateData.type.toUpperCase() as any;
|
||||
|
||||
@@ -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<Show[]> {
|
||||
const shows = await this.prisma.show.findMany({
|
||||
orderBy: { updatedAt: "desc" },
|
||||
@@ -55,6 +111,9 @@ export class ShowService {
|
||||
}
|
||||
|
||||
async createShow(data: CreateShowDto): Promise<Show> {
|
||||
// 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<Show> {
|
||||
// Validate input
|
||||
this.validateShowData(data);
|
||||
|
||||
const updateData = { ...data };
|
||||
if (updateData.type) {
|
||||
updateData.type = updateData.type.toUpperCase() as any;
|
||||
|
||||
Reference in New Issue
Block a user