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 { 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