Compare commits

...

1 Commits

Author SHA1 Message Date
Tzahi12345
010f0fbb1c Added ability to set a pin for settings menu 2023-04-27 21:37:32 -04:00
150 changed files with 482 additions and 152 deletions

View File

@@ -678,22 +678,6 @@ paths:
$ref: '#/components/schemas/SuccessObject'
security:
- Auth query parameter: []
/api/isPinSet:
post:
tags:
- security
summary: Check if pin is set
description: Checks if the pin is set for settings
operationId: post-api-isPinSet
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/inline_response_200_15'
security:
- Auth query parameter: []
/api/generateNewAPIKey:
post:
tags:
@@ -1311,6 +1295,48 @@ paths:
- Auth query parameter: []
tags:
- multi-user mode
/api/setPin:
post:
summary: Set settings pin
operationId: post-api-setPin
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/SuccessObject'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/SetPinRequest'
description: 'Sets a pin for the settings'
security:
- Auth query parameter: []
tags:
- security
/api/auth/pinLogin:
post:
summary: Pin login
operationId: post-api-pin-login
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/PinLoginResponse'
description: Use this endpoint to generate a JWT token for pin authentication. Put anything in the username field.
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
security:
- Auth query parameter: []
tags:
- security
/api/getUsers:
post:
summary: Get all users
@@ -3025,6 +3051,13 @@ components:
type: string
required:
- role
SetPinRequest:
required:
- new_pin
type: object
properties:
new_pin:
type: string
file:
title: file
type: object
@@ -3074,6 +3107,13 @@ components:
type: array
items:
$ref: '#/components/schemas/UserPermission'
PinLoginResponse:
required:
- pin_token
type: object
properties:
pin_token:
type: string
UpdateUserRequest:
required:
- change_object

View File

@@ -742,6 +742,18 @@ const optionalJwt = async function (req, res, next) {
return next();
};
const optionalPin = async function (req, res, next) {
const use_pin = config_api.getConfigItem('ytdl_use_pin');
if (use_pin && req.path.includes('/api/setConfig')) {
if (!req.query.pin_token) {
res.sendStatus(418); // I'm a teapot (RFC 2324)
return;
}
return next();
}
return next();
};
app.get('/api/config', function(req, res) {
let config_file = config_api.getConfigFile();
res.send({
@@ -750,7 +762,7 @@ app.get('/api/config', function(req, res) {
});
});
app.post('/api/setConfig', optionalJwt, function(req, res) {
app.post('/api/setConfig', optionalJwt, optionalPin, function(req, res) {
let new_config_file = req.body.new_config_file;
if (new_config_file && new_config_file['YoutubeDLMaterial']) {
let success = config_api.setConfigFile(new_config_file);
@@ -1934,12 +1946,23 @@ app.post('/api/auth/login'
, auth_api.generateJWT
, auth_api.returnAuthResponse
);
app.post('/api/auth/pinLogin'
, auth_api.passport.authenticate(['local_pin'], {})
, auth_api.generatePinJWT
, auth_api.returnPinAuthResponse
);
app.post('/api/auth/jwtAuth'
, auth_api.passport.authenticate('jwt', { session: false })
, auth_api.passport.authorize('jwt')
, auth_api.generateJWT
, auth_api.returnAuthResponse
);
app.post('/api/auth/pinAuth'
, auth_api.passport.authenticate('pin', { session: false })
, auth_api.passport.authorize('pin')
, auth_api.generatePinJWT
, auth_api.returnPinAuthResponse
);
app.post('/api/auth/changePassword', optionalJwt, async (req, res) => {
let user_uid = req.body.user_uid;
let password = req.body.new_password;
@@ -2029,6 +2052,13 @@ app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => {
res.send({success: success});
});
app.post('/api/setPin', function(req, res) {
const success = auth_api.setPin(req.body.new_pin);
res.send({
success: success
});
});
// notifications
app.post('/api/getNotifications', optionalJwt, async (req, res) => {

View File

@@ -15,7 +15,6 @@ var JwtStrategy = require('passport-jwt').Strategy,
// other required vars
let SERVER_SECRET = null;
let JWT_EXPIRATION = null;
let opts = null;
let saltRounds = null;
exports.initialize = function () {
@@ -50,11 +49,11 @@ exports.initialize = function () {
db_api.users_db.set('jwt_secret', SERVER_SECRET).write();
}
opts = {}
const opts = {}
opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('jwt');
opts.secretOrKey = SERVER_SECRET;
exports.passport.use(new JwtStrategy(opts, async function(jwt_payload, done) {
exports.passport.use('jwt', new JwtStrategy(opts, async function(jwt_payload, done) {
const user = await db_api.getRecord('users', {uid: jwt_payload.user});
if (user) {
return done(null, user);
@@ -63,6 +62,21 @@ exports.initialize = function () {
// or you could create a new account
}
}));
const pin_opts = {}
pin_opts.jwtFromRequest = ExtractJwt.fromUrlQueryParameter('pin_token');
pin_opts.secretOrKey = SERVER_SECRET;
exports.passport.use('pin', new JwtStrategy(pin_opts, {
passwordField: 'pin'},
async function(username, password, done) {
if (await bcrypt.compare(password, config_api.getConfigItem('ytdl_pin_hash'))) {
return done(null, { success: true });
} else {
return done(null, false, { message: 'Incorrect pin' });
}
}
));
}
const setupRoles = async () => {
@@ -188,6 +202,10 @@ exports.login = async (username, password) => {
return await bcrypt.compare(password, user.passhash) ? user : false;
}
exports.pinLogin = async (pin) => {
return await bcrypt.compare(pin, config_api.getConfigItem('ytdl_pin_hash'));
}
exports.passport.use(new LocalStrategy({
usernameField: 'username',
passwordField: 'password'},
@@ -196,6 +214,14 @@ exports.passport.use(new LocalStrategy({
}
));
exports.passport.use('local_pin', new LocalStrategy({
usernameField: 'username',
passwordField: 'password'},
async function(username, password, done) {
return done(null, await exports.pinLogin(password));
}
));
var getLDAPConfiguration = function(req, callback) {
const ldap_config = config_api.getConfigItem('ytdl_ldap_config');
const opts = {server: ldap_config};
@@ -237,6 +263,14 @@ exports.generateJWT = function(req, res, next) {
next();
}
exports.generatePinJWT = function(req, res, next) {
var payload = {
exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION
};
req.token = jwt.sign(payload, SERVER_SECRET);
next();
}
exports.returnAuthResponse = async function(req, res) {
res.status(200).json({
user: req.user,
@@ -246,6 +280,12 @@ exports.returnAuthResponse = async function(req, res) {
});
}
exports.returnPinAuthResponse = async function(req, res) {
res.status(200).json({
pin_token: req.token
});
}
/***************************************
* Authorization: middleware that checks the
* JWT token for validity before allowing
@@ -439,6 +479,13 @@ exports.userPermissions = async function(user_uid) {
return user_permissions;
}
// pin
exports.setPin = async (new_pin) => {
const pin_hash = await bcrypt.hash(new_pin, saltRounds);
return config_api.setConfigItem('ytdl_pin_hash', pin_hash);
}
function getToken(queryParams) {
if (queryParams && queryParams.jwt) {
var parted = queryParams.jwt.split(' ');
@@ -450,7 +497,7 @@ function getToken(queryParams) {
} else {
return null;
}
};
}
function generateUserObject(userid, username, hash, auth_method = 'internal') {
let new_user = {

View File

@@ -202,6 +202,8 @@ const DEFAULT_CONFIG = {
"enable_all_notifications": true,
"allowed_notification_types": [],
"enable_rss_feed": false,
"use_pin": false,
"pin_hash": "",
},
"API": {
"use_API_key": false,

View File

@@ -92,6 +92,14 @@ exports.CONFIG_ITEMS = {
'key': 'ytdl_enable_rss_feed',
'path': 'YoutubeDLMaterial.Extra.enable_rss_feed'
},
'ytdl_use_pin': {
'key': 'ytdl_use_pin',
'path': 'YoutubeDLMaterial.Extra.use_pin'
},
'ytdl_pin_hash': {
'key': 'ytdl_pin_hash',
'path': 'YoutubeDLMaterial.Extra.pin_hash'
},
// API
'ytdl_use_api_key': {

View File

@@ -87,6 +87,7 @@ export type { LoginResponse } from './models/LoginResponse';
export type { Notification } from './models/Notification';
export { NotificationAction } from './models/NotificationAction';
export { NotificationType } from './models/NotificationType';
export type { PinLoginResponse } from './models/PinLoginResponse';
export type { Playlist } from './models/Playlist';
export type { RegisterRequest } from './models/RegisterRequest';
export type { RegisterResponse } from './models/RegisterResponse';
@@ -95,6 +96,7 @@ export type { RestoreDBBackupRequest } from './models/RestoreDBBackupRequest';
export { Schedule } from './models/Schedule';
export type { SetConfigRequest } from './models/SetConfigRequest';
export type { SetNotificationsToReadRequest } from './models/SetNotificationsToReadRequest';
export type { SetPinRequest } from './models/SetPinRequest';
export type { SharingToggle } from './models/SharingToggle';
export type { Sort } from './models/Sort';
export type { SubscribeRequest } from './models/SubscribeRequest';

View File

@@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type PinLoginResponse = {
pin_token: string;
};

View File

@@ -0,0 +1,7 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SetPinRequest = {
new_pin: string;
};

View File

@@ -51,7 +51,7 @@
<a *ngIf="postsService.config && postsService.hasPermission('tasks_manager')" mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/tasks'><ng-container i18n="Navigation menu Tasks Page title">Tasks</ng-container></a>
<ng-container *ngIf="postsService.config && postsService.hasPermission('settings')">
<mat-divider></mat-divider>
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null" routerLink='/settings'><ng-container i18n="Settings menu label">Settings</ng-container></a>
<a mat-list-item (click)="postsService.sidepanel_mode === 'over' ? sidenav.close() : null; pinConfirm('/settings')" [routerLink]="!postsService.config.Extra.use_pin ? '/settings' : null"><ng-container i18n="Settings menu label">Settings</ng-container></a>
</ng-container>
<ng-container *ngIf="postsService.config && allowSubscriptions && postsService.subscriptions && postsService.hasPermission('subscriptions')">
<mat-divider *ngIf="postsService.subscriptions.length > 0"></mat-divider>

View File

@@ -22,6 +22,7 @@ import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-p
import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component';
import { NotificationsComponent } from './components/notifications/notifications.component';
import { ArchiveViewerComponent } from './components/archive-viewer/archive-viewer.component';
import { PinLoginComponent } from './dialogs/pin-login-dialog/pin-login-dialog.component';
@Component({
selector: 'app-root',
@@ -214,6 +215,16 @@ export class AppComponent implements OnInit, AfterViewInit {
});
}
pinConfirm(route: string): void {
if (!this.postsService.config.Extra.use_pin) return;
const dialogRef = this.dialog.open(PinLoginComponent);
dialogRef.afterClosed().subscribe(success => {
if (success) {
this.router.navigate([route]);
}
});
}
notificationCountUpdate(new_count: number): void {
this.notification_count = new_count;
}

View File

@@ -95,6 +95,8 @@ import { GenerateRssUrlComponent } from './dialogs/generate-rss-url/generate-rss
import { SortPropertyComponent } from './components/sort-property/sort-property.component';
import { OnlyNumberDirective } from './directives/only-number.directive';
import { ArchiveViewerComponent } from './components/archive-viewer/archive-viewer.component';
import { SetPinDialogComponent } from './dialogs/set-pin-dialog/set-pin-dialog.component';
import { PinLoginComponent } from './dialogs/pin-login-dialog/pin-login-dialog.component';
registerLocaleData(es, 'es');
@@ -147,7 +149,9 @@ registerLocaleData(es, 'es');
GenerateRssUrlComponent,
SortPropertyComponent,
OnlyNumberDirective,
ArchiveViewerComponent
ArchiveViewerComponent,
SetPinDialogComponent,
PinLoginComponent
],
imports: [
CommonModule,

View File

@@ -0,0 +1,16 @@
<h4 mat-dialog-title i18n="Pin required">Pin required</h4>
<mat-dialog-content>
<mat-form-field color="accent">
<mat-label i18n="Enter pin">Enter pin</mat-label>
<input [(ngModel)]="pin" matInput onlyNumber required type="password">
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close i18n="Cancel">Cancel</button>
<button mat-button [disabled]="!pin" (click)="pinLogin()"><ng-container i18n="Enter pin button">Enter pin</ng-container></button>
<div class="mat-spinner" *ngIf="enterClicked">
<mat-spinner [diameter]="25"></mat-spinner>
</div>
</mat-dialog-actions>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PinLoginComponent } from './pin-login-dialog.component';
describe('PinLoginComponent', () => {
let component: PinLoginComponent;
let fixture: ComponentFixture<PinLoginComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ PinLoginComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(PinLoginComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,34 @@
import { Component } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-pin-login',
templateUrl: './pin-login-dialog.component.html',
styleUrls: ['./pin-login-dialog.component.scss']
})
export class PinLoginComponent {
pin: string;
enterClicked = false;
constructor(private postsService: PostsService, private dialogRef: MatDialogRef<PinLoginComponent>) {
}
pinLogin() {
this.enterClicked = true;
this.postsService.pinLogin(this.pin).subscribe(res => {
this.enterClicked = false;
if (!res['pin_token']) {
this.postsService.openSnackBar($localize`Pin failed!`);
} else {
this.postsService.httpOptions.params = this.postsService.httpOptions.params.set('pin_token', res['pin_token']);
}
this.dialogRef.close(res['pin_token']);
}, err => {
this.enterClicked = false;
this.postsService.openSnackBar($localize`Pin failed!`);
console.error(err);
this.dialogRef.close(false);
});
}
}

View File

@@ -0,0 +1,13 @@
<h4 mat-dialog-title i18n="Set pin">Set pin</h4>
<mat-dialog-content>
<mat-form-field color="accent">
<mat-label i18n="Pin">Pin</mat-label>
<input [(ngModel)]="pin" matInput onlyNumber required>
</mat-form-field>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button mat-dialog-close i18n="Cancel">Cancel</button>
<button mat-button [disabled]="!pin" (click)="setPin()"><ng-container i18n="Set pin button">Set pin</ng-container></button>
</mat-dialog-actions>

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SetPinDialogComponent } from './set-pin-dialog.component';
describe('SetPinDialogComponent', () => {
let component: SetPinDialogComponent;
let fixture: ComponentFixture<SetPinDialogComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ SetPinDialogComponent ]
})
.compileComponents();
fixture = TestBed.createComponent(SetPinDialogComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { PostsService } from 'app/posts.services';
@Component({
selector: 'app-set-pin-dialog',
templateUrl: './set-pin-dialog.component.html',
styleUrls: ['./set-pin-dialog.component.scss']
})
export class SetPinDialogComponent {
pin: string;
constructor(private postsService: PostsService) {
}
setPin() {
this.postsService.setPin(this.pin).subscribe(res => {
if (res['success']) {
this.postsService.openSnackBar($localize`Successfully set pin!`);
}
});
}
}

View File

@@ -113,7 +113,8 @@ import {
ImportArchiveRequest,
Archive,
Subscription,
RestartDownloadResponse
RestartDownloadResponse,
PinLoginResponse
} from '../api-types';
import { isoLangs } from './settings/locales_list';
import { Title } from '@angular/platform-browser';
@@ -734,7 +735,6 @@ export class PostsService implements CanActivate {
return this.http.post<LoginResponse>(this.path + 'auth/login', body, this.httpOptions);
}
// user methods
jwtAuth() {
const call = this.http.post(this.path + 'auth/jwtAuth', {}, this.httpOptions);
call.subscribe(res => {
@@ -752,6 +752,12 @@ export class PostsService implements CanActivate {
return call;
}
// pin methods
pinLogin(pin: string) {
const body: LoginRequest = {username: 'username', password: pin};
return this.http.post<PinLoginResponse>(this.path + 'auth/pinLogin', body, this.httpOptions);
}
logout() {
this.user = null;
this.permissions = null;
@@ -903,6 +909,11 @@ export class PostsService implements CanActivate {
this.httpOptions);
}
setPin(new_pin: string): Observable<SuccessObject> {
return this.http.post<SuccessObject>(this.path + 'setPin', {new_pin: new_pin},
this.httpOptions);
}
public openSnackBar(message: string, action = ''): void {
this.snackBar.open(message, action, {
duration: 2000,

View File

@@ -257,6 +257,25 @@
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">
<mat-checkbox color="accent" [(ngModel)]="new_config['Extra']['use_pin']"><ng-container i18n="Use pin to hide settings setting">Use pin to hide settings</ng-container></mat-checkbox>
</div>
<div class="col-12 mb-3">
<div class="pin-set" *ngIf="new_config['Extra']['pin_hash']">
<mat-icon>done</mat-icon>&nbsp;<ng-container i18n="Pin set">Pin set!</ng-container>
</div>
<div>
<button mat-stroked-button (click)="openSetPinDialog()">
<ng-container *ngIf="!new_config['Extra']['pin_hash']" i18n="Set pin">Set pin</ng-container>
<ng-container *ngIf="new_config['Extra']['pin_hash']" i18n="Reset pin">Reset pin</ng-container>
</button>
</div>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div *ngIf="new_config" class="container-fluid">
<div class="row">
<div class="col-12 mt-3">

View File

@@ -111,3 +111,9 @@
top: 6px;
margin-left: 10px;
}
.pin-set {
display: flex;
align-items: center;
margin-bottom: 10px;
}

View File

@@ -15,6 +15,7 @@ import { EditCategoryDialogComponent } from 'app/dialogs/edit-category-dialog/ed
import { ActivatedRoute, Router } from '@angular/router';
import { Category, DBInfoResponse } from 'api-types';
import { GenerateRssUrlComponent } from 'app/dialogs/generate-rss-url/generate-rss-url.component';
import { SetPinDialogComponent } from 'app/dialogs/set-pin-dialog/set-pin-dialog.component';
@Component({
selector: 'app-settings',
@@ -373,4 +374,8 @@ export class SettingsComponent implements OnInit {
maxWidth: '880px'
});
}
openSetPinDialog(): void {
this.dialog.open(SetPinDialogComponent);
}
}