Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/config/env.validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const REQUIRED_ENV_KEYS = [
// Optional keys that, when present, must pass a format check.
const BOOLEAN_KEYS = ["AUTH_COOKIE_SECURE"] as const;
const NUMBER_KEYS = ["PORT", "MAIL_PORT"] as const;
const URL_KEYS = ["CLIENT_URL", "API_URL", "GOOGLE_CALLBACK_URL", "CDN_URL"] as const;
const URL_KEYS = [
"CLIENT_URL",
"API_URL",
"GOOGLE_CALLBACK_URL",
"CDN_URL",
] as const;

function isValidUrl(value: string): boolean {
try {
Expand Down
2 changes: 1 addition & 1 deletion src/playlists/dto/get-playlist-embed-code.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,6 @@ export class GetPlaylistEmbedCodeResponseDto {
@ApiProperty({ example: 'pl_101' })
playlistId!: string;

@ApiProperty({ example: '<iframe src="https://example.com/embed/playlists/pl_101"></iframe>' })
@ApiProperty({ example: '<iframe src="https://dev.iqa3.tech/embed/playlists/pl_101"></iframe>' })
embedCode!: string;
}
2 changes: 1 addition & 1 deletion src/playlists/playlists.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ function buildServiceMock() {
}),
getEmbedCode: jest.fn().mockResolvedValue({
playlistId,
embedCode: `<iframe src="https://example.com/embed/playlists/${playlistId}"></iframe>`,
embedCode: `<iframe src="https://dev.iqa3.tech/embed/playlists/${playlistId}"></iframe>`,
}),
addTrack: jest.fn().mockResolvedValue({
message: "Track added to playlist successfully",
Expand Down
2 changes: 1 addition & 1 deletion src/playlists/playlists.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,7 +466,7 @@ export class PlaylistsController {
schema: {
example: {
playlistId: 'pl_101',
embedCode: '<iframe src="https://example.com/embed/playlists/pl_101"></iframe>',
embedCode: '<iframe src="https://dev.iqa3.tech/embed/playlists/pl_101"></iframe>',
},
},
})
Expand Down
30 changes: 28 additions & 2 deletions src/playlists/playlists.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,14 @@ describe("PlaylistsService", () => {
const storageMock = {
upload: jest.fn(),
};
const originalEnv = process.env;

beforeEach(async () => {
process.env = {
...originalEnv,
FRONTEND_URL: "https://dev.iqa3.tech",
};

prisma = buildPrismaMock();

const module: TestingModule = await Test.createTestingModule({
Expand All @@ -87,7 +93,10 @@ describe("PlaylistsService", () => {
service = module.get(PlaylistsService);
});

afterEach(() => jest.clearAllMocks());
afterEach(() => {
process.env = originalEnv;
jest.clearAllMocks();
});

describe("create", () => {
it("creates a PUBLIC playlist with initial tracks list", async () => {
Expand Down Expand Up @@ -1451,16 +1460,33 @@ describe("PlaylistsService", () => {
prisma.playlist.findFirst.mockResolvedValue({
id: "pl_101",
ownerId: "usr_1",
visibility: PlaylistVisibility.PUBLIC,
secretToken: null,
});

const result = await service.getEmbedCode("usr_1", "pl_101");

expect(result).toEqual({
playlistId: "pl_101",
embedCode: '<iframe src="https://example.com/embed/playlists/pl_101"></iframe>',
embedCode: '<iframe src="https://dev.iqa3.tech/embed/playlists/pl_101"></iframe>',
});
});

it("includes the secret token for secret playlists", async () => {
prisma.playlist.findFirst.mockResolvedValue({
id: "pl_101",
ownerId: "usr_1",
visibility: PlaylistVisibility.SECRET,
secretToken: "secret-token-123",
});

const result = await service.getEmbedCode("usr_1", "pl_101");

expect(result.embedCode).toBe(
'<iframe src="https://dev.iqa3.tech/embed/playlists/pl_101?token=secret-token-123"></iframe>',
);
});

it("throws when requester is not owner", async () => {
prisma.playlist.findFirst.mockResolvedValue({
id: "pl_101",
Expand Down
36 changes: 29 additions & 7 deletions src/playlists/playlists.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Injectable,
Logger,
NotFoundException,
InternalServerErrorException,
} from '@nestjs/common';
import { PlaylistType, PlaylistVisibility, Prisma } from '@prisma/client';
import { randomBytes, randomUUID } from 'crypto';
Expand Down Expand Up @@ -1730,8 +1731,7 @@ export class PlaylistsService {
);
}

const embedBaseUrl = this.resolveEmbedBaseUrl();
const embedUrl = new URL(`${embedBaseUrl.replace(/\/+$/, '')}/${playlist.id}`);
const embedUrl = this.buildPlaylistEmbedUrl(playlist.id);

if (playlist.visibility === PlaylistVisibility.SECRET && playlist.secretToken) {
embedUrl.searchParams.set('token', playlist.secretToken);
Expand All @@ -1758,13 +1758,35 @@ export class PlaylistsService {
};
}

private resolveEmbedBaseUrl(): string {
const explicitBaseUrl = process.env.PLAYLIST_EMBED_BASE_URL?.trim();
if (explicitBaseUrl) {
return explicitBaseUrl;
private buildPlaylistEmbedUrl(playlistId: string): URL {
const baseUrl = this.resolveFrontendBaseUrl();
return new URL(`/embed/playlists/${playlistId}`, baseUrl);
}

private resolveFrontendBaseUrl(): string {
const configuredBaseUrl =
process.env.FRONTEND_URL?.trim() ?? process.env.CLIENT_URL?.trim();

if (!configuredBaseUrl) {
const message =
'Playlist embed base URL is not configured. Set FRONTEND_URL or CLIENT_URL to a valid http/https URL.';
this.logger.error(message);
throw new InternalServerErrorException(message);
}

return 'https://example.com/embed/playlists';
try {
const url = new URL(configuredBaseUrl);
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error('URL must use http or https.');
}

return url.toString().replace(/\/+$/, '');
} catch {
const message =
`Playlist embed base URL is invalid: ${configuredBaseUrl}. Set FRONTEND_URL or CLIENT_URL to a valid http/https URL.`;
this.logger.error(message);
throw new InternalServerErrorException(message);
}
}

private validateTrackIdArray(
Expand Down
Loading