Updated Sync Process + Bug Fixes

Reworked Sync Process as old process only fetched once and only added items that did not exist. Now data is refreshed accordingly (Does not apply to item information for now)
Changed Log format to specify number of items updated/inserted (Does not apply to item information for now)

Added censor toggle to password fields
Added Change Password settings
Added toggle to make login optional after first time setup
Added Error boundaries to components that rely on raw Jellyfin Data (Should help with UI errors)
Tweaked Device icon identification process to cater for ios.svg-->apple.svg + opera.svg

Changed proxy process to throw error 500 when response from urls are not of expected type
This commit is contained in:
Thegan Govender
2023-05-29 09:13:26 +02:00
parent 31917368ee
commit beda4f01ae
21 changed files with 565 additions and 156 deletions

View File

@@ -25,7 +25,7 @@ router.get("/test", async (req, res) => {
router.get("/getconfig", async (req, res) => {
try{
const { rows } = await db.query('SELECT "JF_HOST","JF_API_KEY","APP_USER" FROM app_config where "ID"=1');
const { rows } = await db.query('SELECT "JF_HOST","JF_API_KEY","APP_USER","REQUIRE_LOGIN" FROM app_config where "ID"=1');
res.send(rows);
}catch(error)
@@ -62,6 +62,33 @@ router.post("/setconfig", async (req, res) => {
console.log(`ENDPOINT CALLED: /setconfig: `);
});
router.post("/setRequireLogin", async (req, res) => {
try{
const { REQUIRE_LOGIN } = req.body;
if(REQUIRE_LOGIN===undefined)
{
res.status(503);
res.send(rows);
}
let query='UPDATE app_config SET "REQUIRE_LOGIN"=$1 where "ID"=1';
console.log(`ENDPOINT CALLED: /setRequireLogin: `+REQUIRE_LOGIN);
const { rows } = await db.query(
query,
[REQUIRE_LOGIN]
);
res.send(rows);
}catch(error)
{
console.log(error);
}
});
router.get("/CheckForUpdates", async (req, res) => {
try{
@@ -457,6 +484,45 @@ router.post("/validateSettings", async (req, res) => {
});
router.post("/updatePassword", async (req, res) => {
const { current_password,new_password } = req.body;
let result={isValid:true,errorMessage:""};
try{
const { rows } = await db.query(`SELECT "JF_HOST","JF_API_KEY","APP_USER" FROM app_config where "ID"=1 AND "APP_PASSWORD"='${current_password}' `);
if(rows && rows.length>0)
{
if(current_password===new_password)
{
result.isValid=false;
result.errorMessage = "New Password cannot be the same as Old Password";
}else{
await db.query(`UPDATE app_config SET "APP_PASSWORD"='${new_password}' where "ID"=1 AND "APP_PASSWORD"='${current_password}' `);
}
}else{
result.isValid=false;
result.errorMessage = "Old Password is Invalid";
}
}catch(error)
{
console.log(error);
result.errorMessage = error;
}
res.send(result);
});

View File

@@ -13,7 +13,7 @@ router.post('/login', async (req, res) => {
try{
const { username, password } = req.body;
const { rows : login } = await db.query(`SELECT * FROM app_config where "APP_USER"='${username}' and "APP_PASSWORD"='${password}'`);
const { rows : login } = await db.query(`SELECT * FROM app_config where ("APP_USER"='${username}' and "APP_PASSWORD"='${password}') OR "REQUIRE_LOGIN"=false`);
if(login.length>0)
{

View File

@@ -1,5 +1,6 @@
const { Pool } = require('pg');
const pgp = require("pg-promise")();
const {update_query : update_query_map} = require("./models/bulk_insert_update_handler");
const _POSTGRES_USER=process.env.POSTGRES_USER;
@@ -71,17 +72,11 @@ async function insertBulk(table_name, data,columns) {
let message='';
try {
await client.query("BEGIN");
const query = pgp.helpers.insert(
data,
columns,
table_name
);
await client.query(query);
const update_query= update_query_map.find(query => query.table === table_name).query;
await client.query("COMMIT");
message=((data.length||1) + " Rows Inserted.");
const cs = new pgp.helpers.ColumnSet(columns, { table: table_name });
const query = pgp.helpers.insert(data, cs) + update_query; // Update the column names accordingly
await client.query(query);
} catch (error) {
await client.query('ROLLBACK');

View File

@@ -0,0 +1,23 @@
exports.up = async function(knex) {
try
{
const hasTable = await knex.schema.hasTable('app_config');
if (hasTable) {
await knex.schema.alterTable('app_config', function(table) {
table.boolean('REQUIRE_LOGIN').defaultTo(true);
});
}
}catch (error) {
console.error(error);
}
};
exports.down = async function(knex) {
try {
await knex.schema.alterTable('app_config', function(table) {
table.dropColumn('REQUIRE_LOGIN');
});
} catch (error) {
console.error(error);
}
};

View File

@@ -0,0 +1,17 @@
const update_query = [
{table:'jf_activity_watchdog',query:' ON CONFLICT DO NOTHING'},
{table:'jf_item_info',query:' ON CONFLICT ("Id") DO UPDATE SET "Path" = EXCLUDED."Path", "Name" = EXCLUDED."Name", "Size" = EXCLUDED."Size", "Bitrate" = EXCLUDED."Bitrate", "MediaStreams" = EXCLUDED."MediaStreams"'},
{table:'jf_libraries',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "Type" = EXCLUDED."Type", "CollectionType" = EXCLUDED."CollectionType", "ImageTagsPrimary" = EXCLUDED."ImageTagsPrimary"'},
{table:'jf_library_episodes',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PremiereDate" = EXCLUDED."PremiereDate", "OfficialRating" = EXCLUDED."OfficialRating", "CommunityRating" = EXCLUDED."CommunityRating", "RunTimeTicks" = EXCLUDED."RunTimeTicks", "ProductionYear" = EXCLUDED."ProductionYear", "IndexNumber" = EXCLUDED."IndexNumber", "ParentIndexNumber" = EXCLUDED."ParentIndexNumber", "Type" = EXCLUDED."Type", "ParentLogoItemId" = EXCLUDED."ParentLogoItemId", "ParentBackdropItemId" = EXCLUDED."ParentBackdropItemId", "ParentBackdropImageTags" = EXCLUDED."ParentBackdropImageTags", "SeriesId" = EXCLUDED."SeriesId", "SeasonId" = EXCLUDED."SeasonId", "SeasonName" = EXCLUDED."SeasonName", "SeriesName" = EXCLUDED."SeriesName"'},
{table:'jf_library_items',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PremiereDate" = EXCLUDED."PremiereDate", "EndDate" = EXCLUDED."EndDate", "CommunityRating" = EXCLUDED."CommunityRating", "RunTimeTicks" = EXCLUDED."RunTimeTicks", "ProductionYear" = EXCLUDED."ProductionYear", "Type" = EXCLUDED."Type", "Status" = EXCLUDED."Status", "ImageTagsPrimary" = EXCLUDED."ImageTagsPrimary", "ImageTagsBanner" = EXCLUDED."ImageTagsBanner", "ImageTagsLogo" = EXCLUDED."ImageTagsLogo", "ImageTagsThumb" = EXCLUDED."ImageTagsThumb", "BackdropImageTags" = EXCLUDED."BackdropImageTags", "ParentId" = EXCLUDED."ParentId", "PrimaryImageHash" = EXCLUDED."PrimaryImageHash"'},
{table:'jf_library_seasons',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "ParentLogoItemId" = EXCLUDED."ParentLogoItemId", "ParentBackdropItemId" = EXCLUDED."ParentBackdropItemId", "ParentBackdropImageTags" = EXCLUDED."ParentBackdropImageTags", "SeriesPrimaryImageTag" = EXCLUDED."SeriesPrimaryImageTag"'},
{table:'jf_logging',query:' ON CONFLICT DO NOTHING'},
{table:'jf_playback_activity',query:' ON CONFLICT DO NOTHING'},
{table:'jf_playback_reporting_plugin_data',query:' ON CONFLICT DO NOTHING'},
{table:'jf_users',query:' ON CONFLICT ("Id") DO UPDATE SET "Name" = EXCLUDED."Name", "PrimaryImageTag" = EXCLUDED."PrimaryImageTag", "LastLoginDate" = EXCLUDED."LastLoginDate", "LastActivityDate" = EXCLUDED."LastActivityDate"'}
];
module.exports = {
update_query
};

View File

@@ -35,14 +35,14 @@ router.get('/web/assets/img/devices/', async(req, res) => {
if (response.headers['content-type'].startsWith('image/')) {
res.send(response.data);
} else {
res.send(response.data.toString());
res.status(500).send('Error fetching image');
}
return; // Add this line
})
.catch((error) => {
console.error(error);
res.status(500).send('Error fetching image');
res.status(500).send('Error fetching image: '+error);
});
});
@@ -72,12 +72,12 @@ router.get('/Items/Images/Backdrop/', async(req, res) => {
if (response.headers['content-type'].startsWith('image/')) {
res.send(response.data);
} else {
res.send(response.data.toString());
res.status(500).send('Error fetching image');
}
})
.catch((error) => {
// console.error(error);
res.status(500).send('Error fetching image');
res.status(500).send('Error fetching image: '+error);
});
});
@@ -103,12 +103,12 @@ router.get('/Items/Images/Backdrop/', async(req, res) => {
if (response.headers['content-type'].startsWith('image/')) {
res.send(response.data);
} else {
res.send(response.data.toString());
res.status(500).send('Error fetching image');
}
})
.catch((error) => {
// console.error(error);
res.status(500).send('Error fetching image');
res.status(500).send('Error fetching image: '+error);
});
});
@@ -135,12 +135,12 @@ router.get('/Items/Images/Backdrop/', async(req, res) => {
if (response.headers['content-type'].startsWith('image/')) {
res.send(response.data);
} else {
res.send(response.data.toString());
res.status(500).send('Error fetching image');
}
})
.catch((error) => {
// console.error(error);
res.status(500).send('Error fetching image');
res.status(500).send('Error fetching image: '+error);
});
});

View File

@@ -300,7 +300,7 @@ async function syncLibraryItems(refLog)
const userid = admins[0].Id;
const libraries = await _sync.getItem(undefined,userid);
const data = [];
let insertCounter = 0;
let insertMessage='';
let deleteCounter = 0;
//for each item in library run get item using that id as the ParentId (This gets the children of the parent id)
for (let i = 0; i < libraries.length; i++) {
@@ -321,21 +321,21 @@ async function syncLibraryItems(refLog)
let dataToInsert = [];
//filter fix if jf_libraries is empty
if (existingIds.length === 0) {
dataToInsert = await data.map(jf_library_items_mapping);
} else {
dataToInsert = await data
.filter((row) => !existingIds.includes(row.Id))
.map(jf_library_items_mapping);
}
dataToInsert = await data.map(jf_library_items_mapping);
// if (existingIds.length === 0) {
// dataToInsert = await data.map(jf_library_items_mapping);
// } else {
// dataToInsert = await data
// .filter((row) => !existingIds.includes(row.Id))
// .map(jf_library_items_mapping);
// }
if (dataToInsert.length !== 0) {
let result = await db.insertBulk("jf_library_items",dataToInsert,jf_library_items_columns);
if (result.Result === "SUCCESS") {
insertCounter += dataToInsert.length;
insertMessage = `${dataToInsert.length-existingIds.length} Rows Inserted. ${existingIds.length} Rows Updated.`;
} else {
refLog.loggedData.push({
color: "red",
@@ -356,7 +356,7 @@ async function syncLibraryItems(refLog)
}
}
refLog.loggedData.push({color: "dodgerblue",Message: insertCounter + " Library Items Inserted.",});
refLog.loggedData.push({color: "dodgerblue",Message: insertMessage,});
refLog.loggedData.push({color: "orange",Message: deleteCounter + " Library Items Removed.",});
refLog.loggedData.push({ color: "yellow", Message: "Item Sync Complete" });
@@ -389,16 +389,17 @@ async function syncShowItems(refLog)
let insertSeasonsCount = 0;
let insertEpisodeCount = 0;
let updateSeasonsCount = 0;
let updateEpisodeCount = 0;
let deleteSeasonsCount = 0;
let deleteEpisodeCount = 0;
//loop for each show
let show_counter=0;
for (const show of shows) {
const allSeasons = await _sync.getSeasonsAndEpisodes(show.Id,'Seasons');
const allEpisodes =await _sync.getSeasonsAndEpisodes(show.Id,'Episodes');
show_counter++;
refLog.loggedData.push({ Message: "Syncing shows " + (show_counter/shows.length*100).toFixed(2) +"%" ,key:'show_sync'});
const existingIdsSeasons = await db.query(`SELECT * FROM public.jf_library_seasons where "SeriesId" = '${show.Id}'`).then((res) => res.rows.map((row) => row.Id));
@@ -414,37 +415,21 @@ async function syncShowItems(refLog)
.then((res) => res.rows.map((row) => row.EpisodeId));
}
//
let seasonsToInsert = [];
let episodesToInsert = [];
//filter fix if jf_libraries is empty
if (existingIdsSeasons.length === 0) {
// if there are no existing Ids in the table, map all items in the data array to the expected format
seasonsToInsert = await allSeasons.map(jf_library_seasons_mapping);
} else {
// otherwise, filter only new data to insert
seasonsToInsert = await allSeasons
.filter((row) => !existingIdsSeasons.includes(row.Id))
.map(jf_library_seasons_mapping);
}
seasonsToInsert = await allSeasons.map(jf_library_seasons_mapping);
episodesToInsert = await allEpisodes.map(jf_library_episodes_mapping);
if (existingIdsEpisodes.length === 0) {
// if there are no existing Ids in the table, map all items in the data array to the expected format
episodesToInsert = await allEpisodes.map(jf_library_episodes_mapping);
} else {
// otherwise, filter only new data to insert
episodesToInsert = await allEpisodes.filter((row) => !existingIdsEpisodes.includes(row.Id)).map(jf_library_episodes_mapping);
}
///insert delete seasons
//Bulkinsert new data not on db
if (seasonsToInsert.length !== 0) {
let result = await db.insertBulk("jf_library_seasons",seasonsToInsert,jf_library_seasons_columns);
if (result.Result === "SUCCESS") {
insertSeasonsCount += seasonsToInsert.length;
} else {
insertSeasonsCount+=seasonsToInsert.length-existingIdsSeasons.length;
updateSeasonsCount+=existingIdsSeasons.length;
} else {
refLog.loggedData.push({
color: "red",
Message: "Error performing bulk insert:" + result.message,
@@ -469,7 +454,8 @@ async function syncShowItems(refLog)
if (episodesToInsert.length !== 0) {
let result = await db.insertBulk("jf_library_episodes",episodesToInsert,jf_library_episodes_columns);
if (result.Result === "SUCCESS") {
insertEpisodeCount += episodesToInsert.length;
insertEpisodeCount+=episodesToInsert.length-existingIdsEpisodes.length;
updateEpisodeCount+=existingIdsEpisodes.length;
} else {
refLog.loggedData.push({
color: "red",
@@ -495,9 +481,9 @@ async function syncShowItems(refLog)
}
refLog.loggedData.push({color: "dodgerblue",Message: insertSeasonsCount + " Seasons inserted.",});
refLog.loggedData.push({color: "dodgerblue",Message: `Seasons: ${insertSeasonsCount} Rows Inserted. ${updateSeasonsCount} Rows Updated.`});
refLog.loggedData.push({color: "orange",Message: deleteSeasonsCount + " Seasons Removed.",});
refLog.loggedData.push({color: "dodgerblue",Message: insertEpisodeCount + " Episodes inserted.",});
refLog.loggedData.push({color: "dodgerblue",Message: `Episodes: ${insertEpisodeCount} Rows Inserted. ${updateEpisodeCount} Rows Updated.`});
refLog.loggedData.push({color: "orange",Message: deleteEpisodeCount + " Episodes Removed.",});
refLog.loggedData.push({ color: "yellow", Message: "Sync Complete" });
}catch(error)

View File

@@ -1,6 +1,6 @@
{
"name": "jfstat",
"version": "1.0.3",
"version": "1.0.4",
"private": true,
"dependencies": {
"@emotion/react": "^11.10.6",

View File

@@ -11,10 +11,10 @@ async function Config() {
if(response.data.length>0)
{
const { JF_HOST, JF_API_KEY, APP_USER } = response.data[0];
return { hostUrl: JF_HOST, apiKey: JF_API_KEY, username: APP_USER, token:token };
const { JF_HOST, JF_API_KEY, APP_USER,REQUIRE_LOGIN } = response.data[0];
return { hostUrl: JF_HOST, apiKey: JF_API_KEY, username: APP_USER, token:token, requireLogin:REQUIRE_LOGIN };
}
return { hostUrl: null, apiKey: null, username: null, token:token };
return { hostUrl: null, apiKey: null, username: null, token:token,requireLogin:true };
} catch (error) {
// console.log(error);

View File

@@ -1,3 +1,3 @@
export const clientData = ["android","ios","safari","chrome","firefox","edge"]
export const clientData = ["android","ios","safari","chrome","firefox","edge","opera"]

View File

@@ -48,7 +48,6 @@ function formatTotalWatchTime(seconds) {
function Row(data) {
const { row } = data;
const [open, setOpen] = React.useState(false);
// const classes = useRowStyles();
const twelve_hr = JSON.parse(localStorage.getItem('12hr'));
const options = {

View File

@@ -0,0 +1,28 @@
import React from "react";
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state to indicate an error has occurred
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// You can log the error or perform other actions here
console.error(error, errorInfo);
}
render() {
if (this.state.hasError) {
// Render an error message or fallback UI
return <></>;
}
// Render the child components as normal
return this.props.children;
}
}

View File

@@ -12,6 +12,7 @@ import LibraryLastWatched from './library/last-watched';
import RecentlyAdded from './library/recently-added';
import LibraryActivity from './library/library-activity';
import LibraryItems from './library/library-items';
import ErrorBoundary from './general/ErrorBoundary';
import { Tabs, Tab, Button, ButtonGroup } from 'react-bootstrap';
@@ -84,7 +85,10 @@ function LibraryInfo() {
<Tabs defaultActiveKey="tabOverview" activeKey={activeTab} variant='pills'>
<Tab eventKey="tabOverview" className='bg-transparent'>
<LibraryGlobalStats LibraryId={LibraryId}/>
<RecentlyAdded LibraryId={LibraryId}/>
<ErrorBoundary>
<RecentlyAdded LibraryId={LibraryId}/>
</ErrorBoundary>
<LibraryLastWatched LibraryId={LibraryId}/>
</Tab>
<Tab eventKey="tabActivity" className='bg-transparent'>

View File

@@ -84,9 +84,9 @@ function sessionCard(props) {
"/proxy/web/assets/img/devices/?devicename="
+
(props.data.session.Client.toLowerCase().includes("web") ?
( clientData.find(item => props.data.session.DeviceName.toLowerCase().includes(item)) || "other")
( clientData.find(item => props.data.session.DeviceName.toLowerCase().includes(item)).replace('ios','apple') || "other")
:
( clientData.find(item => props.data.session.Client.toLowerCase().includes(item)) || "other")
( clientData.find(item => props.data.session.Client.toLowerCase().includes(item)).replace('ios','apple') || "other")
)}
alt=""
/>

View File

@@ -0,0 +1,209 @@
import React, { useState,useEffect } from "react";
import axios from "axios";
import Form from 'react-bootstrap/Form';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Button from 'react-bootstrap/Button';
import Alert from 'react-bootstrap/Alert';
import ToggleButton from 'react-bootstrap/ToggleButton';
import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup';
import CryptoJS from 'crypto-js';
import EyeFillIcon from 'remixicon-react/EyeFillIcon';
import EyeOffFillIcon from 'remixicon-react/EyeOffFillIcon';
import Config from "../../../lib/config";
import "../../css/settings/settings.css";
import { InputGroup } from "react-bootstrap";
export default function SettingsConfig() {
const [use_password, setuse_password] = useState(true);
const [showPassword, setShowPassword] = useState(false);
const [showCurrentPassword, setShowCurrentPassword] = useState(false);
const [formValues, setFormValues] = useState({});
const [isSubmitted, setisSubmitted] = useState("");
const [config, setConfig] = useState(null);
const [submissionMessage, setsubmissionMessage] = useState("");
const token = localStorage.getItem('token');
useEffect(() => {
const fetchConfig = async () => {
try {
const newConfig = await Config();
setConfig(newConfig);
setuse_password(newConfig.requireLogin);
} catch (error) {
console.log(error);
}
};
fetchConfig();
const intervalId = setInterval(fetchConfig, 60000 * 5);
return () => clearInterval(intervalId);
}, []);
async function updatePassword(_current_password, _new_password) {
const result = await axios
.post("/api/updatePassword", {
current_password:_current_password,
new_password: _new_password
}, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
.catch((error) => {
// let errorMessage= `Error : ${error}`;
});
let data=result.data;
return { isValid:data.isValid, errorMessage:data.errorMessage} ;
}
async function setRequireLogin(requireLogin) {
await axios
.post("/api/setRequireLogin", {
REQUIRE_LOGIN:requireLogin
}, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
.then((data)=>
{
setuse_password(requireLogin);
}
)
.catch((error) => {
// let errorMessage= `Error : ${error}`;
});
}
async function handleFormSubmit(event) {
event.preventDefault();
setisSubmitted("");
if(!formValues.JS_PASSWORD || formValues.JS_PASSWORD.length<6)
{
setisSubmitted("Failed");
setsubmissionMessage("Unable to update password: New Password Must be at least 6 characters long");
return;
}
let hashedOldPassword= CryptoJS.SHA3(formValues.JS_C_PASSWORD).toString();
let hashedNewPassword= CryptoJS.SHA3(formValues.JS_PASSWORD).toString();
let result = await updatePassword(
hashedOldPassword,
hashedNewPassword
);
if (result.isValid) {
setisSubmitted("Success");
setsubmissionMessage("Successfully updated password");
return;
}else
{
setisSubmitted("Failed");
setsubmissionMessage("Unable to update password: "+ result.errorMessage);
return;
}
}
function handleFormChange(event) {
setFormValues({ ...formValues, [event.target.name]: event.target.value });
}
function togglePasswordRequired(isRequired){
// console.log(isRequired);
setRequireLogin(isRequired);
};
return (
<div>
<h1>Security</h1>
<Form onSubmit={handleFormSubmit} className="settings-form">
<Form.Group as={Row} className="mb-3" >
<Form.Label column className="">
Current Password
</Form.Label>
<Col sm="10">
<InputGroup>
<Form.Control id="JS_C_PASSWORD" name="JS_C_PASSWORD" value={formValues.JS_C_PASSWORD || ""} onChange={handleFormChange} type={showCurrentPassword ? "text" : "password"}/>
<Button variant="outline-primary" type="button" onClick={() => setShowCurrentPassword(!showCurrentPassword)}>{showCurrentPassword?<EyeFillIcon/>:<EyeOffFillIcon/>}</Button>
</InputGroup>
</Col>
</Form.Group>
<Form.Group as={Row} className="mb-3" >
<Form.Label column className="">
New Password
</Form.Label>
<Col sm="10">
<InputGroup>
<Form.Control id="JS_PASSWORD" name="JS_PASSWORD" value={formValues.JS_PASSWORD || ""} onChange={handleFormChange} type={showPassword ? "text" : "password"} />
<Button variant="outline-primary" type="button" onClick={() => setShowPassword(!showPassword)}>{showPassword?<EyeFillIcon/>:<EyeOffFillIcon/>}</Button>
</InputGroup>
</Col>
</Form.Group>
{isSubmitted !== "" ? (
isSubmitted === "Failed" ?
<Alert variant="danger">
{submissionMessage}
</Alert>
:
<Alert variant="success" >
{submissionMessage}
</Alert>
) : (
<></>
)}
<div className="d-flex flex-column flex-md-row justify-content-end align-items-md-center">
<Button variant="outline-success" type="submit"> Update </Button>
</div>
</Form>
<Form className="settings-form">
<Form.Group as={Row} className="mb-3">
<Form.Label column className="">Require Login</Form.Label>
<Col >
<ToggleButtonGroup type="checkbox" className="d-flex" >
<ToggleButton variant="outline-primary" active={use_password} onClick={()=> {togglePasswordRequired(true);}}>Yes</ToggleButton>
<ToggleButton variant="outline-primary" active={!use_password} onClick={()=>{togglePasswordRequired(false);}}>No</ToggleButton>
</ToggleButtonGroup>
</Col>
</Form.Group>
</Form>
</div>
);
}

View File

@@ -10,10 +10,13 @@ import Alert from 'react-bootstrap/Alert';
import ToggleButton from 'react-bootstrap/ToggleButton';
import ToggleButtonGroup from 'react-bootstrap/ToggleButtonGroup';
import EyeFillIcon from 'remixicon-react/EyeFillIcon';
import EyeOffFillIcon from 'remixicon-react/EyeOffFillIcon';
import "../../css/settings/settings.css";
import { ButtonGroup } from "react-bootstrap";
import { InputGroup } from "react-bootstrap";
export default function SettingsConfig() {
const [config, setConfig] = useState(null);
@@ -142,7 +145,10 @@ export default function SettingsConfig() {
API Key
</Form.Label>
<Col sm="10">
<InputGroup>
<Form.Control id="JF_API_KEY" name="JF_API_KEY" value={formValues.JF_API_KEY || ""} onChange={handleFormChange} type={showKey ? "text" : "password"} />
<Button variant="outline-primary" type="button" onClick={() => setKeyState(!showKey)}>{showKey?<EyeFillIcon/>:<EyeOffFillIcon/>}</Button>
</InputGroup>
</Col>
</Form.Group>
{isSubmitted !== "" ? (
@@ -159,10 +165,7 @@ export default function SettingsConfig() {
<></>
)}
<div className="d-flex flex-column flex-md-row justify-content-end align-items-md-center">
<ButtonGroup >
<Button variant="outline-success" type="submit"> Save </Button>
<Button variant="outline-secondary" type="button" onClick={() => setKeyState(!showKey)}>Show Key</Button>
</ButtonGroup>
<Button variant="outline-success" type="submit"> Save </Button>
</div>
</Form>

View File

@@ -59,15 +59,22 @@
.settings-form > div> div> .form-control
.settings-form > div> div> .form-control,
.settings-form > div> div> .input-group> .form-control
{
color: white !important;
background-color: var(--background-color) !important;
border-color: var(--background-color) !important;
}
.settings-form > div> div> .input-group> .btn
{
border: none !important;
}
.settings-form > div> div> .form-control:focus
.settings-form > div> div> .form-control:focus,
.settings-form > div> div> .input-group> .form-control:focus
{
box-shadow: none !important;
border-color: var(--primary-color) !important;

View File

@@ -1,10 +1,17 @@
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@500&display=swap');
@import './variables.module.css';
/* *{
margin: 0;
padding: 0;
font-family: 'poppins',sans-serif;
} */
.login-show-password
{
background-color: transparent !important;
border: 0 !important;
}
section{
display: flex;
@@ -12,8 +19,6 @@ section{
align-items: center;
min-height: 100vh;
width: 100%;
/* background: url('background6.jpg')no-repeat; */
background-position: center;
background-size: cover;
}
@@ -41,36 +46,47 @@ h2{
width: 310px;
border-bottom: 2px solid #fff;
}
.inputbox label{
.inputbox .form-label{
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
color: #fff;
color: #fff !important;
font-size: 1em;
pointer-events: none;
transition: .2s;
}
.form-box> form> .inputbox> input:focus ~ label,
.form-box> form> .inputbox> input:valid ~ label{
top: -15px;
.inputbox input:focus ~ .form-label,
.inputbox input:not(:placeholder-shown) ~ .form-label
{
top: -10px;
}
.inputbox input:hover {
background: transparent !important;
box-shadow: none !important;
}
.inputbox input:focus {
background: transparent !important;
box-shadow: none !important;
color: #fff;
}
.inputbox input {
width: 100%;
height: 50px;
background: transparent;
border: none;
outline: none;
font-size: 1em;
color: #fff;
}
.inputbox ion-icon{
position: absolute;
right: 8px;
color: #fff;
font-size: 1.2em;
top: 20px;
}
.forget{
margin: -15px 0 15px ;
font-size: .9em;
@@ -91,15 +107,27 @@ top: -15px;
text-decoration: underline;
}
.setup-button{
color: white !important;
width: 100%;
height: 40px;
border-radius: 40px;
background: #fff;
border: none;
outline: none;
cursor: pointer;
font-size: 1em;
font-weight: 600;
border-radius: 40px !important;
background: var(--primary-color) !important;
border: none !important;
outline: none !important;
font-size: 1em !important;
font-weight: 600 !important;
}
.setup-button:hover{
color: black !important;
width: 100%;
height: 40px;
border-radius: 40px !important;
background: var(--secondary-color) !important;
border: none !important;
outline: none !important;
font-size: 1em !important;
font-weight: 600 !important;
}
.register{
font-size: .9em;

View File

@@ -6,13 +6,19 @@ import Sessions from './components/sessions/sessions'
import HomeStatisticCards from './components/HomeStatisticCards'
import LibraryOverView from './components/libraryOverview'
import RecentlyAdded from './components/library/recently-added'
import ErrorBoundary from './components/general/ErrorBoundary'
export default function Home() {
return (
<div className='Home'>
<Sessions />
<RecentlyAdded/>
<ErrorBoundary>
<Sessions/>
</ErrorBoundary>
<ErrorBoundary>
<RecentlyAdded/>
</ErrorBoundary>
<HomeStatisticCards/>
<LibraryOverView/>

View File

@@ -3,13 +3,22 @@ import axios from "axios";
import Config from "../lib/config";
import CryptoJS from 'crypto-js';
import "./css/setup.css";
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import { InputGroup,Row } from "react-bootstrap";
import EyeFillIcon from 'remixicon-react/EyeFillIcon';
import EyeOffFillIcon from 'remixicon-react/EyeOffFillIcon';
// import LibrarySync from "./components/settings/librarySync";
import Loading from './components/general/loading';
function Login() {
const [config, setConfig] = useState(null);
const [formValues, setFormValues] = useState({});
const [showPassword, setShowPassword] = useState(false);
const [processing, setProcessing] = useState(false);
const [submitButtonText, setsubmitButtonText] = useState("Login");
@@ -22,41 +31,60 @@ function Login() {
setProcessing(true);
event.preventDefault();
let hashedPassword= CryptoJS.SHA3(formValues.JS_PASSWORD).toString();
let hashedPassword= CryptoJS.SHA3(formValues.password).toString();
beginLogin(formValues.JS_USERNAME,hashedPassword);
}
async function beginLogin(JS_USERNAME,hashedPassword)
{
axios
.post("/auth/login", {
username:formValues.username,
password: hashedPassword
.post("/auth/login", {
username:JS_USERNAME,
password: hashedPassword
}, {
headers: {
"Content-Type": "application/json",
},
})
.then(async (response) => {
}, {
headers: {
"Content-Type": "application/json",
},
})
.then(async (response) => {
localStorage.setItem('token',response.data.token);
localStorage.setItem('token',response.data.token);
setProcessing(false);
if(JS_USERNAME)
{
setsubmitButtonText("Success");
setProcessing(false);
window.location.reload();
return;
})
.catch((error) => {
let errorMessage= `Error : ${error.response.status}`;
if (error.code === "ERR_NETWORK") {
errorMessage = `Unable to connect to Jellyfin Server`;
} else if (error.response.status === 401) {
errorMessage = `Invalid Username or Password`;
} else if (error.response.status === 404) {
errorMessage = `Error ${error.response.status}: The requested URL was not found.`;
}
}
})
.catch((error) => {
let errorMessage= `Error : ${error.response.status}`;
if (error.code === "ERR_NETWORK") {
errorMessage = `Unable to connect to Jellyfin Server`;
} else if (error.response.status === 401) {
errorMessage = `Invalid Username or Password`;
} else if (error.response.status === 404) {
errorMessage = `Error ${error.response.status}: The requested URL was not found.`;
}
if(JS_USERNAME)
{
setsubmitButtonText(errorMessage);
setProcessing(false);
});
}
setProcessing(false);
});
}
useEffect(() => {
const fetchConfig = async () => {
try {
@@ -64,13 +92,20 @@ function Login() {
setConfig(newConfig);
} catch (error) {
if (error.code === "ERR_NETWORK") {
console.log(error);
if (error.response.status !== 401 && error.response.status !== 403) {
// console.log(error);
}
}
}
};
if (!config) {
fetchConfig();
beginLogin();
}
}, [config]);
@@ -81,38 +116,38 @@ function Login() {
return (
<section>
<div className="form-box">
<form onSubmit={handleFormSubmit}>
<h2>Login</h2>
<div className="inputbox">
<input
type="text"
id="username"
name="username"
autocomplete="on"
value={formValues.username || ""}
onChange={handleFormChange}
required
/>
<label htmlFor="username">Username</label>
</div>
<div className="inputbox">
<input
type="text"
id="password"
name="password"
autocomplete="on"
value={formValues.password || ""}
onChange={handleFormChange}
required
/>
<label htmlFor="password">Password</label>
</div>
<div className="form-box d-flex flex-column">
<h2>Login</h2>
<Form onSubmit={handleFormSubmit} className="mt-5">
<Form.Group as={Row} className="inputbox" >
<Form.Control id="JS_USERNAME" name="JS_USERNAME" value={formValues.JS_USERNAME || ""} onChange={handleFormChange} placeholder=" "/>
<Form.Label column>
Username
</Form.Label>
</Form.Group>
<button type="submit" className="setup-button">
<Form.Group as={Row} className="inputbox" >
<InputGroup >
<Form.Control className="px-0" id="JS_PASSWORD" name="JS_PASSWORD" value={formValues.JS_PASSWORD || ""} onChange={handleFormChange} type={showPassword ? "text" : "password"} placeholder=" " />
<Button className="login-show-password" type="button" onClick={() => setShowPassword(!showPassword)}>{showPassword?<EyeFillIcon/>:<EyeOffFillIcon/>}</Button>
<Form.Label column >
Password
</Form.Label>
</InputGroup>
</Form.Group>
<Button type="submit" className="setup-button">
{processing ? "Validating..." : submitButtonText}
</button>
</form>
</Button>
</Form>
</div>
</section>
);

View File

@@ -4,6 +4,7 @@ import {Tabs, Tab } from 'react-bootstrap';
import SettingsConfig from "./components/settings/settingsConfig";
import Tasks from "./components/settings/Tasks";
import BackupFiles from "./components/settings/backupfiles";
import SecuritySettings from "./components/settings/security";
import Logs from "./components/settings/logs";
@@ -22,9 +23,11 @@ export default function Settings() {
<Tab eventKey="tabGeneral" className='bg-transparent my-2' title='Settings' style={{minHeight:'500px'}}>
<SettingsConfig/>
<SecuritySettings/>
<Tasks/>
</Tab>
<Tab eventKey="tabBackup" className='bg-transparent my-2' title='Backup' style={{minHeight:'500px'}}>