From 92368dabf88292b00eb4167e64137ac589d1bb9f Mon Sep 17 00:00:00 2001
From: Hayden <64056131+hay-kot@users.noreply.github.com>
Date: Wed, 12 Oct 2022 12:53:22 -0800
Subject: [PATCH] feat: automate demo-site deployment and configuration (#42)
* add demo env variable
* setup initialization when demo
* disable password when in demo mode
* expose demo status to API
* improve UI for demo instance
---
backend/app/api/demo.go | 62 +++++++++++++++++
backend/app/api/docs/docs.go | 3 +
backend/app/api/docs/swagger.json | 3 +
backend/app/api/docs/swagger.yaml | 2 +
backend/app/api/main.go | 6 ++
backend/app/api/routes.go | 89 ++++++++++++------------
backend/app/api/v1/controller.go | 15 +++-
backend/app/api/v1/v1_ctrl_user.go | 5 ++
backend/internal/config/conf.go | 1 +
fly.toml | 1 +
frontend/lib/api/types/data-contracts.ts | 1 +
frontend/pages/index.vue | 23 ++++++
12 files changed, 166 insertions(+), 45 deletions(-)
create mode 100644 backend/app/api/demo.go
diff --git a/backend/app/api/demo.go b/backend/app/api/demo.go
new file mode 100644
index 0000000..2bde834
--- /dev/null
+++ b/backend/app/api/demo.go
@@ -0,0 +1,62 @@
+package main
+
+import (
+ "context"
+ "encoding/csv"
+ "strings"
+
+ "github.com/hay-kot/homebox/backend/internal/services"
+ "github.com/rs/zerolog/log"
+)
+
+func (a *app) SetupDemo() {
+ csvText := `Import Ref,Location,Labels,Quantity,Name,Description,Insured,Serial Number,Model Number,Manufacturer,Notes,Purchase From,Purchased Price,Purchased Time,Lifetime Warranty,Warranty Expires,Warranty Details,Sold To,Sold Price,Sold Time,Sold Notes
+,Garage,IOT;Home Assistant; Z-Wave,1,Zooz Universal Relay ZEN17,"Zooz 700 Series Z-Wave Universal Relay ZEN17 for Awnings, Garage Doors, Sprinklers, and More | 2 NO-C-NC Relays (20A, 10A) | Signal Repeater | Hub Required (Compatible with SmartThings and Hubitat)",,,ZEN17,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
+,Living Room,IOT;Home Assistant; Z-Wave,1,Zooz Motion Sensor,"Zooz Z-Wave Plus S2 Motion Sensor ZSE18 with Magnetic Mount, Works with Vera and SmartThings",,,ZSE18,Zooz,,Amazon,29.95,10/15/2021,,,,,,,
+,Office,IOT;Home Assistant; Z-Wave,1,Zooz 110v Power Switch,"Zooz Z-Wave Plus Power Switch ZEN15 for 110V AC Units, Sump Pumps, Humidifiers, and More",,,ZEN15,Zooz,,Amazon,39.95,10/13/2021,,,,,,,
+,Downstairs,IOT;Home Assistant; Z-Wave,1,Ecolink Z-Wave PIR Motion Sensor,"Ecolink Z-Wave PIR Motion Detector Pet Immune, White (PIRZWAVE2.5-ECO)",,,PIRZWAVE2.5-ECO,Ecolink,,Amazon,35.58,10/21/2020,,,,,,,
+,Entry,IOT;Home Assistant; Z-Wave,1,Yale Security Touchscreen Deadbolt,"Yale Security YRD226-ZW2-619 YRD226ZW2619 Touchscreen Deadbolt, Satin Nickel",,,YRD226ZW2619,Yale,,Amazon,120.39,10/14/2020,,,,,,,
+,Kitchen,IOT;Home Assistant; Z-Wave,1,Smart Rocker Light Dimmer,"UltraPro Z-Wave Smart Rocker Light Dimmer with QuickFit and SimpleWire, 3-Way Ready, Compatible with Alexa, Google Assistant, ZWave Hub Required, Repeater/Range Extender, White Paddle Only, 39351",,,39351,Honeywell,,Amazon,65.98,09/30/0202,,,,,,,
+`
+
+ var (
+ registration = services.UserRegistration{
+ Email: "demo@email.com",
+ Name: "Demo",
+ Password: "demo",
+ }
+ )
+
+ // First check if we've already setup a demo user and skip if so
+ _, err := a.services.User.Login(context.Background(), registration.Email, registration.Password)
+ if err == nil {
+ return
+ }
+
+ _, err = a.services.User.RegisterUser(context.Background(), registration)
+ if err != nil {
+ log.Err(err).Msg("Failed to register demo user")
+ log.Fatal().Msg("Failed to setup demo")
+ }
+
+ token, _ := a.services.User.Login(context.Background(), registration.Email, registration.Password)
+ self, _ := a.services.User.GetSelf(context.Background(), token.Raw)
+
+ // Read CSV Text
+ reader := csv.NewReader(strings.NewReader(csvText))
+ reader.Comma = ','
+
+ records, err := reader.ReadAll()
+ if err != nil {
+ log.Err(err).Msg("Failed to read CSV")
+ log.Fatal().Msg("Failed to setup demo")
+ }
+
+ err = a.services.Items.CsvImport(context.Background(), self.GroupID, records)
+ if err != nil {
+ log.Err(err).Msg("Failed to import CSV")
+ log.Fatal().Msg("Failed to setup demo")
+ }
+
+ log.Info().Msg("Demo setup complete")
+}
diff --git a/backend/app/api/docs/docs.go b/backend/app/api/docs/docs.go
index 1eecf5e..346ee39 100644
--- a/backend/app/api/docs/docs.go
+++ b/backend/app/api/docs/docs.go
@@ -1580,6 +1580,9 @@ const docTemplate = `{
"build": {
"$ref": "#/definitions/v1.Build"
},
+ "demo": {
+ "type": "boolean"
+ },
"health": {
"type": "boolean"
},
diff --git a/backend/app/api/docs/swagger.json b/backend/app/api/docs/swagger.json
index 81fb334..4408d9c 100644
--- a/backend/app/api/docs/swagger.json
+++ b/backend/app/api/docs/swagger.json
@@ -1572,6 +1572,9 @@
"build": {
"$ref": "#/definitions/v1.Build"
},
+ "demo": {
+ "type": "boolean"
+ },
"health": {
"type": "boolean"
},
diff --git a/backend/app/api/docs/swagger.yaml b/backend/app/api/docs/swagger.yaml
index 7172c12..876519e 100644
--- a/backend/app/api/docs/swagger.yaml
+++ b/backend/app/api/docs/swagger.yaml
@@ -335,6 +335,8 @@ definitions:
properties:
build:
$ref: '#/definitions/v1.Build'
+ demo:
+ type: boolean
health:
type: boolean
message:
diff --git a/backend/app/api/main.go b/backend/app/api/main.go
index 0309784..350c2f4 100644
--- a/backend/app/api/main.go
+++ b/backend/app/api/main.go
@@ -144,5 +144,11 @@ func run(cfg *config.Config) error {
}
})
+ // TODO: Remove through external API that does setup
+ if cfg.Demo {
+ log.Info().Msg("Running in demo mode, creating demo data")
+ app.SetupDemo()
+ }
+
return app.server.Start(routes)
}
diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go
index 3b131b2..9befd36 100644
--- a/backend/app/api/routes.go
+++ b/backend/app/api/routes.go
@@ -42,58 +42,59 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux {
// API Version 1
v1Base := v1.BaseUrlFunc(prefix)
- v1Ctrl := v1.NewControllerV1(a.services, v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize))
- {
- r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
- Version: Version,
- Commit: Commit,
- BuildTime: BuildTime,
- }))
+ v1Ctrl := v1.NewControllerV1(a.services,
+ v1.WithMaxUploadSize(a.conf.Web.MaxUploadSize),
+ v1.WithDemoStatus(a.conf.Demo), // Disable Password Change in Demo Mode
+ )
+ r.Get(v1Base("/status"), v1Ctrl.HandleBase(func() bool { return true }, v1.Build{
+ Version: Version,
+ Commit: Commit,
+ BuildTime: BuildTime,
+ }))
- r.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
- r.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
+ r.Post(v1Base("/users/register"), v1Ctrl.HandleUserRegistration())
+ r.Post(v1Base("/users/login"), v1Ctrl.HandleAuthLogin())
- // Attachment download URl needs a `token` query param to be passed in the request.
- // and also needs to be outside of the `auth` middleware.
- r.Get(v1Base("/items/{id}/attachments/download"), v1Ctrl.HandleItemAttachmentDownload())
+ // Attachment download URl needs a `token` query param to be passed in the request.
+ // and also needs to be outside of the `auth` middleware.
+ r.Get(v1Base("/items/{id}/attachments/download"), v1Ctrl.HandleItemAttachmentDownload())
- r.Group(func(r chi.Router) {
- r.Use(a.mwAuthToken)
- r.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf())
- r.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate())
- r.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete())
- r.Put(v1Base("/users/self/password"), v1Ctrl.HandleUserUpdatePassword())
- r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
- r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
- r.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword())
+ r.Group(func(r chi.Router) {
+ r.Use(a.mwAuthToken)
+ r.Get(v1Base("/users/self"), v1Ctrl.HandleUserSelf())
+ r.Put(v1Base("/users/self"), v1Ctrl.HandleUserSelfUpdate())
+ r.Delete(v1Base("/users/self"), v1Ctrl.HandleUserSelfDelete())
+ r.Put(v1Base("/users/self/password"), v1Ctrl.HandleUserUpdatePassword())
+ r.Post(v1Base("/users/logout"), v1Ctrl.HandleAuthLogout())
+ r.Get(v1Base("/users/refresh"), v1Ctrl.HandleAuthRefresh())
+ r.Put(v1Base("/users/self/change-password"), v1Ctrl.HandleUserSelfChangePassword())
- r.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate())
+ r.Post(v1Base("/groups/invitations"), v1Ctrl.HandleGroupInvitationsCreate())
- r.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll())
- r.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate())
- r.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet())
- r.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate())
- r.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete())
+ r.Get(v1Base("/locations"), v1Ctrl.HandleLocationGetAll())
+ r.Post(v1Base("/locations"), v1Ctrl.HandleLocationCreate())
+ r.Get(v1Base("/locations/{id}"), v1Ctrl.HandleLocationGet())
+ r.Put(v1Base("/locations/{id}"), v1Ctrl.HandleLocationUpdate())
+ r.Delete(v1Base("/locations/{id}"), v1Ctrl.HandleLocationDelete())
- r.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll())
- r.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate())
- r.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet())
- r.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate())
- r.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete())
+ r.Get(v1Base("/labels"), v1Ctrl.HandleLabelsGetAll())
+ r.Post(v1Base("/labels"), v1Ctrl.HandleLabelsCreate())
+ r.Get(v1Base("/labels/{id}"), v1Ctrl.HandleLabelGet())
+ r.Put(v1Base("/labels/{id}"), v1Ctrl.HandleLabelUpdate())
+ r.Delete(v1Base("/labels/{id}"), v1Ctrl.HandleLabelDelete())
- r.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll())
- r.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport())
- r.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate())
- r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet())
- r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate())
- r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
+ r.Get(v1Base("/items"), v1Ctrl.HandleItemsGetAll())
+ r.Post(v1Base("/items/import"), v1Ctrl.HandleItemsImport())
+ r.Post(v1Base("/items"), v1Ctrl.HandleItemsCreate())
+ r.Get(v1Base("/items/{id}"), v1Ctrl.HandleItemGet())
+ r.Put(v1Base("/items/{id}"), v1Ctrl.HandleItemUpdate())
+ r.Delete(v1Base("/items/{id}"), v1Ctrl.HandleItemDelete())
- r.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate())
- r.Get(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentToken())
- r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate())
- r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete())
- })
- }
+ r.Post(v1Base("/items/{id}/attachments"), v1Ctrl.HandleItemAttachmentCreate())
+ r.Get(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentToken())
+ r.Put(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentUpdate())
+ r.Delete(v1Base("/items/{id}/attachments/{attachment_id}"), v1Ctrl.HandleItemAttachmentDelete())
+ })
r.NotFound(notFoundHandler())
return r
diff --git a/backend/app/api/v1/controller.go b/backend/app/api/v1/controller.go
index c3e7b4b..ee4810f 100644
--- a/backend/app/api/v1/controller.go
+++ b/backend/app/api/v1/controller.go
@@ -13,9 +13,16 @@ func WithMaxUploadSize(maxUploadSize int64) func(*V1Controller) {
}
}
+func WithDemoStatus(demoStatus bool) func(*V1Controller) {
+ return func(ctrl *V1Controller) {
+ ctrl.isDemo = demoStatus
+ }
+}
+
type V1Controller struct {
svc *services.AllServices
maxUploadSize int64
+ isDemo bool
}
type (
@@ -30,7 +37,8 @@ type (
Versions []string `json:"versions"`
Title string `json:"title"`
Message string `json:"message"`
- Build Build
+ Build Build `json:"build"`
+ Demo bool `json:"demo"`
}
)
@@ -48,6 +56,10 @@ func NewControllerV1(svc *services.AllServices, options ...func(*V1Controller))
svc: svc,
}
+ for _, opt := range options {
+ opt(ctrl)
+ }
+
return ctrl
}
@@ -66,6 +78,7 @@ func (ctrl *V1Controller) HandleBase(ready ReadyFunc, build Build) http.HandlerF
Title: "Go API Template",
Message: "Welcome to the Go API Template Application!",
Build: build,
+ Demo: ctrl.isDemo,
})
}
}
diff --git a/backend/app/api/v1/v1_ctrl_user.go b/backend/app/api/v1/v1_ctrl_user.go
index 4c6b71c..bb5f617 100644
--- a/backend/app/api/v1/v1_ctrl_user.go
+++ b/backend/app/api/v1/v1_ctrl_user.go
@@ -136,6 +136,11 @@ type (
// @Security Bearer
func (ctrl *V1Controller) HandleUserSelfChangePassword() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
+ if ctrl.isDemo {
+ server.RespondError(w, http.StatusForbidden, nil)
+ return
+ }
+
var cp ChangePassword
err := server.Decode(r, &cp)
if err != nil {
diff --git a/backend/internal/config/conf.go b/backend/internal/config/conf.go
index 7d63ea8..56488d9 100644
--- a/backend/internal/config/conf.go
+++ b/backend/internal/config/conf.go
@@ -23,6 +23,7 @@ type Config struct {
Log LoggerConf `yaml:"logger"`
Mailer MailerConf `yaml:"mailer"`
Swagger SwaggerConf `yaml:"swagger"`
+ Demo bool `yaml:"demo"`
}
type SwaggerConf struct {
diff --git a/fly.toml b/fly.toml
index 81c3a3c..5a95851 100644
--- a/fly.toml
+++ b/fly.toml
@@ -8,6 +8,7 @@ processes = []
[env]
PORT = "7745"
+ HBOX_DEMO = "true"
[experimental]
allowed_public_ports = []
diff --git a/frontend/lib/api/types/data-contracts.ts b/frontend/lib/api/types/data-contracts.ts
index ae726c4..24520bb 100644
--- a/frontend/lib/api/types/data-contracts.ts
+++ b/frontend/lib/api/types/data-contracts.ts
@@ -227,6 +227,7 @@ export interface UserRegistration {
export interface ApiSummary {
build: Build;
+ demo: boolean;
health: boolean;
message: string;
title: string;
diff --git a/frontend/pages/index.vue b/frontend/pages/index.vue
index 693a6a8..e122d7a 100644
--- a/frontend/pages/index.vue
+++ b/frontend/pages/index.vue
@@ -11,6 +11,24 @@
const api = usePublicApi();
const toast = useNotifier();
+ const { data: status } = useAsyncData(async () => {
+ const { data } = await api.status();
+
+ if (data) {
+ console.log(data);
+ username.value = "demo@email.com";
+ password.value = "demo";
+ }
+ return data;
+ });
+
+ whenever(status, status => {
+ if (status?.demo) {
+ email.value = "demo@email.com";
+ loginPassword.value = "demo";
+ }
+ });
+
const authStore = useAuthStore();
if (!authStore.isTokenExpired) {
navigateTo("/home");
@@ -178,6 +196,11 @@
This is a demo instance Email demo@email.com Password demo