🔐 Removes auth from frontend

This commit is contained in:
Rune Harlyk
2024-10-29 20:58:48 +01:00
parent 1c6b9f79c5
commit 84633e5707
22 changed files with 4225 additions and 4282 deletions
+2406 -1937
View File
File diff suppressed because it is too large Load Diff
+3 -4
View File
@@ -1,4 +1,3 @@
import { user } from '$lib/stores/user';
import { get } from 'svelte/store';
import { Err, Ok, type Result } from './utilities';
import { location } from './stores';
@@ -28,7 +27,6 @@ async function sendRequest<TResponse>(
params?: RequestInit
): Promise<Result<TResponse, Error>> {
endpoint = resolveUrl(endpoint);
const user_token = get(user).bearer_token;
const body = data !== null && typeof data !== 'undefined' ? JSON.stringify(data) : undefined;
const request = {
@@ -37,7 +35,7 @@ async function sendRequest<TResponse>(
body,
headers: {
...params?.headers,
Authorization: user_token ? 'Bearer ' + user_token : 'Basic',
Authorization: 'Basic',
'Content-Type': 'application/json'
}
};
@@ -58,7 +56,8 @@ async function sendRequest<TResponse>(
return Err.new(new ApiError(response), 'An error has occurred');
}
const contentType = response.headers.get('Content-Type') ?? response.headers.get('Content-Type');
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);
+2 -2
View File
@@ -1,8 +1,8 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { user, location } from '$lib/stores';
import { location } from '$lib/stores';
let source = `${$location}/api/camera/stream?access_token=${$user.bearer_token}`;
let source = `${$location}/api/camera/stream`;
onDestroy(() => (source = '#'));
</script>
-111
View File
@@ -1,111 +0,0 @@
<script lang="ts">
import logo from '$lib/assets/logo512.png';
import { PasswordInput } from '$lib/components/input';
import { user } from '$lib/stores/user';
import { notifications } from '$lib/components/toasts/notifications';
import { fade, fly } from 'svelte/transition';
import { api } from '$lib/api';
import type { JWT } from '$lib/types/models';
import { Login } from './icons';
type SignInData = {
password: string;
username: string;
};
let username = '';
let password = '';
let loginFailed = false;
let token = { access_token: '' };
async function signInUser(data: SignInData) {
const result = await api.post<JWT>('/api/signIn', data);
if (result.isErr()) {
username = '';
password = '';
notifications.error('Wrong Username or Password!', 5000);
loginFailed = true;
setTimeout(() => {
loginFailed = false;
}, 1500);
return;
}
token = result.inner;
user.init(token.access_token);
username = $user.username;
notifications.success('User ' + username + ' signed in', 5000);
}
</script>
<div class="hero from-primary/30 to-secondary/30 min-h-screen bg-gradient-to-br">
<div
class="card lg:card-side bg-base-100 face shadow-2xl {loginFailed
? 'failure border-error border-2'
: ''}"
in:fly={{ delay: 200, y: 100, duration: 500 }}
out:fade={{ duration: 200 }}
>
<figure class="bg-base-200"><img src={logo} alt="Logo" class="h-auto w-48 lg:w-64" /></figure>
<div class="card-body w-80">
<h2 class="card-title text-2xl">Login</h2>
<form class="form-control w-full max-w-xs">
<label class="label" for="user">
<span class="label-text text-md">Username</span>
</label>
<input
type="text"
class="input input-bordered w-full max-w-xs"
id="user"
bind:value={username}
/>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<PasswordInput id="pwd" bind:value={password} />
<div class="card-actions mt-4 justify-end">
<button
class="btn btn-primary inline-flex items-center"
on:click={() => signInUser({ username, password })}
>
<Login class="mr-2 h-5 w-5" /><span>Login</span>
</button>
</div>
</form>
</div>
</div>
</div>
<style>
.failure {
animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both;
transform: translate3d(0, 0, 0);
backface-visibility: hidden;
perspective: 1000px;
}
@keyframes shake {
10%,
90% {
transform: translatex(-1px);
}
20%,
80% {
transform: translatex(2px);
}
30%,
50%,
70% {
transform: translatex(-4px);
}
40%,
60% {
transform: translatex(4px);
}
}
</style>
+1 -13
View File
@@ -1,15 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
import { user } from '$lib/stores/user';
import { createEventDispatcher } from 'svelte';
import { useFeatureFlags } from '$lib/stores/featureFlags';
import UserButton from '../menu/UserButton.svelte';
import GithubButton from '../menu/GithubButton.svelte';
import LogoButton from '../menu/LogoButton.svelte';
import MenuList from '../menu/MenuList.svelte';
import {
Connection,
Users,
Settings,
MdiController,
Devices,
@@ -122,12 +119,6 @@
}
]
},
{
title: 'Users',
icon: Users,
href: '/user',
feature: $features.security && $user.admin
},
{
title: 'System',
icon: Settings,
@@ -156,8 +147,7 @@
icon: Update,
href: '/system/update',
feature:
($features.ota || $features.upload_firmware || $features.download_firmware) &&
(!$features.security || $user.admin)
$features.ota || $features.upload_firmware || $features.download_firmware
}
]
}
@@ -188,8 +178,6 @@
<MenuList {menuItems} on:select{updateMenu} class="flex-grow flex-nowrap overflow-y-auto" />
<UserButton />
<div class="divider my-0" />
<div class="flex items-center justify-between">
@@ -1,17 +0,0 @@
<script lang="ts">
import { user } from '$lib/stores';
import { useFeatureFlags } from "$lib/stores";
import { Avatar, Logout } from '../icons';
const features = useFeatureFlags();
</script>
{#if $features.security}
<div class="flex items-center">
<Avatar class="h-8 w-8" />
<span class="flex-grow px-4 text-xl font-bold">{$user.username}</span>
<button class="btn btn-ghost" on:click={user.invalidate}>
<Logout class="h-8 w-8 rotate-180" />
</button>
</div>
{/if}
@@ -1,7 +1,6 @@
<script lang="ts">
import { page } from '$app/stores';
import { openModal, closeAllModals } from 'svelte-modals';
import { user } from '$lib/stores/user';
import { notifications } from '$lib/components/toasts/notifications';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
@@ -68,7 +67,7 @@
}
onMount(async () => {
if ($features.download_firmware && (!$features.security || $user.admin)) {
if ($features.download_firmware) {
await getGithubAPI();
const interval = setInterval(
async () => {
-1
View File
@@ -5,6 +5,5 @@ export * from './socket';
export * from './fullscreen';
export * from './telemetry';
export * from './analytics';
export * from './user';
export * from './featureFlags';
export * from './location-store';
-40
View File
@@ -1,40 +0,0 @@
import { goto } from '$app/navigation';
import { jwtDecode } from 'jwt-decode';
import { persistentStore } from '$lib/utilities';
export type UserProfile = {
username: string;
admin: boolean;
bearer_token: string;
};
type DecodedJWT = Omit<UserProfile, 'bearer_token'>;
const emptyUser: UserProfile = {
username: '',
admin: false,
bearer_token: ''
};
function createUserStore() {
const store = persistentStore<UserProfile>('user', emptyUser);
return {
subscribe: store.subscribe,
init: (access_token: string) => {
const decoded: DecodedJWT = jwtDecode(access_token);
const userProfile: UserProfile = {
bearer_token: access_token,
username: decoded.username,
admin: decoded.admin
};
store.set(userProfile);
},
invalidate: () => {
store.set(emptyUser);
goto('/');
}
};
}
export const user = createUserStore();
+1 -4
View File
@@ -17,8 +17,6 @@ export type GithubRelease = {
}>;
};
export type JWT = { access_token: string };
export type angles = number[] | Int16Array;
export type WifiStatus = {
@@ -91,7 +89,6 @@ export type NTPStatus = {
uptime: number;
};
export type Battery = {
voltage: number;
current: number;
@@ -163,7 +160,7 @@ export interface I2CDevice {
address: number;
part_number: string;
name: string;
};
}
export type CameraSettings = {
framesize: number;
+5 -25
View File
@@ -8,12 +8,9 @@
import '../app.css';
import Menu from '../lib/components/menu/Menu.svelte';
import Statusbar from '../lib/components/statusbar/statusbar.svelte';
import Login from '../lib/components/login.svelte';
import {
telemetry,
analytics,
user,
type UserProfile,
ModesEnum,
kinematicData,
mode,
@@ -21,21 +18,16 @@
servoAngles,
servoAnglesOut,
socket,
location
location,
useFeatureFlags
} from '$lib/stores';
import type { Analytics, Battery, DownloadOTA } from '$lib/types/models';
import { api } from '$lib/api';
import { useFeatureFlags } from '$lib/stores/featureFlags';
const features = useFeatureFlags();
onMount(async () => {
if ($user.bearer_token !== '') {
await validateUser($user);
}
const ws_token = $features.security ? '?access_token=' + $user.bearer_token : '';
const ws = $location ? $location : window.location.host;
socket.init(`ws://${ws}/ws/events${ws_token}`);
socket.init(`ws://${ws}/ws/events`);
addEventListeners();
@@ -75,14 +67,6 @@
socket.off('otastatus', handleOAT);
};
async function validateUser(userdata: UserProfile) {
const result = await api.get('/api/verifyAuthorization');
if (result.isErr()) {
user.invalidate();
console.error('Error:', result.inner);
}
}
const handleOpen = () => {
notifications.success('Connection to device established', 5000);
};
@@ -109,10 +93,7 @@
<title>{$page.data.title}</title>
</svelte:head>
{#if $features?.security && $user.bearer_token === ''}
<Login />
{:else}
<div class="drawer">
<div class="drawer">
<input id="main-menu" type="checkbox" class="drawer-toggle" bind:checked={menuOpen} />
<div class="drawer-content flex flex-col">
<!-- Status bar content here -->
@@ -126,8 +107,7 @@
<label for="main-menu" class="drawer-overlay" />
<Menu on:menuClicked={() => (menuOpen = false)} />
</div>
</div>
{/if}
</div>
<Modals>
<!-- svelte-ignore a11y-click-events-have-key-events -->
+2 -3
View File
@@ -1,14 +1,13 @@
<script lang="ts">
import SettingsCard from '$lib/components/SettingsCard.svelte';
import { WiFi } from '$lib/components/icons';
import { location, socket, useFeatureFlags, user } from '$lib/stores';
import { location, socket, useFeatureFlags } from '$lib/stores';
const features = useFeatureFlags();
const update = () => {
const ws_token = $features.security ? '?access_token=' + $user.bearer_token : '';
const ws = $location ? $location : window.location.host;
socket.init(`ws://${ws}/ws/events${ws_token}`);
socket.init(`ws://${ws}/ws/events`);
};
</script>
+20 -26
View File
@@ -5,7 +5,7 @@
import SettingsCard from '$lib/components/SettingsCard.svelte';
import Collapsible from '$lib/components/Collapsible.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { user, useFeatureFlags } from '$lib/stores';
import { useFeatureFlags } from '$lib/stores';
import { notifications } from '$lib/components/toasts/notifications';
import { TIME_ZONES } from './timezones';
import type { NTPSettings, NTPStatus } from '$lib/types/models';
@@ -19,33 +19,27 @@
async function getNTPStatus() {
const result = await api.get<NTPStatus>('/api/ntpStatus');
if (result.isErr()){
if (result.isErr()) {
console.error('Error:', result.inner);
return
return;
}
ntpStatus = result.inner
ntpStatus = result.inner;
}
async function getNTPSettings() {
const result = await api.get<NTPSettings>('/api/ntpSettings');
if (result.isErr()){
if (result.isErr()) {
console.error('Error:', result.inner);
return
return;
}
ntpSettings = result.inner
ntpSettings = result.inner;
}
const interval = setInterval(async () => {
getNTPStatus();
}, 5000);
const interval = setInterval(async () => getNTPStatus(), 5000);
onDestroy(() => clearInterval(interval));
onMount(() => {
if (!$features.security || $user.admin) {
getNTPSettings();
}
});
onMount(getNTPSettings);
let formField: any;
@@ -55,12 +49,12 @@
async function postNTPSettings(data: NTPSettings) {
const result = await api.post<NTPSettings>('/api/ntpSettings', data);
if (result.isErr()){
if (result.isErr()) {
notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner);
return
return;
}
ntpSettings = result.inner
ntpSettings = result.inner;
}
function handleSubmitNTP() {
@@ -130,13 +124,13 @@
>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1
? 'bg-success'
class="mask mask-hexagon h-auto w-10 {ntpStatus.status === 1 ?
'bg-success'
: 'bg-error'}"
>
<NTP
class="h-auto w-full scale-75 {ntpStatus.status === 1
? 'text-success-content'
class="h-auto w-full scale-75 {ntpStatus.status === 1 ?
'text-success-content'
: 'text-error-content'}"
/>
</div>
@@ -206,7 +200,6 @@
{/await}
</div>
{#if !$features.security || $user.admin}
<Collapsible open={false} on:closed={getNTPSettings}>
<span slot="title">Change NTP Settings</span>
<form
@@ -230,8 +223,10 @@
type="text"
min="3"
max="64"
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.server
? 'border-error border-2'
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.server
) ?
'border-error border-2'
: ''}"
bind:value={ntpSettings.server}
id="server"
@@ -256,5 +251,4 @@
</div>
</form>
</Collapsible>
{/if}
</SettingsCard>
@@ -1,8 +1,6 @@
<script lang="ts">
import { onDestroy, onMount } from 'svelte';
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 SettingsCard from '$lib/components/SettingsCard.svelte';
import Spinner from '$lib/components/Spinner.svelte';
@@ -162,8 +160,8 @@
<div>
<div class="font-bold">CPU Frequency</div>
<div class="text-sm opacity-75">
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2
? 'Dual Core'
{systemInformation.cpu_freq_mhz} MHz {systemInformation.cpu_cores == 2 ?
'Dual Core'
: 'Single Core'}
</div>
</div>
@@ -206,15 +204,19 @@
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
<span>
{(
(systemInformation.sketch_size / systemInformation.free_sketch_space) *
(systemInformation.sketch_size /
systemInformation.free_sketch_space) *
100
).toFixed(1)} % of
{(systemInformation.free_sketch_space / 1000000).toLocaleString('en-US')} MB used
{(systemInformation.free_sketch_space / 1000000).toLocaleString(
'en-US'
)} MB used
</span>
<span>
({(
(systemInformation.free_sketch_space - systemInformation.sketch_size) /
(systemInformation.free_sketch_space -
systemInformation.sketch_size) /
1000000
).toLocaleString('en-US')} MB free)
</span>
@@ -229,9 +231,10 @@
<div>
<div class="font-bold">Flash Chip (Size / Speed)</div>
<div class="text-sm opacity-75">
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB / {(
systemInformation.flash_chip_speed / 1000000
).toLocaleString('en-US')} MHz
{(systemInformation.flash_chip_size / 1000000).toLocaleString('en-US')} MB
/ {(systemInformation.flash_chip_speed / 1000000).toLocaleString(
'en-US'
)} MHz
</div>
</div>
</div>
@@ -244,7 +247,10 @@
<div class="font-bold">File System (Used / Total)</div>
<div class="flex flex-wrap justify-start gap-1 text-sm opacity-75">
<span
>{((systemInformation.fs_used / systemInformation.fs_total) * 100).toFixed(1)} % of {(
>{(
(systemInformation.fs_used / systemInformation.fs_total) *
100
).toFixed(1)} % of {(
systemInformation.fs_total / 1000000
).toLocaleString('en-US')} MB used</span
>
@@ -267,8 +273,8 @@
<div>
<div class="font-bold">Core Temperature</div>
<div class="text-sm opacity-75">
{systemInformation.core_temp == 53.33
? 'NaN'
{systemInformation.core_temp == 53.33 ?
'NaN'
: systemInformation.core_temp.toFixed(2) + ' °C'}
</div>
</div>
@@ -303,17 +309,15 @@
<div class="mt-4 flex flex-wrap justify-end gap-2">
{#if $features.sleep}
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}
><Sleep class="mr-2 h-5 w-5" /><span>Sleep</span></button
>
{/if}
{#if !$features.security || $user.admin}
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}
><Power class="mr-2 h-5 w-5" /><span>Restart</span></button
>
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}
><FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span></button
>
<button class="btn btn-primary inline-flex items-center" on:click={confirmSleep}>
<Sleep class="mr-2 h-5 w-5" /><span>Sleep</span>
</button>
{/if}
<button class="btn btn-primary inline-flex items-center" on:click={confirmRestart}>
<Power class="mr-2 h-5 w-5" /><span>Restart</span>
</button>
<button class="btn btn-secondary inline-flex items-center" on:click={confirmReset}>
<FactoryReset class="mr-2 h-5 w-5" /><span>Factory Reset</span>
</button>
</div>
</SettingsCard>
+2 -3
View File
@@ -1,18 +1,17 @@
<script lang="ts">
import UploadFirmware from './UploadFirmware.svelte';
import GithubFirmwareManager from './GithubFirmwareManager.svelte';
import { user } from '$lib/stores/user';
import { useFeatureFlags } from '$lib/stores';
const features = useFeatureFlags();
</script>
<div class="mx-0 my-1 flex flex-col space-y-4 sm:mx-8 sm:my-8">
{#if $features.download_firmware && (!$features.security || $user.admin)}
{#if $features.download_firmware}
<GithubFirmwareManager />
{/if}
{#if $features.upload_firmware && (!$features.security || $user.admin)}
{#if $features.upload_firmware}
<UploadFirmware />
{/if}
</div>
-195
View File
@@ -1,195 +0,0 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { openModal, closeModal } from 'svelte-modals';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { user } from '$lib/stores/user';
import { notifications } from '$lib/components/toasts/notifications';
import { PasswordInput } from '$lib/components/input';
import SettingsCard from '$lib/components/SettingsCard.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import EditUser from './EditUser.svelte';
import Spinner from '$lib/components/Spinner.svelte';
import { api } from '$lib/api';
import {
Cancel,
Check,
Users,
AddUser,
Admin,
Edit,
Delete,
Warning
} from '$lib/components/icons';
type userSetting = {
username: string;
password: string;
admin: boolean;
};
type SecuritySettings = {
jwt_secret: string;
users: userSetting[];
};
let securitySettings: SecuritySettings;
async function getSecuritySettings() {
const result = await api.get<SecuritySettings>('/api/securitySettings');
if (result.isErr()) {
console.error('Error:', result.inner);
return;
}
securitySettings = result.inner;
}
async function postSecuritySettings(data: SecuritySettings) {
const result = await api.post<SecuritySettings>('/api/securitySettings', data);
if (result.isErr()) {
console.error('Error:', result.inner);
notifications.error('User not authorized.', 3000);
return;
}
securitySettings = result.inner;
if (await validateUser()) {
notifications.success('Security settings updated.', 3000);
}
}
async function validateUser() {
const result = await api.get('/api/verifyAuthorization');
if (result.isErr()) user.invalidate();
return result.isOk();
}
function confirmDelete(index: number) {
openModal(ConfirmDialog, {
title: 'Confirm Delete User',
message:
'Are you sure you want to delete the user "' +
securitySettings.users[index].username +
'"?',
labels: {
cancel: { label: 'Abort', icon: Cancel },
confirm: { label: 'Yes', icon: Check }
},
onConfirm: () => {
securitySettings.users.splice(index, 1);
securitySettings = securitySettings;
closeModal();
postSecuritySettings(securitySettings);
}
});
}
function handleEdit(index: number) {
openModal(EditUser, {
title: 'Edit User',
user: { ...securitySettings.users[index] }, // Shallow Copy
onSaveUser: (editedUser: userSetting) => {
securitySettings.users[index] = editedUser;
closeModal();
postSecuritySettings(securitySettings);
}
});
}
function handleNewUser() {
openModal(EditUser, {
title: 'Add User',
onSaveUser: (newUser: userSetting) => {
securitySettings.users = [...securitySettings.users, newUser];
closeModal();
postSecuritySettings(securitySettings);
}
});
//
}
</script>
{#if $user.admin}
<div
class="mx-0 my-1 flex flex-col space-y-4
sm:mx-8 sm:my-8"
>
<SettingsCard collapsible={false}>
<Users slot="icon" class="lex-shrink-0 mr-2 h-6 w-6 self-end" />
<span slot="title">Manage Users</span>
{#await getSecuritySettings()}
<Spinner />
{:then nothing}
<div class="relative w-full overflow-visible">
<button
class="btn btn-primary text-primary-content btn-md absolute -top-14 right-0"
on:click={handleNewUser}
>
<AddUser class="h-6 w-6" /></button
>
<div class="overflow-x-auto" transition:slide|local={{ duration: 300, easing: cubicOut }}>
<table class="table w-full table-auto">
<thead>
<tr class="font-bold">
<th align="left">Username</th>
<th align="center">Admin</th>
<th align="right" class="pr-8">Edit</th>
</tr>
</thead>
<tbody>
{#each securitySettings.users as user, index}
<tr>
<td align="left">{user.username}</td>
<td align="center">
{#if user.admin}
<Admin class="text-secondary" />
{/if}
</td>
<td align="right">
<span class="my-auto inline-flex flex-row space-x-2">
<button
class="btn btn-ghost btn-circle btn-xs"
on:click={() => handleEdit(index)}
>
<Edit class="h-6 w-6" /></button
>
<button
class="btn btn-ghost btn-circle btn-xs"
on:click={() => confirmDelete(index)}
>
<Delete class="text-error h-6 w-6" />
</button>
</span>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
<div class="divider mb-0" />
<span class="pb-2 text-xl font-medium">Security Settings</span>
<div class="alert alert-warning shadow-lg">
<Warning class="h-6 w-6 flex-shrink-0" />
<span
>The JWT secret is used to sign authentication tokens. If you modify the JWT Secret, all
users will be signed out.</span
>
</div>
<label class="label" for="secret">
<span class="label-text text-md">JWT Secret</span>
</label>
<PasswordInput bind:value={securitySettings.jwt_secret} id="secret" />
<div class="mt-6 flex justify-end">
<button class="btn btn-primary" on:click={() => postSecuritySettings(securitySettings)}
>Apply Settings</button
>
</div>
{/await}
</SettingsCard>
</div>
{:else}
{goto('/')}
{/if}
-7
View File
@@ -1,7 +0,0 @@
import type { PageLoad } from './$types';
export const load = (async () => {
return {
title: 'Users'
};
}) satisfies PageLoad;
-103
View File
@@ -1,103 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import { closeModal } from 'svelte-modals';
import { fly } from 'svelte/transition';
import { PasswordInput } from '$lib/components/input';
import { Cancel, Save } from '$lib/components/icons';
// provided by <Modals />
export let isOpen: boolean;
export let title: string;
export let onSaveUser: any; // Callback on Save
export let user = {
username: '',
password: '',
admin: false
};
let errorUsername = false;
let usernameEditable = false;
onMount(() => {
if (user.username == '') {
usernameEditable = true;
}
});
function handleSave() {
// Validate if username is within range
if (user.username.length < 3 || user.username.length > 32) {
errorUsername = true;
} else {
errorUsername = false;
// Callback on saving
onSaveUser(user);
}
}
</script>
{#if isOpen}
<div
role="dialog"
class="pointer-events-none fixed inset-0 z-50 flex items-center justify-center overflow-y-auto"
transition:fly={{ y: 50 }}
on:introstart
on:outroend
>
<div
class="rounded-box bg-base-100 pointer-events-auto flex min-w-fit max-w-md flex-col justify-between p-4 shadow-lg md:w-[28rem]"
>
<h2 class="text-base-content text-start text-2xl font-bold">{title}</h2>
<div class="divider my-2" />
<form
class="form-control text-base-content mb-1 w-full"
on:submit|preventDefault={handleSave}
novalidate
>
<label class="label" for="username">
<span class="label-text text-md">Username</span>
</label>
<input
type="text"
min="3"
max="32"
class="input input-bordered invalid:border-error w-full invalid:border-2"
bind:value={user.username}
id="username"
disabled={!usernameEditable}
/>
<label for="username" class="label"
><span class="label-text-alt text-error {errorUsername ? '' : 'hidden'}"
>Username must be between 3 and 32 characters long</span
></label
>
<label class="label" for="pwd">
<span class="label-text text-md">Password</span>
</label>
<PasswordInput bind:value={user.password} id="pwd" />
<label class="label my-auto cursor-pointer justify-start gap-4">
<input type="checkbox" bind:checked={user.admin} class="checkbox checkbox-primary" />
<span class="">Is Admin?</span>
</label>
<div class="divider my-2" />
<div class="flex justify-end gap-2">
<button
class="btn btn-neutral text-neutral-content inline-flex items-center"
on:click={closeModal}
type="button"
>
<Cancel class="mr-2 h-5 w-5" /><span>Cancel</span></button
>
<button
class="btn btn-primary text-primary-content inline-flex items-center"
type="submit"
><Save class="mr-2 h-5 w-5" />
Save
</button>
</div>
</form>
</div>
</div>
{/if}
+56 -43
View File
@@ -4,8 +4,6 @@
import { cubicOut } from 'svelte/easing';
import { PasswordInput } from '$lib/components/input';
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 Spinner from '$lib/components/Spinner.svelte';
import type { ApSettings, ApStatus } from '$lib/types/models';
@@ -22,21 +20,21 @@
async function getAPStatus() {
const result = await api.get<ApStatus>('/api/apStatus');
if (result.isErr()){
if (result.isErr()) {
console.error('Error:', result.inner);
return
return;
}
apStatus = result.inner
apStatus = result.inner;
return apStatus;
}
async function getAPSettings() {
const result = await api.get<ApSettings>('/api/apSetting');
if (result.isErr()){
if (result.isErr()) {
console.error('Error:', result.inner);
return
return;
}
apSettings = result.inner
apSettings = result.inner;
return apSettings;
}
@@ -46,11 +44,7 @@
onDestroy(() => clearInterval(interval));
onMount(() => {
if (!$features.security || $user.admin) {
getAPSettings();
}
});
onMount(getAPSettings);
let provisionMode = [
{
@@ -84,13 +78,13 @@
async function postAPSettings(data: ApSettings) {
const result = await api.post<ApSettings>('/api/apSettings', data);
if (result.isErr()){
if (result.isErr()) {
notifications.error('User not authorized.', 3000);
console.error('Error:', result.inner);
return
return;
}
notifications.success('Access Point settings updated.', 3000);
apSettings = result.inner
apSettings = result.inner;
}
function handleSubmitAP() {
@@ -170,9 +164,13 @@
>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status].bg_color}"
class="mask mask-hexagon h-auto w-10 {apStatusDescription[apStatus.status]
.bg_color}"
>
<AP class="h-auto w-full scale-75 {apStatusDescription[apStatus.status].text_color}" />
<AP
class="h-auto w-full scale-75 {apStatusDescription[apStatus.status]
.text_color}"
/>
</div>
<div>
<div class="font-bold">Status</div>
@@ -221,7 +219,6 @@
{/await}
</div>
{#if !$features.security || $user.admin}
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
<div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
@@ -263,8 +260,10 @@
</label>
<input
type="text"
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.ssid
? 'border-error border-2'
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.ssid
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.ssid}
id="ssid"
@@ -273,7 +272,8 @@
required
/>
<label class="label" for="ssid">
<span class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
<span
class="label-text-alt text-error {formErrors.ssid ? '' : 'hidden'}"
>SSID must be between 2 and 32 characters long</span
>
</label>
@@ -293,16 +293,20 @@
type="number"
min="1"
max="13"
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.channel
? 'border-error border-2'
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.channel
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.channel}
id="channel"
required
/>
<label class="label" for="channel">
<span class="label-text-alt text-error {formErrors.channel ? '' : 'hidden'}"
>Must be channel 1 to 13</span
<span
class="label-text-alt text-error {formErrors.channel ? '' : (
'hidden'
)}">Must be channel 1 to 13</span
>
</label>
</div>
@@ -315,16 +319,20 @@
type="number"
min="1"
max="8"
class="input input-bordered invalid:border-error w-full invalid:border-2 {formErrors.max_clients
? 'border-error border-2'
class="input input-bordered invalid:border-error w-full invalid:border-2 {(
formErrors.max_clients
) ?
'border-error border-2'
: ''}"
bind:value={apSettings.max_clients}
id="clients"
required
/>
<label class="label" for="clients">
<span class="label-text-alt text-error {formErrors.max_clients ? '' : 'hidden'}"
>Maximum 8 clients allowed</span
<span
class="label-text-alt text-error {formErrors.max_clients ? '' : (
'hidden'
)}">Maximum 8 clients allowed</span
>
</label>
</div>
@@ -335,8 +343,8 @@
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.local_ip
? 'border-error border-2'
class="input input-bordered w-full {formErrors.local_ip ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
@@ -346,8 +354,10 @@
required
/>
<label class="label" for="localIP">
<span class="label-text-alt text-error {formErrors.local_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span
<span
class="label-text-alt text-error {formErrors.local_ip ? '' : (
'hidden'
)}">Must be a valid IPv4 address</span
>
</label>
</div>
@@ -358,8 +368,8 @@
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.gateway_ip
? 'border-error border-2'
class="input input-bordered w-full {formErrors.gateway_ip ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
@@ -369,8 +379,10 @@
required
/>
<label class="label" for="gateway">
<span class="label-text-alt text-error {formErrors.gateway_ip ? '' : 'hidden'}"
>Must be a valid IPv4 address</span
<span
class="label-text-alt text-error {formErrors.gateway_ip ? '' : (
'hidden'
)}">Must be a valid IPv4 address</span
>
</label>
</div>
@@ -380,8 +392,8 @@
</label>
<input
type="text"
class="input input-bordered w-full {formErrors.subnet_mask
? 'border-error border-2'
class="input input-bordered w-full {formErrors.subnet_mask ?
'border-error border-2'
: ''}"
minlength="7"
maxlength="15"
@@ -391,8 +403,10 @@
required
/>
<label class="label" for="subnet">
<span class="label-text-alt text-error {formErrors.subnet_mask ? '' : 'hidden'}"
>Must be a valid IPv4 address</span
<span
class="label-text-alt text-error {formErrors.subnet_mask ? '' : (
'hidden'
)}">Must be a valid IPv4 address</span
>
</label>
</div>
@@ -413,5 +427,4 @@
</div>
{/await}
</div>
{/if}
</SettingsCard>
+26 -45
View File
@@ -5,8 +5,6 @@
import { openModal, closeModal } from 'svelte-modals';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
import { user } from '$lib/stores/user';
import { page } from '$app/stores';
import { notifications } from '$lib/components/toasts/notifications';
import DragDropList, { VerticalDropZone, reorder, type DropEvent } from 'svelte-dnd-list';
import SettingsCard from '$lib/components/SettingsCard.svelte';
@@ -441,7 +439,6 @@
{/await}
</div>
{#if !$features.security || $user.admin}
<div class="bg-base-200 relative grid w-full max-w-2xl self-center overflow-hidden">
<div
class="min-h-16 flex w-full items-center justify-between space-x-3 p-0 text-xl font-medium"
@@ -488,16 +485,13 @@
let:index
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2"
>
<div class="rounded-box bg-base-100 flex items-center space-x-3 px-4 py-2">
<div class="mask mask-hexagon bg-primary h-auto w-10 shrink-0">
<Router class="text-primary-content h-auto w-full scale-75" />
</div>
<div>
<div class="font-bold">{dndNetworkList[index].ssid}</div>
</div>
{#if !$features.security || $user.admin}
<div class="flex-grow" />
<div class="space-x-0 px-0 mx-0">
<button
@@ -517,7 +511,6 @@
<Delete class="text-error h-6 w-6" />
</button>
</div>
{/if}
</div>
</DragDropList>
</div>
@@ -534,9 +527,7 @@
novalidate
bind:this={formField}
>
<div
class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2"
>
<div class="grid w-full grid-cols-1 content-center gap-x-4 px-4 sm:grid-cols-2">
<div>
<label class="label" for="channel">
<span class="label-text text-md">Host Name</span>
@@ -599,9 +590,9 @@
/>
<label class="label" for="ssid">
<span
class="label-text-alt text-error {formErrors.ssid ? ''
: 'hidden'}"
>SSID must be between 3 and 32 characters long</span
class="label-text-alt text-error {formErrors.ssid ? '' : (
'hidden'
)}">SSID must be between 3 and 32 characters long</span
>
</label>
</div>
@@ -633,9 +624,7 @@
</label>
<input
type="text"
class="input input-bordered w-full {(
formErrors.local_ip
) ?
class="input input-bordered w-full {formErrors.local_ip ?
'border-error border-2'
: ''}"
minlength="7"
@@ -647,9 +636,7 @@
/>
<label class="label" for="localIP">
<span
class="label-text-alt text-error {(
formErrors.local_ip
) ?
class="label-text-alt text-error {formErrors.local_ip ?
''
: 'hidden'}">Must be a valid IPv4 address</span
>
@@ -662,9 +649,7 @@
</label>
<input
type="text"
class="input input-bordered w-full {(
formErrors.gateway_ip
) ?
class="input input-bordered w-full {formErrors.gateway_ip ?
'border-error border-2'
: ''}"
minlength="7"
@@ -689,9 +674,7 @@
</label>
<input
type="text"
class="input input-bordered w-full {(
formErrors.subnet_mask
) ?
class="input input-bordered w-full {formErrors.subnet_mask ?
'border-error border-2'
: ''}"
minlength="7"
@@ -706,8 +689,10 @@
formErrors.subnet_mask
) ?
''
: 'hidden'}">Must be a valid IPv4 address</span
: 'hidden'}"
>
Must be a valid IPv4 address
</span>
</label>
</div>
<div>
@@ -727,10 +712,11 @@
/>
<label class="label" for="gateway">
<span
class="label-text-alt text-error {formErrors.dns_1 ?
''
: 'hidden'}">Must be a valid IPv4 address</span
class="label-text-alt text-error {formErrors.dns_1 ? ''
: 'hidden'}"
>
Must be a valid IPv4 address
</span>
</label>
</div>
<div>
@@ -750,10 +736,11 @@
/>
<label class="label" for="subnet">
<span
class="label-text-alt text-error {formErrors.dns_2 ?
''
: 'hidden'}">Must be a valid IPv4 address</span
class="label-text-alt text-error {formErrors.dns_2 ? ''
: 'hidden'}"
>
Must be a valid IPv4 address
</span>
</label>
</div>
</div>
@@ -762,21 +749,15 @@
<div class="divider mb-2 mt-0" />
<div class="mx-4 flex flex-wrap justify-end gap-2">
<button
class="btn btn-primary"
type="submit"
disabled={!showNetworkEditor}
>{newNetwork ? 'Add Network' : 'Update Network'}</button
>
<button
class="btn btn-primary"
type="button"
on:click={validateHostName}>Apply Settings</button
>
<button class="btn btn-primary" type="submit" disabled={!showNetworkEditor}>
{newNetwork ? 'Add Network' : 'Update Network'}
</button>
<button class="btn btn-primary" type="button" on:click={validateHostName}>
Apply Settings
</button>
</div>
</form>
</div>
{/await}
</div>
{/if}
</SettingsCard>
+1 -6
View File
@@ -5,7 +5,7 @@
The back end exposes a number of API endpoints which are referenced in the table below.
| Method | Endpoint | Authentication | POST JSON Body | Info |
| ------ | --------------------------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| ------ | -------------------- | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| GET | /rest/features | `NONE_REQUIRED` | none | Tells the client which features of the UI should be use |
| GET | /rest/ntpStatus | `IS_AUTHENTICATED` | none | Current NTP connection status |
| GET | /rest/ntpSettings | `IS_ADMIN` | none | Current NTP settings |
@@ -22,11 +22,6 @@ The back end exposes a number of API endpoints which are referenced in the table
| POST | /rest/restart | `IS_ADMIN` | none | Restart the ESP32 |
| POST | /rest/factoryReset | `IS_ADMIN` | none | Reset the ESP32 and all settings to their default values |
| POST | /rest/uploadFirmware | `IS_ADMIN` | none | File upload of firmware.bin |
| POST | /rest/signIn | `NONE_REQUIRED` | `{"password": "admin","username": "admin"}` | Signs a user in and returns access token |
| GET | /rest/securitySettings | `IS_ADMIN` | none | retrieves all user information and roles |
| POST | /rest/securitySettings | `IS_ADMIN` | `{"jwt_secret": "734cb5bb-5597b722", "users": [{"username": "admin", "password": "admin", "admin": true}, {"username": "guest", "password": "guest", "admin": false, }]}` | retrieves all user information and roles |
| GET | /rest/verifyAuthorization | `NONE_REQUIRED` | none | Verifies the content of the auth bearer token |
| GET | /rest/generateToken?username={username} | `IS_ADMIN` | `{"token": "734cb5bb-5597b722"}` | Generates a new JWT token for the user from username |
| POST | /rest/sleep | `IS_AUTHENTICATED` | none | Puts the device in deep sleep mode |
| POST | /rest/downloadUpdate | `IS_ADMIN` | `{"download_url": "https://github.com/theelims/ESP32-sveltekit/releases/download/v0.1.0/firmware_esp32s3.bin"}` | Download link for OTA. This requires a valid SSL certificate and will follow redirects. |
+29 -29
View File
@@ -1,23 +1,22 @@
# Software description
The software make use of a range of different libraries to enhance the functionality.
Up to date list can be seen in platformio.ini file.
The libraries includes:
* Esp32SvelteKit
* PsychicHttp
* ArduinoJson
* Adafruit SSD1306
* Adafruit GFX Library
* Adafruit BusIO
* Adafruit PWM Servo Driver Library
* Adafruit ADS1X15
* Adafruit HMC5883 Unified
* Adafruit Unified Sensor
* I2Cdevlib-MPU6050
* NewPing
* SPI
- Esp32SvelteKit
- PsychicHttp
- ArduinoJson
- Adafruit SSD1306
- Adafruit GFX Library
- Adafruit BusIO
- Adafruit PWM Servo Driver Library
- Adafruit ADS1X15
- Adafruit HMC5883 Unified
- Adafruit Unified Sensor
- I2Cdevlib-MPU6050
- NewPing
- SPI
#### Structure
@@ -36,7 +35,6 @@ To dis-/enable the major feature defines are used. Define them in either feature
| --- | --- | ---
| FT_BATTERY | Whether or not to use battery | 0
| FT_NTP | Whether or not to use time server | 1
| FT_SECURITY | Whether or not to use login system | 0
| FT_SLEEP | Whether or not include sleep management | 0
| FT_UPLOAD_FIRMWARE | Whether or not to use OAT | 0
| FT_DOWNLOAD_FIRMWARE | Whether or not to use github for firmware updates | 0
@@ -50,7 +48,9 @@ To dis-/enable the major feature defines are used. Define them in either feature
### 📲 Controller
The controller is a SvelteKit app, which main focus is to calibrate and control the robot.
<!-- Write about the emulation, stream, controls and link to the space issues -->
It is made to be included and hosted by the robot.
Therefore there is placed a lot of thought behind the functionality and dependencies.
@@ -58,22 +58,22 @@ Therefore there is placed a lot of thought behind the functionality and dependen
For the development dependencies I choose the following
| Dependencies | Description
| --- | ---
| SvelteKit | SvelteKit is an application framework built on top of Svelte, enhancing it with features like routing, server-side rendering, and static site generation. It streamlines the development process by integrating server-side capabilities with Svelte's client-side benefits. Furthermore it make the development process fast and enjoyable.
| Vite | Vite is a frontend tool that is used for building fast and optimized web applications. Is serves code local during development and bundles assets for production
| Typescript | TypeScript's integration of static typing enhances code reliability and maintainability.
| Tailwind CSS | Tailwind CSS accelerates web development with its utility-first approach, ensuring rapid styling and consistent design.
| Dependencies | Description |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| SvelteKit | SvelteKit is an application framework built on top of Svelte, enhancing it with features like routing, server-side rendering, and static site generation. It streamlines the development process by integrating server-side capabilities with Svelte's client-side benefits. Furthermore it make the development process fast and enjoyable. |
| Vite | Vite is a frontend tool that is used for building fast and optimized web applications. Is serves code local during development and bundles assets for production |
| Typescript | TypeScript's integration of static typing enhances code reliability and maintainability. |
| Tailwind CSS | Tailwind CSS accelerates web development with its utility-first approach, ensuring rapid styling and consistent design. |
#### Libraries
For the app functionality I choose the following:
| Dependencies | Description
| --- | ---
| [Three](https://www.npmjs.com/package/three) | Easy to use, lightweight, cross-browser, general purpose 3D library.
| [Urdf-loader](https://www.npmjs.com/package/urdf-loader) | Utilities for loading URDF files into THREE.js and a Web Component that loads and renders the model.
| [Xacro-parser](https://www.npmjs.com/package/xacro-parser) | Javascript parser and loader for processing the ROS Xacro file format.
| [NippleJS](https://www.npmjs.com/package/nipplejs) | A vanilla virtual joystick for touch capable interfaces.
| [Uzip](https://www.npmjs.com/package/uzip) | Simple, tiny and fast ZIP library.
| [ChartJS](https://www.npmjs.com/package/chart.js) | Simple and flexible charting library.
| Dependencies | Description |
| ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| [Three](https://www.npmjs.com/package/three) | Easy to use, lightweight, cross-browser, general purpose 3D library. |
| [Urdf-loader](https://www.npmjs.com/package/urdf-loader) | Utilities for loading URDF files into THREE.js and a Web Component that loads and renders the model. |
| [Xacro-parser](https://www.npmjs.com/package/xacro-parser) | Javascript parser and loader for processing the ROS Xacro file format. |
| [NippleJS](https://www.npmjs.com/package/nipplejs) | A vanilla virtual joystick for touch capable interfaces. |
| [Uzip](https://www.npmjs.com/package/uzip) | Simple, tiny and fast ZIP library. |
| [ChartJS](https://www.npmjs.com/package/chart.js) | Simple and flexible charting library. |