fix(web): guard authenticated routes instead of asserting the session user

library, playlists and playlists/[id] read locals.user!.sub with no
check, so an anonymous request threw a TypeError and returned 500
instead of redirecting to login. Adds requireUser(), which redirects to
/login?returnTo=... and returns a non-null SessionUser, and drops the
five non-null assertions.

/upload had no load at all: the page rendered for anonymous users and
only failed on submit. It now redirects up front.
This commit is contained in:
2026-07-20 07:28:04 +02:00
parent ba7ee2c59e
commit a40f612700
5 changed files with 37 additions and 11 deletions
+7 -1
View File
@@ -1,6 +1,7 @@
import createClient from 'openapi-fetch';
import { error, fail, type ActionFailure } from '@sveltejs/kit';
import { error, fail, redirect, type ActionFailure } from '@sveltejs/kit';
import type { paths } from '$lib/api/schema';
import type { SessionUser } from '$lib/types';
import { apiConfig } from '$lib/server/config';
interface ApiClientOptions {
@@ -16,6 +17,11 @@ export function createApiClient({ fetch, accessToken }: ApiClientOptions) {
});
}
export function requireUser(locals: App.Locals, url: URL): SessionUser {
if (!locals.user) redirect(302, `/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`);
return locals.user;
}
export function requireAccessToken(locals: App.Locals, message = 'Inicia sesión.'): string {
if (!locals.accessToken) error(401, message);
return locals.accessToken;
@@ -1,13 +1,20 @@
import type { PageServerLoad, Actions } from './$types';
import { fail } from '@sveltejs/kit';
import { createApiClient, requireAccessTokenAction, unwrapOrError, unwrapOrFail } from '$lib/server/api';
import {
createApiClient,
requireAccessTokenAction,
requireUser,
unwrapOrError,
unwrapOrFail
} from '$lib/server/api';
const PAGE_SIZE = 50;
export const load: PageServerLoad = async ({ locals, fetch }) => {
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const result = await api.GET('/tracks/users/{userId}', {
params: { path: { userId: locals.user!.sub }, query: { pageNumber: 1, pageSize: PAGE_SIZE } }
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: PAGE_SIZE } }
});
return { tracks: unwrapOrError(result, 'No se pudo cargar tu biblioteca.') };
@@ -4,16 +4,18 @@ import {
createApiClient,
playlistCoverTrackIds,
requireAccessTokenAction,
requireUser,
unwrapOrError
} from '$lib/server/api';
import { uploadPresignedImage } from '$lib/server/upload';
const MAX_NAME = 100;
export const load: PageServerLoad = async ({ locals, fetch }) => {
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const result = await api.GET('/playlists/users/{userId}', {
params: { path: { userId: locals.user!.sub }, query: { pageNumber: 1, pageSize: 50 } }
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
});
const data = unwrapOrError(result, 'No se pudieron cargar tus playlists.');
@@ -3,6 +3,7 @@ import { fail, redirect } from '@sveltejs/kit';
import {
createApiClient,
requireAccessTokenAction,
requireUser,
unwrapOrError,
unwrapOrFail
} from '$lib/server/api';
@@ -14,7 +15,8 @@ import type { YouTubeSong } from '$lib/types';
const MAX_NAME = 100;
const SUGGESTIONS_LIMIT = 20;
export const load: PageServerLoad = async ({ params, locals, fetch }) => {
export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const [playlistRes, tracksRes, libraryRes, playlistsRes] = await Promise.all([
@@ -23,10 +25,10 @@ export const load: PageServerLoad = async ({ params, locals, fetch }) => {
params: { path: { playlistId: params.id }, query: { pageNumber: 1, pageSize: 200 } }
}),
api.GET('/tracks/users/{userId}', {
params: { path: { userId: locals.user!.sub }, query: { pageNumber: 1, pageSize: 50 } }
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
}),
api.GET('/playlists/users/{userId}', {
params: { path: { userId: locals.user!.sub }, query: { pageNumber: 1, pageSize: 50 } }
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
})
]);
@@ -1,10 +1,19 @@
import type { Actions } from './$types';
import type { Actions, PageServerLoad } from './$types';
import { fail } from '@sveltejs/kit';
import { createApiClient, requireAccessTokenAction, unwrapOrFail } from '$lib/server/api';
import {
createApiClient,
requireAccessTokenAction,
requireUser,
unwrapOrFail
} from '$lib/server/api';
import { putPresigned, extOf, contentTypeOf, AUDIO_TYPES, IMAGE_TYPES } from '$lib/server/upload';
const MAX_TITLE = 100;
export const load: PageServerLoad = ({ locals, url }) => {
requireUser(locals, url);
};
export const actions: Actions = {
default: async ({ request, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals, 'Inicia sesión para subir música.');