From 91f6dbcb5bf65a15b88eb6058875d553972cd5e1 Mon Sep 17 00:00:00 2001 From: Isaac Grynsztein Date: Fri, 13 Mar 2020 02:20:07 -0400 Subject: [PATCH] added ability to set pin for settings. pin is md5'd by the backend --- backend/app.js | 45 ++++++++- backend/config/default.json | 3 +- backend/config/encrypted.json | 3 +- backend/consts.js | 4 + backend/package.json | 1 + docker-compose.yml | 1 + src/app/app.component.ts | 23 +++++ src/app/app.module.ts | 7 +- .../check-or-set-pin-dialog.component.html | 18 ++++ .../check-or-set-pin-dialog.component.scss | 6 ++ .../check-or-set-pin-dialog.component.spec.ts | 25 +++++ .../check-or-set-pin-dialog.component.ts | 96 +++++++++++++++++++ src/app/posts.services.ts | 12 +++ src/app/settings/settings.component.html | 4 + src/app/settings/settings.component.ts | 12 ++- src/assets/default.json | 83 ++++++++-------- 16 files changed, 296 insertions(+), 47 deletions(-) create mode 100644 src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.html create mode 100644 src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.scss create mode 100644 src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.spec.ts create mode 100644 src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.ts diff --git a/backend/app.js b/backend/app.js index 027dd1d..922da68 100644 --- a/backend/app.js +++ b/backend/app.js @@ -9,6 +9,7 @@ var express = require("express"); var bodyParser = require("body-parser"); var archiver = require('archiver'); const low = require('lowdb') +var md5 = require('md5'); var URL = require('url').URL; const shortid = require('shortid') const url_api = require('url'); @@ -29,7 +30,8 @@ db.defaults( video: [] }, configWriteFlag: false, - subscriptions: [] + subscriptions: [], + pin_md5: '' }).write(); // config values @@ -1176,6 +1178,47 @@ app.post('/api/downloadArchive', async (req, res) => { }); +app.post('/api/isPinSet', async (req, res) => { + let stored_pin = db.get('pin_md5').value(); + let is_set = false; + if (!stored_pin || stored_pin.length === 0) { + } else { + is_set = true; + } + + res.send({ + is_set: is_set + }); +}); + +app.post('/api/setPin', async (req, res) => { + let unhashed_pin = req.body.pin; + let hashed_pin = md5(unhashed_pin); + + db.set('pin_md5', hashed_pin).write(); + + res.send({ + success: true + }); +}); + +app.post('/api/checkPin', async (req, res) => { + let input_pin = req.body.input_pin; + let input_pin_md5 = md5(input_pin); + + let stored_pin = db.get('pin_md5').value(); + + let successful = false; + + if (input_pin_md5 === stored_pin) { + successful = true; + } + + res.send({ + success: successful + }); +}); + app.get('/api/video/:id', function(req , res){ var head; let optionalParams = url_api.parse(req.url,true).query; diff --git a/backend/config/default.json b/backend/config/default.json index 0eb440f..1d97e51 100644 --- a/backend/config/default.json +++ b/backend/config/default.json @@ -19,7 +19,8 @@ "file_manager_enabled": true, "allow_quality_select": true, "download_only_mode": false, - "allow_multi_download_mode": true + "allow_multi_download_mode": true, + "settings_pin_required": false }, "API": { "use_youtube_API": false, diff --git a/backend/config/encrypted.json b/backend/config/encrypted.json index a0fe6c2..81f4dd1 100644 --- a/backend/config/encrypted.json +++ b/backend/config/encrypted.json @@ -19,7 +19,8 @@ "file_manager_enabled": true, "allow_quality_select": true, "download_only_mode": false, - "allow_multi_download_mode": true + "allow_multi_download_mode": true, + "settings_pin_required": false }, "API": { "use_youtube_API": false, diff --git a/backend/consts.js b/backend/consts.js index 10c82c7..f86be1c 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -60,6 +60,10 @@ let CONFIG_ITEMS = { 'key': 'ytdl_allow_multi_download_mode', 'path': 'YoutubeDLMaterial.Extra.allow_multi_download_mode' }, + 'ytdl_settings_pin_required': { + 'key': 'ytdl_settings_pin_required', + 'path': 'YoutubeDLMaterial.Extra.settings_pin_required' + }, // API 'ytdl_use_youtube_api': { diff --git a/backend/package.json b/backend/package.json index 6da5631..e0bd6fa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -25,6 +25,7 @@ "exe": "^1.0.2", "express": "^4.17.1", "lowdb": "^1.0.0", + "md5": "^2.2.1", "shortid": "^2.2.15", "uuidv4": "^6.0.6", "youtube-dl": "^3.0.2" diff --git a/docker-compose.yml b/docker-compose.yml index 6db5ceb..aaab668 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: ytdl_allow_quality_select: 'true' ytdl_download_only_mode: 'false' ytdl_allow_multi_download_mode: 'true' + ytdl_settings_pin_required: 'false' ytdl_use_youtube_api: 'false' ytdl_youtube_api_key: 'false' ytdl_default_theme: default diff --git a/src/app/app.component.ts b/src/app/app.component.ts index dd7fbb3..c03d1c3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -19,6 +19,7 @@ import { Router, NavigationStart, NavigationEnd } from '@angular/router'; import { OverlayContainer } from '@angular/cdk/overlay'; import { THEMES_CONFIG } from '../themes'; import { SettingsComponent } from './settings/settings.component'; +import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component'; @Component({ selector: 'app-root', @@ -35,6 +36,8 @@ export class AppComponent implements OnInit { defaultTheme = null; allowThemeChange = null; allowSubscriptions = false; + // defaults to true to prevent attack + settingsPinRequired = true; @ViewChild('sidenav', {static: false}) sidenav: MatSidenav; @ViewChild('hamburgerMenu', {static: false, read: ElementRef}) hamburgerMenuButton: ElementRef; @@ -73,6 +76,7 @@ export class AppComponent implements OnInit { this.postsService.loadNavItems().subscribe(res => { // loads settings const result = !this.postsService.debugMode ? res['config_file'] : res; this.topBarTitle = result['YoutubeDLMaterial']['Extra']['title_top']; + this.settingsPinRequired = result['YoutubeDLMaterial']['Extra']['settings_pin_required']; const themingExists = result['YoutubeDLMaterial']['Themes']; this.defaultTheme = themingExists ? result['YoutubeDLMaterial']['Themes']['default_theme'] : 'default'; this.allowThemeChange = themingExists ? result['YoutubeDLMaterial']['Themes']['allow_theme_change'] : true; @@ -158,10 +162,29 @@ onSetTheme(theme, old_theme) { } openSettingsDialog() { + if (this.settingsPinRequired) { + this.openPinDialog(); + } else { + this.actuallyOpenSettingsDialog(); + } + } + + actuallyOpenSettingsDialog() { const dialogRef = this.dialog.open(SettingsComponent, { width: '80vw' }); } + openPinDialog() { + const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, { + }); + + dialogRef.afterClosed().subscribe(res => { + if (res) { + this.actuallyOpenSettingsDialog(); + } + }) + } + } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3eb3a7d..05709d3 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -37,6 +37,7 @@ import { SubscriptionComponent } from './subscription//subscription/subscription import { SubscriptionFileCardComponent } from './subscription/subscription-file-card/subscription-file-card.component'; import { SubscriptionInfoDialogComponent } from './dialogs/subscription-info-dialog/subscription-info-dialog.component'; import { SettingsComponent } from './settings/settings.component'; +import { CheckOrSetPinDialogComponent } from './dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component'; export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { return (element.id === 'video' ? videoFilesMouseHovering || videoFilesOpened : audioFilesMouseHovering || audioFilesOpened); @@ -56,7 +57,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible SubscriptionComponent, SubscriptionFileCardComponent, SubscriptionInfoDialogComponent, - SettingsComponent + SettingsComponent, + CheckOrSetPinDialogComponent ], imports: [ BrowserModule, @@ -102,7 +104,8 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible CreatePlaylistComponent, SubscribeDialogComponent, SubscriptionInfoDialogComponent, - SettingsComponent + SettingsComponent, + CheckOrSetPinDialogComponent ], providers: [PostsService], bootstrap: [AppComponent] diff --git a/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.html b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.html new file mode 100644 index 0000000..082df0e --- /dev/null +++ b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.html @@ -0,0 +1,18 @@ +

{{dialog_title}}

+ + +
+
+ + + +
+
+ +
+
+
+ + + + \ No newline at end of file diff --git a/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.scss b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.scss new file mode 100644 index 0000000..3bdb589 --- /dev/null +++ b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.scss @@ -0,0 +1,6 @@ +.spinner-div { + position: absolute; + margin: 0 auto; + top: 30%; + left: 42%; +} \ No newline at end of file diff --git a/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.spec.ts b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.spec.ts new file mode 100644 index 0000000..0f200b2 --- /dev/null +++ b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CheckOrSetPinDialogComponent } from './check-or-set-pin-dialog.component'; + +describe('CheckOrSetPinDialogComponent', () => { + let component: CheckOrSetPinDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CheckOrSetPinDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CheckOrSetPinDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.ts b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.ts new file mode 100644 index 0000000..a0fddc5 --- /dev/null +++ b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.ts @@ -0,0 +1,96 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { MAT_DIALOG_DATA, MatDialogRef, MatSnackBar } from '@angular/material'; + +@Component({ + selector: 'app-check-or-set-pin-dialog', + templateUrl: './check-or-set-pin-dialog.component.html', + styleUrls: ['./check-or-set-pin-dialog.component.scss'] +}) +export class CheckOrSetPinDialogComponent implements OnInit { + + pinSetChecked = false; + pinSet = true; + resetMode = false; + dialog_title = ''; + input_placeholder = null; + input = ''; + button_label = ''; + + constructor(private postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: any, + public dialogRef: MatDialogRef, private snackBar: MatSnackBar) { } + + ngOnInit() { + if (this.data) { + console.log('is reset mode'); + this.resetMode = this.data.resetMode; + } + + if (this.resetMode) { + this.pinSetChecked = true; + this.notSetLogic(); + } else { + this.isPinSet(); + } + } + + isPinSet() { + this.postsService.isPinSet().subscribe(res => { + this.pinSetChecked = true; + if (res['is_set']) { + this.isSetLogic(); + } else { + this.notSetLogic(); + } + }); + } + + isSetLogic() { + this.pinSet = true; + this.dialog_title = 'Pin Required'; + this.input_placeholder = 'Pin'; + this.button_label = 'Submit' + } + + notSetLogic() { + this.pinSet = false; + this.dialog_title = 'Set your pin'; + this.input_placeholder = 'New pin'; + this.button_label = 'Set Pin' + } + + doAction() { + // pin set must have been checked, and input must not be empty + if (!this.pinSetChecked || this.input.length === 0) { + return; + } + + if (this.pinSet) { + this.postsService.checkPin(this.input).subscribe(res => { + if (res['success']) { + this.dialogRef.close(true); + } else { + this.dialogRef.close(false); + this.openSnackBar('Pin is incorrect!'); + } + }); + } else { + this.postsService.setPin(this.input).subscribe(res => { + if (res['success']) { + this.dialogRef.close(true); + this.openSnackBar('Pin successfully set!'); + } else { + this.dialogRef.close(false); + this.openSnackBar('Failed to set pin!'); + } + }); + } + } + + public openSnackBar(message: string, action: string = '') { + this.snackBar.open(message, action, { + duration: 2000, + }); + } + +} diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index f618f39..c6b646d 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -130,6 +130,18 @@ export class PostsService { return this.http.post(this.path + 'getVideoInfos', {fileNames: fileNames, type: type, urlMode: urlMode}); } + isPinSet() { + return this.http.post(this.path + 'isPinSet', {}); + } + + setPin(unhashed_pin) { + return this.http.post(this.path + 'setPin', {pin: unhashed_pin}); + } + + checkPin(unhashed_pin) { + return this.http.post(this.path + 'checkPin', {input_pin: unhashed_pin}); + } + createPlaylist(playlistName, fileNames, type, thumbnailURL) { return this.http.post(this.path + 'createPlaylist', {playlistName: playlistName, fileNames: fileNames, diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index 6594c44..7ff4901 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -115,6 +115,10 @@
Allow multi-download mode
+
+ Require pin for settings + +
diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index 7386852..a38fcf8 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; +import { MatDialog } from '@angular/material'; +import { CheckOrSetPinDialogComponent } from 'app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component'; @Component({ selector: 'app-settings', @@ -12,7 +14,7 @@ export class SettingsComponent implements OnInit { new_config = null loading_config = false; - constructor(private postsService: PostsService) { } + constructor(private postsService: PostsService, private dialog: MatDialog) { } ngOnInit() { this.getConfig(); @@ -46,4 +48,12 @@ export class SettingsComponent implements OnInit { }) } + setNewPin() { + const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, { + data: { + resetMode: true + } + }); + } + } diff --git a/src/assets/default.json b/src/assets/default.json index 94aad55..6f24bcb 100644 --- a/src/assets/default.json +++ b/src/assets/default.json @@ -1,44 +1,45 @@ { - "YoutubeDLMaterial": { - "Host": { - "url": "http://localhost", - "port": "17442" - }, - "Encryption": { - "use-encryption": false, - "cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem", - "key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem" - }, - "Downloader": { - "path-audio": "audio/", - "path-video": "video/", - "custom_args": "" - }, - "Extra": { - "title_top": "Youtube Downloader", - "file_manager_enabled": true, - "allow_quality_select": true, - "download_only_mode": false, - "allow_multi_download_mode": true - }, - "API": { - "use_youtube_API": false, - "youtube_API_key": "" - }, - "Themes": { - "default_theme": "default", - "allow_theme_change": true - }, - "Subscriptions": { - "allow_subscriptions": true, - "subscriptions_base_path": "subscriptions/", - "subscriptions_check_interval": "300", - "subscriptions_use_youtubedl_archive": true - }, - "Advanced": { - "use_default_downloading_agent": true, - "custom_downloading_agent": "", - "allow_advanced_download": true - } + "YoutubeDLMaterial": { + "Host": { + "url": "http://localhost", + "port": "17442" + }, + "Encryption": { + "use-encryption": false, + "cert-file-path": "/etc/letsencrypt/live/example.com/fullchain.pem", + "key-file-path": "/etc/letsencrypt/live/example.com/privkey.pem" + }, + "Downloader": { + "path-audio": "audio/", + "path-video": "video/", + "custom_args": "" + }, + "Extra": { + "title_top": "Youtube Downloader", + "file_manager_enabled": true, + "allow_quality_select": true, + "download_only_mode": false, + "allow_multi_download_mode": true, + "settings_pin_required": false + }, + "API": { + "use_youtube_API": false, + "youtube_API_key": "" + }, + "Themes": { + "default_theme": "default", + "allow_theme_change": true + }, + "Subscriptions": { + "allow_subscriptions": true, + "subscriptions_base_path": "subscriptions/", + "subscriptions_check_interval": "300", + "subscriptions_use_youtubedl_archive": true + }, + "Advanced": { + "use_default_downloading_agent": true, + "custom_downloading_agent": "", + "allow_advanced_download": true } } +} \ No newline at end of file