From b685b955df96db7a6e68697b95b8743d09171da5 Mon Sep 17 00:00:00 2001 From: Tzahi12345 Date: Fri, 1 May 2020 03:34:35 -0400 Subject: [PATCH] Added roles and permissions system, as well as the ability to modify users and their roles Downloads manager now uses device fingerprint as identifier rather than a randomly generated sessionID --- backend/app.js | 97 +++++++- backend/authentication/auth.js | 114 ++++++++- backend/consts.js | 10 + src/app/app.component.html | 6 +- src/app/app.module.ts | 16 +- src/app/components/login/login.component.html | 2 +- src/app/components/login/login.component.ts | 3 + .../manage-role/manage-role.component.html | 19 ++ .../manage-role/manage-role.component.scss | 4 + .../manage-role/manage-role.component.spec.ts | 25 ++ .../manage-role/manage-role.component.ts | 61 +++++ .../manage-user/manage-user.component.html | 31 +++ .../manage-user/manage-user.component.scss | 4 + .../manage-user/manage-user.component.spec.ts | 25 ++ .../manage-user/manage-user.component.ts | 69 ++++++ .../modify-users/modify-users.component.html | 107 +++++++++ .../modify-users/modify-users.component.scss | 5 + .../modify-users.component.spec.ts | 25 ++ .../modify-users/modify-users.component.ts | 219 ++++++++++++++++++ .../add-user-dialog.component.html | 19 ++ .../add-user-dialog.component.scss | 0 .../add-user-dialog.component.spec.ts | 25 ++ .../add-user-dialog.component.ts | 32 +++ .../set-default-admin-dialog.component.html | 2 +- src/app/main/main.component.html | 2 +- src/app/main/main.component.ts | 6 +- src/app/player/player.component.html | 4 +- src/app/player/player.component.ts | 2 +- src/app/posts.services.ts | 71 ++++-- src/app/settings/settings.component.html | 3 + src/app/settings/settings.component.ts | 2 +- 31 files changed, 974 insertions(+), 36 deletions(-) create mode 100644 src/app/components/manage-role/manage-role.component.html create mode 100644 src/app/components/manage-role/manage-role.component.scss create mode 100644 src/app/components/manage-role/manage-role.component.spec.ts create mode 100644 src/app/components/manage-role/manage-role.component.ts create mode 100644 src/app/components/manage-user/manage-user.component.html create mode 100644 src/app/components/manage-user/manage-user.component.scss create mode 100644 src/app/components/manage-user/manage-user.component.spec.ts create mode 100644 src/app/components/manage-user/manage-user.component.ts create mode 100644 src/app/components/modify-users/modify-users.component.html create mode 100644 src/app/components/modify-users/modify-users.component.scss create mode 100644 src/app/components/modify-users/modify-users.component.spec.ts create mode 100644 src/app/components/modify-users/modify-users.component.ts create mode 100644 src/app/dialogs/add-user-dialog/add-user-dialog.component.html create mode 100644 src/app/dialogs/add-user-dialog/add-user-dialog.component.scss create mode 100644 src/app/dialogs/add-user-dialog/add-user-dialog.component.spec.ts create mode 100644 src/app/dialogs/add-user-dialog/add-user-dialog.component.ts diff --git a/backend/app.js b/backend/app.js index 0b69026..1376471 100644 --- a/backend/app.js +++ b/backend/app.js @@ -91,7 +91,25 @@ db.defaults( users_db.defaults( { - users: [] + users: [], + roles: { + "admin": { + "permissions": [ + 'filemanager', + 'settings', + 'subscriptions', + 'sharing', + 'advanced_download', + 'downloads_manager' + ] + }, "user": { + "permissions": [ + 'filemanager', + 'subscriptions', + 'sharing' + ] + } + } } ).write(); @@ -2737,7 +2755,7 @@ app.post('/api/auth/jwtAuth' ); app.post('/api/auth/changePassword', optionalJwt, async (req, res) => { let user_uid = req.user.uid; - let password = req.body.password; + let password = req.body.new_password; let success = await auth_api.changeUserPassword(user_uid, password); res.send({success: success}); }); @@ -2746,6 +2764,81 @@ app.post('/api/auth/adminExists', async (req, res) => { res.send({exists: exists}); }); +// user management +app.post('/api/getUsers', optionalJwt, async (req, res) => { + let users = users_db.get('users').value(); + res.send({users: users}); +}); +app.post('/api/getRoles', optionalJwt, async (req, res) => { + let roles = users_db.get('roles').value(); + res.send({roles: roles}); +}); + +app.post('/api/changeUser', optionalJwt, async (req, res) => { + let change_obj = req.body.change_object; + try { + const user_db_obj = users_db.get('users').find({uid: change_obj.uid}); + if (change_obj.name) { + user_db_obj.assign({name: change_obj.name}).write(); + } + if (change_obj.role) { + user_db_obj.assign({role: change_obj.role}).write(); + } + res.send({success: true}); + } catch (err) { + logger.error(err); + res.send({success: false}); + } +}); + +app.post('/api/deleteUser', optionalJwt, async (req, res) => { + let uid = req.body.uid; + try { + let usersFileFolder = config_api.getConfigItem('ytdl_users_base_path'); + const user_folder = path.join(__dirname, usersFileFolder, uid); + const user_db_obj = users_db.get('users').find({uid: uid}); + if (user_db_obj.value()) { + // user exists, let's delete + deleteFolderRecursive(user_folder); + users_db.get('users').remove({uid: uid}).write(); + } + res.send({success: true}); + } catch (err) { + logger.error(err); + res.send({success: false}); + } +}); + +app.post('/api/changeUserPermissions', optionalJwt, async (req, res) => { + const user_uid = req.body.user_uid; + const permission = req.body.permission; + const new_value = req.body.new_value; + + if (!permission || !new_value) { + res.sendStatus(400); + return; + } + + const success = auth_api.changeUserPermissions(user_uid, permission, new_value); + + res.send({success: success}); +}); + +app.post('/api/changeRolePermissions', optionalJwt, async (req, res) => { + const role = req.body.role; + const permission = req.body.permission; + const new_value = req.body.new_value; + + if (!permission || !new_value) { + res.sendStatus(400); + return; + } + + const success = auth_api.changeRolePermissions(role, permission, new_value); + + res.send({success: success}); +}); + app.use(function(req, res, next) { //if the request is not html then move along var accept = req.accepts('html', 'json', 'xml'); diff --git a/backend/authentication/auth.js b/backend/authentication/auth.js index b8930a8..7ca09fa 100644 --- a/backend/authentication/auth.js +++ b/backend/authentication/auth.js @@ -1,5 +1,6 @@ const path = require('path'); const config_api = require('../config'); +const consts = require('../consts'); var subscriptions_api = require('../subscriptions') const fs = require('fs-extra'); var jwt = require('jsonwebtoken'); @@ -97,7 +98,9 @@ exports.registerUser = function(req, res) { }, subscriptions: [], created: Date.now(), - role: userid === 'admin' ? 'admin' : 'user' + role: userid === 'admin' ? 'admin' : 'user', + permissions: [], + permission_overrides: [] }; // check if user exists if (users_db.get('users').find({uid: userid}).value()) { @@ -200,8 +203,7 @@ exports.authenticateViaPassport = function(req, res, next) { exports.generateJWT = function(req, res, next) { var payload = { exp: Math.floor(Date.now() / 1000) + JWT_EXPIRATION - , user: req.user, -// , role: role + , user: req.user }; req.token = jwt.sign(payload, SERVER_SECRET); next(); @@ -210,7 +212,9 @@ exports.generateJWT = function(req, res, next) { exports.returnAuthResponse = function(req, res) { res.status(200).json({ user: req.user, - token: req.token + token: req.token, + permissions: exports.userPermissions(req.user.uid), + available_permissions: consts['AVAILABLE_PERMISSIONS'] }); } @@ -252,6 +256,40 @@ exports.changeUserPassword = async function(user_uid, new_pass) { }); } +// change user permissions +exports.changeUserPermissions = function(user_uid, permission, new_value) { + try { + const user_db_obj = users_db.get('users').find({uid: user_uid}); + user_db_obj.get('permissions').pull(permission).write(); + user_db_obj.get('permission_overrides').pull(permission).write(); + if (new_value === 'yes') { + user_db_obj.get('permissions').push(permission).write(); + user_db_obj.get('permission_overrides').push(permission).write(); + } else if (new_value === 'no') { + user_db_obj.get('permission_overrides').push(permission).write(); + } + return true; + } catch (err) { + logger.error(err); + return false; + } +} + +// change role permissions +exports.changeRolePermissions = function(role, permission, new_value) { + try { + const role_db_obj = users_db.get('roles').get(role); + role_db_obj.get('permissions').pull(permission).write(); + if (new_value === 'yes') { + role_db_obj.get('permissions').push(permission).write(); + } + return true; + } catch (err) { + logger.error(err); + return false; + } +} + exports.adminExists = function() { return !!users_db.get('users').find({uid: 'admin'}).value(); } @@ -410,6 +448,74 @@ exports.changeSharingMode = function(user_uid, file_uid, type, is_playlist, enab return success; } +exports.userHasPermission = function(user_uid, permission) { + const user_obj = users_db.get('users').find({uid: user_uid}).value(); + const role = user_obj['role']; + if (!role) { + // role doesn't exist + logger.error('Invalid role ' + role); + return false; + } + const role_permissions = (users_db.get('roles').value())['permissions']; + + const user_has_explicit_permission = user_obj['permissions'].includes(permission); + const permission_in_overrides = user_obj['permission_overrides'].includes(permission); + + // check if user has a negative/positive override + if (user_has_explicit_permission && permission_in_overrides) { + // positive override + return true; + } else if (!user_has_explicit_permission && permission_in_overrides) { + // negative override + return false; + } + + // no overrides, let's check if the role has the permission + if (role_permissions.includes(permission)) { + return true; + } else { + logger.verbose(`User ${user_uid} failed to get permission ${permission}`); + return false; + } +} + +exports.userPermissions = function(user_uid) { + let user_permissions = []; + const user_obj = users_db.get('users').find({uid: user_uid}).value(); + const role = user_obj['role']; + if (!role) { + // role doesn't exist + logger.error('Invalid role ' + role); + return null; + } + const role_permissions = users_db.get('roles').get(role).get('permissions').value() + + for (let i = 0; i < consts['AVAILABLE_PERMISSIONS'].length; i++) { + let permission = consts['AVAILABLE_PERMISSIONS'][i]; + + const user_has_explicit_permission = user_obj['permissions'].includes(permission); + const permission_in_overrides = user_obj['permission_overrides'].includes(permission); + + // check if user has a negative/positive override + if (user_has_explicit_permission && permission_in_overrides) { + // positive override + user_permissions.push(permission); + } else if (!user_has_explicit_permission && permission_in_overrides) { + // negative override + continue; + } + + // no overrides, let's check if the role has the permission + if (role_permissions.includes(permission)) { + user_permissions.push(permission); + } else { + continue; + } + } + + return user_permissions; +} + function getToken(queryParams) { if (queryParams && queryParams.jwt) { var parted = queryParams.jwt.split(' '); diff --git a/backend/consts.js b/backend/consts.js index f5b62ad..cb8dc0a 100644 --- a/backend/consts.js +++ b/backend/consts.js @@ -142,7 +142,17 @@ let CONFIG_ITEMS = { }, }; +AVAILABLE_PERMISSIONS = [ + 'filemanager', + 'settings', + 'subscriptions', + 'sharing', + 'advanced_download', + 'downloads_manager' +]; + module.exports = { CONFIG_ITEMS: CONFIG_ITEMS, + AVAILABLE_PERMISSIONS: AVAILABLE_PERMISSIONS, CURRENT_VERSION: 'v3.6' } \ No newline at end of file diff --git a/src/app/app.component.html b/src/app/app.component.html index 5de160c..8f58860 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -23,7 +23,7 @@ Dark - @@ -41,8 +41,8 @@ Home - Subscriptions - Downloads + Subscriptions + Downloads diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 4d4a46d..96b91b0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -25,6 +25,9 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatToolbarModule } from '@angular/material/toolbar'; import { MatTabsModule } from '@angular/material/tabs'; +import {MatPaginatorModule} from '@angular/material/paginator'; +import {MatSortModule} from '@angular/material/sort'; +import {MatTableModule} from '@angular/material/table'; import {DragDropModule} from '@angular/cdk/drag-drop'; import {ClipboardModule} from '@angular/cdk/clipboard'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; @@ -61,6 +64,10 @@ import { LoginComponent } from './components/login/login.component'; import { DownloadsComponent } from './components/downloads/downloads.component'; import { UserProfileDialogComponent } from './dialogs/user-profile-dialog/user-profile-dialog.component'; import { SetDefaultAdminDialogComponent } from './dialogs/set-default-admin-dialog/set-default-admin-dialog.component'; +import { ModifyUsersComponent } from './components/modify-users/modify-users.component'; +import { AddUserDialogComponent } from './dialogs/add-user-dialog/add-user-dialog.component'; +import { ManageUserComponent } from './components/manage-user/manage-user.component'; +import { ManageRoleComponent } from './components/manage-role/manage-role.component'; registerLocaleData(es, 'es'); export function isVisible({ event, element, scrollContainer, offset }: IsVisibleProps) { @@ -93,7 +100,11 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible LoginComponent, DownloadsComponent, UserProfileDialogComponent, - SetDefaultAdminDialogComponent + SetDefaultAdminDialogComponent, + ModifyUsersComponent, + AddUserDialogComponent, + ManageUserComponent, + ManageRoleComponent ], imports: [ CommonModule, @@ -127,6 +138,9 @@ export function isVisible({ event, element, scrollContainer, offset }: IsVisible MatAutocompleteModule, MatTabsModule, MatTooltipModule, + MatPaginatorModule, + MatSortModule, + MatTableModule, DragDropModule, ClipboardModule, VgCoreModule, diff --git a/src/app/components/login/login.component.html b/src/app/components/login/login.component.html index 3833599..7bc4ac7 100644 --- a/src/app/components/login/login.component.html +++ b/src/app/components/login/login.component.html @@ -8,7 +8,7 @@
- +
diff --git a/src/app/components/login/login.component.ts b/src/app/components/login/login.component.ts index a592302..0665ee0 100644 --- a/src/app/components/login/login.component.ts +++ b/src/app/components/login/login.component.ts @@ -40,6 +40,9 @@ export class LoginComponent implements OnInit { } login() { + if (this.loginPasswordInput === '') { + return; + } this.loggingIn = true; this.postsService.login(this.loginUsernameInput, this.loginPasswordInput).subscribe(res => { this.loggingIn = false; diff --git a/src/app/components/manage-role/manage-role.component.html b/src/app/components/manage-role/manage-role.component.html new file mode 100644 index 0000000..b3e8de9 --- /dev/null +++ b/src/app/components/manage-role/manage-role.component.html @@ -0,0 +1,19 @@ +

Manage role - {{role.name}}

+ + + + +

{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}

+ + + Yes + No + + +
+
+
+ + + + \ No newline at end of file diff --git a/src/app/components/manage-role/manage-role.component.scss b/src/app/components/manage-role/manage-role.component.scss new file mode 100644 index 0000000..167abd6 --- /dev/null +++ b/src/app/components/manage-role/manage-role.component.scss @@ -0,0 +1,4 @@ +.mat-radio-button { + margin-right: 10px; + margin-top: 5px; +} \ No newline at end of file diff --git a/src/app/components/manage-role/manage-role.component.spec.ts b/src/app/components/manage-role/manage-role.component.spec.ts new file mode 100644 index 0000000..2e9579e --- /dev/null +++ b/src/app/components/manage-role/manage-role.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ManageRoleComponent } from './manage-role.component'; + +describe('ManageRoleComponent', () => { + let component: ManageRoleComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ManageRoleComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ManageRoleComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/manage-role/manage-role.component.ts b/src/app/components/manage-role/manage-role.component.ts new file mode 100644 index 0000000..4e05e29 --- /dev/null +++ b/src/app/components/manage-role/manage-role.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { PostsService } from 'app/posts.services'; + +@Component({ + selector: 'app-manage-role', + templateUrl: './manage-role.component.html', + styleUrls: ['./manage-role.component.scss'] +}) +export class ManageRoleComponent implements OnInit { + + role = null; + available_permissions = null; + permissions = null; + + permissionToLabel = { + 'filemanager': 'File manager', + 'settings': 'Settings access', + 'subscriptions': 'Subscriptions', + 'sharing': 'Share files', + 'advanced_download': 'Use advanced download mode', + 'downloads_manager': 'Use downloads manager' + } + + constructor(public postsService: PostsService, private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any) { + if (this.data) { + this.role = this.data.role; + this.available_permissions = this.postsService.available_permissions; + this.parsePermissions(); + } + } + + ngOnInit(): void { + } + + parsePermissions() { + this.permissions = {}; + for (let i = 0; i < this.available_permissions.length; i++) { + const permission = this.available_permissions[i]; + if (this.role.permissions.includes(permission)) { + this.permissions[permission] = 'yes'; + } else { + this.permissions[permission] = 'no'; + } + } + } + + changeRolePermissions(change, permission) { + this.postsService.setRolePermission(this.role.name, permission, change.value).subscribe(res => { + if (res['success']) { + + } else { + this.permissions[permission] = this.permissions[permission] === 'yes' ? 'no' : 'yes'; + } + }, err => { + this.permissions[permission] = this.permissions[permission] === 'yes' ? 'no' : 'yes'; + }); + } + +} diff --git a/src/app/components/manage-user/manage-user.component.html b/src/app/components/manage-user/manage-user.component.html new file mode 100644 index 0000000..853cd72 --- /dev/null +++ b/src/app/components/manage-user/manage-user.component.html @@ -0,0 +1,31 @@ +

Manage user - {{user.name}}

+ + +

User UID: {{user.uid}}

+ +
+ + + + +
+ +
+ + +

{{permissionToLabel[permission] ? permissionToLabel[permission] : permission}}

+ + + Use default + Yes + No + + +
+
+
+
+ + + + \ No newline at end of file diff --git a/src/app/components/manage-user/manage-user.component.scss b/src/app/components/manage-user/manage-user.component.scss new file mode 100644 index 0000000..167abd6 --- /dev/null +++ b/src/app/components/manage-user/manage-user.component.scss @@ -0,0 +1,4 @@ +.mat-radio-button { + margin-right: 10px; + margin-top: 5px; +} \ No newline at end of file diff --git a/src/app/components/manage-user/manage-user.component.spec.ts b/src/app/components/manage-user/manage-user.component.spec.ts new file mode 100644 index 0000000..f8fe3a7 --- /dev/null +++ b/src/app/components/manage-user/manage-user.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ManageUserComponent } from './manage-user.component'; + +describe('ManageUserComponent', () => { + let component: ManageUserComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ManageUserComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ManageUserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/manage-user/manage-user.component.ts b/src/app/components/manage-user/manage-user.component.ts new file mode 100644 index 0000000..61b38f4 --- /dev/null +++ b/src/app/components/manage-user/manage-user.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { MAT_DIALOG_DATA } from '@angular/material/dialog'; + +@Component({ + selector: 'app-manage-user', + templateUrl: './manage-user.component.html', + styleUrls: ['./manage-user.component.scss'] +}) +export class ManageUserComponent implements OnInit { + + user = null; + newPasswordInput = ''; + available_permissions = null; + permissions = null; + + permissionToLabel = { + 'filemanager': 'File manager', + 'settings': 'Settings access', + 'subscriptions': 'Subscriptions', + 'sharing': 'Share files', + 'advanced_download': 'Use advanced download mode', + 'downloads_manager': 'Use downloads manager' + } + + settingNewPassword = false; + + constructor(public postsService: PostsService, @Inject(MAT_DIALOG_DATA) public data: any) { + if (this.data) { + this.user = this.data.user; + this.available_permissions = this.postsService.available_permissions; + this.parsePermissions(); + } + } + + ngOnInit(): void { + } + + parsePermissions() { + this.permissions = {}; + for (let i = 0; i < this.available_permissions.length; i++) { + const permission = this.available_permissions[i]; + if (this.user.permission_overrides.includes(permission)) { + if (this.user.permissions.includes(permission)) { + this.permissions[permission] = 'yes'; + } else { + this.permissions[permission] = 'no'; + } + } else { + this.permissions[permission] = 'default'; + } + } + } + + changeUserPermissions(change, permission) { + this.postsService.setUserPermission(this.user.uid, permission, change.value).subscribe(res => { + // console.log(res); + }); + } + + setNewPassword() { + this.settingNewPassword = true; + this.postsService.changeUserPassword(this.user.uid, this.newPasswordInput).subscribe(res => { + this.newPasswordInput = ''; + this.settingNewPassword = false; + }); + } + +} diff --git a/src/app/components/modify-users/modify-users.component.html b/src/app/components/modify-users/modify-users.component.html new file mode 100644 index 0000000..036ad2b --- /dev/null +++ b/src/app/components/modify-users/modify-users.component.html @@ -0,0 +1,107 @@ +
+
+
+
+
+ + + +
+ +
+ + + + + + User name + + + + + + + + + + + {{row.name}} + + + + + + + Role + + + + + + Admin + User + + + + + + {{row.role}} + + + + + + + Actions + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + +
+ +
+ +
+ + + +
\ No newline at end of file diff --git a/src/app/components/modify-users/modify-users.component.scss b/src/app/components/modify-users/modify-users.component.scss new file mode 100644 index 0000000..558267e --- /dev/null +++ b/src/app/components/modify-users/modify-users.component.scss @@ -0,0 +1,5 @@ +.edit-role { + position: relative; + top: -80px; + left: 35px; +} \ No newline at end of file diff --git a/src/app/components/modify-users/modify-users.component.spec.ts b/src/app/components/modify-users/modify-users.component.spec.ts new file mode 100644 index 0000000..e5e8ef8 --- /dev/null +++ b/src/app/components/modify-users/modify-users.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ModifyUsersComponent } from './modify-users.component'; + +describe('ModifyUsersComponent', () => { + let component: ModifyUsersComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ModifyUsersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ModifyUsersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/modify-users/modify-users.component.ts b/src/app/components/modify-users/modify-users.component.ts new file mode 100644 index 0000000..9e54fda --- /dev/null +++ b/src/app/components/modify-users/modify-users.component.ts @@ -0,0 +1,219 @@ +import { Component, OnInit, Input, ViewChild, AfterViewInit } from '@angular/core'; +import { MatPaginator, PageEvent } from '@angular/material/paginator'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { PostsService } from 'app/posts.services'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { AddUserDialogComponent } from 'app/dialogs/add-user-dialog/add-user-dialog.component'; +import { ManageUserComponent } from '../manage-user/manage-user.component'; +import { ManageRoleComponent } from '../manage-role/manage-role.component'; + +@Component({ + selector: 'app-modify-users', + templateUrl: './modify-users.component.html', + styleUrls: ['./modify-users.component.scss'] +}) +export class ModifyUsersComponent implements OnInit, AfterViewInit { + + displayedColumns = ['name', 'role', 'actions']; + dataSource = new MatTableDataSource(); + + deleteDialogContentSubstring = 'Are you sure you want delete user '; + + @ViewChild(MatPaginator) paginator: MatPaginator; + @ViewChild(MatSort) sort: MatSort; + + // MatPaginator Inputs + length = 100; + @Input() pageSize = 5; + pageSizeOptions: number[] = [5, 10, 25, 100]; + + // MatPaginator Output + pageEvent: PageEvent; + users: any; + editObject = null; + constructedObject = {}; + roles = null; + + + constructor(public postsService: PostsService, public snackBar: MatSnackBar, public dialog: MatDialog, + private dialogRef: MatDialogRef) { } + + ngOnInit() { + this.getArray(); + this.getRoles(); + } + + ngAfterViewInit() { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + } + + /** + * Set the paginator and sort after the view init since this component will + * be able to query its view for the initialized paginator and sort. + */ + afterGetData() { + this.dataSource.sort = this.sort; + } + + setPageSizeOptions(setPageSizeOptionsInput: string) { + this.pageSizeOptions = setPageSizeOptionsInput.split(',').map(str => +str); + } + + applyFilter(filterValue: string) { + filterValue = filterValue.trim(); // Remove whitespace + filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches + this.dataSource.filter = filterValue; + } + + private getArray() { + this.postsService.getUsers().subscribe(res => { + this.users = res['users']; + this.createAndSortData(); + this.afterGetData(); + }); + } + + getRoles() { + this.postsService.getRoles().subscribe(res => { + this.roles = []; + const roles = res['roles']; + const role_names = Object.keys(roles); + for (let i = 0; i < role_names.length; i++) { + const role_name = role_names[i]; + this.roles.push({ + name: role_name, + permissions: roles[role_name]['permissions'] + }); + } + }); + } + + openAddUserDialog() { + const dialogRef = this.dialog.open(AddUserDialogComponent); + dialogRef.afterClosed().subscribe(user => { + if (user && !user.error) { + this.openSnackBar('Successfully added user ' + user.name); + this.getArray(); + } else if (user && user.error) { + this.openSnackBar('Failed to add user'); + } + }); + } + + finishEditing(user_uid) { + let has_finished = false; + if (this.constructedObject && this.constructedObject['name'] && this.constructedObject['role']) { + if (!isEmptyOrSpaces(this.constructedObject['name']) && !isEmptyOrSpaces(this.constructedObject['role'])) { + has_finished = true; + const index_of_object = this.indexOfUser(user_uid); + this.users[index_of_object] = this.constructedObject; + this.constructedObject = {}; + this.editObject = null; + this.setUser(this.users[index_of_object]); + this.createAndSortData(); + } + } + } + + enableEditMode(user_uid) { + if (this.uidInUserList(user_uid) && this.indexOfUser(user_uid) > -1) { + const users_index = this.indexOfUser(user_uid); + this.editObject = this.users[users_index]; + this.constructedObject['name'] = this.users[users_index].name; + this.constructedObject['uid'] = this.users[users_index].uid; + this.constructedObject['role'] = this.users[users_index].role; + } + } + + disableEditMode() { + this.editObject = null; + } + + // checks if user is in users array by name + uidInUserList(user_uid) { + for (let i = 0; i < this.users.length; i++) { + if (this.users[i].uid === user_uid) { + return true; + } + } + return false; + } + + // gets index of user in users array by name + indexOfUser(user_uid) { + for (let i = 0; i < this.users.length; i++) { + if (this.users[i].uid === user_uid) { + return i; + } + } + return -1; + } + + setUser(change_obj) { + this.postsService.changeUser(change_obj).subscribe(res => { + this.getArray(); + }); + } + + manageUser(user_uid) { + const index_of_object = this.indexOfUser(user_uid); + const user_obj = this.users[index_of_object]; + this.dialog.open(ManageUserComponent, { + data: { + user: user_obj + }, + width: '65vw' + }); + } + + removeUser(user_uid) { + this.postsService.deleteUser(user_uid).subscribe(res => { + this.getArray(); + }, err => { + this.getArray(); + }); + } + + createAndSortData() { + // Sorts the data by last finished + this.users.sort((a, b) => b.name > a.name); + + const filteredData = []; + for (let i = 0; i < this.users.length; i++) { + filteredData.push(JSON.parse(JSON.stringify(this.users[i]))); + } + + // Assign the data to the data source for the table to render + this.dataSource.data = filteredData; + } + + openModifyRole(role) { + const dialogRef = this.dialog.open(ManageRoleComponent, { + data: { + role: role + } + }); + + dialogRef.afterClosed().subscribe(success => { + this.getRoles(); + }); + } + + closeDialog() { + this.dialogRef.close(); + } + + public openSnackBar(message: string, action: string = '') { + this.snackBar.open(message, action, { + duration: 2000, + }); + } + +} + +function isEmptyOrSpaces(str){ + return str === null || str.match(/^ *$/) !== null; +} diff --git a/src/app/dialogs/add-user-dialog/add-user-dialog.component.html b/src/app/dialogs/add-user-dialog/add-user-dialog.component.html new file mode 100644 index 0000000..68cb91f --- /dev/null +++ b/src/app/dialogs/add-user-dialog/add-user-dialog.component.html @@ -0,0 +1,19 @@ +

Register a user

+ + +
+ + + +
+
+ + + +
+
+ + + + + \ No newline at end of file diff --git a/src/app/dialogs/add-user-dialog/add-user-dialog.component.scss b/src/app/dialogs/add-user-dialog/add-user-dialog.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/dialogs/add-user-dialog/add-user-dialog.component.spec.ts b/src/app/dialogs/add-user-dialog/add-user-dialog.component.spec.ts new file mode 100644 index 0000000..2eee5ca --- /dev/null +++ b/src/app/dialogs/add-user-dialog/add-user-dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddUserDialogComponent } from './add-user-dialog.component'; + +describe('AddUserDialogComponent', () => { + let component: AddUserDialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ AddUserDialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AddUserDialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/dialogs/add-user-dialog/add-user-dialog.component.ts b/src/app/dialogs/add-user-dialog/add-user-dialog.component.ts new file mode 100644 index 0000000..383103a --- /dev/null +++ b/src/app/dialogs/add-user-dialog/add-user-dialog.component.ts @@ -0,0 +1,32 @@ +import { Component, OnInit } from '@angular/core'; +import { PostsService } from 'app/posts.services'; +import { MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-add-user-dialog', + templateUrl: './add-user-dialog.component.html', + styleUrls: ['./add-user-dialog.component.scss'] +}) +export class AddUserDialogComponent implements OnInit { + + usernameInput = ''; + passwordInput = ''; + + constructor(private postsService: PostsService, public dialogRef: MatDialogRef) { } + + ngOnInit(): void { + } + + createUser() { + this.postsService.register(this.usernameInput, this.passwordInput).subscribe(res => { + if (res['user']) { + this.dialogRef.close(res['user']); + } else { + this.dialogRef.close({error: 'Unknown error'}); + } + }, err => { + this.dialogRef.close({error: err}); + }); + } + +} diff --git a/src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html b/src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html index 1d800b9..4cfbda1 100644 --- a/src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html +++ b/src/app/dialogs/set-default-admin-dialog/set-default-admin-dialog.component.html @@ -7,7 +7,7 @@
- +
diff --git a/src/app/main/main.component.html b/src/app/main/main.component.html index 46079fa..3d41d80 100644 --- a/src/app/main/main.component.html +++ b/src/app/main/main.component.html @@ -186,7 +186,7 @@ -
+
diff --git a/src/app/main/main.component.ts b/src/app/main/main.component.ts index 36bd9f2..aef078f 100644 --- a/src/app/main/main.component.ts +++ b/src/app/main/main.component.ts @@ -3,7 +3,6 @@ import {PostsService} from '../posts.services'; import {FileCardComponent} from '../file-card/file-card.component'; import { Observable } from 'rxjs/Observable'; import {FormControl, Validators} from '@angular/forms'; -import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; import { MatDialog } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { saveAs } from 'file-saver'; @@ -215,7 +214,7 @@ export class MainComponent implements OnInit { simulatedOutput = ''; - constructor(private postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar, + constructor(public postsService: PostsService, private youtubeSearch: YoutubeSearchService, public snackBar: MatSnackBar, private router: Router, public dialog: MatDialog, private platform: Platform, private route: ActivatedRoute) { this.audioOnly = false; } @@ -242,7 +241,8 @@ export class MainComponent implements OnInit { this.postsService.config['API']['youtube_API_key']; this.youtubeAPIKey = this.youtubeSearchEnabled ? this.postsService.config['API']['youtube_API_key'] : null; this.allowQualitySelect = this.postsService.config['Extra']['allow_quality_select']; - this.allowAdvancedDownload = this.postsService.config['Advanced']['allow_advanced_download']; + this.allowAdvancedDownload = this.postsService.config['Advanced']['allow_advanced_download'] + && (!this.postsService.isLoggedIn || this.postsService.permissions.includes('advanced_download')); this.useDefaultDownloadingAgent = this.postsService.config['Advanced']['use_default_downloading_agent']; this.customDownloadingAgent = this.postsService.config['Advanced']['custom_downloading_agent']; diff --git a/src/app/player/player.component.html b/src/app/player/player.component.html index 6c03c3e..fce9125 100644 --- a/src/app/player/player.component.html +++ b/src/app/player/player.component.html @@ -26,10 +26,10 @@
- +
- +
\ No newline at end of file diff --git a/src/app/player/player.component.ts b/src/app/player/player.component.ts index 84bba1c..2cad9c6 100644 --- a/src/app/player/player.component.ts +++ b/src/app/player/player.component.ts @@ -90,7 +90,7 @@ export class PlayerComponent implements OnInit { } } - constructor(private postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router, + constructor(public postsService: PostsService, private route: ActivatedRoute, private dialog: MatDialog, private router: Router, public snackBar: MatSnackBar) { } diff --git a/src/app/posts.services.ts b/src/app/posts.services.ts index 42c6168..a4bb0dc 100644 --- a/src/app/posts.services.ts +++ b/src/app/posts.services.ts @@ -10,6 +10,7 @@ import { DOCUMENT } from '@angular/common'; import { BehaviorSubject } from 'rxjs'; import { v4 as uuid } from 'uuid'; import { MatSnackBar } from '@angular/material/snack-bar'; +import * as Fingerprint2 from 'fingerprintjs2'; @Injectable() export class PostsService implements CanActivate { @@ -30,9 +31,13 @@ export class PostsService implements CanActivate { debugMode = false; + // must be reset after logout isLoggedIn = false; token = null; user = null; + permissions = null; + + available_permissions = null; reload_config = new BehaviorSubject(false); config_reloaded = new BehaviorSubject(false); @@ -48,13 +53,13 @@ export class PostsService implements CanActivate { // this.startPath = window.location.href + '/api/'; // this.startPathSSL = window.location.href + '/api/'; this.path = this.document.location.origin + '/api/'; - this.session_id = uuid(); + if (isDevMode()) { this.debugMode = true; this.path = 'http://localhost:17442/api/'; } - this.http_params = `apiKey=${this.auth_token}&sessionID=${this.session_id}` + this.http_params = `apiKey=${this.auth_token}` this.httpOptions = { params: new HttpParams({ @@ -62,6 +67,12 @@ export class PostsService implements CanActivate { }), }; + Fingerprint2.get(components => { + // set identity as user id doesn't necessarily exist + this.session_id = Fingerprint2.x64hash128(components.map(function (pair) { return pair.value; }).join(), 31); + this.httpOptions.params = this.httpOptions.params.set('sessionID', this.session_id); + }); + // get config this.loadNavItems().subscribe(res => { const result = !this.debugMode ? res['config_file'] : res; @@ -71,11 +82,8 @@ export class PostsService implements CanActivate { // login stuff if (localStorage.getItem('jwt_token')) { this.token = localStorage.getItem('jwt_token'); - this.httpOptions = { - params: new HttpParams({ - fromString: `apiKey=${this.auth_token}&sessionID=${this.session_id}&jwt=${this.token}` - }), - }; + this.httpOptions.params = this.httpOptions.params.set('jwt', this.token); + this.jwtAuth(); } else { this.sendToLogin(); @@ -321,18 +329,16 @@ export class PostsService implements CanActivate { return this.http.get('https://api.github.com/repos/tzahi12345/youtubedl-material/releases'); } - afterLogin(user, token) { + afterLogin(user, token, permissions, available_permissions) { this.isLoggedIn = true; this.user = user; + this.permissions = permissions; + this.available_permissions = available_permissions; this.token = token; localStorage.setItem('jwt_token', this.token); - this.httpOptions = { - params: new HttpParams({ - fromString: `apiKey=${this.auth_token}&sessionID=${this.session_id}&jwt=${this.token}` - }), - }; + this.httpOptions.params = this.httpOptions.params.set('jwt', this.token); // needed to re-initialize parts of app after login this.config_reloaded.next(true); @@ -347,7 +353,7 @@ export class PostsService implements CanActivate { const call = this.http.post(this.path + 'auth/login', {userid: username, password: password}, this.httpOptions); call.subscribe(res => { if (res['token']) { - this.afterLogin(res['user'], res['token']); + this.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']); } }); return call; @@ -358,7 +364,7 @@ export class PostsService implements CanActivate { const call = this.http.post(this.path + 'auth/jwtAuth', {}, this.httpOptions); call.subscribe(res => { if (res['token']) { - this.afterLogin(res['user'], res['token']); + this.afterLogin(res['user'], res['token'], res['permissions'], res['available_permissions']); this.setInitialized(); } }, err => { @@ -371,6 +377,7 @@ export class PostsService implements CanActivate { logout() { this.user = null; + this.permissions = null; this.isLoggedIn = false; localStorage.setItem('jwt_token', null); if (this.router.url !== '/login') { @@ -430,7 +437,9 @@ export class PostsService implements CanActivate { } checkAdminCreationStatus() { - console.log('checking c stat'); + if (!this.config['Advanced']['multi_user_mode']) { + return; + } this.adminExists().subscribe(res => { if (!res['exists']) { // must create admin account @@ -439,6 +448,36 @@ export class PostsService implements CanActivate { }); } + changeUser(change_obj) { + return this.http.post(this.path + 'changeUser', {change_object: change_obj}, this.httpOptions); + } + + deleteUser(uid) { + return this.http.post(this.path + 'deleteUser', {uid: uid}, this.httpOptions); + } + + changeUserPassword(user_uid, new_password) { + return this.http.post(this.path + 'auth/changePassword', {user_uid: user_uid, new_password: new_password}, this.httpOptions); + } + + getUsers() { + return this.http.post(this.path + 'getUsers', {}, this.httpOptions); + } + + getRoles() { + return this.http.post(this.path + 'getRoles', {}, this.httpOptions); + } + + setUserPermission(user_uid, permission, new_value) { + return this.http.post(this.path + 'changeUserPermissions', {user_uid: user_uid, permission: permission, new_value: new_value}, + this.httpOptions); + } + + setRolePermission(role_name, permission, new_value) { + return this.http.post(this.path + 'changeRolePermissions', {role: role_name, permission: permission, new_value: new_value}, + this.httpOptions); + } + public openSnackBar(message: string, action: string = '') { this.snackBar.open(message, action, { duration: 2000, diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index e2e9102..b2696d5 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -266,6 +266,9 @@
+ + + diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index c3021a3..c31c28a 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -39,7 +39,7 @@ export class SettingsComponent implements OnInit { this._settingsSame = val; } - constructor(private postsService: PostsService, private snackBar: MatSnackBar, private sanitizer: DomSanitizer, + constructor(public postsService: PostsService, private snackBar: MatSnackBar, private sanitizer: DomSanitizer, private dialog: MatDialog) { } ngOnInit() {