mirror of
https://github.com/seriousm4x/UpSnap.git
synced 2026-04-05 08:53:55 -04:00
123
backend/migrations/1692114046_updated_devices.go
Normal file
123
backend/migrations/1692114046_updated_devices.go
Normal file
@@ -0,0 +1,123 @@
|
||||
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/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("z5lghx2r3tm45n1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add
|
||||
new_sol_enabled := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "ueieydw5",
|
||||
"name": "sol_enabled",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
}`), new_sol_enabled)
|
||||
collection.Schema.AddField(new_sol_enabled)
|
||||
|
||||
// add
|
||||
new_sol_auth := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "yfszwhpw",
|
||||
"name": "sol_auth",
|
||||
"type": "bool",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {}
|
||||
}`), new_sol_auth)
|
||||
collection.Schema.AddField(new_sol_auth)
|
||||
|
||||
// add
|
||||
new_sol_user := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "dbcmnrmp",
|
||||
"name": "sol_user",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}`), new_sol_user)
|
||||
collection.Schema.AddField(new_sol_user)
|
||||
|
||||
// add
|
||||
new_sol_password := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "hvz8stfy",
|
||||
"name": "sol_password",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": null,
|
||||
"pattern": ""
|
||||
}
|
||||
}`), new_sol_password)
|
||||
collection.Schema.AddField(new_sol_password)
|
||||
|
||||
// add
|
||||
new_sol_port := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "6kfqheid",
|
||||
"name": "sol_port",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
}`), new_sol_port)
|
||||
collection.Schema.AddField(new_sol_port)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("z5lghx2r3tm45n1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("ueieydw5")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("yfszwhpw")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("dbcmnrmp")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("hvz8stfy")
|
||||
|
||||
// remove
|
||||
collection.Schema.RemoveField("6kfqheid")
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
64
backend/migrations/1692114046_updated_ports.go
Normal file
64
backend/migrations/1692114046_updated_ports.go
Normal file
@@ -0,0 +1,64 @@
|
||||
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/schema"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("cti4l8f4mz8df3r")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
edit_number := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "8nwuncgg",
|
||||
"name": "number",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": 1,
|
||||
"max": 65535
|
||||
}
|
||||
}`), edit_number)
|
||||
collection.Schema.AddField(edit_number)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
}, func(db dbx.Builder) error {
|
||||
dao := daos.New(db);
|
||||
|
||||
collection, err := dao.FindCollectionByNameOrId("cti4l8f4mz8df3r")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update
|
||||
edit_number := &schema.SchemaField{}
|
||||
json.Unmarshal([]byte(`{
|
||||
"system": false,
|
||||
"id": "8nwuncgg",
|
||||
"name": "number",
|
||||
"type": "number",
|
||||
"required": true,
|
||||
"unique": false,
|
||||
"options": {
|
||||
"min": null,
|
||||
"max": 65535
|
||||
}
|
||||
}`), edit_number)
|
||||
collection.Schema.AddField(edit_number)
|
||||
|
||||
return dao.SaveCollection(collection)
|
||||
})
|
||||
}
|
||||
52
backend/networking/sleep.go
Normal file
52
backend/networking/sleep.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package networking
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/pocketbase/pocketbase/models"
|
||||
"github.com/seriousm4x/upsnap/logger"
|
||||
)
|
||||
|
||||
type SolResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func SleepDevice(device *models.Record) (SolResponse, error) {
|
||||
logger.Info.Println("sleep triggered for", device.GetString("name"))
|
||||
|
||||
var solResp SolResponse
|
||||
var url string
|
||||
|
||||
if device.GetBool("sol_auth") {
|
||||
url = fmt.Sprintf("http://%s:%s@%s:%d/sleep?format=JSON",
|
||||
device.GetString("sol_user"), device.GetString("sol_password"), device.GetString("ip"), device.GetInt("sol_port"))
|
||||
} else {
|
||||
url = fmt.Sprintf("http://%s:%d/sleep?format=JSON", device.GetString("ip"), device.GetInt("sol_port"))
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return solResp, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return solResp, err
|
||||
}
|
||||
|
||||
var solResp SolResponse
|
||||
if err := json.Unmarshal(body, &solResp); err != nil {
|
||||
return solResp, err
|
||||
}
|
||||
|
||||
return solResp, errors.New("status code was not 200")
|
||||
}
|
||||
|
||||
return solResp, nil
|
||||
}
|
||||
@@ -20,11 +20,11 @@ func HandlerWake(c echo.Context) error {
|
||||
if err != nil {
|
||||
return apis.NewNotFoundError("The device does not exist.", err)
|
||||
}
|
||||
record.Set("status", "pending")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
go func(*models.Record) {
|
||||
record.Set("status", "pending")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
if err := networking.WakeDevice(record); err != nil {
|
||||
logger.Error.Println(err)
|
||||
record.Set("status", "offline")
|
||||
@@ -38,16 +38,41 @@ func HandlerWake(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, record)
|
||||
}
|
||||
|
||||
func HandlerSleep(c echo.Context) error {
|
||||
record, err := App.Dao().FindFirstRecordByData("devices", "id", c.PathParam("id"))
|
||||
if err != nil {
|
||||
return apis.NewNotFoundError("The device does not exist.", err)
|
||||
}
|
||||
record.Set("status", "pending")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
resp, err := networking.SleepDevice(record)
|
||||
if err != nil {
|
||||
logger.Error.Println(err)
|
||||
record.Set("status", "online")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
return apis.NewBadRequestError(resp.Message, nil)
|
||||
}
|
||||
record.Set("status", "offline")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
return c.JSON(http.StatusOK, nil)
|
||||
}
|
||||
|
||||
func HandlerShutdown(c echo.Context) error {
|
||||
record, err := App.Dao().FindFirstRecordByData("devices", "id", c.PathParam("id"))
|
||||
if err != nil {
|
||||
return apis.NewNotFoundError("The device does not exist.", err)
|
||||
}
|
||||
record.Set("status", "pending")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
go func(*models.Record) {
|
||||
record.Set("status", "pending")
|
||||
if err := App.Dao().SaveRecord(record); err != nil {
|
||||
logger.Error.Println("Failed to save record:", err)
|
||||
}
|
||||
if err := networking.ShutdownDevice(record); err != nil {
|
||||
logger.Error.Println(err)
|
||||
record.Set("status", "online")
|
||||
|
||||
@@ -44,6 +44,16 @@ func StartPocketBase(distDirFS fs.FS) {
|
||||
},
|
||||
})
|
||||
|
||||
e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/upsnap/sleep/:id",
|
||||
Handler: HandlerSleep,
|
||||
Middlewares: []echo.MiddlewareFunc{
|
||||
apis.ActivityLogger(App),
|
||||
RequireUpSnapPermission(),
|
||||
},
|
||||
})
|
||||
|
||||
e.Router.AddRoute(echo.Route{
|
||||
Method: http.MethodGet,
|
||||
Path: "/api/upsnap/shutdown/:id",
|
||||
|
||||
1829
frontend/pnpm-lock.yaml
generated
1829
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { formatDistance, parseISO } from 'date-fns';
|
||||
import { pocketbase, isAdmin, backendUrl, permission } from '$lib/stores/pocketbase';
|
||||
import DeviceCardNic from './DeviceCardNic.svelte';
|
||||
import { scale } from 'svelte/transition';
|
||||
import Fa from 'svelte-fa';
|
||||
import { faCircleArrowDown, faCircleArrowUp, faLock } from '@fortawesome/free-solid-svg-icons';
|
||||
import { isAdmin, permission } from '$lib/stores/pocketbase';
|
||||
import {
|
||||
faBed,
|
||||
faCircleArrowDown,
|
||||
faCircleArrowUp,
|
||||
faLock,
|
||||
faPen
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { scale } from 'svelte/transition';
|
||||
import { formatDistance, parseISO } from 'date-fns';
|
||||
import toast from 'svelte-french-toast';
|
||||
import type { Device } from '$lib/types/device';
|
||||
|
||||
export let device: Device;
|
||||
@@ -18,6 +25,16 @@
|
||||
now = Date.now();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function sleep() {
|
||||
fetch(`${backendUrl}api/upsnap/sleep/${device.id}`, {
|
||||
headers: {
|
||||
Authorization: $pocketbase.authStore.token
|
||||
}
|
||||
}).catch((err) => {
|
||||
toast.error(err.message);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="card bg-base-300 shadow-md rounded-3xl" transition:scale={{ delay: 0, duration: 200 }}>
|
||||
@@ -33,36 +50,54 @@
|
||||
<!-- TODO: change to nic array once backend supports it -->
|
||||
<DeviceCardNic {device} />
|
||||
</ul>
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
{#if device.wake_cron_enabled}
|
||||
<div class="tooltip" data-tip="Wake cron">
|
||||
<span class="badge badge-success gap-1 p-3"
|
||||
><Fa icon={faCircleArrowUp} />{device.wake_cron}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if device.shutdown_cron_enabled}
|
||||
<div class="tooltip" data-tip="Shutdown cron">
|
||||
<span class="badge badge-error gap-1 p-3"
|
||||
><Fa icon={faCircleArrowDown} />{device.shutdown_cron}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if device.password}
|
||||
<div class="tooltip" data-tip="Wake password">
|
||||
<span class="badge gap-1 p-3"><Fa icon={faLock} />Password</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="card-actions mt-auto">
|
||||
{#if device.wake_cron_enabled || device.shutdown_cron_enabled || device.password}
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
{#if device.wake_cron_enabled}
|
||||
<div class="tooltip" data-tip="Wake cron">
|
||||
<span class="badge badge-success gap-1 p-3"
|
||||
><Fa icon={faCircleArrowUp} />{device.wake_cron}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if device.shutdown_cron_enabled}
|
||||
<div class="tooltip" data-tip="Shutdown cron">
|
||||
<span class="badge badge-error gap-1 p-3"
|
||||
><Fa icon={faCircleArrowDown} />{device.shutdown_cron}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if device.password}
|
||||
<div class="tooltip" data-tip="Wake password">
|
||||
<span class="badge gap-1 p-3"><Fa icon={faLock} />Password</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="card-actions mt-auto items-center">
|
||||
<span class="tooltip" data-tip="Last status change: {device.updated}">
|
||||
{formatDistance(parseISO(device.updated), now, {
|
||||
includeSeconds: true,
|
||||
addSuffix: true
|
||||
})}
|
||||
</span>
|
||||
{#if $isAdmin || $permission.update?.includes(device.id)}
|
||||
<a class="btn btn-sm btn-neutral ms-auto" href="/device/{device.id}">Edit</a>
|
||||
{#if $isAdmin || $permission.update?.includes(device.id) || $permission.power?.includes(device.id)}
|
||||
<div class="dropdown dropdown-top dropdown-end bg-base-300 ms-auto">
|
||||
<label tabindex="-1" class="btn btn-sm m-1" for="more-{device.id}">More</label>
|
||||
<ul
|
||||
id="more-{device.id}"
|
||||
tabindex="-1"
|
||||
class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-fit"
|
||||
>
|
||||
{#if ($isAdmin || $permission.power?.includes(device.id)) && device.sol_enabled}
|
||||
<li><button on:click={() => sleep()}><Fa icon={faBed} />Sleep</button></li>
|
||||
{/if}
|
||||
{#if $isAdmin || $permission.update?.includes(device.id)}
|
||||
<li>
|
||||
<a href="/device/{device.id}"><Fa icon={faPen} />Edit</a>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -325,6 +325,128 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card w-full bg-base-300 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Sleep-On-LAN</h2>
|
||||
<p class="mt-2">
|
||||
You can put computers to sleep using the <a
|
||||
class="link"
|
||||
href="https://github.com/SR-G/sleep-on-lan"
|
||||
target="_blank">Sleep-On-LAN</a
|
||||
>
|
||||
tool. Sleep-On-LAN (SOL) is an external tool/daemon that operates on the PCs you want to put
|
||||
to sleep, providing a REST endpoint to put the PC to sleep. For instructions on setting up Sleep-On-LAN,
|
||||
please refer to the
|
||||
<a href="https://github.com/SR-G/sleep-on-lan#usage" class="link" target="_blank">Usage</a> section.
|
||||
</p>
|
||||
<p>
|
||||
SOL is confiugred to send requests over HTTP instead of UDP to enable authorization and make
|
||||
requests more reliable.
|
||||
</p>
|
||||
<p class="font-bold">
|
||||
Therefore, please ensure that you include <span class="badge">HTTP:<YOURPORT></span>
|
||||
in the <span class="badge">Listeners</span> section of the
|
||||
<a href="https://github.com/SR-G/sleep-on-lan#configuration" class="link" target="_blank"
|
||||
>SOL configuration</a
|
||||
>.
|
||||
</p>
|
||||
<div class="flex flex-row flex-wrap gap-4 items-end mt-4">
|
||||
<div>
|
||||
<div class="form-control flex flex-row flex-wrap gap-4">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
id="sol-enable"
|
||||
type="checkbox"
|
||||
class="toggle toggle-success"
|
||||
bind:checked={device.sol_enabled}
|
||||
/>
|
||||
<label class="label cursor-pointer" for="sol-enable">
|
||||
<span class="label-text">Enable Sleep-On-LAN</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control flex flex-col">
|
||||
<label class="label" for="sol-port">
|
||||
<span class="label-text"
|
||||
>SOL Port
|
||||
{#if device.sol_enabled}
|
||||
<span class="text-error">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="sol-port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
placeholder="8009"
|
||||
class="input w-80"
|
||||
bind:value={device.sol_port}
|
||||
disabled={!device.sol_enabled}
|
||||
required={device.sol_enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{#if device.sol_enabled}
|
||||
<div>
|
||||
<div class="form-control flex flex-row flex-wrap gap-4">
|
||||
<div class="flex flex-row gap-2 items-center">
|
||||
<input
|
||||
id="sol-auth"
|
||||
type="checkbox"
|
||||
class="toggle toggle-success"
|
||||
bind:checked={device.sol_auth}
|
||||
/>
|
||||
<label class="label cursor-pointer" for="sol-auth">
|
||||
<span class="label-text">Authorization</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control flex flex-col">
|
||||
<label class="label" for="sol-user">
|
||||
<span class="label-text"
|
||||
>SOL User
|
||||
{#if device.sol_auth}
|
||||
<span class="text-error">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="sol-user"
|
||||
type="text"
|
||||
placeholder="username"
|
||||
class="input w-80"
|
||||
bind:value={device.sol_user}
|
||||
disabled={!device.sol_auth}
|
||||
required={device.sol_auth}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control flex flex-col">
|
||||
<label class="label" for="sol-password">
|
||||
<span class="label-text"
|
||||
>SOL Password
|
||||
{#if device.sol_auth}
|
||||
<span class="text-error">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="sol-password"
|
||||
type="password"
|
||||
placeholder="password"
|
||||
class="input w-80"
|
||||
bind:value={device.sol_password}
|
||||
disabled={!device.sol_auth}
|
||||
required={device.sol_auth}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card w-full bg-base-300 shadow-xl mt-6">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Shutdown</h2>
|
||||
|
||||
@@ -20,6 +20,11 @@ export type Device = Record & {
|
||||
groups: Group[];
|
||||
};
|
||||
created_by: string;
|
||||
sol_enabled: boolean;
|
||||
sol_auth: boolean;
|
||||
sol_user: string;
|
||||
sol_passwort: string;
|
||||
sol_port: number;
|
||||
};
|
||||
|
||||
export type Port = Record & {
|
||||
|
||||
Reference in New Issue
Block a user