add authentification #42 #55

* 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:
Maxi Quoß
2023-03-05 02:44:47 +01:00
parent 40f6bddff6
commit 8d479669f4
13 changed files with 426 additions and 48 deletions

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

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

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

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

View File

@@ -41,6 +41,7 @@ func StartPocketBase(distDirFS fs.FS) {
Handler: HandlerWake, Handler: HandlerWake,
Middlewares: []echo.MiddlewareFunc{ Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(App), apis.ActivityLogger(App),
apis.RequireAdminOrRecordAuth("users"),
}, },
}) })
@@ -51,6 +52,7 @@ func StartPocketBase(distDirFS fs.FS) {
Handler: HandlerShutdown, Handler: HandlerShutdown,
Middlewares: []echo.MiddlewareFunc{ Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(App), apis.ActivityLogger(App),
apis.RequireAdminOrRecordAuth("users"),
}, },
}) })
@@ -61,6 +63,7 @@ func StartPocketBase(distDirFS fs.FS) {
Handler: HandlerScan, Handler: HandlerScan,
Middlewares: []echo.MiddlewareFunc{ Middlewares: []echo.MiddlewareFunc{
apis.ActivityLogger(App), apis.ActivityLogger(App),
apis.RequireAdminAuth(),
}, },
}) })

View File

@@ -1,6 +1,7 @@
<script> <script>
import { dev } from '$app/environment'; import { dev } from '$app/environment';
import { parseISO, formatDistance } from 'date-fns'; import { parseISO, formatDistance } from 'date-fns';
import { pocketbase } from '@stores/pocketbase';
import { import {
faPowerOff, faPowerOff,
faEllipsisVertical, faEllipsisVertical,
@@ -16,11 +17,19 @@
export let now; export let now;
function shutdown() { 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() { 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> </script>

View 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>

View File

@@ -1,17 +1,44 @@
<script> <script>
import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { theme } from '@stores/theme'; import { theme } from '@stores/theme';
import { pocketbase, settings } from '@stores/pocketbase'; import { pocketbase, authorizedStore, settings } from '@stores/pocketbase';
import { faSun, faMoon, faCircleHalfStroke, faBrush } from '@fortawesome/free-solid-svg-icons'; import {
faSun,
faMoon,
faCircleHalfStroke,
faBrush,
faRightFromBracket
} from '@fortawesome/free-solid-svg-icons';
import Fa from 'svelte-fa'; import Fa from 'svelte-fa';
import { onMount } from 'svelte';
let userInfo = {
usernameOrEmail: '',
role: ''
};
onMount(async () => { onMount(async () => {
$pocketbase.collection('settings').subscribe('*', function (e) { $pocketbase.collection('settings').subscribe('*', function (e) {
settings.set(e.record); settings.set(e.record);
document.title = $settings.website_title; 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> </script>
<svelte:head> <svelte:head>
@@ -22,7 +49,7 @@
{/if} {/if}
</svelte:head> </svelte:head>
<nav class="navbar navbar-expand"> <nav class="navbar navbar-expand-sm">
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
<img <img
@@ -33,23 +60,36 @@
class:me-2={$settings.website_title !== ''} class:me-2={$settings.website_title !== ''}
/>{$settings.website_title ? $settings.website_title : ''} />{$settings.website_title ? $settings.website_title : ''}
</a> </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"> <ul class="navbar-nav">
<li class="nav-item"> {#if userInfo.role !== 'user'}
<a <li class="nav-item">
class="nav-link" <a
class:active={$page.url.pathname === '/' ? true : false} class="nav-link"
href="/">Home</a class:active={$page.url.pathname === '/' ? true : false}
> href="/">Home</a
</li> >
<li class="nav-item"> </li>
<a <li class="nav-item me-3">
class="nav-link" <a
class:active={$page.url.pathname === '/settings/' ? true : false} class="nav-link"
href="/settings/">Settings</a class:active={$page.url.pathname === '/settings/' ? true : false}
> href="/settings/">Settings</a
</li> >
<li class="nav-item dropdown ms-3"> </li>
{/if}
<li class="nav-item dropdown">
<div <div
class="nav-link dropdown-toggle" class="nav-link dropdown-toggle"
role="button" role="button"
@@ -59,7 +99,7 @@
<Fa icon={faBrush} class="me-2" /> <Fa icon={faBrush} class="me-2" />
Theme Theme
</div> </div>
<ul class="dropdown-menu border-0 p-1 shadow-sm"> <ul class="dropdown-menu border-0 p-1 shadow-sm mb-2">
<li> <li>
<div <div
class="dropdown-item" class="dropdown-item"
@@ -96,6 +136,16 @@
</ul> </ul>
</li> </li>
</ul> </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} &quot;{userInfo.usernameOrEmail}&quot;"
on:click={() => logout()}
>
<Fa icon={faRightFromBracket} class="me-2" />Logout
</button>
</div>
</div> </div>
</div> </div>
</nav> </nav>

View File

@@ -1,32 +1,19 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { theme } from '@stores/theme';
import Navbar from '@components/Navbar.svelte'; import Navbar from '@components/Navbar.svelte';
import Login from '@components/Login.svelte';
import Transition from '@components/Transition.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; let preferesDark;
let isAuth = false;
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);
});
onMount(() => { onMount(() => {
// import bootstrap js // import bootstrap js
import('bootstrap/js/dist/dropdown'); import('bootstrap/js/dist/dropdown');
import('bootstrap/js/dist/collapse');
// set dark mode // set dark mode
preferesDark = window.matchMedia('(prefers-color-scheme: dark)'); preferesDark = window.matchMedia('(prefers-color-scheme: dark)');
@@ -46,6 +33,16 @@
} }
document.documentElement.setAttribute('data-bs-theme', t); 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> </script>
@@ -67,11 +64,14 @@
</script> </script>
</svelte:head> </svelte:head>
<Navbar /> {#if isAuth}
<Navbar />
<Transition url={$page.url}> <Transition url={$page.url}>
<slot /> <slot />
</Transition> </Transition>
{:else}
<Login />
{/if}
<style lang="scss" global> <style lang="scss" global>
@import '../scss/main.scss'; @import '../scss/main.scss';

View File

@@ -1,7 +1,7 @@
<script> <script>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import DeviceCard from '@components/DeviceCard.svelte'; import DeviceCard from '@components/DeviceCard.svelte';
import { pocketbase, devices } from '@stores/pocketbase'; import { pocketbase, devices, settings } from '@stores/pocketbase';
import { sortDevices } from '../sorts'; import { sortDevices } from '../sorts';
onMount(async () => { onMount(async () => {
@@ -32,6 +32,20 @@
$devices[device.id].expand.ports[portIdx] = e.record; $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 // update device date

View File

@@ -67,7 +67,18 @@
async function scanDevices() { async function scanDevices() {
buttons.scan.state = 'waiting'; 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`) fetch(`${dev ? 'http://localhost:8090' : ''}/api/upsnap/scan`)
.then((res) => res.json()) .then((res) => res.json())

View File

@@ -41,6 +41,7 @@ $dropdown-min-width: 0;
@import '../../node_modules/bootstrap/scss/buttons'; @import '../../node_modules/bootstrap/scss/buttons';
@import '../../node_modules/bootstrap/scss/spinners'; @import '../../node_modules/bootstrap/scss/spinners';
@import '../../node_modules/bootstrap/scss/badge'; @import '../../node_modules/bootstrap/scss/badge';
@import '../../node_modules/bootstrap/scss/transitions';
@import '../../node_modules/bootstrap/scss/utilities/api'; @import '../../node_modules/bootstrap/scss/utilities/api';
@@ -59,6 +60,12 @@ section {
} }
} }
.navbar-toggler {
&:focus {
box-shadow: none !important;
}
}
.btn { .btn {
border-radius: 0.5rem; border-radius: 0.5rem;
} }

View File

@@ -18,3 +18,4 @@ export const settings = writable({
website_title: '' website_title: ''
}); });
export const devices = writable({}); export const devices = writable({});
export const authorizedStore = writable(false);