Compare commits

...

12 Commits

Author SHA1 Message Date
Tzahi12345
010f0fbb1c Added ability to set a pin for settings menu 2023-04-27 21:37:32 -04:00
Tzahi12345
f973426bd2 Hotfix for error that prevents downloads from occurring 2023-04-24 21:11:10 -04:00
Tzahi12345
5a379a6a2b Updated package-lock.json (#877) 2023-04-24 20:03:11 -04:00
Tzahi12345
d76aaf83f6 Merge pull request #707 from Tzahi12345/categories-fix
Categories matching bug fix
2023-04-24 10:35:08 -04:00
Tzahi12345
d3b88412c6 Fixed thumbnails for auto-generated playlists 2023-04-23 22:27:09 -04:00
Tzahi12345
6cee892e18 Added label for category field in video-info-dialog 2023-04-23 22:20:50 -04:00
Tzahi12345
e2438a236b Adjusted category UI styling to Angular 15 updates 2023-04-23 22:14:20 -04:00
Tzahi12345
955c401f0b Merge branch 'master' of https://github.com/Tzahi12345/YoutubeDL-Material into categories-fix 2023-04-22 17:47:16 -04:00
Tzahi12345
527b1f1cb9 Merge pull request #872 from Tzahi12345/twitch-downloader-fix
Twitch chat download fixes
2023-04-21 00:16:06 -04:00
Isaac Abadi
2e52ec22e0 Default sort for videos is now download date 2022-09-15 21:38:19 -04:00
Isaac Abadi
efdd0dd228 Categories fix for yt-dlp 2022-09-15 21:38:00 -04:00
Isaac Abadi
415c97cb09 Fixed issue where categories were not being properly applied to matching files (#701) 2022-07-07 01:07:22 -04:00
158 changed files with 581 additions and 181 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

@@ -162,6 +162,7 @@ app.use(bodyParser.json());
// use passport
app.use(auth_api.passport.initialize());
app.use(auth_api.passport.session());
// actual functions
@@ -741,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({
@@ -749,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);
@@ -1933,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;
@@ -2028,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

@@ -245,11 +245,10 @@ async function collectInfo(download_uid) {
options.customOutput = category['custom_output'];
options.noRelativePath = true;
args = await exports.generateArgs(url, type, options, download['user_uid']);
args = utils.filterArgs(args, ['--no-simulate']);
info = await exports.getVideoInfoByURL(url, args, download_uid);
}
download['category'] = category;
const stripped_category = category ? {name: category['name'], uid: category['uid']} : null;
// setup info required to calculate download progress
@@ -272,6 +271,7 @@ async function collectInfo(download_uid) {
files_to_check_for_progress: files_to_check_for_progress,
expected_file_size: expected_file_size,
title: playlist_title ? playlist_title : info['title'],
category: stripped_category,
prefetched_info: null
});
}
@@ -552,7 +552,8 @@ exports.generateArgs = async (url, type, options, user_uid = null, simulated = f
exports.getVideoInfoByURL = async (url, args = [], download_uid = null) => {
return new Promise(resolve => {
// remove bad args
const new_args = [...args];
const temp_args = utils.filterArgs(args, ['--no-simulate']);
const new_args = [...temp_args];
const archiveArgIndex = new_args.indexOf('--download-archive');
if (archiveArgIndex !== -1) {

View File

@@ -255,7 +255,7 @@
"array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
},
"array.prototype.findindex": {
"version": "2.2.1",
@@ -891,7 +891,7 @@
"ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"enabled": {
"version": "2.0.0",
@@ -901,7 +901,7 @@
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
},
"end-of-stream": {
"version": "1.4.4",
@@ -1012,7 +1012,7 @@
"escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"escape-string-regexp": {
"version": "4.0.0",
@@ -1027,7 +1027,7 @@
"etag": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
},
"eventemitter3": {
"version": "3.1.2",
@@ -1122,6 +1122,33 @@
}
}
},
"express-session": {
"version": "1.17.3",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.3.tgz",
"integrity": "sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw==",
"requires": {
"cookie": "0.4.2",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"dependencies": {
"depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -1256,7 +1283,7 @@
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
"integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="
},
"fs-constants": {
"version": "1.0.0",
@@ -1521,9 +1548,9 @@
}
},
"http-cache-semantics": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ=="
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="
},
"http-errors": {
"version": "1.8.1",
@@ -2167,7 +2194,7 @@
"merge-descriptors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"merge-stream": {
"version": "2.0.0",
@@ -2177,7 +2204,7 @@
"methods": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
"integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
},
"mime": {
"version": "1.6.0",
@@ -2769,12 +2796,12 @@
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10="
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"performance-now": {
"version": "2.1.0",
@@ -2847,6 +2874,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz",
"integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw=="
},
"random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -2905,13 +2937,24 @@
}
},
"regexp.prototype.flags": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz",
"integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz",
"integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==",
"requires": {
"call-bind": "^1.0.2",
"define-properties": "^1.1.3",
"functions-have-names": "^1.2.2"
"define-properties": "^1.2.0",
"functions-have-names": "^1.2.3"
},
"dependencies": {
"define-properties": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.0.tgz",
"integrity": "sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==",
"requires": {
"has-property-descriptors": "^1.0.0",
"object-keys": "^1.1.1"
}
}
}
},
"request": {
@@ -3455,6 +3498,14 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
},
"unbox-primitive": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
@@ -3474,7 +3525,7 @@
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="
},
"unzipper": {
"version": "0.10.10",

View File

@@ -27,6 +27,7 @@
"compression": "^1.7.4",
"config": "^3.2.3",
"express": "^4.17.3",
"express-session": "^1.17.3",
"feed": "^4.2.2",
"fluent-ffmpeg": "^2.1.2",
"fs-extra": "^9.0.0",

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "youtube-dl-material",
"version": "4.3.0",
"version": "4.3.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

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

@@ -2,7 +2,7 @@
<div class="container">
<div class="row justify-content-center">
<div *ngFor="let playlist of playlists; let i = index" class="mb-2 mt-2" [ngClass]="[ postsService.card_size === 'small' ? 'col-2 small-col' : '', postsService.card_size === 'medium' ? 'col-6 col-lg-4 medium-col' : '', postsService.card_size === 'large' ? 'col-12 large-col' : '' ]">
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [loading]="false"></app-unified-file-card>
<app-unified-file-card [index]="i" [card_size]="postsService.card_size" [locale]="postsService.locale" (goToFile)="goToPlaylist($event)" [file_obj]="playlist" [is_playlist]="true" (editPlaylist)="editPlaylistDialog($event)" (deleteFile)="deletePlaylist($event)" [baseStreamPath]="postsService.path" [jwtString]="postsService.isLoggedIn ? this.postsService.token : ''" [loading]="false"></app-unified-file-card>
</div>
</div>
</div>

View File

@@ -31,9 +31,11 @@
<mat-form-field class="value-input">
<input matInput [(ngModel)]="rule['value']">
</mat-form-field>
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
<span class="rule-buttons">
<button [disabled]="i === category['rules'].length-1" (click)="swapRules(i, i+1)" mat-icon-button><mat-icon>arrow_downward</mat-icon></button>
<button [disabled]="i === 0" (click)="swapRules(i, i-1)" mat-icon-button><mat-icon>arrow_upward</mat-icon></button>
<button (click)="removeRule(i)" mat-icon-button><mat-icon>cancel</mat-icon></button>
</span>
</mat-list-item>
</mat-list>

View File

@@ -1,5 +1,5 @@
.operator-select {
width: 55px;
width: 90px;
}
.property-select {
@@ -14,3 +14,16 @@
.value-input {
margin-left: 10px;
}
:host ::ng-deep.mdc-list-item {
height: 75px !important;
}
:host ::ng-deep.mdc-list-item__content {
pointer-events: unset;
}
.rule-buttons {
position: relative;
top: 8px;
}

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

@@ -37,7 +37,8 @@
<input [(ngModel)]="new_file.thumbnailURL" matInput [disabled]="!editing || new_file.thumbnailPath">
</mat-form-field>
<mat-form-field *ngIf="initialized && postsService.categories" class="info-field">
<mat-select placeholder="Category" i18n-placeholder="Category" [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-label i18n="Category">Category</mat-label>
<mat-select [value]="category" (selectionChange)="categoryChanged($event)" [compareWith]="categoryComparisonFunction" [disabled]="!editing">
<mat-option [value]="{}">
N/A
</mat-option>

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);
}
}