diff --git a/src/app.d.ts b/src/app.d.ts index 04c58a5..e6d550a 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -7,7 +7,7 @@ declare global { interface Error { message: string; details?: string; - bibleQuote?: any; + bibleQuote?: { text: string; reference: string } | null; lang?: string; } interface Locals { diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 29c25dd..7593fbd 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -31,7 +31,7 @@ async function authorization({ event, resolve }: Parameters[0]) { else if (!session.user?.groups?.includes('rezepte_users')) { error(403, { message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.' - } as any); + }); } } @@ -42,7 +42,7 @@ async function authorization({ event, resolve }: Parameters[0]) { if (event.url.pathname.startsWith('/api/cospend')) { error(401, { message: 'Anmeldung erforderlich. Du musst angemeldet sein, um auf diesen Bereich zugreifen zu können.' - } as any); + }); } // For page routes, redirect to login const callbackUrl = encodeURIComponent(event.url.pathname + event.url.search); @@ -51,7 +51,7 @@ async function authorization({ event, resolve }: Parameters[0]) { else if (!session.user?.groups?.includes('cospend')) { error(403, { message: 'Zugriff verweigert. Du hast keine Berechtigung für diesen Bereich. Falls du glaubst, dass dies ein Fehler ist, wende dich bitte an Alexander.' - } as any); + }); } } @@ -60,7 +60,7 @@ async function authorization({ event, resolve }: Parameters[0]) { } // Bible verse functionality for error pages -async function getRandomVerse(fetch: typeof globalThis.fetch, pathname: string): Promise { +async function getRandomVerse(fetch: typeof globalThis.fetch, pathname: string): Promise<{ text: string; reference: string } | null> { const isEnglish = pathname.startsWith('/faith/') || pathname.startsWith('/recipes/'); const endpoint = isEnglish ? '/api/faith/bibel/zufallszitat' : '/api/glaube/bibel/zufallszitat'; try { diff --git a/src/lib/js/recipeJsonLd.ts b/src/lib/js/recipeJsonLd.ts index 46b2809..d4ce935 100644 --- a/src/lib/js/recipeJsonLd.ts +++ b/src/lib/js/recipeJsonLd.ts @@ -26,8 +26,37 @@ function parseTimeToISO8601(timeString: string): string | undefined { return undefined; } -export function generateRecipeJsonLd(data: any) { - const jsonLd: any = { +import type { RecipeModelType } from '$types/types'; + +interface HowToStep { + "@type": "HowToStep"; + name: string; + text: string; +} + +interface RecipeJsonLd { + "@context": string; + "@type": string; + name: string; + description: string; + author: { "@type": string; name: string }; + datePublished?: string; + dateModified?: string; + recipeCategory: string; + keywords?: string; + image: { "@type": string; url: string; width: number; height: number }; + recipeIngredient: string[]; + recipeInstructions: HowToStep[]; + url: string; + recipeYield?: string; + prepTime?: string; + cookTime?: string; + totalTime?: string; + [key: string]: unknown; +} + +export function generateRecipeJsonLd(data: RecipeModelType) { + const jsonLd: RecipeJsonLd = { "@context": "https://schema.org", "@type": "Recipe", "name": data.name?.replace(/<[^>]*>/g, ''), // Strip HTML tags @@ -47,7 +76,7 @@ export function generateRecipeJsonLd(data: any) { "height": 800 }, "recipeIngredient": [] as string[], - "recipeInstructions": [] as any[], + "recipeInstructions": [] as HowToStep[], "url": `https://bocken.org/rezepte/${data.short_name}` }; diff --git a/src/lib/js/searchFilter.svelte.ts b/src/lib/js/searchFilter.svelte.ts index b81c9f2..2dddaf3 100644 --- a/src/lib/js/searchFilter.svelte.ts +++ b/src/lib/js/searchFilter.svelte.ts @@ -3,7 +3,7 @@ * Extracts duplicated search state logic from multiple pages. */ -type Recipe = { _id: string; [key: string]: any }; +type Recipe = { _id: string; [key: string]: unknown }; export function createSearchFilter(getRecipes: () => T[]) { let matchedRecipeIds = $state(new Set()); diff --git a/src/lib/server/ai/ollama.ts b/src/lib/server/ai/ollama.ts index 9204ef5..85ca123 100644 --- a/src/lib/server/ai/ollama.ts +++ b/src/lib/server/ai/ollama.ts @@ -96,7 +96,7 @@ export async function listOllamaModels(): Promise { } const data = await response.json(); - return data.models?.map((m: any) => m.name) || []; + return data.models?.map((m: { name: string }) => m.name) || []; } catch (error) { console.error('Failed to list Ollama models:', error); return []; diff --git a/src/lib/server/cache.ts b/src/lib/server/cache.ts index 6e45d0a..f34e791 100644 --- a/src/lib/server/cache.ts +++ b/src/lib/server/cache.ts @@ -312,7 +312,7 @@ export async function invalidateRecipeCaches(): Promise { */ export async function invalidateCospendCaches(usernames: string[], paymentId?: string): Promise { try { - const invalidations: Promise[] = []; + const invalidations: Promise[] = []; // Invalidate balance and debts caches for all affected users for (const username of usernames) { diff --git a/src/lib/server/favorites.ts b/src/lib/server/favorites.ts index 8b155d2..434a822 100644 --- a/src/lib/server/favorites.ts +++ b/src/lib/server/favorites.ts @@ -2,13 +2,18 @@ * Utility functions for handling user favorites on the server side */ -export async function getUserFavorites(fetch: any, locals: any): Promise { +import type { BriefRecipeType } from '$types/types'; +import type { Session } from '@auth/sveltekit'; + +type BriefRecipeWithFavorite = BriefRecipeType & { isFavorite: boolean }; + +export async function getUserFavorites(fetch: typeof globalThis.fetch, locals: App.Locals): Promise { const session = await locals.auth(); - + if (!session?.user?.nickname) { return []; } - + try { const favRes = await fetch('/api/rezepte/favorites'); if (favRes.ok) { @@ -19,17 +24,17 @@ export async function getUserFavorites(fetch: any, locals: any): Promise ({ ...recipe, isFavorite: userFavorites.some(favId => favId.toString() === recipe._id.toString()) @@ -37,18 +42,18 @@ export function addFavoriteStatusToRecipes(recipes: any[], userFavorites: string } export async function loadRecipesWithFavorites( - fetch: any, - locals: any, - recipeLoader: () => Promise -): Promise<{ recipes: any[], session: any }> { + fetch: typeof globalThis.fetch, + locals: App.Locals, + recipeLoader: () => Promise +): Promise<{ recipes: BriefRecipeWithFavorite[], session: Session | null }> { const [recipes, userFavorites, session] = await Promise.all([ recipeLoader(), getUserFavorites(fetch, locals), locals.auth() ]); - + return { recipes: addFavoriteStatusToRecipes(recipes, userFavorites), session }; -} \ No newline at end of file +} diff --git a/src/lib/server/recipeHelpers.ts b/src/lib/server/recipeHelpers.ts index c33ea11..6e24bf6 100644 --- a/src/lib/server/recipeHelpers.ts +++ b/src/lib/server/recipeHelpers.ts @@ -1,4 +1,4 @@ -import type { BriefRecipeType } from '$types/types'; +import type { BriefRecipeType, RecipeModelType } from '$types/types'; /** * Check whether a recipeLang param refers to the English version. @@ -30,7 +30,7 @@ export function briefQueryConfig(recipeLang: string) { * For English, extracts from translations.en and adds germanShortName. * For German, passes through root-level fields. */ -export function toBrief(recipe: any, recipeLang: string): BriefRecipeType { +export function toBrief(recipe: RecipeModelType, recipeLang: string): BriefRecipeType { if (isEnglish(recipeLang)) { return { _id: recipe._id, diff --git a/src/lib/server/scheduler.ts b/src/lib/server/scheduler.ts index 5246ec8..6ba8f4c 100644 --- a/src/lib/server/scheduler.ts +++ b/src/lib/server/scheduler.ts @@ -27,8 +27,8 @@ class RecurringPaymentScheduler { await this.processRecurringPayments(); }, { - timezone: 'Europe/Zurich' // Adjust timezone as needed - } as any); + timezone: 'Europe/Zurich' + }); console.log('[Scheduler] Recurring payments scheduler started (runs every minute)'); } @@ -154,7 +154,8 @@ class RecurringPaymentScheduler { return { isRunning: this.isRunning, isScheduled: this.task !== null, - nextRun: (this.task as any)?.nextDate?.()?.toISOString?.() + // node-cron's ScheduledTask type doesn't expose nextDate, but it exists at runtime + nextRun: (this.task as unknown as { nextDate?: () => { toISOString: () => string } })?.nextDate?.()?.toISOString?.() }; } } diff --git a/src/lib/utils/settlements.ts b/src/lib/utils/settlements.ts index b508639..6b7bc46 100644 --- a/src/lib/utils/settlements.ts +++ b/src/lib/utils/settlements.ts @@ -1,11 +1,16 @@ // Utility functions for identifying and handling settlement payments +import type { IPayment } from '$models/Payment'; +import type { IPaymentSplit } from '$models/PaymentSplit'; + +type PaymentWithSplits = IPayment & { splits?: IPaymentSplit[] }; + /** * Identifies if a payment is a settlement payment based on category */ -export function isSettlementPayment(payment: any): boolean { +export function isSettlementPayment(payment: PaymentWithSplits | null | undefined): boolean { if (!payment) return false; - + // Check if category is settlement return payment.category === 'settlement'; } @@ -20,44 +25,44 @@ export function getSettlementIcon(): string { /** * Gets appropriate styling classes for settlement payments */ -export function getSettlementClasses(payment: any): string[] { +export function getSettlementClasses(payment: PaymentWithSplits): string[] { if (!isSettlementPayment(payment)) { return []; } - + return ['settlement-payment']; } /** * Gets settlement-specific display text */ -export function getSettlementDisplayText(payment: any): string { +export function getSettlementDisplayText(payment: PaymentWithSplits): string { if (!isSettlementPayment(payment)) { return ''; } - + return 'Settlement'; } /** * Gets the other user in a settlement (the one who didn't pay) */ -export function getSettlementReceiver(payment: any): string { +export function getSettlementReceiver(payment: PaymentWithSplits): string { if (!isSettlementPayment(payment) || !payment.splits) { return ''; } - + // Find the user who has a positive amount (the receiver) - const receiver = payment.splits.find((split: any) => split.amount > 0); + const receiver = payment.splits.find((split) => split.amount > 0); if (receiver && receiver.username) { return receiver.username; } - + // Fallback: find the user who is not the payer - const otherUser = payment.splits.find((split: any) => split.username !== payment.paidBy); + const otherUser = payment.splits.find((split) => split.username !== payment.paidBy); if (otherUser && otherUser.username) { return otherUser.username; } - + return ''; -} \ No newline at end of file +} diff --git a/src/models/Recipe.ts b/src/models/Recipe.ts index ef2fa4c..85f8efd 100644 --- a/src/models/Recipe.ts +++ b/src/models/Recipe.ts @@ -175,6 +175,8 @@ const RecipeSchema = new mongoose.Schema( RecipeSchema.index({ "translations.en.short_name": 1 }); RecipeSchema.index({ "translations.en.translationStatus": 1 }); -let _recipeModel: any; -try { _recipeModel = mongoose.model("Recipe"); } catch { _recipeModel = mongoose.model("Recipe", RecipeSchema); } -export const Recipe = _recipeModel as mongoose.Model; +import type { RecipeModelType } from '$types/types'; + +let _recipeModel: mongoose.Model; +try { _recipeModel = mongoose.model("Recipe"); } catch { _recipeModel = mongoose.model("Recipe", RecipeSchema); } +export const Recipe = _recipeModel; diff --git a/src/models/RecurringPayment.ts b/src/models/RecurringPayment.ts index a286b1f..ba57c8d 100644 --- a/src/models/RecurringPayment.ts +++ b/src/models/RecurringPayment.ts @@ -93,7 +93,7 @@ const RecurringPaymentSchema = new mongoose.Schema( cronExpression: { type: String, validate: { - validator: function(this: any, value: string) { + validator: function(this: IRecurringPayment, value: string) { // Only validate if frequency is custom if (this.frequency === 'custom') { return value != null && value.trim().length > 0; diff --git a/src/models/RosaryStreak.ts b/src/models/RosaryStreak.ts index 7a68e1d..5531606 100644 --- a/src/models/RosaryStreak.ts +++ b/src/models/RosaryStreak.ts @@ -9,7 +9,12 @@ const RosaryStreakSchema = new mongoose.Schema( { timestamps: true } ); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let _model: any; -try { _model = mongoose.model("RosaryStreak"); } catch { _model = mongoose.model("RosaryStreak", RosaryStreakSchema); } -export const RosaryStreak = _model as mongoose.Model; +interface IRosaryStreak { + username: string; + length: number; + lastPrayed: string | null; +} + +let _model: mongoose.Model; +try { _model = mongoose.model("RosaryStreak"); } catch { _model = mongoose.model("RosaryStreak", RosaryStreakSchema); } +export const RosaryStreak = _model; diff --git a/src/routes/[recipeLang=recipeLang]/add/+page.server.ts b/src/routes/[recipeLang=recipeLang]/add/+page.server.ts index 144cb0e..513fbdb 100644 --- a/src/routes/[recipeLang=recipeLang]/add/+page.server.ts +++ b/src/routes/[recipeLang=recipeLang]/add/+page.server.ts @@ -82,10 +82,11 @@ export const actions = { caption: '', color }]; - } catch (imageError: any) { + } catch (imageError: unknown) { console.error('[RecipeAdd] Image processing error:', imageError); + const message = imageError instanceof Error ? imageError.message : String(imageError); return fail(400, { - error: `Failed to process image: ${imageError.message}`, + error: `Failed to process image: ${message}`, errors: ['Image processing failed'], values: Object.fromEntries(formData) }); @@ -114,16 +115,19 @@ export const actions = { // Redirect to the new recipe page throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`); - } catch (dbError: any) { + } catch (dbError: unknown) { // Re-throw redirects (they're not errors) - if (dbError?.status >= 300 && dbError?.status < 400) { - throw dbError; + if (dbError && typeof dbError === 'object' && 'status' in dbError) { + const status = (dbError as { status: number }).status; + if (status >= 300 && status < 400) { + throw dbError; + } } console.error('Database error creating recipe:', dbError); // Check for duplicate key error - if (dbError.code === 11000) { + if (dbError && typeof dbError === 'object' && 'code' in dbError && (dbError as { code: number }).code === 11000) { return fail(400, { error: `A recipe with the short name "${recipeData.short_name}" already exists. Please choose a different short name.`, errors: ['Duplicate short_name'], @@ -131,23 +135,28 @@ export const actions = { }); } + const dbMessage = dbError instanceof Error ? dbError.message : String(dbError); return fail(500, { - error: `Failed to create recipe: ${dbError.message || 'Unknown database error'}`, - errors: [dbError.message], + error: `Failed to create recipe: ${dbMessage || 'Unknown database error'}`, + errors: [dbMessage], values: Object.fromEntries(formData) }); } - } catch (error: any) { + } catch (error: unknown) { // Re-throw redirects (they're not errors) - if (error?.status >= 300 && error?.status < 400) { - throw error; + if (error && typeof error === 'object' && 'status' in error) { + const status = (error as { status: number }).status; + if (status >= 300 && status < 400) { + throw error; + } } console.error('Error processing recipe submission:', error); + const message = error instanceof Error ? error.message : String(error); return fail(500, { - error: `Failed to process recipe: ${error.message || 'Unknown error'}`, - errors: [error.message] + error: `Failed to process recipe: ${message || 'Unknown error'}`, + errors: [message] }); } } diff --git a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.server.ts b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.server.ts index 8afa6a0..0a344be 100644 --- a/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.server.ts +++ b/src/routes/[recipeLang=recipeLang]/edit/[name]/+page.server.ts @@ -136,10 +136,11 @@ export const actions = { caption: existingImagePath ? (recipeData.images?.[0]?.caption || '') : '', color }]; - } catch (imageError: any) { + } catch (imageError: unknown) { console.error('Image processing error:', imageError); + const message = imageError instanceof Error ? imageError.message : String(imageError); return fail(400, { - error: `Failed to process image: ${imageError.message}`, + error: `Failed to process image: ${message}`, errors: ['Image processing failed'], values: Object.fromEntries(formData) }); @@ -213,16 +214,16 @@ export const actions = { // Redirect to the updated recipe page (might have new short_name) throw redirect(303, `/${params.recipeLang}/${recipeData.short_name}`); - } catch (dbError: any) { + } catch (dbError: unknown) { // Re-throw redirects (they're not errors) - if (dbError?.status >= 300 && dbError?.status < 400) { + if (dbError && typeof dbError === 'object' && 'status' in dbError && (dbError as { status: number }).status >= 300 && (dbError as { status: number }).status < 400) { throw dbError; } console.error('Database error updating recipe:', dbError); // Check for duplicate key error - if (dbError.code === 11000) { + if (dbError && typeof dbError === 'object' && 'code' in dbError && (dbError as { code: number }).code === 11000) { return fail(400, { error: `A recipe with the short name "${recipeData.short_name}" already exists. Please choose a different short name.`, errors: ['Duplicate short_name'], @@ -230,23 +231,28 @@ export const actions = { }); } + const dbMessage = dbError instanceof Error ? dbError.message : String(dbError); return fail(500, { - error: `Failed to update recipe: ${dbError.message || 'Unknown database error'}`, - errors: [dbError.message], + error: `Failed to update recipe: ${dbMessage || 'Unknown database error'}`, + errors: [dbMessage], values: Object.fromEntries(formData) }); } - } catch (error: any) { + } catch (error: unknown) { // Re-throw redirects (they're not errors) - if (error?.status >= 300 && error?.status < 400) { - throw error; + if (error && typeof error === 'object' && 'status' in error) { + const status = (error as { status: number }).status; + if (status >= 300 && status < 400) { + throw error; + } } console.error('Error processing recipe update:', error); + const message = error instanceof Error ? error.message : String(error); return fail(500, { - error: `Failed to process recipe update: ${error.message || 'Unknown error'}`, - errors: [error.message] + error: `Failed to process recipe update: ${message || 'Unknown error'}`, + errors: [message] }); } } diff --git a/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts b/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts index d7514f5..9fb91dc 100644 --- a/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/img/add/+server.ts @@ -125,12 +125,13 @@ export const POST = (async ({ request, locals }) => { unhashedFilename: unhashedFilename, color }); - } catch (err: any) { + } catch (err: unknown) { // Re-throw errors that already have status codes - if (err.status) throw err; + if (err && typeof err === 'object' && 'status' in err) throw err; // Log and throw generic error for unexpected failures console.error('[API:ImgAdd] Upload error:', err); - throw error(500, `Failed to upload image: ${err.message || 'Unknown error'}`); + const message = err instanceof Error ? err.message : String(err); + throw error(500, `Failed to upload image: ${message || 'Unknown error'}`); } }) satisfies RequestHandler; diff --git a/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts index 6060de7..e0f7d2b 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/[name]/+server.ts @@ -2,12 +2,14 @@ import { json, type RequestHandler } from '@sveltejs/kit'; import { Recipe } from '$models/Recipe'; import { dbConnect } from '$utils/db'; import { error } from '@sveltejs/kit'; -import type { RecipeModelType } from '$types/types'; +import type { RecipeModelType, IngredientItem, InstructionItem } from '$types/types'; import { isEnglish } from '$lib/server/recipeHelpers'; +type RecipeItem = (IngredientItem | InstructionItem) & { baseRecipeRef?: Record; resolvedRecipe?: Record }; + /** Recursively map populated baseRecipeRef to resolvedRecipe field */ -function mapBaseRecipeRefs(items: any[]): any[] { - return items.map((item: any) => { +function mapBaseRecipeRefs(items: RecipeItem[]): RecipeItem[] { + return items.map((item) => { if (item.type === 'reference' && item.baseRecipeRef) { const resolvedRecipe = { ...item.baseRecipeRef }; if (resolvedRecipe.ingredients) { @@ -84,6 +86,7 @@ export const GET: RequestHandler = async ({ params }) => { } ]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- mongoose query builder requires chained .populate() calls let dbQuery: any = Recipe.findOne(query); for (const p of populatePaths) { dbQuery = dbQuery.populate(p); @@ -100,7 +103,7 @@ export const GET: RequestHandler = async ({ params }) => { } const t = rawRecipe.translations.en; - let recipe: any = { + let recipe: Record = { _id: rawRecipe._id, short_name: t.short_name, name: t.name, @@ -128,17 +131,17 @@ export const GET: RequestHandler = async ({ params }) => { }; if (recipe.ingredients) { - recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients); + recipe.ingredients = mapBaseRecipeRefs(recipe.ingredients as RecipeItem[]); } if (recipe.instructions) { - recipe.instructions = mapBaseRecipeRefs(recipe.instructions); + recipe.instructions = mapBaseRecipeRefs(recipe.instructions as RecipeItem[]); } // Merge English alt/caption with original image paths const imagesArray = Array.isArray(rawRecipe.images) ? rawRecipe.images : (rawRecipe.images ? [rawRecipe.images] : []); if (imagesArray.length > 0) { const translatedImages = t.images || []; - recipe.images = imagesArray.map((img: any, index: number) => ({ + recipe.images = imagesArray.map((img: { mediapath: string; alt?: string; caption?: string; color?: string }, index: number) => ({ mediapath: img.mediapath, alt: translatedImages[index]?.alt || img.alt || '', caption: translatedImages[index]?.caption || img.caption || '', diff --git a/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts b/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts index e50c785..b73b74c 100644 --- a/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/items/all_brief/+server.ts @@ -17,7 +17,7 @@ export const GET: RequestHandler = async ({ params }) => { recipes = JSON.parse(cached); } else { await dbConnect(); - const dbRecipes: any[] = await Recipe.find(approvalFilter, projection).lean(); + const dbRecipes = await Recipe.find(approvalFilter, projection).lean(); recipes = dbRecipes.map(r => toBrief(r, params.recipeLang!)); await cache.set(cacheKey, JSON.stringify(recipes), 3600); } diff --git a/src/routes/api/[recipeLang=recipeLang]/search/+server.ts b/src/routes/api/[recipeLang=recipeLang]/search/+server.ts index c22414a..9a3d40d 100644 --- a/src/routes/api/[recipeLang=recipeLang]/search/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/search/+server.ts @@ -30,7 +30,7 @@ export const GET: RequestHandler = async ({ url, params, locals }) => { const favoritesOnly = url.searchParams.get('favorites') === 'true'; try { - let dbQuery: any = { ...approvalFilter }; + let dbQuery: Record = { ...approvalFilter }; if (category) { dbQuery[`${prefix}category`] = category; @@ -49,9 +49,10 @@ export const GET: RequestHandler = async ({ url, params, locals }) => { let recipes: BriefRecipeType[] = dbRecipes.map(r => toBrief(r, params.recipeLang!)); // Handle favorites filter - if (favoritesOnly && (locals as any).session?.user) { + const session = await locals.auth(); + if (favoritesOnly && session?.user) { const { UserFavorites } = await import('$models/UserFavorites'); - const userFavorites = await UserFavorites.findOne({ username: (locals as any).session.user.username }); + const userFavorites = await UserFavorites.findOne({ username: session.user.nickname }); if (userFavorites?.favorites) { const favoriteIds = userFavorites.favorites; recipes = recipes.filter(recipe => favoriteIds.some(id => id.toString() === recipe._id?.toString())); diff --git a/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts b/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts index 7f46553..25e4129 100644 --- a/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/translate/+server.ts @@ -52,25 +52,27 @@ export const POST: RequestHandler = async ({ request }) => { translationMetadata, }); - } catch (err: any) { + } catch (err: unknown) { console.error('Translation API error:', err); + const message = err instanceof Error ? err.message : String(err); + // Handle specific error cases - if (err.message?.includes('DeepL API')) { - throw error(503, `Translation service error: ${err.message}`); + if (message?.includes('DeepL API')) { + throw error(503, `Translation service error: ${message}`); } - if (err.message?.includes('API key not configured')) { + if (message?.includes('API key not configured')) { throw error(500, 'Translation service is not configured. Please add DEEPL_API_KEY to environment variables.'); } // Re-throw SvelteKit errors - if (err.status) { + if (err && typeof err === 'object' && 'status' in err) { throw err; } // Generic error - throw error(500, `Translation failed: ${err.message || 'Unknown error'}`); + throw error(500, `Translation failed: ${message || 'Unknown error'}`); } }; @@ -88,11 +90,12 @@ export const GET: RequestHandler = async () => { service: 'DeepL Translation API', status: isConfigured ? 'ready' : 'not configured', }); - } catch (err: any) { + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); return json({ configured: false, status: 'error', - error: err.message, + error: message, }, { status: 500 }); } }; diff --git a/src/routes/api/cospend/debts/+server.ts b/src/routes/api/cospend/debts/+server.ts index 26c9018..74df154 100644 --- a/src/routes/api/cospend/debts/+server.ts +++ b/src/routes/api/cospend/debts/+server.ts @@ -1,10 +1,12 @@ import type { RequestHandler } from '@sveltejs/kit'; import { PaymentSplit } from '$models/PaymentSplit'; -import { Payment } from '$models/Payment'; +import type { IPayment } from '$models/Payment'; import { dbConnect } from '$utils/db'; import { error, json } from '@sveltejs/kit'; import cache from '$lib/server/cache'; +type PopulatedPayment = IPayment & { _id: import('mongoose').Types.ObjectId }; + interface DebtSummary { username: string; netAmount: number; // positive = you owe them, negative = they owe you @@ -42,7 +44,7 @@ export const GET: RequestHandler = async ({ locals }) => { .lean(); // Get all other users who have splits with payments involving the current user - const paymentIds = userSplits.map(split => (split.paymentId as any)._id); + const paymentIds = userSplits.map(split => (split.paymentId as unknown as PopulatedPayment)._id); const allRelatedSplits = await PaymentSplit.find({ paymentId: { $in: paymentIds }, username: { $ne: currentUser } @@ -55,12 +57,12 @@ export const GET: RequestHandler = async ({ locals }) => { // Process current user's splits to understand what they owe/are owed for (const split of userSplits) { - const payment = split.paymentId as any; + const payment = split.paymentId as unknown as PopulatedPayment; if (!payment) continue; // Find other participants in this payment const otherSplits = allRelatedSplits.filter(s => - (s.paymentId as any)._id.toString() === (split.paymentId as any)._id.toString() + (s.paymentId as unknown as PopulatedPayment)._id.toString() === payment._id.toString() ); for (const otherSplit of otherSplits) { diff --git a/src/routes/api/cospend/monthly-expenses/+server.ts b/src/routes/api/cospend/monthly-expenses/+server.ts index 249d7cf..8c43d48 100644 --- a/src/routes/api/cospend/monthly-expenses/+server.ts +++ b/src/routes/api/cospend/monthly-expenses/+server.ts @@ -76,7 +76,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { } ]; - const results = await Payment.aggregate(pipeline as any); + const results = await Payment.aggregate(pipeline); // Transform data into chart-friendly format const monthsMap = new Map(); @@ -91,7 +91,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { } // Populate data - results.forEach((result: any) => { + results.forEach((result: { _id: { yearMonth: string; category: string }; totalAmount: number }) => { const { yearMonth, category } = result._id; const amount = result.totalAmount; @@ -112,7 +112,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { let firstMonthWithData = 0; for (let i = 0; i < allMonths.length; i++) { const monthData = monthsMap.get(allMonths[i]); - const hasData = Object.values(monthData).some((value: any) => value > 0); + const hasData = Object.values(monthData).some((value) => (value as number) > 0); if (hasData) { firstMonthWithData = i; break; diff --git a/src/routes/api/cospend/payments/+server.ts b/src/routes/api/cospend/payments/+server.ts index dc416ee..5f6adaf 100644 --- a/src/routes/api/cospend/payments/+server.ts +++ b/src/routes/api/cospend/payments/+server.ts @@ -6,6 +6,13 @@ import { convertToCHF, isValidCurrencyCode } from '$lib/utils/currency'; import { error, json } from '@sveltejs/kit'; import cache, { invalidateCospendCaches } from '$lib/server/cache'; +interface SplitInput { + username: string; + amount: number; + proportion?: number; + personalAmount?: number; +} + export const GET: RequestHandler = async ({ locals, url }) => { const auth = await locals.auth(); if (!auth || !auth.user?.nickname) { @@ -79,7 +86,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // Validate personal + equal split method if (splitMethod === 'personal_equal' && splits) { - const totalPersonal = splits.reduce((sum: number, split: any) => { + const totalPersonal = splits.reduce((sum: number, split: SplitInput) => { return sum + (parseFloat(split.personalAmount) || 0); }, 0); @@ -125,7 +132,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { }); // Convert split amounts to CHF if needed - const convertedSplits = splits.map((split: any) => { + const convertedSplits = splits.map((split: SplitInput) => { let convertedAmount = split.amount; let convertedPersonalAmount = split.personalAmount; @@ -146,14 +153,14 @@ export const POST: RequestHandler = async ({ request, locals }) => { }; }); - const splitPromises = convertedSplits.map((split: any) => { + const splitPromises = convertedSplits.map((split: { paymentId: unknown; username: string; amount: number; proportion?: number; personalAmount?: number }) => { return PaymentSplit.create(split); }); await Promise.all(splitPromises); // Invalidate caches for all affected users - const affectedUsernames = splits.map((split: any) => split.username); + const affectedUsernames = splits.map((split: SplitInput) => split.username); await invalidateCospendCaches(affectedUsernames, payment._id.toString()); return json({ diff --git a/src/routes/api/cospend/payments/[id]/+server.ts b/src/routes/api/cospend/payments/[id]/+server.ts index 0a81012..8d6d7be 100644 --- a/src/routes/api/cospend/payments/[id]/+server.ts +++ b/src/routes/api/cospend/payments/[id]/+server.ts @@ -36,8 +36,8 @@ export const GET: RequestHandler = async ({ params, locals }) => { await cache.set(cacheKey, JSON.stringify(result), 1800); return json(result); - } catch (e: any) { - if (e.status === 404) throw e; + } catch (e: unknown) { + if (e && typeof e === 'object' && 'status' in e && (e as { status: number }).status === 404) throw e; throw error(500, 'Failed to fetch payment'); } finally { // Connection will be reused @@ -89,7 +89,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { if (data.splits) { await PaymentSplit.deleteMany({ paymentId: id }); - const splitPromises = data.splits.map((split: any) => { + const splitPromises = data.splits.map((split: { username: string; amount: number; proportion?: number; personalAmount?: number }) => { return PaymentSplit.create({ paymentId: id, username: split.username, @@ -100,7 +100,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { }); await Promise.all(splitPromises); - newUsernames = data.splits.map((split: any) => split.username); + newUsernames = data.splits.map((split: { username: string }) => split.username); } // Invalidate caches for all users (old and new) @@ -108,8 +108,8 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { await invalidateCospendCaches(allAffectedUsers, id); return json({ success: true, payment: updatedPayment }); - } catch (e: any) { - if (e.status) throw e; + } catch (e: unknown) { + if (e && typeof e === 'object' && 'status' in e) throw e; throw error(500, 'Failed to update payment'); } finally { // Connection will be reused @@ -148,8 +148,8 @@ export const DELETE: RequestHandler = async ({ params, locals }) => { await invalidateCospendCaches(affectedUsernames, id); return json({ success: true }); - } catch (e: any) { - if (e.status) throw e; + } catch (e: unknown) { + if (e && typeof e === 'object' && 'status' in e) throw e; throw error(500, 'Failed to delete payment'); } finally { // Connection will be reused diff --git a/src/routes/api/cospend/recurring-payments/+server.ts b/src/routes/api/cospend/recurring-payments/+server.ts index dc1086d..f7ae69d 100644 --- a/src/routes/api/cospend/recurring-payments/+server.ts +++ b/src/routes/api/cospend/recurring-payments/+server.ts @@ -1,5 +1,5 @@ import type { RequestHandler } from '@sveltejs/kit'; -import { RecurringPayment } from '$models/RecurringPayment'; +import { RecurringPayment, type IRecurringPayment } from '$models/RecurringPayment'; import { dbConnect } from '$utils/db'; import { error, json } from '@sveltejs/kit'; import { calculateNextExecutionDate, validateCronExpression } from '$lib/utils/recurring'; @@ -17,7 +17,7 @@ export const GET: RequestHandler = async ({ locals, url }) => { await dbConnect(); try { - const query: any = {}; + const query: Record = {}; if (activeOnly) { query.isActive = true; } @@ -86,7 +86,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { // Validate personal + equal split method if (splitMethod === 'personal_equal' && splits) { - const totalPersonal = splits.reduce((sum: number, split: any) => { + const totalPersonal = splits.reduce((sum: number, split: { personalAmount?: number }) => { return sum + (parseFloat(split.personalAmount) || 0); }, 0); @@ -121,7 +121,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { ...recurringPaymentData, frequency, cronExpression - } as any, recurringPaymentData.startDate); + } as IRecurringPayment, recurringPaymentData.startDate); const recurringPayment = await RecurringPayment.create(recurringPaymentData); diff --git a/src/routes/api/cospend/recurring-payments/[id]/+server.ts b/src/routes/api/cospend/recurring-payments/[id]/+server.ts index 2aaa771..b1b7aa3 100644 --- a/src/routes/api/cospend/recurring-payments/[id]/+server.ts +++ b/src/routes/api/cospend/recurring-payments/[id]/+server.ts @@ -69,7 +69,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { throw error(404, 'Recurring payment not found'); } - const updateData: any = {}; + const updateData: Record = {}; if (title !== undefined) updateData.title = title; if (description !== undefined) updateData.description = description; @@ -113,7 +113,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { // Validate personal + equal split method if (splitMethod === 'personal_equal' && splits && amount) { - const totalPersonal = splits.reduce((sum: number, split: any) => { + const totalPersonal = splits.reduce((sum: number, split: { personalAmount?: number }) => { return sum + (parseFloat(split.personalAmount) || 0); }, 0); diff --git a/src/routes/api/cospend/upload/+server.ts b/src/routes/api/cospend/upload/+server.ts index 95f4cad..9367be5 100644 --- a/src/routes/api/cospend/upload/+server.ts +++ b/src/routes/api/cospend/upload/+server.ts @@ -55,8 +55,8 @@ export const POST: RequestHandler = async ({ request, locals }) => { path: publicPath }); - } catch (err: any) { - if (err.status) throw err; + } catch (err: unknown) { + if (err && typeof err === 'object' && 'status' in err) throw err; console.error('Upload error:', err); throw error(500, 'Failed to upload file'); } diff --git a/src/routes/api/fitness/exercises/+server.ts b/src/routes/api/fitness/exercises/+server.ts index 44ba38e..29d675c 100644 --- a/src/routes/api/fitness/exercises/+server.ts +++ b/src/routes/api/fitness/exercises/+server.ts @@ -23,7 +23,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { const offset = parseInt(url.searchParams.get('offset') || '0'); // Build query - let query: any = { isActive: true }; + let query: Record = { isActive: true }; // Text search if (search) { diff --git a/src/routes/api/fitness/sessions/[id]/+server.ts b/src/routes/api/fitness/sessions/[id]/+server.ts index 4a49474..8219ee5 100644 --- a/src/routes/api/fitness/sessions/[id]/+server.ts +++ b/src/routes/api/fitness/sessions/[id]/+server.ts @@ -55,7 +55,7 @@ export const PUT: RequestHandler = async ({ params, request, locals }) => { return json({ error: 'At least one exercise is required' }, { status: 400 }); } - const updateData: any = {}; + const updateData: Record = {}; if (name) updateData.name = name; if (exercises) updateData.exercises = exercises; if (startTime) updateData.startTime = new Date(startTime); diff --git a/src/routes/api/fitness/templates/+server.ts b/src/routes/api/fitness/templates/+server.ts index 3e8d25d..1d9cabc 100644 --- a/src/routes/api/fitness/templates/+server.ts +++ b/src/routes/api/fitness/templates/+server.ts @@ -15,7 +15,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { const includePublic = url.searchParams.get('include_public') === 'true'; - let query: any = { + let query: Record = { $or: [ { createdBy: session.user.nickname } ] diff --git a/src/routes/api/generate-alt-text-bulk/+server.ts b/src/routes/api/generate-alt-text-bulk/+server.ts index 9039ffb..71cda09 100644 --- a/src/routes/api/generate-alt-text-bulk/+server.ts +++ b/src/routes/api/generate-alt-text-bulk/+server.ts @@ -22,7 +22,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { } // Build query based on filter - let query: any = { images: { $exists: true, $ne: [] } }; + let query: Record = { images: { $exists: true, $ne: [] } }; if (filter === 'missing') { // Find recipes with images but missing alt text diff --git a/src/routes/api/recalculate-image-colors/+server.ts b/src/routes/api/recalculate-image-colors/+server.ts index 1bda546..2e265b3 100644 --- a/src/routes/api/recalculate-image-colors/+server.ts +++ b/src/routes/api/recalculate-image-colors/+server.ts @@ -16,7 +16,7 @@ export const POST: RequestHandler = async ({ request, locals }) => { const body = await request.json(); const { filter = 'missing', limit = 50 } = body; - let query: any = { images: { $exists: true, $ne: [] } }; + let query: Record = { images: { $exists: true, $ne: [] } }; if (filter === 'missing') { query = { diff --git a/src/routes/cospend/payments/add/+page.server.ts b/src/routes/cospend/payments/add/+page.server.ts index 0108ce1..7e574da 100644 --- a/src/routes/cospend/payments/add/+page.server.ts +++ b/src/routes/cospend/payments/add/+page.server.ts @@ -219,8 +219,8 @@ export const actions: Actions = { // Success - redirect to dashboard throw redirect(303, '/cospend'); - } catch (error: any) { - if (error.status === 303) throw error; // Re-throw redirect + } catch (error: unknown) { + if (error && typeof error === 'object' && 'status' in error && (error as { status: number }).status === 303) throw error; // Re-throw redirect console.error('Error creating payment:', error); return fail(500, { diff --git a/src/routes/cospend/settle/+page.server.ts b/src/routes/cospend/settle/+page.server.ts index 8cf9af4..2920f17 100644 --- a/src/routes/cospend/settle/+page.server.ts +++ b/src/routes/cospend/settle/+page.server.ts @@ -114,13 +114,14 @@ export const actions: Actions = { // Redirect back to dashboard on success throw redirect(303, '/cospend'); - } catch (error: any) { - if (error.status === 303) { + } catch (error: unknown) { + if (error && typeof error === 'object' && 'status' in error && (error as { status: number }).status === 303) { throw error; // Re-throw redirect } + const message = error instanceof Error ? error.message : String(error); return fail(500, { - error: error.message, + error: message, values: { settlementType, fromUser, diff --git a/src/types/types.ts b/src/types/types.ts index 2859499..f8c07e8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -42,7 +42,11 @@ export type IngredientReference = { name: string; short_name: string; ingredients?: IngredientSection[]; - translations?: any; + translations?: { + en?: { + ingredients?: IngredientItem[]; + }; + }; }; }; @@ -71,7 +75,11 @@ export type InstructionReference = { name: string; short_name: string; instructions?: InstructionSection[]; - translations?: any; + translations?: { + en?: { + instructions?: InstructionItem[]; + }; + }; }; }; diff --git a/src/utils/recipeFormHelpers.ts b/src/utils/recipeFormHelpers.ts index b841222..16f44cc 100644 --- a/src/utils/recipeFormHelpers.ts +++ b/src/utils/recipeFormHelpers.ts @@ -5,6 +5,8 @@ * for SvelteKit form actions with progressive enhancement support. */ +import type { IngredientItem, InstructionItem, TranslatedRecipeType, TranslationMetadata } from '$types/types'; + export interface RecipeFormData { // Basic fields name: string; @@ -22,8 +24,8 @@ export interface RecipeFormData { note?: string; // Complex nested structures - ingredients: any[]; - instructions: any[]; + ingredients: IngredientItem[]; + instructions: InstructionItem[]; // Additional info add_info: { @@ -65,9 +67,9 @@ export interface RecipeFormData { // Translation data (optional) translations?: { - en?: any; + en?: TranslatedRecipeType; }; - translationMetadata?: any; + translationMetadata?: TranslationMetadata; } /** @@ -112,7 +114,7 @@ export function extractRecipeFromFormData(formData: FormData): RecipeFormData { const note = formData.get('note')?.toString(); // Complex nested structures (JSON-encoded) - let ingredients: any[] = []; + let ingredients: IngredientItem[] = []; const ingredientsData = formData.get('ingredients_json')?.toString(); if (ingredientsData) { try { @@ -122,7 +124,7 @@ export function extractRecipeFromFormData(formData: FormData): RecipeFormData { } } - let instructions: any[] = []; + let instructions: InstructionItem[] = []; const instructionsData = formData.get('instructions_json')?.toString(); if (instructionsData) { try { @@ -265,7 +267,7 @@ export function validateRecipeData(data: RecipeFormData): string[] { * Detects which fields have changed between two recipe objects * Used for edit forms to enable partial translation updates */ -export function detectChangedFields(original: any, current: any): string[] { +export function detectChangedFields(original: Record, current: Record): string[] { const changedFields: string[] = []; // Simple field comparison @@ -347,8 +349,8 @@ export function parseSeasonData(formData: FormData): number[] { * Serializes complex recipe data for storage * Ensures all required fields are present and properly typed */ -export function serializeRecipeForDatabase(data: RecipeFormData): any { - const recipe: any = { +export function serializeRecipeForDatabase(data: RecipeFormData): Record { + const recipe: Record = { name: data.name, short_name: data.short_name, description: data.description, diff --git a/src/utils/translation.ts b/src/utils/translation.ts index df07a56..79c5763 100644 --- a/src/utils/translation.ts +++ b/src/utils/translation.ts @@ -459,8 +459,8 @@ class DeepLTranslationService { const changed: string[] = []; for (const field of fieldsToCheck) { - const oldValue = JSON.stringify((oldRecipe as any)[field] || ''); - const newValue = JSON.stringify((newRecipe as any)[field] || ''); + const oldValue = JSON.stringify(oldRecipe[field as keyof RecipeModelType] || ''); + const newValue = JSON.stringify(newRecipe[field as keyof RecipeModelType] || ''); if (oldValue !== newValue) { changed.push(field); }