Features, Polish, Improvements
- Added logout, add/remove devices - Page structure w/ dark theme - Template files now use target extension - Accessibility improvements - Semantic improvements
This commit is contained in:
parent
63906235a4
commit
2485c7ab7a
|
@ -8,16 +8,16 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
Alias string
|
Alias string `form:"Alias"`
|
||||||
MAC string
|
MAC string `form:"MAC"`
|
||||||
IP string
|
IP string `form:"IP"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
Server string
|
Server string
|
||||||
PassHash string
|
PassHash string
|
||||||
SessionTTL float64
|
SessionTTL float64
|
||||||
Device []Device
|
Devices []Device
|
||||||
}
|
}
|
||||||
|
|
||||||
var Config config
|
var Config config
|
||||||
|
|
|
@ -2,12 +2,12 @@ Server = ":8080" # The address the webserver should bind to
|
||||||
PassHash = "$2a$10$I.26oCzkjZ8qwfhbmeYM3.kppBjxtPsxkeE1Y.ULjVvA1IBPcQP42" # "password"
|
PassHash = "$2a$10$I.26oCzkjZ8qwfhbmeYM3.kppBjxtPsxkeE1Y.ULjVvA1IBPcQP42" # "password"
|
||||||
SessionTTL = 60 # How many minutes sessions last for
|
SessionTTL = 60 # How many minutes sessions last for
|
||||||
|
|
||||||
[[Device]]
|
[[Devices]]
|
||||||
Alias = "SomeDevice"
|
Alias = "SomeDevice"
|
||||||
MAC = "DE-AD-BE-EF-F0-05" # Delimiter dashes/colons, upper/lowercase
|
MAC = "DE-AD-BE-EF-F0-05" # Delimiter dashes/colons, upper/lowercase
|
||||||
IP = "192.168.178.255" # Broadcast for most home networks
|
IP = "192.168.178.255" # Broadcast for most home networks
|
||||||
|
|
||||||
[[Device]]
|
[[Devices]]
|
||||||
Alias = "Another Device"
|
Alias = "Another Device"
|
||||||
MAC = ""
|
MAC = ""
|
||||||
IP = ""
|
IP = ""
|
34
web/auth.go
34
web/auth.go
|
@ -5,7 +5,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.ulra.eu/adro/miniwol/config"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
var sessions map[string]time.Time
|
var sessions map[string]time.Time
|
||||||
|
@ -39,3 +43,33 @@ func withAuth(handler echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return handler(c)
|
return handler(c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
func auth(c echo.Context) error {
|
||||||
|
password := c.FormValue("Password")
|
||||||
|
if bcrypt.CompareHashAndPassword([]byte(config.Config.PassHash), []byte(password)) != nil {
|
||||||
|
return c.String(401, "Wrong Password")
|
||||||
|
}
|
||||||
|
token := uuid.New().String()
|
||||||
|
sessions[token] = time.Now().Add(time.Second * time.Duration(config.Config.SessionTTL*60))
|
||||||
|
c.SetCookie(&http.Cookie{
|
||||||
|
Name: "session",
|
||||||
|
Value: token,
|
||||||
|
Path: "/",
|
||||||
|
Secure: true,
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
|
Expires: sessions[token],
|
||||||
|
})
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func deauth(c echo.Context) error {
|
||||||
|
session, err := c.Cookie("session")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(sessions, session.Value)
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/")
|
||||||
|
}
|
||||||
|
|
68
web/device.go
Normal file
68
web/device.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.ulra.eu/adro/miniwol/config"
|
||||||
|
"git.ulra.eu/adro/miniwol/lib"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func add(c echo.Context) error {
|
||||||
|
device := config.Device{}
|
||||||
|
err := c.Bind(&device)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
config.Config.Devices = append(config.Config.Devices, device)
|
||||||
|
config.Save()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func wake(c echo.Context) error {
|
||||||
|
_device := config.Device{}
|
||||||
|
err := c.Bind(&_device)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, device := range config.Config.Devices {
|
||||||
|
if device == _device {
|
||||||
|
if !strings.Contains(device.IP, ":") {
|
||||||
|
device.IP += ":9"
|
||||||
|
}
|
||||||
|
err := lib.SendPacket(":0", device.IP, device.MAC)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("device not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(c echo.Context) error {
|
||||||
|
_device := config.Device{}
|
||||||
|
err := c.Bind(&_device)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, device := range config.Config.Devices {
|
||||||
|
if device == _device {
|
||||||
|
config.Config.Devices = append(config.Config.Devices[:i], config.Config.Devices[i+1:]...)
|
||||||
|
err := config.Save()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.Redirect(http.StatusSeeOther, "/")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("device not found")
|
||||||
|
}
|
51
web/template/device.html
Normal file
51
web/template/device.html
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<form action="/add" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Add Device</legend>
|
||||||
|
|
||||||
|
<label for="alias">Alias</label><input id="alias" name="Alias">
|
||||||
|
<label for="mac">MAC</label><input id="mac" name="MAC">
|
||||||
|
<label for="ip">IP</label><input id="ip" name="IP">
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<fieldset>
|
||||||
|
<legend>Devices</legend>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Alias</th>
|
||||||
|
<th>MAC</th>
|
||||||
|
<th>IP/Broadcast</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
{{range $i, $d := .Devices}}
|
||||||
|
<tr>
|
||||||
|
<td>{{$d.Alias}}</td>
|
||||||
|
<td>{{$d.MAC}}</td>
|
||||||
|
<td>{{$d.IP}}</td>
|
||||||
|
<td class="actions">
|
||||||
|
<form action="/wake" method="post">
|
||||||
|
<input type="text" name="Alias" value="{{$d.Alias}}" hidden>
|
||||||
|
<input type="text" name="MAC" value="{{$d.MAC}}" hidden>
|
||||||
|
<input type="text" name="IP" value="{{$d.IP}}" hidden>
|
||||||
|
<input type="submit" value="Wake">
|
||||||
|
</form>
|
||||||
|
<form action="/remove" method="post">
|
||||||
|
<input type="text" name="Alias" value="{{$d.Alias}}" hidden>
|
||||||
|
<input type="text" name="MAC" value="{{$d.MAC}}" hidden>
|
||||||
|
<input type="text" name="IP" value="{{$d.IP}}" hidden>
|
||||||
|
<input type="submit" value="Remove">
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</table>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body > div {
|
||||||
|
flex-flow: row wrap;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,22 +0,0 @@
|
||||||
<table>
|
|
||||||
<tr>
|
|
||||||
<th>Device Alias</th>
|
|
||||||
<th>MAC Address</th>
|
|
||||||
<th>IP/Broadcast</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
{{range $i, $d := .Device}}
|
|
||||||
<tr>
|
|
||||||
<td>{{$d.Alias}}</td>
|
|
||||||
<td>{{$d.MAC}}</td>
|
|
||||||
<td>{{$d.IP}}</td>
|
|
||||||
<td>
|
|
||||||
<form action="/wake" method="post">
|
|
||||||
<input type="text" name="index" value="{{$i}}" hidden>
|
|
||||||
<input type="text" name="alias" value="{{$d.Alias}}" hidden>
|
|
||||||
<input type="submit" value="Wake">
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{{end}}
|
|
||||||
</table>
|
|
8
web/template/login.html
Normal file
8
web/template/login.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<form action="/auth" method="post">
|
||||||
|
<fieldset>
|
||||||
|
<legend>Login</legend>
|
||||||
|
|
||||||
|
<label for="pw">Password</label><input type="password" id="pw" name="Password">
|
||||||
|
<input type="submit" value="Submit">
|
||||||
|
</fieldset>
|
||||||
|
</form>
|
|
@ -1,3 +0,0 @@
|
||||||
<form action="/auth" method="post">
|
|
||||||
<label for="pw">Password: </label><input type="password" name="password">
|
|
||||||
</form>
|
|
24
web/template/page.html
Normal file
24
web/template/page.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta name="description" content="Small web server to send Wake-on-LAN requests to its local network">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
<title>{{ .Title }} - miniwol</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<h1>miniwol</h1>
|
||||||
|
<div style="flex-grow: 1;"></div>
|
||||||
|
{{ if .Auth }}
|
||||||
|
<form action="/deauth" method="post">
|
||||||
|
<input type="submit" value="Logout">
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
||||||
|
</nav>
|
||||||
|
<div>{{ .Content }}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,13 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<link rel="icon" href="data:,">
|
|
||||||
<title>{{ .Title }}</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{ .Content }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
96
web/template/style.css
Normal file
96
web/template/style.css
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
color: inherit;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav, div, fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column nowrap;
|
||||||
|
padding: 0.5rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldset > label:after {
|
||||||
|
content: ":";
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
max-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit] {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
padding: 0.25rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions > form {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
grid-column: 1 / 3;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
legend {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colors */
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--cl-fg: #222;
|
||||||
|
--cl-bg-page: #eee;
|
||||||
|
--cl-bg-block: #ddd;
|
||||||
|
--cl-bg-input: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--cl-fg: #ddd;
|
||||||
|
--cl-bg-page: #333;
|
||||||
|
--cl-bg-block: #222;
|
||||||
|
--cl-bg-input: #444;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: var(--cl-bg-page);
|
||||||
|
color: var(--cl-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav, fieldset, table {
|
||||||
|
background: var(--cl-bg-block);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
border: 1px solid var(--cl-fg);
|
||||||
|
background: var(--cl-bg-input);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=submit]:hover,
|
||||||
|
tr:nth-child(even) {
|
||||||
|
background: var(--cl-bg-page);
|
||||||
|
}
|
58
web/web.go
58
web/web.go
|
@ -3,19 +3,11 @@ package web
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.ulra.eu/adro/miniwol/config"
|
"git.ulra.eu/adro/miniwol/config"
|
||||||
"git.ulra.eu/adro/miniwol/lib"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"golang.org/x/crypto/bcrypt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed template/*
|
//go:embed template/*
|
||||||
|
@ -25,7 +17,7 @@ var templates *template.Template
|
||||||
func init() {
|
func init() {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
templates, err = template.ParseFS(templateFS, "template/*.html.tmpl")
|
templates, err = template.ParseFS(templateFS, "template/*.html")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
@ -35,25 +27,31 @@ func Run() error {
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
|
|
||||||
e.GET("/", index)
|
e.GET("/", index)
|
||||||
|
e.GET("/style.css", style)
|
||||||
e.POST("/auth", auth)
|
e.POST("/auth", auth)
|
||||||
|
e.POST("/deauth", withAuth(deauth))
|
||||||
|
e.POST("/add", withAuth(add))
|
||||||
e.POST("/wake", withAuth(wake))
|
e.POST("/wake", withAuth(wake))
|
||||||
|
e.POST("/remove", withAuth(remove))
|
||||||
|
|
||||||
return e.Start(config.Config.Server)
|
return e.Start(config.Config.Server)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Page(c echo.Context, code int, title string, page string, data interface{}) error {
|
func Page(c echo.Context, code int, title string, page string, auth bool, data interface{}) error {
|
||||||
var contentBuffer bytes.Buffer
|
var contentBuffer bytes.Buffer
|
||||||
err := templates.ExecuteTemplate(&contentBuffer, page, data)
|
err := templates.ExecuteTemplate(&contentBuffer, page, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var pageBuffer bytes.Buffer
|
var pageBuffer bytes.Buffer
|
||||||
err = templates.ExecuteTemplate(&pageBuffer, "page.html.tmpl", struct {
|
err = templates.ExecuteTemplate(&pageBuffer, "page.html", struct {
|
||||||
Title string
|
Title string
|
||||||
Content template.HTML
|
Content template.HTML
|
||||||
|
Auth bool
|
||||||
}{
|
}{
|
||||||
Title: title,
|
Title: title,
|
||||||
Content: template.HTML(contentBuffer.String()),
|
Content: template.HTML(contentBuffer.String()),
|
||||||
|
Auth: auth,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -65,44 +63,16 @@ func Page(c echo.Context, code int, title string, page string, data interface{})
|
||||||
func index(c echo.Context) error {
|
func index(c echo.Context) error {
|
||||||
session, err := c.Cookie("session")
|
session, err := c.Cookie("session")
|
||||||
if err != nil || checkAuth(session.Value) != nil {
|
if err != nil || checkAuth(session.Value) != nil {
|
||||||
return Page(c, 200, "Login", "login.html.tmpl", nil)
|
return Page(c, 200, "Login", "login.html", false, nil)
|
||||||
} else {
|
} else {
|
||||||
return Page(c, 200, "Device", "device.html.tmpl", config.Config)
|
return Page(c, 200, "Devices", "device.html", true, config.Config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func auth(c echo.Context) error {
|
func style(c echo.Context) error {
|
||||||
password := c.FormValue("password")
|
styleData, err := templateFS.ReadFile("template/style.css")
|
||||||
if bcrypt.CompareHashAndPassword([]byte(config.Config.PassHash), []byte(password)) != nil {
|
|
||||||
return c.String(401, "Wrong Password")
|
|
||||||
}
|
|
||||||
token := uuid.New().String()
|
|
||||||
sessions[token] = time.Now().Add(time.Second * time.Duration(config.Config.SessionTTL*60))
|
|
||||||
c.SetCookie(&http.Cookie{
|
|
||||||
Name: "session",
|
|
||||||
Value: token,
|
|
||||||
Path: "/",
|
|
||||||
Secure: true,
|
|
||||||
HttpOnly: true,
|
|
||||||
SameSite: http.SameSiteStrictMode,
|
|
||||||
Expires: sessions[token],
|
|
||||||
})
|
|
||||||
return c.Redirect(http.StatusSeeOther, "/")
|
|
||||||
}
|
|
||||||
|
|
||||||
func wake(c echo.Context) error {
|
|
||||||
for i, device := range config.Config.Device {
|
|
||||||
if c.FormValue("alias") == device.Alias && c.FormValue("index") == fmt.Sprint(i) {
|
|
||||||
if !strings.Contains(device.IP, ":") {
|
|
||||||
device.IP += ":9"
|
|
||||||
}
|
|
||||||
err := lib.SendPacket(":0", device.IP, device.MAC)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return c.Blob(200, "text/css", styleData)
|
||||||
return c.Redirect(http.StatusSeeOther, "/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.New("device not found")
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user