🪄 Adds api service with updates

This commit is contained in:
Rune Harlyk
2024-05-08 13:26:40 +02:00
committed by Rune Harlyk
parent 4c66c428e6
commit b7ae17f3bf
19 changed files with 391 additions and 573 deletions
+73
View File
@@ -0,0 +1,73 @@
import { user } from '$lib/stores/user';
import { get } from 'svelte/store';
import { Err, Ok, type Result } from './utilities';
export namespace api {
export function get<TResponse>(endpoint: string, params?: RequestInit) {
return sendRequest<TResponse>(endpoint, 'GET', null, params);
}
export function post<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'POST', data);
}
export function put<TResponse>(endpoint: string, data?: unknown) {
return sendRequest<TResponse>(endpoint, 'PUT', data);
}
export function remove<TResponse>(endpoint: string) {
return sendRequest<TResponse>(endpoint, 'DELETE');
}
}
async function sendRequest<TResponse>(
endpoint: string,
method: string,
data?: unknown,
params?: RequestInit
): Promise<Result<TResponse, Error>> {
const user_token = get(user).bearer_token;
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
const request = {
...params,
method,
body,
headers: {
...params?.headers,
Authorization: user_token ? 'Bearer ' + user_token : 'Basic',
'Content-Type': 'application/json'
}
};
let response;
try {
response = await fetch(endpoint, request);
} catch (error) {
return Err.new(new Error(), 'An error has occurred');
}
const isResponseOk = response.status >= 200 && response.status < 400;
if (!isResponseOk) {
if (response.status === 401) {
return Err.new(new ApiError(response), 'User was not authorized');
}
return Err.new(new ApiError(response), 'An error has occurred');
}
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
if (contentType && contentType.includes('application/json')) {
const data = await response.json();
return Ok.new(data as TResponse);
} else {
// Handle empty object as response
return Ok.new(null as TResponse);
}
}
export class ApiError extends Error {
constructor(public readonly response: Response) {
super(`${response.status}`);
}
}
+42 -52
View File
@@ -10,6 +10,8 @@
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'; import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { GithubRelease } from '$lib/models';
export let update = false; export let update = false;
@@ -17,67 +19,55 @@
let firmwareDownloadLink: string; let firmwareDownloadLink: string;
async function getGithubAPI() { async function getGithubAPI() {
try { const headers = {
const response = await fetch( accept: 'application/vnd.github+json',
'https://api.github.com/repos/' + $page.data.github + '/releases/latest', 'X-GitHub-Api-Version': '2022-11-28'
{ }
method: 'GET', const result = await api.get<GithubRelease>(`https://api.github.com/repos/${$page.data.github}/releases/latest`, {headers})
headers: { if (result.inner.message === "404" || result.inner.message == "Not Found") {
accept: 'application/vnd.github+json', console.warn('Error: Could not find releases in the repository');
'X-GitHub-Api-Version': '2022-11-28' return
} }
} if (result.isErr()) {
); console.error('Error:', result.inner);
const results = await response.json(); return
if (results.message == "Not Found") { }
console.error('Error: Could not find releases in the repository');
return; const results = result.inner;
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($page.data.features.firmware_built_target)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
} }
}
update = false;
firmwareVersion = '';
if (compareVersions(results.tag_name, $page.data.features.firmware_version) === 1) {
// iterate over assets and find the correct one
for (let i = 0; i < results.assets.length; i++) {
// check if the asset is of type *.bin
if (
results.assets[i].name.includes('.bin') &&
results.assets[i].name.includes($page.data.features.firmware_built_target)
) {
update = true;
firmwareVersion = results.tag_name;
firmwareDownloadLink = results.assets[i].browser_download_url;
notifications.info('Firmware update available.', 5000);
}
}
}
} catch (error) {
console.error('Error:', error);
}
} }
async function postGithubDownload(url: string) { async function postGithubDownload(url: string) {
try { const result = await api.post('/api/downloadUpdate', { download_url: url });
const apiResponse = await fetch('/api/downloadUpdate', { if (result.isErr()){
method: 'POST', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json'
},
body: JSON.stringify({ download_url: url })
});
} catch (error) {
console.error('Error:', error);
}
} }
onMount(() => { onMount(async () => {
if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) { if ($page.data.features.download_firmware && (!$page.data.features.security || $user.admin)) {
getGithubAPI(); await getGithubAPI();
const interval = setInterval( const interval = setInterval(
async () => { async () => {
getGithubAPI(); await getGithubAPI();
}, },
60 * 60 * 1000 60 * 60 * 1000
); // once per hour ); // once per hour
+16 -1
View File
@@ -7,6 +7,17 @@ export interface ControllerInput {
speed: number; speed: number;
} }
export type GithubRelease = {
message: string;
tag_name: string;
assets: Array<{
name: string;
browser_download_url: string;
}>;
};
export type JWT = { access_token: string };
export type angles = number[] | Int16Array; export type angles = number[] | Int16Array;
export type WifiStatus = { export type WifiStatus = {
@@ -26,7 +37,11 @@ export type WifiStatus = {
export type WifiSettings = { export type WifiSettings = {
hostname: string; hostname: string;
priority_RSSI: boolean; priority_RSSI: boolean;
wifi_networks: networkItem[]; wifi_networks: NetworkItem[];
};
export type NetworkList = {
networks: NetworkItem[];
}; };
export type KnownNetworkItem = { export type KnownNetworkItem = {
+27
View File
@@ -7,3 +7,30 @@ export const humanFileSize = (size: number): string => {
export const capitalize = (str: string): string => { export const capitalize = (str: string): string => {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}; };
export const convertSeconds = (seconds: number) => {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
};
+6 -14
View File
@@ -15,6 +15,7 @@
import Login from './login.svelte'; import Login from './login.svelte';
import { ModesEnum, mode, outControllerData, servoAngles, servoAnglesOut, socket } from '$lib/stores'; import { ModesEnum, mode, outControllerData, servoAngles, servoAnglesOut, socket } from '$lib/stores';
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models'; import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
import { api } from '$lib/api';
onMount(async () => { onMount(async () => {
if ($user.bearer_token !== '') { if ($user.bearer_token !== '') {
@@ -64,20 +65,11 @@
}; };
async function validateUser(userdata: userProfile) { async function validateUser(userdata: userProfile) {
try { const result = await api.get('/api/verifyAuthorization')
const response = await fetch('/api/verifyAuthorization', { if (result.isErr()){
method: 'GET', user.invalidate();
headers: { console.error('Error:', result.inner);
Authorization: 'Bearer ' + userdata.bearer_token, }
'Content-Type': 'application/json'
}
});
if (response.status !== 200) {
user.invalidate();
}
} catch (error) {
console.error('Error:', error);
}
} }
const handleOpen = () => { const handleOpen = () => {
+22 -43
View File
@@ -12,6 +12,7 @@
import MQTT from '~icons/tabler/topology-star-3'; import MQTT from '~icons/tabler/topology-star-3';
import Client from '~icons/tabler/robot'; import Client from '~icons/tabler/robot';
import type { MQTTSettings, MQTTStatus } from '$lib/models'; import type { MQTTSettings, MQTTStatus } from '$lib/models';
import { api } from '$lib/api';
let mqttSettings: MQTTSettings; let mqttSettings: MQTTSettings;
let mqttStatus: MQTTStatus; let mqttStatus: MQTTStatus;
@@ -19,34 +20,22 @@
let formField: any; let formField: any;
async function getMQTTStatus() { async function getMQTTStatus() {
try { const result = await api.get<MQTTStatus>('/api/mqttStatus');
const response = await fetch('/api/mqttStatus', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' mqttStatus = result.inner
}
});
mqttStatus = await response.json();
} catch (error) {
console.error('Error:', error);
}
return mqttStatus; return mqttStatus;
} }
async function getMQTTSettings() { async function getMQTTSettings() {
try { const result = await api.get<MQTTSettings>('/api/mqttSettings');
const response = await fetch('/api/mqttSettings', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' mqttSettings = result.inner
}
});
mqttSettings = await response.json();
} catch (error) {
console.error('Error:', error);
}
return mqttSettings; return mqttSettings;
} }
@@ -70,25 +59,15 @@
}; };
async function postMQTTSettings(data: MQTTSettings) { async function postMQTTSettings(data: MQTTSettings) {
try { const result = await api.post<MQTTSettings>('/api/mqttSettings', data);
const response = await fetch('/api/mqttSettings', { if (result.isErr()){
method: 'POST', console.error('Error:', result.inner);
headers: { notifications.error('User not authorized.', 3000);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return
'Content-Type': 'application/json' }
}, notifications.success('MQTT settings updated.', 3000);
body: JSON.stringify(data) mqttSettings = result.inner
}); return mqttSettings;
if (response.status == 200) {
notifications.success('MQTT settings updated.', 3000);
mqttSettings = await response.json();
} else {
notifications.error('User not authorized.', 3000);
}
} catch (error) {
console.error('Error:', error);
}
return;
} }
function handleSubmitMQTT() { function handleSubmitMQTT() {
@@ -2,32 +2,24 @@
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing'; import { cubicOut } from 'svelte/easing';
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import Spinner from '$lib/components/Spinner.svelte'; import Spinner from '$lib/components/Spinner.svelte';
import MQTT from '~icons/tabler/topology-star-3'; import MQTT from '~icons/tabler/topology-star-3';
import Info from '~icons/tabler/info-circle'; import Info from '~icons/tabler/info-circle';
import type { BrokerSettings } from '$lib/types/models'; import type { BrokerSettings } from '$lib/types/models';
import { api } from '$lib/api';
let brokerSettings: BrokerSettings; let brokerSettings: BrokerSettings;
let formField: any; let formField: any;
async function getBrokerSettings() { async function getBrokerSettings() {
try { const result = await api.get<BrokerSettings>('/api/brokerSettings');
const response = await fetch('/api/brokerSettings', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' brokerSettings = result.inner
}
});
brokerSettings = await response.json();
} catch (error) {
console.error('Error:', error);
}
return;
} }
let formErrors = { let formErrors = {
@@ -37,25 +29,14 @@
}; };
async function postBrokerSettings() { async function postBrokerSettings() {
try { const result = await api.post<BrokerSettings>('/api/brokerSettings', brokerSettings);
const response = await fetch('/api/brokerSettings', { if (result.isErr()){
method: 'POST', console.error('Error:', result.inner);
headers: { notifications.error('User not authorized.', 3000);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return
'Content-Type': 'application/json' }
}, notifications.success('Broker settings updated.', 3000);
body: JSON.stringify(brokerSettings) brokerSettings = result.inner
});
if (response.status == 200) {
notifications.success('Broker settings updated.', 3000);
brokerSettings = await response.json();
} else {
notifications.error('User not authorized.', 3000);
}
} catch (error) {
console.error('Error:', error);
}
return;
} }
function handleSubmitBroker() { function handleSubmitBroker() {
+20 -45
View File
@@ -15,40 +15,27 @@
import UTC from '~icons/tabler/clock-pin'; import UTC from '~icons/tabler/clock-pin';
import Stopwatch from '~icons/tabler/24-hours'; import Stopwatch from '~icons/tabler/24-hours';
import type { NTPSettings, NTPStatus } from '$lib/types/models'; import type { NTPSettings, NTPStatus } from '$lib/types/models';
import { api } from '$lib/api';
let ntpSettings: NTPSettings; let ntpSettings: NTPSettings;
let ntpStatus: NTPStatus; let ntpStatus: NTPStatus;
async function getNTPStatus() { async function getNTPStatus() {
try { const result = await api.get<NTPStatus>('/api/ntpStatus');
const response = await fetch('/api/ntpStatus', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' ntpStatus = result.inner
}
});
ntpStatus = await response.json();
} catch (error) {
console.error('Error:', error);
}
return;
} }
async function getNTPSettings() { async function getNTPSettings() {
try { const result = await api.get<NTPSettings>('/api/ntpSettings');
const response = await fetch('/api/ntpSettings', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' ntpSettings = result.inner
}
});
ntpSettings = await response.json();
} catch (error) {
console.error('Error:', error);
}
return;
} }
const interval = setInterval(async () => { const interval = setInterval(async () => {
@@ -70,25 +57,13 @@
}; };
async function postNTPSettings(data: NTPSettings) { async function postNTPSettings(data: NTPSettings) {
try { const result = await api.post<NTPSettings>('/api/ntpSettings', data);
const response = await fetch('/api/ntpSettings', { if (result.isErr()){
method: 'POST', notifications.error('User not authorized.', 3000);
headers: { console.error('Error:', result.inner);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return
'Content-Type': 'application/json' }
}, ntpSettings = result.inner
body: JSON.stringify(data)
});
if (response.status == 200) {
notifications.success('Security settings updated.', 3000);
ntpSettings = await response.json();
} else {
notifications.error('User not authorized.', 3000);
}
} catch (error) {
console.error('Error:', error);
}
} }
function handleSubmitNTP() { function handleSubmitNTP() {
+17 -25
View File
@@ -5,6 +5,8 @@
import { notifications } from '$lib/components/toasts/notifications'; import { notifications } from '$lib/components/toasts/notifications';
import { fade, fly } from 'svelte/transition'; import { fade, fly } from 'svelte/transition';
import Login from '~icons/tabler/login'; import Login from '~icons/tabler/login';
import { api } from '$lib/api';
import type { JWT } from '$lib/models';
type SignInData = { type SignInData = {
password: string; password: string;
@@ -19,31 +21,21 @@
let token = { access_token: '' }; let token = { access_token: '' };
async function signInUser(data: SignInData) { async function signInUser(data: SignInData) {
try { const result = await api.post<JWT>('/api/signIn', data)
const response = await fetch('/api/signIn', { if (result.isErr()){
method: 'POST', username = '';
headers: { password = '';
'Content-Type': 'application/json' notifications.error('Wrong Username or Password!', 5000);
}, loginFailed = true;
body: JSON.stringify(data) setTimeout(() => {
}); loginFailed = false;
if (response.status === 200) { }, 1500);
token = await response.json(); return
user.init(token.access_token); }
let username = $user.username; token = result.inner;
notifications.success('User ' + username + ' signed in', 5000); user.init(token.access_token);
} else { username = $user.username;
username = ''; notifications.success('User ' + username + ' signed in', 5000);
password = '';
notifications.error('Wrong Username or Password!', 5000);
loginFailed = true;
setTimeout(() => {
loginFailed = false;
}, 1500);
}
} catch (error) {
console.error('Error:', error);
}
} }
</script> </script>
+2 -9
View File
@@ -2,7 +2,6 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { telemetry } from '$lib/stores/telemetry'; import { telemetry } from '$lib/stores/telemetry';
import { openModal, closeModal } from 'svelte-modals'; import { openModal, closeModal } from 'svelte-modals';
import { user } from '$lib/stores/user';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import WiFiOff from '~icons/tabler/wifi-off'; import WiFiOff from '~icons/tabler/wifi-off';
import Hamburger from '~icons/tabler/menu-2'; import Hamburger from '~icons/tabler/menu-2';
@@ -13,15 +12,9 @@
import UpdateIndicator from '$lib/components/UpdateIndicator.svelte'; import UpdateIndicator from '$lib/components/UpdateIndicator.svelte';
import MdiWeatherSunny from '~icons/mdi/weather-sunny'; import MdiWeatherSunny from '~icons/mdi/weather-sunny';
import MdiMoonAndStars from '~icons/mdi/moon-and-stars'; import MdiMoonAndStars from '~icons/mdi/moon-and-stars';
import { api } from '$lib/api';
async function postSleep() { const postSleep = async () => await api.post('/api/sleep')
const response = await fetch('/api/sleep', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmSleep() { function confirmSleep() {
openModal(ConfirmDialog, { openModal(ConfirmDialog, {
@@ -3,11 +3,12 @@
import Spinner from "$lib/components/Spinner.svelte"; import Spinner from "$lib/components/Spinner.svelte";
import FolderIcon from '~icons/mdi/folder-outline'; import FolderIcon from '~icons/mdi/folder-outline';
import Folder from "./Folder.svelte"; import Folder from "./Folder.svelte";
import { api } from "$lib/api";
const getFiles = async () => { const getFiles = async () => {
const response = await fetch('/api/files/list'); const result = await api.get('/api/files/list')
if (response.ok) { if (result.isOk()) {
return response.json(); return result.inner;
} }
}; };
</script> </script>
@@ -26,25 +26,25 @@
import SDK from '~icons/tabler/sdk'; import SDK from '~icons/tabler/sdk';
import type { SystemInformation, Analytics } from '$lib/types/models'; import type { SystemInformation, Analytics } from '$lib/types/models';
import { socket } from '$lib/stores/socket'; import { socket } from '$lib/stores/socket';
import { api } from '$lib/api';
import { convertSeconds } from '$lib/utilities';
let systemInformation: SystemInformation; let systemInformation: SystemInformation;
async function getSystemStatus() { async function getSystemStatus() {
try { const result = await api.get<SystemInformation>('/api/systemStatus');
const response = await fetch('/api/systemStatus', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' systemInformation = result.inner
}
});
systemInformation = await response.json();
} catch (error) {
console.log('Error:', error);
}
return systemInformation; return systemInformation;
} }
const postFactoryReset = async () => await api.post('/api/factoryReset')
const postSleep = async () => await api.post('api/sleep')
onMount(() => socket.on('analytics', handleSystemData)); onMount(() => socket.on('analytics', handleSystemData));
onDestroy(() => socket.off('analytics', handleSystemData)); onDestroy(() => socket.off('analytics', handleSystemData));
@@ -52,14 +52,7 @@
const handleSystemData = (data: Analytics) => const handleSystemData = (data: Analytics) =>
(systemInformation = { ...systemInformation, ...data }); (systemInformation = { ...systemInformation, ...data });
async function postRestart() { const postRestart = async () => await api.post('/api/restart');
const response = await fetch('/api/restart', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmRestart() { function confirmRestart() {
openModal(ConfirmDialog, { openModal(ConfirmDialog, {
@@ -76,15 +69,6 @@
}); });
} }
async function postFactoryReset() {
const response = await fetch('/api/factoryReset', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmReset() { function confirmReset() {
openModal(ConfirmDialog, { openModal(ConfirmDialog, {
title: 'Confirm Factory Reset', title: 'Confirm Factory Reset',
@@ -100,15 +84,6 @@
}); });
} }
async function postSleep() {
const response = await fetch('/api/sleep', {
method: 'POST',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
}
});
}
function confirmSleep() { function confirmSleep() {
openModal(ConfirmDialog, { openModal(ConfirmDialog, {
title: 'Confirm Going to Sleep', title: 'Confirm Going to Sleep',
@@ -123,33 +98,6 @@
} }
}); });
} }
function convertSeconds(seconds: number) {
// Calculate the number of seconds, minutes, hours, and days
let minutes = Math.floor(seconds / 60);
let hours = Math.floor(minutes / 60);
let days = Math.floor(hours / 24);
// Calculate the remaining hours, minutes, and seconds
hours = hours % 24;
minutes = minutes % 60;
seconds = seconds % 60;
// Create the formatted string
let result = '';
if (days > 0) {
result += days + ' day' + (days > 1 ? 's' : '') + ' ';
}
if (hours > 0) {
result += hours + ' hour' + (hours > 1 ? 's' : '') + ' ';
}
if (minutes > 0) {
result += minutes + ' minute' + (minutes > 1 ? 's' : '') + ' ';
}
result += seconds + ' second' + (seconds > 1 ? 's' : '');
return result;
}
</script> </script>
<SettingsCard collapsible={false}> <SettingsCard collapsible={false}>
@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { user } from '$lib/stores/user';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { openModal, closeModal, closeAllModals } from 'svelte-modals'; import { openModal, closeModal, closeAllModals } from 'svelte-modals';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
@@ -14,43 +13,29 @@
import Error from '~icons/tabler/circle-x'; import Error from '~icons/tabler/circle-x';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte'; import GithubUpdateDialog from '$lib/components/GithubUpdateDialog.svelte';
import { assets } from '$app/paths';
import InfoDialog from '$lib/components/InfoDialog.svelte'; import InfoDialog from '$lib/components/InfoDialog.svelte';
import Check from '~icons/tabler/check'; import Check from '~icons/tabler/check';
import { api } from '$lib/api';
async function getGithubAPI() { async function getGithubAPI() {
try { const headers = {
const githubResponse = await fetch( accept: 'application/vnd.github+json',
'https://api.github.com/repos/' + $page.data.github + '/releases', 'X-GitHub-Api-Version': '2022-11-28'
{ }
method: 'GET', const result = await api.get(`https://api.github.com/repos/${$page.data.github}/releases`, {headers})
headers: { if (result.isErr()) {
accept: 'application/vnd.github+json', console.error('Error:', result.inner);
'X-GitHub-Api-Version': '2022-11-28' return
} }
} return result.inner as any;
);
const results = await githubResponse.json();
return results;
} catch (error) {
console.error('Error:', error);
}
return;
} }
async function postGithubDownload(url: string) { async function postGithubDownload(url: string) {
try { const result = await api.post('/api/downloadUpdate', { download_url: url })
const apiResponse = await fetch('/api/downloadUpdate', { if (result.isErr()) {
method: 'POST', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json'
},
body: JSON.stringify({ download_url: url })
});
} catch (error) {
console.error('Error:', error);
}
} }
function confirmGithubUpdate(assets: any) { function confirmGithubUpdate(assets: any) {
@@ -1,30 +1,20 @@
<script lang="ts"> <script lang="ts">
import { openModal, closeModal } from 'svelte-modals'; import { openModal, closeModal } from 'svelte-modals';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import SettingsCard from '$lib/components/SettingsCard.svelte'; import SettingsCard from '$lib/components/SettingsCard.svelte';
import OTA from '~icons/tabler/file-upload'; import OTA from '~icons/tabler/file-upload';
import Warning from '~icons/tabler/alert-triangle'; import Warning from '~icons/tabler/alert-triangle';
import Cancel from '~icons/tabler/x'; import Cancel from '~icons/tabler/x';
import { api } from '$lib/api';
let files: FileList; let files: FileList;
async function uploadBIN() { async function uploadBIN() {
try { const formData = new FormData();
const formData = new FormData(); formData.append('file', files[0]);
formData.append('file', files[0]); const result = await api.post('/api/uploadFirmware', formData)
const response = await fetch('/api/uploadFirmware', { if (result.isErr())
method: 'POST', console.error('Error:', result.inner);
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic'
},
body: formData
});
const result = await response.json();
} catch (error) {
console.error('Error:', error);
}
} }
function confirmBinUpload() { function confirmBinUpload() {
+21 -52
View File
@@ -20,6 +20,7 @@
import Warning from '~icons/tabler/alert-triangle'; import Warning from '~icons/tabler/alert-triangle';
import Cancel from '~icons/tabler/x'; import Cancel from '~icons/tabler/x';
import Check from '~icons/tabler/check'; import Check from '~icons/tabler/check';
import { api } from '$lib/api';
type userSetting = { type userSetting = {
username: string; username: string;
@@ -35,63 +36,31 @@
let securitySettings: SecuritySettings; let securitySettings: SecuritySettings;
async function getSecuritySettings() { async function getSecuritySettings() {
try { const result = await api.get<SecuritySettings>('/api/securitySettings')
const response = await fetch('/api/securitySettings', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' securitySettings = result.inner
}
});
securitySettings = await response.json();
} catch (error) {
console.error('Error:', error);
}
return;
} }
async function postSecuritySettings(data: SecuritySettings) { async function postSecuritySettings(data: SecuritySettings) {
try { const result = await api.post<SecuritySettings>('/api/securitySettings', data)
const response = await fetch('/api/securitySettings', { if (result.isErr()){
method: 'POST', console.error('Error:', result.inner);
headers: { notifications.error('User not authorized.', 3000);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return
'Content-Type': 'application/json' }
}, securitySettings = result.inner
body: JSON.stringify(data) if (await validateUser()) {
}); notifications.success('Security settings updated.', 3000);
}
securitySettings = await response.json();
if (response.status == 200) {
if (await validateUser($user)) {
notifications.success('Security settings updated.', 3000);
}
} else {
notifications.error('User not authorized.', 3000);
}
} catch (error) {
console.error('Error:', error);
}
return;
} }
async function validateUser(userdata: userProfile) { async function validateUser() {
try { const result = await api.get('/api/verifyAuthorization')
const response = await fetch('/api/verifyAuthorization', { if (result.isErr()) user.invalidate();
method: 'GET', return result.isOk();
headers: {
Authorization: 'Bearer ' + userdata.bearer_token,
'Content-Type': 'application/json'
}
});
if (response.status !== 200) {
user.invalidate();
return false;
}
} catch (error) {
console.error('Error:', error);
}
return true;
} }
function confirmDelete(index: number) { function confirmDelete(index: number) {
+21 -42
View File
@@ -13,6 +13,7 @@
import Home from '~icons/tabler/home'; import Home from '~icons/tabler/home';
import Devices from '~icons/tabler/devices'; import Devices from '~icons/tabler/devices';
import type { ApSettings, ApStatus } from '$lib/types/models'; import type { ApSettings, ApStatus } from '$lib/types/models';
import { api } from '$lib/api';
let apSettings: ApSettings; let apSettings: ApSettings;
let apStatus: ApStatus; let apStatus: ApStatus;
@@ -20,34 +21,22 @@
let formField: any; let formField: any;
async function getAPStatus() { async function getAPStatus() {
try { const result = await api.get<ApStatus>('/api/apStatus');
const response = await fetch('/api/apStatus', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' apStatus = result.inner
}
});
apStatus = await response.json();
} catch (error) {
console.error('Error:', error);
}
return apStatus; return apStatus;
} }
async function getAPSettings() { async function getAPSettings() {
try { const result = await api.get<ApSettings>('/api/apSetting');
const response = await fetch('/api/apSettings', { if (result.isErr()){
method: 'GET', console.error('Error:', result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' apSettings = result.inner
}
});
apSettings = await response.json();
} catch (error) {
console.error('Error:', error);
}
return apSettings; return apSettings;
} }
@@ -94,24 +83,14 @@
}; };
async function postAPSettings(data: ApSettings) { async function postAPSettings(data: ApSettings) {
try { const result = await api.post<ApSettings>('/api/apSettings', data);
const response = await fetch('/api/apSettings', { if (result.isErr()){
method: 'POST', notifications.error('User not authorized.', 3000);
headers: { console.error('Error:', result.inner);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return
'Content-Type': 'application/json' }
}, notifications.success('Access Point settings updated.', 3000);
body: JSON.stringify(data) apSettings = result.inner
});
if (response.status == 200) {
notifications.success('Access Point settings updated.', 3000);
apSettings = await response.json();
} else {
notifications.error('User not authorized.', 3000);
}
} catch (error) {
console.error('Error:', error);
}
} }
function handleSubmitAP() { function handleSubmitAP() {
+17 -32
View File
@@ -2,8 +2,6 @@
import { closeModal } from 'svelte-modals'; import { closeModal } from 'svelte-modals';
import { focusTrap } from 'svelte-focus-trap'; import { focusTrap } from 'svelte-focus-trap';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import Network from '~icons/tabler/router'; import Network from '~icons/tabler/router';
import AP from '~icons/tabler/access-point'; import AP from '~icons/tabler/access-point';
import Cancel from '~icons/tabler/x'; import Cancel from '~icons/tabler/x';
@@ -11,6 +9,8 @@
import { onMount, onDestroy } from 'svelte'; import { onMount, onDestroy } from 'svelte';
import RssiIndicator from '$lib/components/RSSIIndicator.svelte'; import RssiIndicator from '$lib/components/RSSIIndicator.svelte';
import type { NetworkItem } from '$lib/types/models'; import type { NetworkItem } from '$lib/types/models';
import { api } from '$lib/api';
import type { NetworkList } from '$lib/models';
// provided by <Modals /> // provided by <Modals />
export let isOpen: boolean; export let isOpen: boolean;
@@ -36,13 +36,7 @@
async function scanNetworks() { async function scanNetworks() {
scanActive = true; scanActive = true;
const scan = await fetch('/api/scanNetworks', { await api.get('/api/scanNetworks');
method: 'GET',
headers: {
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic',
'Content-Type': 'application/json'
}
});
if ((await pollingResults()) == false) { if ((await pollingResults()) == false) {
pollingId = setInterval(() => pollingResults(), 1000); pollingId = setInterval(() => pollingResults(), 1000);
} }
@@ -50,28 +44,19 @@
} }
async function pollingResults() { async function pollingResults() {
const response = await fetch('/api/listNetworks', { const result = await api.get<NetworkList>('/api/listNetworks');
method: 'GET', if (result.isErr()){
headers: { console.error(`Error occurred while fetching: `, result.inner);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return false
'Content-Type': 'application/json' }
} let response = result.inner
}); listOfNetworks = response.networks;
try { scanActive = false;
const result = await response.json(); if (listOfNetworks.length) {
listOfNetworks = result.networks; clearInterval(pollingId);
if (listOfNetworks.length) { pollingId = 0;
scanActive = false; }
clearInterval(pollingId); return listOfNetworks.length;
pollingId = 0;
return true;
} else {
scanActive = false;
return false;
}
} catch {
return false;
}
} }
onMount(() => { onMount(() => {
@@ -96,7 +81,7 @@
use:focusTrap use:focusTrap
> >
<div <div
class="bg-base-100 shadow rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg" class="bg-base-100 rounded-box pointer-events-auto flex max-h-full min-w-fit max-w-md flex-col justify-between p-4 shadow-lg"
> >
<h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2> <h2 class="text-base-content text-start text-2xl font-bold">Scan Networks</h2>
<div class="divider my-2" /> <div class="divider my-2" />
+22 -43
View File
@@ -34,6 +34,7 @@
import InfoDialog from '$lib/components/InfoDialog.svelte'; import InfoDialog from '$lib/components/InfoDialog.svelte';
import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models'; import type { KnownNetworkItem, WifiSettings, WifiStatus } from '$lib/types/models';
import { socket } from '$lib/stores'; import { socket } from '$lib/stores';
import { api } from '$lib/api';
let networkEditable: KnownNetworkItem = { let networkEditable: KnownNetworkItem = {
ssid: '', ssid: '',
@@ -72,35 +73,23 @@
let formErrorhostname = false; let formErrorhostname = false;
async function getWifiStatus() { async function getWifiStatus() {
try { const result = await api.get<WifiStatus>('/api/wifiStatus');
const response = await fetch('/api/wifiStatus', { if (result.isErr()){
method: 'GET', console.error(`Error occurred while fetching: `, result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' wifiStatus = result.inner
}
});
wifiStatus = await response.json();
} catch (error) {
console.error('Error:', error);
}
return wifiStatus; return wifiStatus;
} }
async function getWifiSettings() { async function getWifiSettings() {
try { const result = await api.get<WifiSettings>('/api/wifiSettings');
const response = await fetch('/api/wifiSettings', { if (result.isErr()){
method: 'GET', console.error(`Error occurred while fetching: `, result.inner);
headers: { return
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', }
'Content-Type': 'application/json' wifiSettings = result.inner
} dndNetworkList = wifiSettings.wifi_networks;
});
wifiSettings = await response.json();
} catch (error) {
console.error('Error:', error);
}
dndNetworkList = wifiSettings.wifi_networks;
return wifiSettings; return wifiSettings;
} }
@@ -114,24 +103,14 @@
}); });
async function postWiFiSettings(data: WifiSettings) { async function postWiFiSettings(data: WifiSettings) {
try { const result = await api.post<WifiSettings>('/api/wifiSettings', data);
const response = await fetch('/api/wifiSettings', { if (result.isErr()){
method: 'POST', console.error(`Error occurred while fetching: `, result.inner);
headers: { notifications.error('User not authorized.', 3000);
Authorization: $page.data.features.security ? 'Bearer ' + $user.bearer_token : 'Basic', return
'Content-Type': 'application/json' }
}, wifiSettings = result.inner
body: JSON.stringify(data) notifications.success('Wi-Fi settings updated.', 3000);
});
if (response.status == 200) {
notifications.success('Wi-Fi settings updated.', 3000);
wifiSettings = await response.json();
} else {
notifications.error('User not authorized.', 3000);
}
} catch (error) {
console.error('Error:', error);
}
} }
function validateHostName() { function validateHostName() {
+31 -66
View File
@@ -7,6 +7,7 @@
* *
* Copyright (C) 2018 - 2023 rjwats * Copyright (C) 2018 - 2023 rjwats
* Copyright (C) 2023 theelims * Copyright (C) 2023 theelims
* Copyright (C) 2024 runeharlyk
* *
* All Rights Reserved. This software may be modified and distributed under * All Rights Reserved. This software may be modified and distributed under
* the terms of the LGPL v3 license. See the LICENSE file for details. * the terms of the LGPL v3 license. See the LICENSE file for details.
@@ -14,74 +15,38 @@
#include <FeaturesService.h> #include <FeaturesService.h>
FeaturesService::FeaturesService(PsychicHttpServer *server) : _server(server) FeaturesService::FeaturesService(PsychicHttpServer *server) : _server(server) {}
{
void FeaturesService::begin() {
_server->on(FEATURES_SERVICE_PATH, HTTP_GET, [&](PsychicRequest *request) {
PsychicJsonResponse response = PsychicJsonResponse(request, false);
JsonObject root = response.getRoot();
root["security"] = FT_SECURITY;
root["mqtt"] = FT_MQTT;
root["ntp"] = FT_NTP;
root["upload_firmware"] = FT_UPLOAD_FIRMWARE;
root["download_firmware"] = FT_DOWNLOAD_FIRMWARE;
root["sleep"] = FT_SLEEP;
root["battery"] = FT_BATTERY;
root["analytics"] = FT_ANALYTICS;
root["firmware_version"] = APP_VERSION;
root["firmware_name"] = APP_NAME;
root["firmware_built_target"] = BUILD_TARGET;
// Iterate over user features
for (auto &element : userFeatures) {
root[element.feature.c_str()] = element.enabled;
}
return response.send();
});
ESP_LOGV("FeaturesService", "Registered GET endpoint: %s",
FEATURES_SERVICE_PATH);
} }
void FeaturesService::begin() void FeaturesService::addFeature(String feature, bool enabled) {
{
_server->on(FEATURES_SERVICE_PATH, HTTP_GET, [&](PsychicRequest *request)
{
PsychicJsonResponse response = PsychicJsonResponse(request, false);
JsonObject root = response.getRoot();
#if FT_ENABLED(FT_SECURITY)
root["security"] = true;
#else
root["security"] = false;
#endif
#if FT_ENABLED(FT_MQTT)
root["mqtt"] = true;
#else
root["mqtt"] = false;
#endif
#if FT_ENABLED(FT_NTP)
root["ntp"] = true;
#else
root["ntp"] = false;
#endif
#if FT_ENABLED(FT_UPLOAD_FIRMWARE)
root["upload_firmware"] = true;
#else
root["upload_firmware"] = false;
#endif
#if FT_ENABLED(FT_DOWNLOAD_FIRMWARE)
root["download_firmware"] = true;
#else
root["download_firmware"] = false;
#endif
#if FT_ENABLED(FT_SLEEP)
root["sleep"] = true;
#else
root["sleep"] = false;
#endif
#if FT_ENABLED(FT_BATTERY)
root["battery"] = true;
#else
root["battery"] = false;
#endif
#if FT_ENABLED(FT_ANALYTICS)
root["analytics"] = true;
#else
root["analytics"] = false;
#endif
root["firmware_version"] = APP_VERSION;
root["firmware_name"] = APP_NAME;
root["firmware_built_target"] = BUILD_TARGET;
// Iterate over user features
for (auto &element : userFeatures)
{
root[element.feature.c_str()] = element.enabled;
}
return response.send(); });
ESP_LOGV("FeaturesService", "Registered GET endpoint: %s", FEATURES_SERVICE_PATH);
}
void FeaturesService::addFeature(String feature, bool enabled)
{
UserFeature newFeature; UserFeature newFeature;
newFeature.feature = feature; newFeature.feature = feature;
newFeature.enabled = enabled; newFeature.enabled = enabled;