fix: support overview scene URLs with embedded passwords
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { normalizeCameraUrl, readWeekCamera } from './week-camera';
|
||||
|
||||
describe('week camera settings', () => {
|
||||
it('accepts a room scene link without a view ID', () => {
|
||||
const url = new URL(normalizeCameraUrl('https://vdo.ninja/?scene=0&room=Alex_12345&password=xxxx'));
|
||||
expect(url.searchParams.get('scene')).toBe('0');
|
||||
expect(url.searchParams.get('room')).toBe('Alex_12345');
|
||||
expect(url.searchParams.get('password')).toBe('xxxx');
|
||||
expect(url.searchParams.has('view')).toBe(false);
|
||||
});
|
||||
it('keeps only viewing parameters', () => {
|
||||
const url = new URL(normalizeCameraUrl('https://vdo.ninja/?room=track&view=camera&push=bad&director=bad&password=secret'));
|
||||
expect(url.searchParams.get('room')).toBe('track');
|
||||
@@ -8,7 +15,7 @@ describe('week camera settings', () => {
|
||||
expect(url.searchParams.has('scene')).toBe(true);
|
||||
expect(url.searchParams.has('push')).toBe(false);
|
||||
expect(url.searchParams.has('director')).toBe(false);
|
||||
expect(url.searchParams.has('password')).toBe(false);
|
||||
expect(url.searchParams.get('password')).toBe('secret');
|
||||
});
|
||||
|
||||
it.each(['http://vdo.ninja/?room=x&view=y', 'https://example.com/?room=x&view=y', 'https://vdo.ninja/?room=x', 'javascript:alert(1)', 'https://vdo.ninja:8443/?room=x&view=y'])('rejects unsafe or incomplete URL %s', (url) => {
|
||||
@@ -17,7 +24,14 @@ describe('week camera settings', () => {
|
||||
|
||||
it('supports unset camera settings and preserves password whitespace', () => {
|
||||
expect(normalizeCameraUrl(' ')).toBe('');
|
||||
expect(readWeekCamera(null)).toEqual({ url: '', password: '' });
|
||||
expect(readWeekCamera({ cameraViewUrl: 'url', cameraPassword: ' a&b ' })).toEqual({ url: 'url', password: ' a&b ' });
|
||||
expect(readWeekCamera(null)).toEqual({ url: '' });
|
||||
const legacy = readWeekCamera({ cameraViewUrl: 'https://vdo.ninja/?room=x&view=y', cameraPassword: ' a&b ' });
|
||||
expect(new URL(legacy.url).searchParams.get('password')).toBe(' a&b ');
|
||||
});
|
||||
|
||||
it('preserves encoded passwords and prefers the link over legacy settings', () => {
|
||||
const url = normalizeCameraUrl('https://vdo.ninja/?room=x&view=y&password=%20a%26b%2B%23%20');
|
||||
expect(new URL(url).searchParams.get('password')).toBe(' a&b+# ');
|
||||
expect(readWeekCamera({ cameraViewUrl: url, cameraPassword: 'old' })).toEqual({ url });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
export function readWeekCamera(settings: unknown): { url: string; password: string } {
|
||||
export function readWeekCamera(settings: unknown): { url: string } {
|
||||
const value = settings && typeof settings === 'object' ? settings as Record<string, unknown> : {};
|
||||
return {
|
||||
url: typeof value.cameraViewUrl === 'string' ? value.cameraViewUrl : '',
|
||||
password: typeof value.cameraPassword === 'string' ? value.cameraPassword : '',
|
||||
};
|
||||
let url = typeof value.cameraViewUrl === 'string' ? value.cameraViewUrl : '';
|
||||
// Keep previously saved separate passwords when reading older settings.
|
||||
if (url && typeof value.cameraPassword === 'string' && value.cameraPassword) {
|
||||
const parsed = new URL(url);
|
||||
if (!parsed.searchParams.has('password')) parsed.searchParams.set('password', value.cameraPassword);
|
||||
url = parsed.toString();
|
||||
}
|
||||
return { url };
|
||||
}
|
||||
|
||||
export function normalizeCameraUrl(input: string): string {
|
||||
@@ -12,13 +16,14 @@ export function normalizeCameraUrl(input: string): string {
|
||||
if (url.protocol !== 'https:' || url.hostname !== 'vdo.ninja' || url.port || url.username || url.password || url.pathname !== '/') {
|
||||
throw new Error('올바른 오버뷰 시청 URL을 입력해주세요.');
|
||||
}
|
||||
if (!url.searchParams.get('room')?.trim() || !url.searchParams.get('view')?.trim()) {
|
||||
throw new Error('시청 URL에 room과 view가 필요합니다.');
|
||||
if (!url.searchParams.get('room')?.trim() || (!url.searchParams.get('view')?.trim() && !url.searchParams.has('scene'))) {
|
||||
throw new Error('시청 URL에 room과 scene 또는 view가 필요합니다.');
|
||||
}
|
||||
// Only viewer parameters are accepted; never retain publishing/director options.
|
||||
const view = new URL('https://vdo.ninja/');
|
||||
view.searchParams.set('scene', '');
|
||||
view.searchParams.set('scene', url.searchParams.get('scene') ?? '');
|
||||
view.searchParams.set('room', url.searchParams.get('room')!);
|
||||
view.searchParams.set('view', url.searchParams.get('view')!);
|
||||
if (url.searchParams.get('view')?.trim()) view.searchParams.set('view', url.searchParams.get('view')!);
|
||||
if (url.searchParams.has('password')) view.searchParams.set('password', url.searchParams.get('password')!);
|
||||
return view.toString();
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ export class UpdateInstructorCurriculumDto {
|
||||
@IsString()
|
||||
cameraViewUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cameraPassword?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
courseId?: string | null;
|
||||
|
||||
@@ -1601,11 +1601,11 @@ export class InstructorService {
|
||||
try {
|
||||
cameraViewUrl = input.cameraViewUrl === undefined ? undefined : normalizeCameraUrl(input.cameraViewUrl);
|
||||
} catch {
|
||||
throw new BadRequestException('오버뷰 시청 URL에 올바른 room과 view를 지정해주세요.');
|
||||
throw new BadRequestException('오버뷰 시청 URL에 올바른 room과 scene 또는 view를 지정해주세요.');
|
||||
}
|
||||
const cameraSettings = {
|
||||
cameraViewUrl: cameraViewUrl ?? existingOverride?.cameraViewUrl ?? '',
|
||||
cameraPassword: cameraViewUrl === '' ? '' : input.cameraPassword ?? existingOverride?.cameraPassword ?? '',
|
||||
cameraPassword: cameraViewUrl !== undefined ? '' : existingOverride?.cameraPassword ?? '',
|
||||
};
|
||||
|
||||
const existingLearningSteps = this.resolveLearningSteps(
|
||||
@@ -1776,7 +1776,7 @@ export class InstructorService {
|
||||
selectedTagLabel = tagOption.label;
|
||||
}
|
||||
|
||||
if (!targetWeek || input.cameraViewUrl !== undefined || input.cameraPassword !== undefined || input.tagLabel !== undefined || input.title || input.duration || input.overview || input.deliverable || input.isTeamProject !== undefined || input.learningSteps || input.topics || input.topicDetails || input.topicVideoUrls || input.practiceContents || input.disabledModuleIds) {
|
||||
if (!targetWeek || input.cameraViewUrl !== undefined || input.tagLabel !== undefined || input.title || input.duration || input.overview || input.deliverable || input.isTeamProject !== undefined || input.learningSteps || input.topics || input.topicDetails || input.topicVideoUrls || input.practiceContents || input.disabledModuleIds) {
|
||||
await this.prisma.instructorCurriculumWeekOverride.upsert({
|
||||
where: {
|
||||
organizationName_courseId_week: {
|
||||
|
||||
Reference in New Issue
Block a user