diff --git a/README.md b/README.md index a8c0d1d..a329fb8 100644 --- a/README.md +++ b/README.md @@ -96,17 +96,7 @@ If you are looking to setup YoutubeDL-Material with Docker, this section is for ## API -You can use the internal API on your server to run downloads on your instance without using the frontend. All of the available endpoints can be seen over [here](https://github.com/Tzahi12345/YoutubeDL-Material/blob/master/backend/app.js) -- search for '/api/' on the page to find all the endpoints. I will expand on the available endpoints in the future, but for now I'd like to highlight the two most useful ones: - -#### Downloading audio files -`curl -XPOST -H "Content-type: application/json" -d '{"url": ""}' 'http://localhost:17442/api/tomp3'` - -Remember to replace `` with the actual URL. - -#### Downloading video files -`curl -XPOST -H "Content-type: application/json" -d '{"url": ""}' 'http://localhost:17442/api/tomp4'` - -Remember to replace `` with the actual URL. +View how to use the backend API on the [API Wiki page](). ## Contributing diff --git a/backend/app.js b/backend/app.js index 5752fa9..9160980 100644 --- a/backend/app.js +++ b/backend/app.js @@ -10,6 +10,7 @@ var bodyParser = require("body-parser"); var archiver = require('archiver'); var mergeFiles = require('merge-files'); const low = require('lowdb') +var md5 = require('md5'); const NodeID3 = require('node-id3') const downloader = require('youtube-dl/lib/downloader') const fetch = require('node-fetch'); @@ -33,7 +34,8 @@ db.defaults( video: [] }, configWriteFlag: false, - subscriptions: [] + subscriptions: [], + pin_md5: '' }).write(); // config values @@ -1434,6 +1436,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/appdata/default.json b/backend/appdata/default.json index 6cefae0..ec92dd6 100644 --- a/backend/appdata/default.json +++ b/backend/appdata/default.json @@ -20,7 +20,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/appdata/encrypted.json b/backend/appdata/encrypted.json index df9c76c..da14a36 100644 --- a/backend/appdata/encrypted.json +++ b/backend/appdata/encrypted.json @@ -20,7 +20,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 767dc0a..0483521 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -62,6 +62,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 8639577..80426a5 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", "node-id3": "^0.1.14", "merge-files": "^0.1.2", "node-fetch": "^2.6.0", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 6f01687..52441b5 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -21,6 +21,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'; import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component'; @Component({ @@ -38,6 +39,8 @@ export class AppComponent implements OnInit { defaultTheme = null; allowThemeChange = null; allowSubscriptions = false; + // defaults to true to prevent attack + settingsPinRequired = true; @ViewChild('sidenav') sidenav: MatSidenav; @ViewChild('hamburgerMenu', { read: ElementRef }) hamburgerMenuButton: ElementRef; @@ -76,6 +79,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; @@ -161,11 +165,31 @@ 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(); + } + }); + + } + openAboutDialog() { const dialogRef = this.dialog.open(AboutDialogComponent, { width: '80vw' diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 00dc243..2d2bd77 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -48,6 +48,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'; import es from '@angular/common/locales/es'; import { AboutDialogComponent } from './dialogs/about-dialog/about-dialog.component'; @@ -74,6 +75,7 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible SubscriptionFileCardComponent, SubscriptionInfoDialogComponent, SettingsComponent, + CheckOrSetPinDialogComponent, AboutDialogComponent, VideoInfoDialogComponent, ArgModifierDialogComponent, @@ -120,6 +122,14 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible RouterModule, AppRoutingModule, ], + entryComponents: [ + InputDialogComponent, + CreatePlaylistComponent, + SubscribeDialogComponent, + SubscriptionInfoDialogComponent, + SettingsComponent, + CheckOrSetPinDialogComponent + ], providers: [ PostsService ], @@ -128,4 +138,5 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible ], bootstrap: [AppComponent] }) + export class AppModule { } 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..e33b8c4 --- /dev/null +++ b/src/app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component.ts @@ -0,0 +1,97 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { MatSnackBar } from '@angular/material/snack-bar'; + +@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 97b63de..55fc0b1 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -131,6 +131,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 b596f08..8e6c583 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -130,6 +130,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 7b9fe2f..d9f8fc5 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit } from '@angular/core'; import { PostsService } from 'app/posts.services'; +import { CheckOrSetPinDialogComponent } from 'app/dialogs/check-or-set-pin-dialog/check-or-set-pin-dialog.component'; import { isoLangs } from './locales_list'; import { MatSnackBar } from '@angular/material/snack-bar'; import {DomSanitizer} from '@angular/platform-browser'; @@ -58,6 +59,14 @@ export class SettingsComponent implements OnInit { }) } + setNewPin() { + const dialogRef = this.dialog.open(CheckOrSetPinDialogComponent, { + data: { + resetMode: true + } + }); + } + localeSelectChanged(new_val) { localStorage.setItem('locale', new_val); this.openSnackBar('Language successfully changed! Reload to update the page.') diff --git a/src/assets/default.json b/src/assets/default.json index 6a7e5b8..1cb9ad0 100644 --- a/src/assets/default.json +++ b/src/assets/default.json @@ -20,7 +20,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,