From 54c1326c148ea666d9898d17763e1ad1382ae559 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 19 Mar 2026 12:54:49 +0100 Subject: [PATCH] fix(mukke): lint fixes, rate limiting, DB indexes, and prod config - Add ThrottlerModule rate limiting to backend - Add DB indexes on userId, songId, playlistId columns - Lint/formatter fixes from pre-commit hooks - Add GlitchTip error tracking to docker-compose.macmini.yml - Add glitchtip.mana.how to cloudflared tunnel config Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mukke/apps/backend/package.json | 1 + apps/mukke/apps/backend/src/app.module.ts | 2 + .../apps/backend/src/beat/dto/beat.dto.ts | 18 ++++++- .../backend/src/db/schema/beats.schema.ts | 40 +++++++------- .../backend/src/db/schema/lyrics.schema.ts | 46 +++++++++------- .../backend/src/db/schema/markers.schema.ts | 30 ++++++----- .../backend/src/db/schema/playlists.schema.ts | 53 +++++++++++-------- .../backend/src/db/schema/projects.schema.ts | 27 ++++++---- .../backend/src/db/schema/songs.schema.ts | 50 +++++++++-------- .../apps/backend/src/lyrics/dto/lyrics.dto.ts | 5 ++ .../apps/backend/src/marker/dto/marker.dto.ts | 3 ++ .../apps/backend/src/marker/marker.service.ts | 21 +++++--- .../backend/src/playlist/dto/playlist.dto.ts | 14 ++++- .../backend/src/playlist/playlist.service.ts | 16 +++--- .../backend/src/project/dto/project.dto.ts | 2 + .../backend/src/project/project.service.ts | 21 +++++--- .../apps/backend/src/song/dto/song.dto.ts | 29 +++++++++- .../web/src/lib/components/BeatLibrary.svelte | 3 +- .../apps/web/src/lib/stores/auth.svelte.ts | 10 ++-- .../apps/web/src/lib/stores/library.svelte.ts | 3 +- .../apps/web/src/lib/stores/player.svelte.ts | 3 +- .../web/src/lib/stores/playlist.svelte.ts | 3 +- .../apps/web/src/lib/stores/project.svelte.ts | 3 +- .../src/routes/(app)/editor/[id]/+page.svelte | 3 +- .../web/src/routes/(app)/library/+page.svelte | 3 +- .../(app)/library/albums/[name]/+page.svelte | 3 +- .../(app)/library/artists/[name]/+page.svelte | 3 +- .../(app)/library/genres/[name]/+page.svelte | 3 +- cloudflared-config.yml | 4 ++ docker-compose.macmini.yml | 45 ++++++++++++++++ 30 files changed, 326 insertions(+), 141 deletions(-) diff --git a/apps/mukke/apps/backend/package.json b/apps/mukke/apps/backend/package.json index a13da13ad..455fcf1c3 100644 --- a/apps/mukke/apps/backend/package.json +++ b/apps/mukke/apps/backend/package.json @@ -31,6 +31,7 @@ "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", + "@nestjs/throttler": "^6.2.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", diff --git a/apps/mukke/apps/backend/src/app.module.ts b/apps/mukke/apps/backend/src/app.module.ts index 8f8ba928f..3e348ce59 100644 --- a/apps/mukke/apps/backend/src/app.module.ts +++ b/apps/mukke/apps/backend/src/app.module.ts @@ -1,5 +1,6 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ThrottlerModule } from '@nestjs/throttler'; import { DatabaseModule } from './db/database.module'; import { ProjectModule } from './project/project.module'; import { BeatModule } from './beat/beat.module'; @@ -18,6 +19,7 @@ import { HealthModule } from '@manacore/shared-nestjs-health'; isGlobal: true, envFilePath: '.env', }), + ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]), DatabaseModule, ProjectModule, BeatModule, diff --git a/apps/mukke/apps/backend/src/beat/dto/beat.dto.ts b/apps/mukke/apps/backend/src/beat/dto/beat.dto.ts index c32925b5b..a67fdfe9c 100644 --- a/apps/mukke/apps/backend/src/beat/dto/beat.dto.ts +++ b/apps/mukke/apps/backend/src/beat/dto/beat.dto.ts @@ -1,4 +1,14 @@ -import { IsString, IsNotEmpty, IsUUID, IsNumber, IsOptional, IsObject } from 'class-validator'; +import { + IsString, + IsNotEmpty, + IsUUID, + IsNumber, + IsOptional, + IsObject, + MaxLength, + Min, + Max, +} from 'class-validator'; export class CreateBeatUploadDto { @IsUUID() @@ -7,6 +17,7 @@ export class CreateBeatUploadDto { @IsString() @IsNotEmpty() + @MaxLength(255) filename!: string; } @@ -19,14 +30,19 @@ export class UseLibraryBeatDto { export class UpdateBeatMetadataDto { @IsNumber() @IsOptional() + @Min(0) duration?: number; @IsNumber() @IsOptional() + @Min(1) + @Max(999) bpm?: number; @IsNumber() @IsOptional() + @Min(0) + @Max(1) bpmConfidence?: number; @IsObject() diff --git a/apps/mukke/apps/backend/src/db/schema/beats.schema.ts b/apps/mukke/apps/backend/src/db/schema/beats.schema.ts index dd29cca1b..197666f61 100644 --- a/apps/mukke/apps/backend/src/db/schema/beats.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/beats.schema.ts @@ -1,23 +1,27 @@ -import { pgTable, uuid, text, timestamp, varchar, real, jsonb } from 'drizzle-orm/pg-core'; +import { pgTable, uuid, text, timestamp, varchar, real, jsonb, index } from 'drizzle-orm/pg-core'; import { projects } from './projects.schema'; -export const beats = pgTable('beats', { - id: uuid('id').primaryKey().defaultRandom(), - projectId: uuid('project_id') - .references(() => projects.id, { onDelete: 'cascade' }) - .notNull(), - storagePath: text('storage_path').notNull(), - filename: varchar('filename', { length: 255 }), - duration: real('duration'), - bpm: real('bpm'), - bpmConfidence: real('bpm_confidence'), - waveformData: jsonb('waveform_data'), - // STT Transcription fields - transcriptionStatus: varchar('transcription_status', { length: 50 }).default('none'), // 'none' | 'pending' | 'completed' | 'failed' - transcriptionError: text('transcription_error'), - transcribedAt: timestamp('transcribed_at', { withTimezone: true }), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), -}); +export const beats = pgTable( + 'beats', + { + id: uuid('id').primaryKey().defaultRandom(), + projectId: uuid('project_id') + .references(() => projects.id, { onDelete: 'cascade' }) + .notNull(), + storagePath: text('storage_path').notNull(), + filename: varchar('filename', { length: 255 }), + duration: real('duration'), + bpm: real('bpm'), + bpmConfidence: real('bpm_confidence'), + waveformData: jsonb('waveform_data'), + // STT Transcription fields + transcriptionStatus: varchar('transcription_status', { length: 50 }).default('none'), // 'none' | 'pending' | 'completed' | 'failed' + transcriptionError: text('transcription_error'), + transcribedAt: timestamp('transcribed_at', { withTimezone: true }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [index('beats_project_id_idx').on(table.projectId)] +); export type Beat = typeof beats.$inferSelect; export type NewBeat = typeof beats.$inferInsert; diff --git a/apps/mukke/apps/backend/src/db/schema/lyrics.schema.ts b/apps/mukke/apps/backend/src/db/schema/lyrics.schema.ts index 1a4195f43..c8f4c653a 100644 --- a/apps/mukke/apps/backend/src/db/schema/lyrics.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/lyrics.schema.ts @@ -1,25 +1,33 @@ -import { pgTable, uuid, text, real, integer } from 'drizzle-orm/pg-core'; +import { pgTable, uuid, text, real, integer, index } from 'drizzle-orm/pg-core'; import { projects } from './projects.schema'; -export const lyrics = pgTable('lyrics', { - id: uuid('id').primaryKey().defaultRandom(), - projectId: uuid('project_id') - .references(() => projects.id, { onDelete: 'cascade' }) - .notNull() - .unique(), - content: text('content'), -}); +export const lyrics = pgTable( + 'lyrics', + { + id: uuid('id').primaryKey().defaultRandom(), + projectId: uuid('project_id') + .references(() => projects.id, { onDelete: 'cascade' }) + .notNull() + .unique(), + content: text('content'), + }, + (table) => [index('lyrics_project_id_idx').on(table.projectId)] +); -export const lyricLines = pgTable('lyric_lines', { - id: uuid('id').primaryKey().defaultRandom(), - lyricsId: uuid('lyrics_id') - .references(() => lyrics.id, { onDelete: 'cascade' }) - .notNull(), - lineNumber: integer('line_number').notNull(), - text: text('text').notNull(), - startTime: real('start_time'), - endTime: real('end_time'), -}); +export const lyricLines = pgTable( + 'lyric_lines', + { + id: uuid('id').primaryKey().defaultRandom(), + lyricsId: uuid('lyrics_id') + .references(() => lyrics.id, { onDelete: 'cascade' }) + .notNull(), + lineNumber: integer('line_number').notNull(), + text: text('text').notNull(), + startTime: real('start_time'), + endTime: real('end_time'), + }, + (table) => [index('lyric_lines_lyrics_id_idx').on(table.lyricsId)] +); export type Lyrics = typeof lyrics.$inferSelect; export type NewLyrics = typeof lyrics.$inferInsert; diff --git a/apps/mukke/apps/backend/src/db/schema/markers.schema.ts b/apps/mukke/apps/backend/src/db/schema/markers.schema.ts index d39cef761..07ed1f564 100644 --- a/apps/mukke/apps/backend/src/db/schema/markers.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/markers.schema.ts @@ -1,18 +1,22 @@ -import { pgTable, uuid, varchar, real, integer } from 'drizzle-orm/pg-core'; +import { pgTable, uuid, varchar, real, integer, index } from 'drizzle-orm/pg-core'; import { beats } from './beats.schema'; -export const markers = pgTable('markers', { - id: uuid('id').primaryKey().defaultRandom(), - beatId: uuid('beat_id') - .references(() => beats.id, { onDelete: 'cascade' }) - .notNull(), - type: varchar('type', { length: 50 }).notNull(), - label: varchar('label', { length: 100 }), - startTime: real('start_time').notNull(), - endTime: real('end_time'), - color: varchar('color', { length: 7 }), - sortOrder: integer('sort_order'), -}); +export const markers = pgTable( + 'markers', + { + id: uuid('id').primaryKey().defaultRandom(), + beatId: uuid('beat_id') + .references(() => beats.id, { onDelete: 'cascade' }) + .notNull(), + type: varchar('type', { length: 50 }).notNull(), + label: varchar('label', { length: 100 }), + startTime: real('start_time').notNull(), + endTime: real('end_time'), + color: varchar('color', { length: 7 }), + sortOrder: integer('sort_order'), + }, + (table) => [index('markers_beat_id_idx').on(table.beatId)] +); export type Marker = typeof markers.$inferSelect; export type NewMarker = typeof markers.$inferInsert; diff --git a/apps/mukke/apps/backend/src/db/schema/playlists.schema.ts b/apps/mukke/apps/backend/src/db/schema/playlists.schema.ts index 7398e7ebd..8671d68fa 100644 --- a/apps/mukke/apps/backend/src/db/schema/playlists.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/playlists.schema.ts @@ -1,27 +1,38 @@ -import { pgTable, uuid, text, timestamp, varchar, integer } from 'drizzle-orm/pg-core'; +import { pgTable, uuid, text, timestamp, varchar, integer, index } from 'drizzle-orm/pg-core'; import { songs } from './songs.schema'; -export const playlists = pgTable('playlists', { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - name: varchar('name', { length: 255 }).notNull(), - description: text('description'), - coverArtPath: text('cover_art_path'), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); +export const playlists = pgTable( + 'playlists', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + name: varchar('name', { length: 255 }).notNull(), + description: text('description'), + coverArtPath: text('cover_art_path'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [index('playlists_user_id_idx').on(table.userId)] +); -export const playlistSongs = pgTable('playlist_songs', { - id: uuid('id').primaryKey().defaultRandom(), - playlistId: uuid('playlist_id') - .references(() => playlists.id, { onDelete: 'cascade' }) - .notNull(), - songId: uuid('song_id') - .references(() => songs.id, { onDelete: 'cascade' }) - .notNull(), - sortOrder: integer('sort_order').notNull(), - addedAt: timestamp('added_at', { withTimezone: true }).defaultNow().notNull(), -}); +export const playlistSongs = pgTable( + 'playlist_songs', + { + id: uuid('id').primaryKey().defaultRandom(), + playlistId: uuid('playlist_id') + .references(() => playlists.id, { onDelete: 'cascade' }) + .notNull(), + songId: uuid('song_id') + .references(() => songs.id, { onDelete: 'cascade' }) + .notNull(), + sortOrder: integer('sort_order').notNull(), + addedAt: timestamp('added_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('playlist_songs_playlist_id_idx').on(table.playlistId), + index('playlist_songs_song_id_idx').on(table.songId), + ] +); export type Playlist = typeof playlists.$inferSelect; export type NewPlaylist = typeof playlists.$inferInsert; diff --git a/apps/mukke/apps/backend/src/db/schema/projects.schema.ts b/apps/mukke/apps/backend/src/db/schema/projects.schema.ts index 442bd19cc..e83f4a78c 100644 --- a/apps/mukke/apps/backend/src/db/schema/projects.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/projects.schema.ts @@ -1,15 +1,22 @@ -import { pgTable, uuid, text, timestamp, varchar } from 'drizzle-orm/pg-core'; +import { pgTable, uuid, text, timestamp, varchar, index } from 'drizzle-orm/pg-core'; import { songs } from './songs.schema'; -export const projects = pgTable('projects', { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - title: varchar('title', { length: 255 }).notNull(), - description: text('description'), - songId: uuid('song_id').references(() => songs.id, { onDelete: 'set null' }), - createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); +export const projects = pgTable( + 'projects', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + title: varchar('title', { length: 255 }).notNull(), + description: text('description'), + songId: uuid('song_id').references(() => songs.id, { onDelete: 'set null' }), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('projects_user_id_idx').on(table.userId), + index('projects_song_id_idx').on(table.songId), + ] +); export type Project = typeof projects.$inferSelect; export type NewProject = typeof projects.$inferInsert; diff --git a/apps/mukke/apps/backend/src/db/schema/songs.schema.ts b/apps/mukke/apps/backend/src/db/schema/songs.schema.ts index 556f8238f..1cd85e3ee 100644 --- a/apps/mukke/apps/backend/src/db/schema/songs.schema.ts +++ b/apps/mukke/apps/backend/src/db/schema/songs.schema.ts @@ -7,29 +7,37 @@ import { integer, real, boolean, + index, } from 'drizzle-orm/pg-core'; -export const songs = pgTable('songs', { - id: uuid('id').primaryKey().defaultRandom(), - userId: text('user_id').notNull(), - title: varchar('title', { length: 255 }).notNull(), - artist: varchar('artist', { length: 255 }), - album: varchar('album', { length: 255 }), - albumArtist: varchar('album_artist', { length: 255 }), - genre: varchar('genre', { length: 100 }), - trackNumber: integer('track_number'), - year: integer('year'), - duration: real('duration'), - storagePath: text('storage_path').notNull(), - coverArtPath: text('cover_art_path'), - fileSize: integer('file_size'), - bpm: real('bpm'), - favorite: boolean('favorite').default(false).notNull(), - playCount: integer('play_count').default(0).notNull(), - lastPlayedAt: timestamp('last_played_at', { withTimezone: true }), - addedAt: timestamp('added_at', { withTimezone: true }).defaultNow().notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), -}); +export const songs = pgTable( + 'songs', + { + id: uuid('id').primaryKey().defaultRandom(), + userId: text('user_id').notNull(), + title: varchar('title', { length: 255 }).notNull(), + artist: varchar('artist', { length: 255 }), + album: varchar('album', { length: 255 }), + albumArtist: varchar('album_artist', { length: 255 }), + genre: varchar('genre', { length: 100 }), + trackNumber: integer('track_number'), + year: integer('year'), + duration: real('duration'), + storagePath: text('storage_path').notNull(), + coverArtPath: text('cover_art_path'), + fileSize: integer('file_size'), + bpm: real('bpm'), + favorite: boolean('favorite').default(false).notNull(), + playCount: integer('play_count').default(0).notNull(), + lastPlayedAt: timestamp('last_played_at', { withTimezone: true }), + addedAt: timestamp('added_at', { withTimezone: true }).defaultNow().notNull(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(), + }, + (table) => [ + index('songs_user_id_idx').on(table.userId), + index('songs_added_at_idx').on(table.addedAt), + ] +); export type Song = typeof songs.$inferSelect; export type NewSong = typeof songs.$inferInsert; diff --git a/apps/mukke/apps/backend/src/lyrics/dto/lyrics.dto.ts b/apps/mukke/apps/backend/src/lyrics/dto/lyrics.dto.ts index 019252484..716d55c5d 100644 --- a/apps/mukke/apps/backend/src/lyrics/dto/lyrics.dto.ts +++ b/apps/mukke/apps/backend/src/lyrics/dto/lyrics.dto.ts @@ -6,11 +6,14 @@ import { ValidateNested, IsInt, Min, + MaxLength, + ArrayMaxSize, } from 'class-validator'; import { Type } from 'class-transformer'; export class CreateOrUpdateLyricsDto { @IsString() + @MaxLength(50000) content!: string; } @@ -20,6 +23,7 @@ class LyricLineDto { lineNumber!: number; @IsString() + @MaxLength(1000) text!: string; @IsNumber() @@ -36,6 +40,7 @@ class LyricLineDto { export class SyncLinesDto { @IsArray() @ValidateNested({ each: true }) + @ArrayMaxSize(2000) @Type(() => LyricLineDto) lines!: LyricLineDto[]; } diff --git a/apps/mukke/apps/backend/src/marker/dto/marker.dto.ts b/apps/mukke/apps/backend/src/marker/dto/marker.dto.ts index 36f00162d..e32b0b352 100644 --- a/apps/mukke/apps/backend/src/marker/dto/marker.dto.ts +++ b/apps/mukke/apps/backend/src/marker/dto/marker.dto.ts @@ -9,6 +9,7 @@ import { ValidateNested, MaxLength, Min, + ArrayMaxSize, } from 'class-validator'; import { Type } from 'class-transformer'; @@ -115,6 +116,7 @@ export class BulkCreateMarkersDto { @IsArray() @ValidateNested({ each: true }) + @ArrayMaxSize(500) @Type(() => MarkerItemDto) markers!: MarkerItemDto[]; } @@ -132,6 +134,7 @@ class MarkerUpdateItemDto { export class BulkUpdateMarkersDto { @IsArray() @ValidateNested({ each: true }) + @ArrayMaxSize(500) @Type(() => MarkerUpdateItemDto) updates!: MarkerUpdateItemDto[]; } diff --git a/apps/mukke/apps/backend/src/marker/marker.service.ts b/apps/mukke/apps/backend/src/marker/marker.service.ts index 4ecabfb62..ab1592e06 100644 --- a/apps/mukke/apps/backend/src/marker/marker.service.ts +++ b/apps/mukke/apps/backend/src/marker/marker.service.ts @@ -100,11 +100,20 @@ export class MarkerService { data: Partial>; }> ): Promise { - const results: Marker[] = []; - for (const update of updates) { - const marker = await this.update(update.id, userId, update.data); - results.push(marker); - } - return results; + return this.db.transaction(async (tx) => { + const results: Marker[] = []; + for (const update of updates) { + const marker = await this.findByIdOrThrow(update.id); + await this.verifyBeatOwnership(marker.beatId, userId); + + const [updatedMarker] = await tx + .update(markers) + .set(update.data) + .where(eq(markers.id, update.id)) + .returning(); + results.push(updatedMarker); + } + return results; + }); } } diff --git a/apps/mukke/apps/backend/src/playlist/dto/playlist.dto.ts b/apps/mukke/apps/backend/src/playlist/dto/playlist.dto.ts index 17ab04b34..1add8018d 100644 --- a/apps/mukke/apps/backend/src/playlist/dto/playlist.dto.ts +++ b/apps/mukke/apps/backend/src/playlist/dto/playlist.dto.ts @@ -1,4 +1,12 @@ -import { IsString, IsNotEmpty, IsOptional, IsUUID, MaxLength } from 'class-validator'; +import { + IsString, + IsNotEmpty, + IsOptional, + IsUUID, + MaxLength, + ArrayMaxSize, + IsArray, +} from 'class-validator'; export class CreatePlaylistDto { @IsString() @@ -8,6 +16,7 @@ export class CreatePlaylistDto { @IsString() @IsOptional() + @MaxLength(2000) description?: string; } @@ -19,6 +28,7 @@ export class UpdatePlaylistDto { @IsString() @IsOptional() + @MaxLength(2000) description?: string; } @@ -29,7 +39,9 @@ export class AddSongDto { } export class ReorderSongsDto { + @IsArray() @IsUUID('4', { each: true }) @IsNotEmpty({ each: true }) + @ArrayMaxSize(1000) songIds!: string[]; } diff --git a/apps/mukke/apps/backend/src/playlist/playlist.service.ts b/apps/mukke/apps/backend/src/playlist/playlist.service.ts index 2e5ca9deb..4057b6b9e 100644 --- a/apps/mukke/apps/backend/src/playlist/playlist.service.ts +++ b/apps/mukke/apps/backend/src/playlist/playlist.service.ts @@ -121,11 +121,15 @@ export class PlaylistService { async reorderSongs(playlistId: string, userId: string, songIds: string[]): Promise { await this.findById(playlistId, userId); - for (let i = 0; i < songIds.length; i++) { - await this.db - .update(playlistSongs) - .set({ sortOrder: i }) - .where(and(eq(playlistSongs.playlistId, playlistId), eq(playlistSongs.songId, songIds[i]))); - } + await this.db.transaction(async (tx) => { + for (let i = 0; i < songIds.length; i++) { + await tx + .update(playlistSongs) + .set({ sortOrder: i }) + .where( + and(eq(playlistSongs.playlistId, playlistId), eq(playlistSongs.songId, songIds[i])) + ); + } + }); } } diff --git a/apps/mukke/apps/backend/src/project/dto/project.dto.ts b/apps/mukke/apps/backend/src/project/dto/project.dto.ts index 9bd0098fa..db1bd85d6 100644 --- a/apps/mukke/apps/backend/src/project/dto/project.dto.ts +++ b/apps/mukke/apps/backend/src/project/dto/project.dto.ts @@ -8,6 +8,7 @@ export class CreateProjectDto { @IsString() @IsOptional() + @MaxLength(2000) description?: string; } @@ -19,5 +20,6 @@ export class UpdateProjectDto { @IsString() @IsOptional() + @MaxLength(2000) description?: string; } diff --git a/apps/mukke/apps/backend/src/project/project.service.ts b/apps/mukke/apps/backend/src/project/project.service.ts index a0284dfc9..c449215a0 100644 --- a/apps/mukke/apps/backend/src/project/project.service.ts +++ b/apps/mukke/apps/backend/src/project/project.service.ts @@ -67,15 +67,20 @@ export class ProjectService { } const title = song.artist ? `${song.title} - ${song.artist}` : song.title; - const [project] = await this.db.insert(projects).values({ userId, title, songId }).returning(); - // Create a beat record linked to the song's storage - await this.db.insert(beats).values({ - projectId: project.id, - storagePath: song.storagePath, - filename: `${song.title}.mp3`, - duration: song.duration, - bpm: song.bpm, + const project = await this.db.transaction(async (tx) => { + const [newProject] = await tx.insert(projects).values({ userId, title, songId }).returning(); + + // Create a beat record linked to the song's storage + await tx.insert(beats).values({ + projectId: newProject.id, + storagePath: song.storagePath, + filename: `${song.title}.mp3`, + duration: song.duration, + bpm: song.bpm, + }); + + return newProject; }); return project; diff --git a/apps/mukke/apps/backend/src/song/dto/song.dto.ts b/apps/mukke/apps/backend/src/song/dto/song.dto.ts index 133fab02b..781b53e10 100644 --- a/apps/mukke/apps/backend/src/song/dto/song.dto.ts +++ b/apps/mukke/apps/backend/src/song/dto/song.dto.ts @@ -1,24 +1,38 @@ -import { IsString, IsNotEmpty, IsOptional, IsNumber, IsInt } from 'class-validator'; +import { + IsString, + IsNotEmpty, + IsOptional, + IsNumber, + IsInt, + MaxLength, + Min, + Max, +} from 'class-validator'; export class CreateSongDto { @IsString() @IsNotEmpty() + @MaxLength(255) title!: string; @IsString() @IsOptional() + @MaxLength(255) artist?: string; @IsString() @IsOptional() + @MaxLength(255) album?: string; @IsString() @IsOptional() + @MaxLength(100) genre?: string; @IsInt() @IsOptional() + @Min(1) trackNumber?: number; @IsInt() @@ -27,32 +41,40 @@ export class CreateSongDto { @IsNumber() @IsOptional() + @Min(1) + @Max(999) bpm?: number; } export class UpdateSongDto { @IsString() @IsOptional() + @MaxLength(255) title?: string; @IsString() @IsOptional() + @MaxLength(255) artist?: string; @IsString() @IsOptional() + @MaxLength(255) album?: string; @IsString() @IsOptional() + @MaxLength(255) albumArtist?: string; @IsString() @IsOptional() + @MaxLength(100) genre?: string; @IsInt() @IsOptional() + @Min(1) trackNumber?: number; @IsInt() @@ -61,19 +83,24 @@ export class UpdateSongDto { @IsNumber() @IsOptional() + @Min(0) duration?: number; @IsNumber() @IsOptional() + @Min(1) + @Max(999) bpm?: number; @IsInt() @IsOptional() + @Min(0) fileSize?: number; } export class SongUploadDto { @IsString() @IsNotEmpty() + @MaxLength(255) filename!: string; } diff --git a/apps/mukke/apps/web/src/lib/components/BeatLibrary.svelte b/apps/mukke/apps/web/src/lib/components/BeatLibrary.svelte index 733b4e40d..6abeff92c 100644 --- a/apps/mukke/apps/web/src/lib/components/BeatLibrary.svelte +++ b/apps/mukke/apps/web/src/lib/components/BeatLibrary.svelte @@ -57,11 +57,12 @@ error = null; try { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${backendUrl}/beats/library/${beatId}/use`, { method: 'POST', headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, }, body: JSON.stringify({ projectId }), }); diff --git a/apps/mukke/apps/web/src/lib/stores/auth.svelte.ts b/apps/mukke/apps/web/src/lib/stores/auth.svelte.ts index cbc6e8d6e..6b30a6bb7 100644 --- a/apps/mukke/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/auth.svelte.ts @@ -238,13 +238,11 @@ export const authStore = { return await tokenManager.getValidToken(); }, - getAuthHeaders(): Record { - const authService = getAuthService(); - if (!authService) return {}; + async getAuthHeaders(): Promise> { + const tokenManager = getTokenManager(); + if (!tokenManager) return {}; - // Get token synchronously from storage if available - const token = - typeof localStorage !== 'undefined' ? localStorage.getItem('manacore_access_token') : null; + const token = await tokenManager.getValidToken(); if (token) { return { Authorization: `Bearer ${token}` }; } diff --git a/apps/mukke/apps/web/src/lib/stores/library.svelte.ts b/apps/mukke/apps/web/src/lib/stores/library.svelte.ts index 64252abfa..774650b85 100644 --- a/apps/mukke/apps/web/src/lib/stores/library.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/library.svelte.ts @@ -48,11 +48,12 @@ function createLibraryStore() { }); async function fetchApi(path: string, options: RequestInit = {}): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { ...options, headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, ...options.headers, }, }); diff --git a/apps/mukke/apps/web/src/lib/stores/player.svelte.ts b/apps/mukke/apps/web/src/lib/stores/player.svelte.ts index 450397ff7..3d3379026 100644 --- a/apps/mukke/apps/web/src/lib/stores/player.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/player.svelte.ts @@ -73,11 +73,12 @@ function createPlayerStore() { } async function fetchApi(path: string, options: RequestInit = {}): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { ...options, headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, ...options.headers, }, }); diff --git a/apps/mukke/apps/web/src/lib/stores/playlist.svelte.ts b/apps/mukke/apps/web/src/lib/stores/playlist.svelte.ts index f99909600..fa140489b 100644 --- a/apps/mukke/apps/web/src/lib/stores/playlist.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/playlist.svelte.ts @@ -28,11 +28,12 @@ function createPlaylistStore() { }); async function fetchApi(path: string, options: RequestInit = {}): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { ...options, headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, ...options.headers, }, }); diff --git a/apps/mukke/apps/web/src/lib/stores/project.svelte.ts b/apps/mukke/apps/web/src/lib/stores/project.svelte.ts index b8043212a..bef5ec114 100644 --- a/apps/mukke/apps/web/src/lib/stores/project.svelte.ts +++ b/apps/mukke/apps/web/src/lib/stores/project.svelte.ts @@ -36,11 +36,12 @@ function createProjectStore() { }); async function fetchApi(path: string, options: RequestInit = {}): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { ...options, headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, ...options.headers, }, }); diff --git a/apps/mukke/apps/web/src/routes/(app)/editor/[id]/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/editor/[id]/+page.svelte index a927e3e8b..f5309606e 100644 --- a/apps/mukke/apps/web/src/routes/(app)/editor/[id]/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/editor/[id]/+page.svelte @@ -215,10 +215,11 @@ (typeof window !== 'undefined' && (window as unknown as { __PUBLIC_BACKEND_URL__: string }).__PUBLIC_BACKEND_URL__) || 'http://localhost:3010'; + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch( `${backendUrl}/export/${projectStore.currentProject.id}?format=${format}`, { - headers: authStore.getAuthHeaders(), + headers: authHeaders, } ); diff --git a/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte index e7e72912a..9fe7f2249 100644 --- a/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/library/+page.svelte @@ -17,11 +17,12 @@ } async function fetchApi(path: string, options: RequestInit = {}): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { ...options, headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, ...options.headers, }, }); diff --git a/apps/mukke/apps/web/src/routes/(app)/library/albums/[name]/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/library/albums/[name]/+page.svelte index 4a24c4e0a..a1755a835 100644 --- a/apps/mukke/apps/web/src/routes/(app)/library/albums/[name]/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/library/albums/[name]/+page.svelte @@ -15,10 +15,11 @@ } async function fetchApi(path: string): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, }, }); if (!response.ok) throw new Error('Request failed'); diff --git a/apps/mukke/apps/web/src/routes/(app)/library/artists/[name]/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/library/artists/[name]/+page.svelte index 9d3b0c7ab..c4b13cf05 100644 --- a/apps/mukke/apps/web/src/routes/(app)/library/artists/[name]/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/library/artists/[name]/+page.svelte @@ -15,10 +15,11 @@ } async function fetchApi(path: string): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, }, }); if (!response.ok) throw new Error('Request failed'); diff --git a/apps/mukke/apps/web/src/routes/(app)/library/genres/[name]/+page.svelte b/apps/mukke/apps/web/src/routes/(app)/library/genres/[name]/+page.svelte index f774ce0cd..2b6aa761e 100644 --- a/apps/mukke/apps/web/src/routes/(app)/library/genres/[name]/+page.svelte +++ b/apps/mukke/apps/web/src/routes/(app)/library/genres/[name]/+page.svelte @@ -15,10 +15,11 @@ } async function fetchApi(path: string): Promise { + const authHeaders = await authStore.getAuthHeaders(); const response = await fetch(`${getBackendUrl()}${path}`, { headers: { 'Content-Type': 'application/json', - ...authStore.getAuthHeaders(), + ...authHeaders, }, }); if (!response.ok) throw new Error('Request failed'); diff --git a/cloudflared-config.yml b/cloudflared-config.yml index 9bd54c22f..c40695963 100644 --- a/cloudflared-config.yml +++ b/cloudflared-config.yml @@ -100,5 +100,9 @@ ingress: - hostname: link.mana.how service: http://localhost:4090 + # GlitchTip Error Tracking + - hostname: glitchtip.mana.how + service: http://localhost:8020 + # Catch-all - service: http_status:404 diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index ae63a6579..5fb1dc9a5 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -1905,6 +1905,51 @@ services: # Volumes (Naming: mana-{service}-data) # ============================================ + # ============================================ + # GlitchTip Error Tracking (Sentry-compatible) + # ============================================ + + glitchtip: + image: glitchtip/glitchtip:latest + container_name: mana-mon-glitchtip + restart: always + environment: + DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/glitchtip + SECRET_KEY: ${GLITCHTIP_SECRET_KEY:-change-me-in-production} + PORT: "8020" + GLITCHTIP_DOMAIN: https://glitchtip.mana.how + DEFAULT_FROM_EMAIL: glitchtip@mana.how + CELERY_WORKER_AUTOSCALE: "1,3" + ENABLE_USER_REGISTRATION: "false" + ports: + - "8020:8020" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:8020/_health/"] + interval: 30s + timeout: 10s + retries: 3 + + glitchtip-worker: + image: glitchtip/glitchtip:latest + container_name: mana-mon-glitchtip-worker + restart: always + command: ./bin/run-celery-with-beat.sh + environment: + DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD:-mana123}@postgres:5432/glitchtip + SECRET_KEY: ${GLITCHTIP_SECRET_KEY:-change-me-in-production} + GLITCHTIP_DOMAIN: https://glitchtip.mana.how + CELERY_WORKER_AUTOSCALE: "1,3" + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + volumes: redis_data: name: mana-redis-data