Compare commits
18 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4aad657c9d | ||
![]() |
000c12845c | ||
![]() |
ae7db8290c | ||
![]() |
88c31a5133 | ||
![]() |
18a6bef20a | ||
![]() |
7b99b75def | ||
![]() |
48e433ff1e | ||
![]() |
f3d43bbd65 | ||
![]() |
186275d1dd | ||
![]() |
8068f82f13 | ||
![]() |
c4f2006e8f | ||
![]() |
7b50441da2 | ||
![]() |
940f6ea1b5 | ||
![]() |
a48d1238a2 | ||
![]() |
7aaa0c0aab | ||
![]() |
00bf392931 | ||
![]() |
7d2ad1cd49 | ||
![]() |
307b467ac1 |
@ -3,7 +3,7 @@ run-name: ${{ gitea.actor }} build
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build-go:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@ -21,3 +21,21 @@ jobs:
|
||||
run: cd admin && go get .
|
||||
- name: "[admin] Build"
|
||||
run: cd admin && go build -v ./...
|
||||
build-nuxt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup Bun 1.2.0
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
bun-version: 1.2.0
|
||||
- name: Display Bun version
|
||||
run: bun --version
|
||||
- name: test 1
|
||||
run: ls
|
||||
- name: test 2
|
||||
run: cd web && ls
|
||||
- name: "[web] Install dependencies"
|
||||
run: cd web && bun install
|
||||
- name: "[web] Build"
|
||||
run: cd web && bun run build
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -85,8 +85,6 @@ __pycache__/
|
||||
|
||||
## Node
|
||||
node_modules
|
||||
package-lock.json
|
||||
package.json
|
||||
/src/doc/rustc-dev-guide/mermaid.min.js
|
||||
|
||||
## Rustdoc GUI tests
|
||||
|
@ -59,7 +59,7 @@ func makeGetReq(url string) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", token)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -84,7 +84,7 @@ func makePostReq(url string, body any) ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", token)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import gc
|
||||
import utime
|
||||
import ujson
|
||||
import network
|
||||
@ -17,6 +18,7 @@ class App:
|
||||
|
||||
opened: bool = False
|
||||
previously_opened: bool = False
|
||||
wlan: network.WLAN
|
||||
|
||||
led = Pin(15, Pin.OUT)
|
||||
trigger = Pin(2, Pin.OUT)
|
||||
@ -55,18 +57,18 @@ class App:
|
||||
|
||||
def connect(self):
|
||||
print("Connecting to Wi-Fi...")
|
||||
wlan = network.WLAN(network.STA_IF)
|
||||
self.wlan = network.WLAN(network.STA_IF)
|
||||
|
||||
wlan.active(False)
|
||||
self.wlan.active(False)
|
||||
utime.sleep_ms(250)
|
||||
wlan.active(True)
|
||||
self.wlan.active(True)
|
||||
utime.sleep_ms(250)
|
||||
|
||||
wlan.connect(self.ssid, self.password)
|
||||
self.wlan.connect(self.ssid, self.password)
|
||||
utime.sleep_ms(100)
|
||||
|
||||
retry_count = 0
|
||||
while not wlan.isconnected():
|
||||
while not self.wlan.isconnected():
|
||||
if retry_count >= MAX_CONNECTION_RETRIES:
|
||||
print("Max connection retries reached")
|
||||
exit(1)
|
||||
@ -80,22 +82,14 @@ class App:
|
||||
|
||||
if retry_count % 10 == 0:
|
||||
print("Attempting to restart connection...")
|
||||
wlan.connect(self.ssid, self.password)
|
||||
self.wlan.connect(self.ssid, self.password)
|
||||
for _ in range(10):
|
||||
self.led.toggle()
|
||||
utime.sleep_ms(50)
|
||||
|
||||
print(f"Connected with IP {wlan.ifconfig()[0]}")
|
||||
print(f"Connected with IP {self.wlan.ifconfig()[0]}")
|
||||
self.update_server()
|
||||
|
||||
def health_check_server(self):
|
||||
print("Health checking server...", end="\r")
|
||||
try:
|
||||
r = urequests.get(f"{self.server}/")
|
||||
print(f"Server healthy [{r.status_code}]{" " * 8}")
|
||||
except Exception as e:
|
||||
print(f"Error occurred: {e}")
|
||||
|
||||
def update_server(self):
|
||||
print("Updating state...", end="\r")
|
||||
data = {"opened": self.opened}
|
||||
@ -111,24 +105,32 @@ class App:
|
||||
except Exception as e:
|
||||
print(f"Error occurred: {e}")
|
||||
|
||||
def measure_distance(self):
|
||||
def measure_distance(self) -> float:
|
||||
self.trigger.low()
|
||||
utime.sleep_us(2)
|
||||
self.trigger.high()
|
||||
utime.sleep_us(10)
|
||||
self.trigger.low()
|
||||
|
||||
start_time = utime.ticks_us()
|
||||
sent_time = utime.ticks_us()
|
||||
while self.echo.value() == 0:
|
||||
print("hey", end="\r")
|
||||
sent_time = utime.ticks_us()
|
||||
if sent_time - start_time >= 100_000: # if it takes more than 100ms, stop
|
||||
return -1
|
||||
|
||||
start_time = utime.ticks_us()
|
||||
received_time = utime.ticks_us()
|
||||
while self.echo.value() == 1:
|
||||
print("ho", end="\r")
|
||||
received_time = utime.ticks_us()
|
||||
if received_time - start_time >= 100_000: # same
|
||||
return -1
|
||||
|
||||
delta_time = received_time - sent_time
|
||||
distance = delta_time / 1_000_000 * SOUND_SPEED / 2
|
||||
print(f"Distance: {distance} cm")
|
||||
print(f"Distance: {distance:0<5} cm; mem_free = {gc.mem_free()}")
|
||||
|
||||
return distance
|
||||
|
||||
@ -139,6 +141,9 @@ class App:
|
||||
i = 0
|
||||
while True:
|
||||
try:
|
||||
if not self.wlan.isconnected():
|
||||
self.connect()
|
||||
|
||||
distance = self.measure_distance()
|
||||
self.opened = distance >= THRESHOLD_DISTANCE
|
||||
|
||||
@ -157,13 +162,14 @@ class App:
|
||||
self.previously_opened = True
|
||||
|
||||
if i >= 20:
|
||||
print("Blinking...")
|
||||
self.led.toggle()
|
||||
utime.sleep_ms(100)
|
||||
self.led.toggle()
|
||||
utime.sleep_ms(400)
|
||||
|
||||
i = 0
|
||||
self.health_check_server()
|
||||
gc.collect()
|
||||
else:
|
||||
utime.sleep_ms(500)
|
||||
|
||||
@ -172,5 +178,6 @@ class App:
|
||||
|
||||
i += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
App().run()
|
||||
|
@ -9,6 +9,7 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
|
@ -1,5 +1,7 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
|
126
server/main.go
126
server/main.go
@ -24,12 +24,13 @@ var token string
|
||||
|
||||
// the condition is: distance >= threshold (is door open)
|
||||
var opened bool
|
||||
var openedChange = make(chan any)
|
||||
|
||||
// is door locked
|
||||
var locked bool = false
|
||||
|
||||
// alert the user when door locked and but open?
|
||||
var alert bool = false
|
||||
// alerts the user when door locked and but open?
|
||||
var alerts bool = false
|
||||
var gotifyToken string
|
||||
var gotifyURL string
|
||||
|
||||
@ -44,23 +45,23 @@ type ChangeLockReq struct {
|
||||
}
|
||||
|
||||
type ChangeAlertReq struct {
|
||||
Alert bool `json:"alert"`
|
||||
For int `json:"for"`
|
||||
Alerts bool `json:"alerts"`
|
||||
For int `json:"for"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
_ = godotenv.Load()
|
||||
token = os.Getenv("TOKEN")
|
||||
|
||||
alertRaw := strings.ToLower(os.Getenv("USE_ALERTS"))
|
||||
alert = alertRaw == "true" || alertRaw == "t" || alertRaw == "1" || alertRaw == "y" || alertRaw == "yes"
|
||||
alertsRaw := strings.ToLower(os.Getenv("USE_ALERTS"))
|
||||
alerts = alertsRaw == "true" || alertsRaw == "t" || alertsRaw == "1" || alertsRaw == "y" || alertsRaw == "yes"
|
||||
|
||||
gotifyToken = os.Getenv("GOTIFY_TOKEN")
|
||||
gotifyURL = os.Getenv("GOTIFY_URL")
|
||||
if alert && gotifyToken == "" {
|
||||
if alerts && gotifyToken == "" {
|
||||
log.Fatal("GOTIFY_TOKEN can't be empty when alerts are enabled")
|
||||
}
|
||||
if alert && gotifyURL == "" {
|
||||
if alerts && gotifyURL == "" {
|
||||
log.Fatal("GOTIFY_URL can't be empty when alerts are enabled")
|
||||
}
|
||||
|
||||
@ -73,6 +74,10 @@ func main() {
|
||||
LogLevel: log.ERROR,
|
||||
}))
|
||||
app.Use(middleware.Secure())
|
||||
app.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
AllowOrigins: []string{"*"},
|
||||
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
|
||||
}))
|
||||
|
||||
app.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
|
||||
Format: "${time_custom} ${method} ${uri} ---> ${status} in ${latency_human} (${bytes_out} bytes)\n",
|
||||
@ -110,110 +115,25 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
app.GET("/", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, "Hello world!")
|
||||
})
|
||||
app.GET("/", helloWorld)
|
||||
|
||||
app.GET("/open", authed(func(c echo.Context) error {
|
||||
mut.Lock()
|
||||
o := opened
|
||||
mut.Unlock()
|
||||
app.GET("/opened", authed(getOpened))
|
||||
app.GET("/opened/ws", authed(getOpenedWs))
|
||||
app.POST("/opened", authed(setOpened))
|
||||
|
||||
return c.JSON(http.StatusOK, o)
|
||||
}))
|
||||
app.GET("/locked", authed(getLocked))
|
||||
app.POST("/locked", authed(setLocked))
|
||||
|
||||
app.POST("/open", authed(func(c echo.Context) error {
|
||||
var data ChangeOpenReq
|
||||
err := c.Bind(&data)
|
||||
if err != nil {
|
||||
return c.NoContent(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if data.Opened == opened {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
opened = data.Opened
|
||||
if locked && alert {
|
||||
var action string
|
||||
if opened {
|
||||
action = "opened"
|
||||
} else {
|
||||
action = "closed"
|
||||
}
|
||||
|
||||
go sendAlert(action)
|
||||
}
|
||||
|
||||
mut.Unlock()
|
||||
return c.NoContent(http.StatusOK)
|
||||
}))
|
||||
|
||||
app.GET("/lock", authed(func(c echo.Context) error {
|
||||
mut.Lock()
|
||||
l := locked
|
||||
mut.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, l)
|
||||
}))
|
||||
|
||||
app.POST("/lock", authed(func(c echo.Context) error {
|
||||
var data ChangeLockReq
|
||||
err := c.Bind(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.Locked == locked {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
locked = data.Locked
|
||||
mut.Unlock()
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}))
|
||||
|
||||
app.GET("/alert", authed(func(c echo.Context) error {
|
||||
mut.Lock()
|
||||
a := alert
|
||||
mut.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, a)
|
||||
}))
|
||||
|
||||
app.POST("/alert", authed(func(c echo.Context) error {
|
||||
var data ChangeAlertReq
|
||||
err := c.Bind(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.For > 0 {
|
||||
go func() {
|
||||
time.Sleep(time.Duration(data.For) * time.Second)
|
||||
|
||||
mut.Lock()
|
||||
alert = !data.Alert
|
||||
mut.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
alert = data.Alert
|
||||
mut.Unlock()
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}))
|
||||
app.GET("/alerts", authed(getAlerts))
|
||||
app.POST("/alerts", authed(setAlerts))
|
||||
|
||||
log.Fatal(app.Start(":1323"))
|
||||
}
|
||||
|
||||
func authed(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if c.Request().Header.Get("Authorization") != token {
|
||||
provided := c.Request().Header.Get("Authorization")
|
||||
if provided != fmt.Sprintf("Bearer %s", token) {
|
||||
return c.NoContent(http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
|
143
server/routes.go
Normal file
143
server/routes.go
Normal file
@ -0,0 +1,143 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/gommon/log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{}
|
||||
|
||||
func helloWorld(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, "Hello world!")
|
||||
}
|
||||
|
||||
func getOpened(c echo.Context) error {
|
||||
mut.Lock()
|
||||
o := opened
|
||||
mut.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, o)
|
||||
}
|
||||
|
||||
func getOpenedWs(c echo.Context) error {
|
||||
ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = ws.Close() }()
|
||||
|
||||
// empty the channel
|
||||
emptied := false
|
||||
for !emptied {
|
||||
select {
|
||||
case <-openedChange:
|
||||
default:
|
||||
emptied = true
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
<-openedChange
|
||||
mut.Lock()
|
||||
|
||||
err = ws.WriteJSON(opened)
|
||||
mut.Unlock()
|
||||
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func setOpened(c echo.Context) error {
|
||||
var data ChangeOpenReq
|
||||
err := c.Bind(&data)
|
||||
if err != nil {
|
||||
return c.NoContent(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if data.Opened == opened {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
opened = data.Opened
|
||||
if locked && alerts {
|
||||
var action string
|
||||
if opened {
|
||||
action = "opened"
|
||||
} else {
|
||||
action = "closed"
|
||||
}
|
||||
|
||||
go sendAlert(action)
|
||||
}
|
||||
|
||||
openedChange <- 0
|
||||
mut.Unlock()
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func getLocked(c echo.Context) error {
|
||||
mut.Lock()
|
||||
l := locked
|
||||
mut.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, l)
|
||||
}
|
||||
|
||||
func setLocked(c echo.Context) error {
|
||||
var data ChangeLockReq
|
||||
err := c.Bind(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.Locked == locked {
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
locked = data.Locked
|
||||
mut.Unlock()
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
||||
|
||||
func getAlerts(c echo.Context) error {
|
||||
mut.Lock()
|
||||
a := alerts
|
||||
mut.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, a)
|
||||
}
|
||||
|
||||
func setAlerts(c echo.Context) error {
|
||||
var data ChangeAlertReq
|
||||
err := c.Bind(&data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if data.For > 0 {
|
||||
go func() {
|
||||
time.Sleep(time.Duration(data.For) * time.Second)
|
||||
|
||||
mut.Lock()
|
||||
alerts = !data.Alerts
|
||||
mut.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
mut.Lock()
|
||||
alerts = data.Alerts
|
||||
mut.Unlock()
|
||||
|
||||
return c.NoContent(http.StatusOK)
|
||||
}
|
10
web/.dockerignore
Normal file
10
web/.dockerignore
Normal file
@ -0,0 +1,10 @@
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
node_modules/
|
||||
.output/
|
||||
.data/
|
||||
.nuxt/
|
||||
.cache/
|
||||
|
||||
.env*
|
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Nuxt dev/build outputs
|
||||
.output
|
||||
.data
|
||||
.nuxt
|
||||
.nitro
|
||||
.cache
|
||||
dist
|
||||
|
||||
# Node dependencies
|
||||
node_modules
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.fleet
|
||||
.idea
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
6
web/.prettierrc
Normal file
6
web/.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": false
|
||||
}
|
20
web/Dockerfile
Normal file
20
web/Dockerfile
Normal file
@ -0,0 +1,20 @@
|
||||
FROM oven/bun:1 AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json bun.lock .
|
||||
RUN bun install --frozen-lockfile --production
|
||||
|
||||
COPY . .
|
||||
RUN bun run build
|
||||
|
||||
FROM oven/bun:1
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/.output .
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["bun", "/app/server/index.mjs"]
|
9
web/app.vue
Normal file
9
web/app.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@import "assets/css/main.css";
|
||||
</style>
|
2
web/assets/css/main.css
Normal file
2
web/assets/css/main.css
Normal file
@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
3
web/assets/css/tailwind.css
Normal file
3
web/assets/css/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
2027
web/bun.lock
Normal file
2027
web/bun.lock
Normal file
File diff suppressed because it is too large
Load Diff
42
web/composables/useAPI.ts
Normal file
42
web/composables/useAPI.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import type { FetchResponse } from "ofetch"
|
||||
import { AxiosError, type AxiosResponse } from "axios"
|
||||
|
||||
export function useAPI(route: string = "/") {
|
||||
return `https://api.door.svitan.dev${route}`
|
||||
}
|
||||
|
||||
export async function handleRequestError(error: unknown) {
|
||||
const toast = useToast()
|
||||
let message = undefined
|
||||
if (error instanceof Error) {
|
||||
message = error.message
|
||||
}
|
||||
|
||||
toast.add({
|
||||
title: "Error occurred",
|
||||
description: message,
|
||||
color: "error",
|
||||
})
|
||||
}
|
||||
|
||||
export function handleResponse<T extends { toString(): string }>(
|
||||
response: AxiosResponse<T>,
|
||||
success: (response: AxiosResponse<T>) => void = () => {}
|
||||
) {
|
||||
const token = useToken()
|
||||
const toast = useToast()
|
||||
|
||||
if (response.status === 200) {
|
||||
success(response)
|
||||
} else if (response.status === 401) {
|
||||
toast.add({ title: "Token not valid", color: "error" })
|
||||
token.value = ""
|
||||
navigateTo("/token")
|
||||
} else {
|
||||
toast.add({
|
||||
title: "Error occurred",
|
||||
description: response.data.toString(),
|
||||
color: "error",
|
||||
})
|
||||
}
|
||||
}
|
10
web/composables/useToken.ts
Normal file
10
web/composables/useToken.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export function useToken() {
|
||||
return useCookie<string | undefined>("token")
|
||||
}
|
||||
|
||||
export function useHeaders(override: string | undefined = undefined) {
|
||||
const token = useToken()
|
||||
return {
|
||||
Authorization: `Bearer ${override ?? token.value}`,
|
||||
}
|
||||
}
|
6
web/nuxt.config.ts
Normal file
6
web/nuxt.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||
export default defineNuxtConfig({
|
||||
compatibilityDate: "2025-05-15",
|
||||
devtools: { enabled: true },
|
||||
modules: ["@nuxt/icon", "@nuxt/ui", "@nuxtjs/tailwindcss"],
|
||||
});
|
26
web/package.json
Normal file
26
web/package.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "nuxt-app",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
"generate": "nuxt generate",
|
||||
"preview": "nuxt preview",
|
||||
"postinstall": "nuxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nuxt/icon": "1.13.0",
|
||||
"@nuxt/ui": "3.1.3",
|
||||
"@nuxtjs/tailwindcss": "7.0.0-beta.0",
|
||||
"axios": "^1.9.0",
|
||||
"nuxt": "^3.17.5",
|
||||
"typescript": "^5.6.3",
|
||||
"valibot": "^1.1.0",
|
||||
"vue": "^3.5.16",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
164
web/pages/index.vue
Normal file
164
web/pages/index.vue
Normal file
@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<main
|
||||
class="flex items-center justify-center min-w-screen min-h-screen gap-4 flex-wrap"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
|
||||
<div
|
||||
:class="`flex items-center justify-center border-solid border-2 border-${open ? (locked ? 'error' : 'secondary') : 'primary'} rounded-lg w-full h-32`"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
open
|
||||
? 'material-symbols:door-open'
|
||||
: 'material-symbols:door-front'
|
||||
"
|
||||
class="size-12 !w-16 !h-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p
|
||||
:class="`flex items-center justify-center text-${open ? (locked ? 'error' : 'secondary') : 'primary'} text-xl w-full h-10`"
|
||||
>
|
||||
{{ open ? "Opened" : "Closed" }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
|
||||
<div
|
||||
:class="`flex items-center justify-center border-solid border-2 border-${locked ? 'primary' : 'secondary'} rounded-lg w-full h-32`"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
locked
|
||||
? 'material-symbols:lock'
|
||||
: 'material-symbols:lock-open'
|
||||
"
|
||||
class="size-12 !w-16 !h-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
:icon="
|
||||
locked
|
||||
? 'material-symbols:lock-open'
|
||||
: 'material-symbols:lock'
|
||||
"
|
||||
size="xl"
|
||||
:color="locked ? 'primary' : 'secondary'"
|
||||
variant="solid"
|
||||
class="flex items-center justify-center w-full h-10"
|
||||
@click="toggleLock"
|
||||
>
|
||||
{{ locked ? "Unlock" : "Lock" }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center flex-col gap-y-2 w-32">
|
||||
<div
|
||||
:class="`flex items-center justify-center border-solid border-2 border-${alert ? 'primary' : 'secondary'} rounded-lg w-full h-32`"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
alert
|
||||
? 'material-symbols:volume-up'
|
||||
: 'material-symbols:volume-off'
|
||||
"
|
||||
class="size-12 !w-16 !h-16"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
:icon="
|
||||
alert
|
||||
? 'material-symbols:volume-off'
|
||||
: 'material-symbols:volume-up'
|
||||
"
|
||||
size="xl"
|
||||
:color="alert ? 'primary' : 'secondary'"
|
||||
variant="solid"
|
||||
class="flex items-center justify-center w-full h-10"
|
||||
@click="toggleAlert"
|
||||
>
|
||||
{{ alert ? "Turn off" : "Turn on" }}
|
||||
</UButton>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import axios from "axios"
|
||||
|
||||
const token = useToken()
|
||||
|
||||
const open = ref(true)
|
||||
const locked = ref(true)
|
||||
const alert = ref(false)
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
async function toggleLock() {
|
||||
const desired = !locked.value
|
||||
axios
|
||||
.post(
|
||||
useAPI("/lock"),
|
||||
{
|
||||
locked: desired,
|
||||
},
|
||||
{
|
||||
headers: useHeaders(),
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
handleResponse(res, () => {
|
||||
toast.add({
|
||||
title: `The door was ${desired ? "locked" : "unlocked"}`,
|
||||
})
|
||||
locked.value = desired
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
async function toggleAlert() {
|
||||
const desired = !alert.value
|
||||
axios
|
||||
.post(useAPI("/alert"), { alert: desired }, { headers: useHeaders() })
|
||||
.then((res) => {
|
||||
handleResponse(res, () => {
|
||||
toast.add({
|
||||
title: `Alerts were turned ${desired ? "on" : "off"}`,
|
||||
})
|
||||
alert.value = desired
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!token.value) {
|
||||
return navigateTo("/token")
|
||||
}
|
||||
|
||||
axios
|
||||
.get<boolean>(useAPI("/lock"), {
|
||||
headers: useHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
handleResponse(res, (response) => {
|
||||
locked.value = response.data
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
|
||||
axios
|
||||
.get<boolean>(useAPI("/alert"), {
|
||||
headers: useHeaders(),
|
||||
})
|
||||
.then((res) => {
|
||||
handleResponse(res, (response) => {
|
||||
alert.value = response.data
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
})
|
||||
</script>
|
60
web/pages/token.vue
Normal file
60
web/pages/token.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<main class="flex items-center justify-center min-w-screen min-h-screen">
|
||||
<UForm
|
||||
:schema="schema"
|
||||
:state="state"
|
||||
@submit="onSubmit"
|
||||
class="flex flex-col items-end justify-center gap-y-2"
|
||||
>
|
||||
<UFormField label="Token" name="token" size="xl" required>
|
||||
<UInput
|
||||
v-model="state.token"
|
||||
placeholder="Your token..."
|
||||
size="xl"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" size="xl"> Submit </UButton>
|
||||
</UForm>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as v from "valibot"
|
||||
import type { FormSubmitEvent } from "@nuxt/ui"
|
||||
import axios from "axios"
|
||||
|
||||
const token = useToken()
|
||||
|
||||
const schema = v.object({
|
||||
token: v.pipe(v.string(), v.nonEmpty("Please enter your token")),
|
||||
})
|
||||
type Schema = v.InferOutput<typeof schema>
|
||||
|
||||
const state = reactive({
|
||||
token: "",
|
||||
})
|
||||
const toast = useToast()
|
||||
|
||||
function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
const received = event.data.token
|
||||
axios
|
||||
.get<boolean>(useAPI("/open"), {
|
||||
headers: useHeaders(received),
|
||||
})
|
||||
.then((res) => {
|
||||
handleResponse(res, () => {
|
||||
toast.add({ title: "Token saved", color: "success" })
|
||||
token.value = received
|
||||
navigateTo("/")
|
||||
})
|
||||
})
|
||||
.catch(handleRequestError)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (token.value) {
|
||||
return navigateTo("/")
|
||||
}
|
||||
})
|
||||
</script>
|
BIN
web/public/favicon.ico
Normal file
BIN
web/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 264 KiB |
2
web/public/robots.txt
Normal file
2
web/public/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow: /
|
3
web/server/tsconfig.json
Normal file
3
web/server/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "../.nuxt/tsconfig.server.json"
|
||||
}
|
11
web/tailwind.config.js
Normal file
11
web/tailwind.config.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
purge: [],
|
||||
darkMode: "class", // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
4
web/tsconfig.json
Normal file
4
web/tsconfig.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user