mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-04-05 08:53:52 -04:00
* from now on, upsnap always requires authentification (user or admin) * users can view the dashboard and use wake/shutdown (read only) * admins can do everything (add, modify and delete) * make navbar collapse on small screens
This commit is contained in:
55
backend/migrations/1677961980_created_users.go
Normal file
55
backend/migrations/1677961980_created_users.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
jsonData := `{
|
||||
"id": "27do0wbcuyfmbmx",
|
||||
"created": "2023-03-04 20:33:00.558Z",
|
||||
"updated": "2023-03-04 20:33:00.558Z",
|
||||
"name": "users",
|
||||
"type": "auth",
|
||||
"system": false,
|
||||
"schema": [],
|
||||
"listRule": null,
|
||||
"viewRule": null,
|
||||
"createRule": null,
|
||||
"updateRule": null,
|
||||
"deleteRule": null,
|
||||
"options": {
|
||||
"allowEmailAuth": true,
|
||||
"allowOAuth2Auth": false,
|
||||
"allowUsernameAuth": true,
|
||||
"exceptEmailDomains": [],
|
||||
"manageRule": null,
|
||||
"minPasswordLength": 8,
|
||||
"onlyEmailDomains": [],
|
||||
"requireEmail": false
|
||||
}
|
||||
}`
|
||||
|
||||
collection := &models.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return daos.New(db).SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("27do0wbcuyfmbmx")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return dao.DeleteCollection(collection)
|
||||
})
|
||||
}
|
||||
50
backend/migrations/1677962170_updated_devices.go
Normal file
50
backend/migrations/1677962170_updated_devices.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("z5lghx2r3tm45n1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = types.Pointer("@request.auth.id != \"\"")
|
||||
|
||||
collection.ViewRule = types.Pointer("@request.auth.id != \"\"")
|
||||
|
||||
collection.CreateRule = nil
|
||||
|
||||
collection.UpdateRule = nil
|
||||
|
||||
collection.DeleteRule = nil
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("z5lghx2r3tm45n1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = nil
|
||||
|
||||
collection.ViewRule = nil
|
||||
|
||||
collection.CreateRule = types.Pointer("")
|
||||
|
||||
collection.UpdateRule = types.Pointer("")
|
||||
|
||||
collection.DeleteRule = types.Pointer("")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
50
backend/migrations/1677962204_updated_ports.go
Normal file
50
backend/migrations/1677962204_updated_ports.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("cti4l8f4mz8df3r")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = types.Pointer("@request.auth.id != \"\"")
|
||||
|
||||
collection.ViewRule = types.Pointer("@request.auth.id != \"\"")
|
||||
|
||||
collection.CreateRule = nil
|
||||
|
||||
collection.UpdateRule = nil
|
||||
|
||||
collection.DeleteRule = nil
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("cti4l8f4mz8df3r")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = nil
|
||||
|
||||
collection.ViewRule = nil
|
||||
|
||||
collection.CreateRule = types.Pointer("")
|
||||
|
||||
collection.UpdateRule = types.Pointer("")
|
||||
|
||||
collection.DeleteRule = types.Pointer("")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
50
backend/migrations/1677962213_updated_settings.go
Normal file
50
backend/migrations/1677962213_updated_settings.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/pocketbase/pocketbase/daos"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
"github.com/pocketbase/pocketbase/tools/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("nmj3ko20gzkg8n3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = types.Pointer("@request.auth.id != \"\"")
|
||||
|
||||
collection.ViewRule = types.Pointer("@request.auth.id != \"\"")
|
||||
|
||||
collection.CreateRule = nil
|
||||
|
||||
collection.UpdateRule = nil
|
||||
|
||||
collection.DeleteRule = nil
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("nmj3ko20gzkg8n3")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
collection.ListRule = nil
|
||||
|
||||
collection.ViewRule = nil
|
||||
|
||||
collection.CreateRule = types.Pointer("")
|
||||
|
||||
collection.UpdateRule = types.Pointer("")
|
||||
|
||||
collection.DeleteRule = types.Pointer("")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
@@ -41,6 +41,7 @@ func StartPocketBase(distDirFS fs.FS) {
|
||||
Handler: HandlerWake,
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(App),
|
||||
apis.RequireAdminOrRecordAuth("users"),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -51,6 +52,7 @@ func StartPocketBase(distDirFS fs.FS) {
|
||||
Handler: HandlerShutdown,
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(App),
|
||||
apis.RequireAdminOrRecordAuth("users"),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,6 +63,7 @@ func StartPocketBase(distDirFS fs.FS) {
|
||||
Handler: HandlerScan,
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(App),
|
||||
apis.RequireAdminAuth(),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { dev } from '$app/environment';
|
||||
import { parseISO, formatDistance } from 'date-fns';
|
||||
import { pocketbase } from '@stores/pocketbase';
|
||||
import {
|
||||
faPowerOff,
|
||||
faEllipsisVertical,
|
||||
@@ -16,11 +17,19 @@
|
||||
export let now;
|
||||
|
||||
function shutdown() {
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/shutdown/${device.id}`);
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/shutdown/${device.id}`, {
|
||||
headers: {
|
||||
Authorization: $pocketbase.authStore.baseToken
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function wake() {
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/wake/${device.id}`);
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/wake/${device.id}`, {
|
||||
headers: {
|
||||
Authorization: $pocketbase.authStore.baseToken
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
78
frontend/src/components/Login.svelte
Normal file
78
frontend/src/components/Login.svelte
Normal file
@@ -0,0 +1,78 @@
|
||||
<script>
|
||||
import { pocketbase, authorizedStore } from '@stores/pocketbase';
|
||||
|
||||
let username;
|
||||
let password;
|
||||
let isAdmin;
|
||||
let error = {
|
||||
hidden: true,
|
||||
msg: ''
|
||||
};
|
||||
|
||||
async function login() {
|
||||
if (isAdmin) {
|
||||
$pocketbase.admins
|
||||
.authWithPassword(username, password)
|
||||
.then(() => {
|
||||
authorizedStore.set(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
authorizedStore.set(false);
|
||||
error.msg = err;
|
||||
error.hidden = false;
|
||||
});
|
||||
} else {
|
||||
$pocketbase
|
||||
.collection('users')
|
||||
.authWithPassword(username, password)
|
||||
.then(() => {
|
||||
authorizedStore.set(true);
|
||||
})
|
||||
.catch((err) => {
|
||||
authorizedStore.set(false);
|
||||
error.msg = err;
|
||||
error.hidden = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container text-dark-emphasis h-100">
|
||||
<div class="row h-100 justify-content-center align-items-center">
|
||||
<div class="col-md-6 col-lg-5">
|
||||
<form class="w-100" on:submit|preventDefault={login}>
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username or email address</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="username"
|
||||
bind:value={username}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="form-check-input"
|
||||
id="isAdminCheck"
|
||||
bind:checked={isAdmin}
|
||||
/>
|
||||
<label class="form-check-label" for="isAdminCheck">Admin Login</label>
|
||||
</div>
|
||||
<div class="callout callout-danger" class:d-none={error.hidden}>{error.msg}</div>
|
||||
<button class="btn btn-secondary w-100" type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,17 +1,44 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
import { pocketbase, settings } from '@stores/pocketbase';
|
||||
import { faSun, faMoon, faCircleHalfStroke, faBrush } from '@fortawesome/free-solid-svg-icons';
|
||||
import { pocketbase, authorizedStore, settings } from '@stores/pocketbase';
|
||||
import {
|
||||
faSun,
|
||||
faMoon,
|
||||
faCircleHalfStroke,
|
||||
faBrush,
|
||||
faRightFromBracket
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import Fa from 'svelte-fa';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let userInfo = {
|
||||
usernameOrEmail: '',
|
||||
role: ''
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
$pocketbase.collection('settings').subscribe('*', function (e) {
|
||||
settings.set(e.record);
|
||||
document.title = $settings.website_title;
|
||||
});
|
||||
|
||||
if ($pocketbase.authStore.baseModel?.collectionName === 'users') {
|
||||
userInfo.role = 'user';
|
||||
} else {
|
||||
userInfo.role = 'admin';
|
||||
}
|
||||
if ($pocketbase.authStore.baseModel?.username) {
|
||||
userInfo.usernameOrEmail = $pocketbase.authStore.baseModel.username;
|
||||
} else {
|
||||
userInfo.usernameOrEmail = $pocketbase.authStore.baseModel.email;
|
||||
}
|
||||
});
|
||||
|
||||
async function logout() {
|
||||
$pocketbase.authStore.clear();
|
||||
authorizedStore.set(false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -22,7 +49,7 @@
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<nav class="navbar navbar-expand">
|
||||
<nav class="navbar navbar-expand-sm">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img
|
||||
@@ -33,23 +60,36 @@
|
||||
class:me-2={$settings.website_title !== ''}
|
||||
/>{$settings.website_title ? $settings.website_title : ''}
|
||||
</a>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<button
|
||||
class="navbar-toggler border-0"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span class="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/' ? true : false}
|
||||
href="/">Home</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/settings/' ? true : false}
|
||||
href="/settings/">Settings</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item dropdown ms-3">
|
||||
{#if userInfo.role !== 'user'}
|
||||
<li class="nav-item">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/' ? true : false}
|
||||
href="/">Home</a
|
||||
>
|
||||
</li>
|
||||
<li class="nav-item me-3">
|
||||
<a
|
||||
class="nav-link"
|
||||
class:active={$page.url.pathname === '/settings/' ? true : false}
|
||||
href="/settings/">Settings</a
|
||||
>
|
||||
</li>
|
||||
{/if}
|
||||
<li class="nav-item dropdown">
|
||||
<div
|
||||
class="nav-link dropdown-toggle"
|
||||
role="button"
|
||||
@@ -59,7 +99,7 @@
|
||||
<Fa icon={faBrush} class="me-2" />
|
||||
Theme
|
||||
</div>
|
||||
<ul class="dropdown-menu border-0 p-1 shadow-sm">
|
||||
<ul class="dropdown-menu border-0 p-1 shadow-sm mb-2">
|
||||
<li>
|
||||
<div
|
||||
class="dropdown-item"
|
||||
@@ -96,6 +136,16 @@
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="ms-auto d-flex">
|
||||
<button
|
||||
class="text-dark-emphasis nav-link active border-0"
|
||||
data-toggle="tooltip"
|
||||
title="Logged in as {userInfo.role} "{userInfo.usernameOrEmail}""
|
||||
on:click={() => logout()}
|
||||
>
|
||||
<Fa icon={faRightFromBracket} class="me-2" />Logout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,32 +1,19 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { theme } from '@stores/theme';
|
||||
import Navbar from '@components/Navbar.svelte';
|
||||
import Login from '@components/Login.svelte';
|
||||
import Transition from '@components/Transition.svelte';
|
||||
import { pocketbase, settings, devices } from '@stores/pocketbase';
|
||||
import { theme } from '@stores/theme';
|
||||
import { pocketbase, authorizedStore } from '@stores/pocketbase';
|
||||
|
||||
let preferesDark;
|
||||
|
||||
onMount(async () => {
|
||||
let settingsRes = {};
|
||||
settingsRes = await $pocketbase.collection('settings').getList(1, 1);
|
||||
settings.set(settingsRes.items[0]);
|
||||
|
||||
let tempDevices = {};
|
||||
const devicesRes = await $pocketbase.collection('devices').getFullList(200, {
|
||||
sort: 'name',
|
||||
expand: 'ports'
|
||||
});
|
||||
devicesRes.forEach((device) => {
|
||||
tempDevices[device.id] = device;
|
||||
});
|
||||
devices.set(tempDevices);
|
||||
});
|
||||
let isAuth = false;
|
||||
|
||||
onMount(() => {
|
||||
// import bootstrap js
|
||||
import('bootstrap/js/dist/dropdown');
|
||||
import('bootstrap/js/dist/collapse');
|
||||
|
||||
// set dark mode
|
||||
preferesDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
@@ -46,6 +33,16 @@
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', t);
|
||||
});
|
||||
|
||||
authorizedStore.subscribe((state) => {
|
||||
isAuth = state;
|
||||
});
|
||||
|
||||
const pbCookie = localStorage.getItem('pocketbase_auth');
|
||||
if (pbCookie) {
|
||||
$pocketbase.authStore.loadFromCookie('pb_auth=' + pbCookie);
|
||||
authorizedStore.set($pocketbase.authStore.isValid);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -67,11 +64,14 @@
|
||||
</script>
|
||||
</svelte:head>
|
||||
|
||||
<Navbar />
|
||||
|
||||
<Transition url={$page.url}>
|
||||
<slot />
|
||||
</Transition>
|
||||
{#if isAuth}
|
||||
<Navbar />
|
||||
<Transition url={$page.url}>
|
||||
<slot />
|
||||
</Transition>
|
||||
{:else}
|
||||
<Login />
|
||||
{/if}
|
||||
|
||||
<style lang="scss" global>
|
||||
@import '../scss/main.scss';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import DeviceCard from '@components/DeviceCard.svelte';
|
||||
import { pocketbase, devices } from '@stores/pocketbase';
|
||||
import { pocketbase, devices, settings } from '@stores/pocketbase';
|
||||
import { sortDevices } from '../sorts';
|
||||
|
||||
onMount(async () => {
|
||||
@@ -32,6 +32,20 @@
|
||||
$devices[device.id].expand.ports[portIdx] = e.record;
|
||||
}
|
||||
});
|
||||
|
||||
let settingsRes = {};
|
||||
settingsRes = await $pocketbase.collection('settings').getList(1, 1);
|
||||
settings.set(settingsRes.items[0]);
|
||||
|
||||
let tempDevices = {};
|
||||
const devicesRes = await $pocketbase.collection('devices').getFullList(200, {
|
||||
sort: 'name',
|
||||
expand: 'ports'
|
||||
});
|
||||
devicesRes.forEach((device) => {
|
||||
tempDevices[device.id] = device;
|
||||
});
|
||||
devices.set(tempDevices);
|
||||
});
|
||||
|
||||
// update device date
|
||||
|
||||
@@ -67,7 +67,18 @@
|
||||
|
||||
async function scanDevices() {
|
||||
buttons.scan.state = 'waiting';
|
||||
await $pocketbase.collection('settings').update($settings.id, $settings);
|
||||
await $pocketbase
|
||||
.collection('settings')
|
||||
.update($settings.id, $settings)
|
||||
.catch((err) => {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
buttons.scan.error = '';
|
||||
buttons.scan.state = 'none';
|
||||
}, 3000);
|
||||
buttons.scan.error = err;
|
||||
buttons.scan.state = 'failed';
|
||||
});
|
||||
|
||||
fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/scan`)
|
||||
.then((res) => res.json())
|
||||
|
||||
@@ -41,6 +41,7 @@ $dropdown-min-width: 0;
|
||||
@import '../../node_modules/bootstrap/scss/buttons';
|
||||
@import '../../node_modules/bootstrap/scss/spinners';
|
||||
@import '../../node_modules/bootstrap/scss/badge';
|
||||
@import '../../node_modules/bootstrap/scss/transitions';
|
||||
|
||||
@import '../../node_modules/bootstrap/scss/utilities/api';
|
||||
|
||||
@@ -59,6 +60,12 @@ section {
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-toggler {
|
||||
&:focus {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -18,3 +18,4 @@ export const settings = writable({
|
||||
website_title: ''
|
||||
});
|
||||
export const devices = writable({});
|
||||
export const authorizedStore = writable(false);
|
||||
|
||||
Reference in New Issue
Block a user