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 {
|
||||
Alias string
|
||||
MAC string
|
||||
IP string
|
||||
Alias string `form:"Alias"`
|
||||
MAC string `form:"MAC"`
|
||||
IP string `form:"IP"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Server string
|
||||
PassHash string
|
||||
SessionTTL float64
|
||||
Device []Device
|
||||
Devices []Device
|
||||
}
|
||||
|
||||
var Config config
|
||||
|
|
|
@ -2,12 +2,12 @@ Server = ":8080" # The address the webserver should bind to
|
|||
PassHash = "$2a$10$I.26oCzkjZ8qwfhbmeYM3.kppBjxtPsxkeE1Y.ULjVvA1IBPcQP42" # "password"
|
||||
SessionTTL = 60 # How many minutes sessions last for
|
||||
|
||||
[[Device]]
|
||||
[[Devices]]
|
||||
Alias = "SomeDevice"
|
||||
MAC = "DE-AD-BE-EF-F0-05" # Delimiter dashes/colons, upper/lowercase
|
||||
IP = "192.168.178.255" # Broadcast for most home networks
|
||||
|
||||
[[Device]]
|
||||
[[Devices]]
|
||||
Alias = "Another Device"
|
||||
MAC = ""
|
||||
IP = ""
|
34
web/auth.go
34
web/auth.go
|
@ -5,7 +5,11 @@ import (
|
|||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.ulra.eu/adro/miniwol/config"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var sessions map[string]time.Time
|
||||
|
@ -39,3 +43,33 @@ func withAuth(handler echo.HandlerFunc) echo.HandlerFunc {
|
|||
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);
|
||||
}
|
62
web/web.go
62
web/web.go
|
@ -3,19 +3,11 @@ package web
|
|||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.ulra.eu/adro/miniwol/config"
|
||||
"git.ulra.eu/adro/miniwol/lib"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/labstack/echo/v4"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
//go:embed template/*
|
||||
|
@ -25,7 +17,7 @@ var templates *template.Template
|
|||
func init() {
|
||||
var err error
|
||||
|
||||
templates, err = template.ParseFS(templateFS, "template/*.html.tmpl")
|
||||
templates, err = template.ParseFS(templateFS, "template/*.html")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -35,25 +27,31 @@ func Run() error {
|
|||
e := echo.New()
|
||||
|
||||
e.GET("/", index)
|
||||
e.GET("/style.css", style)
|
||||
e.POST("/auth", auth)
|
||||
e.POST("/deauth", withAuth(deauth))
|
||||
e.POST("/add", withAuth(add))
|
||||
e.POST("/wake", withAuth(wake))
|
||||
e.POST("/remove", withAuth(remove))
|
||||
|
||||
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
|
||||
err := templates.ExecuteTemplate(&contentBuffer, page, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pageBuffer bytes.Buffer
|
||||
err = templates.ExecuteTemplate(&pageBuffer, "page.html.tmpl", struct {
|
||||
err = templates.ExecuteTemplate(&pageBuffer, "page.html", struct {
|
||||
Title string
|
||||
Content template.HTML
|
||||
Auth bool
|
||||
}{
|
||||
Title: title,
|
||||
Content: template.HTML(contentBuffer.String()),
|
||||
Auth: auth,
|
||||
})
|
||||
if err != nil {
|
||||
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 {
|
||||
session, err := c.Cookie("session")
|
||||
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 {
|
||||
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 {
|
||||
password := c.FormValue("password")
|
||||
if bcrypt.CompareHashAndPassword([]byte(config.Config.PassHash), []byte(password)) != nil {
|
||||
return c.String(401, "Wrong Password")
|
||||
func style(c echo.Context) error {
|
||||
styleData, err := templateFS.ReadFile("template/style.css")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/")
|
||||
}
|
||||
}
|
||||
return errors.New("device not found")
|
||||
return c.Blob(200, "text/css", styleData)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user