diff --git a/.github/workflows/build_and_release.yml b/.github/workflows/build_and_release.yml index 89b5ae4..f98980f 100644 --- a/.github/workflows/build_and_release.yml +++ b/.github/workflows/build_and_release.yml @@ -18,10 +18,16 @@ jobs: uses: actions/setup-go@v2 with: go-version: 1.21 - + + - name: Install MinGW-w64 + run: | + choco install mingw -y + echo "C:\ProgramData\chocolatey\lib\mingw\tools\install\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + - name: Build Go binary run: | cd backend + $env:CGO_ENABLED=1 go build -o ../WinSenseConnect.exe - name: Set up Node.js diff --git a/.gitignore b/.gitignore index ca22a49..c71fa95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # /frontend /scripts/*.ps1 !scripts/test_notification.ps1 +/data/** *.exe *.log *.txt diff --git a/backend/config.go b/backend/config.go index f837621..6d67399 100644 --- a/backend/config.go +++ b/backend/config.go @@ -1,59 +1,31 @@ package main import ( - "encoding/json" "fmt" - "os" - "path/filepath" ) type Config struct { - BrokerAddress string `json:"broker_address"` - Username string `json:"username"` - Password string `json:"password"` - ClientID string `json:"client_id"` - Topic string `json:"topic"` - LogLevel string `json:"log_level"` - ScriptTimeout int `json:"script_timeout"` - Commands map[string]ScriptConfig `json:"commands"` - SensorConfig SensorConfig `json:"sensor_config"` -} - -type ScriptConfig struct { - ScriptPath string `json:"script_path"` - RunAsUser bool `json:"run_as_user"` -} -type SensorConfig struct { - Enabled bool `json:"enabled"` - Interval int `json:"interval"` - SensorTopic string `json:"sensor_topic"` + ID int64 `json:"id"` + BrokerAddress string `json:"broker_address"` + Username string `json:"username"` + Password string `json:"password"` + ClientID string `json:"client_id"` + Topic string `json:"topic"` + LogLevel string `json:"log_level"` + ScriptTimeout int `json:"script_timeout"` + SensorConfigEnabled bool `json:"sensor_config_enabled"` + Commands map[string]ScriptConfig `json:"commands"` + Sensors map[string]SensorConfig `json:"sensors"` } func (p *program) loadConfig() error { p.logger.Debug("Starting to load config...") - exePath, err := os.Executable() - if err != nil { - p.logger.Error(fmt.Sprintf("Failed to get executable path: %v", err)) - return fmt.Errorf("failed to get executable path: %v", err) - } - p.logger.Debug(fmt.Sprintf("Executable path: %s", exePath)) - - configPath := filepath.Join(filepath.Dir(exePath), "config.json") - p.logger.Debug(fmt.Sprintf("Config path: %s", configPath)) - - file, err := os.Open(configPath) + conf, err := p.db.GetConfig() if err != nil { - p.logger.Error(fmt.Sprintf("Failed to open config file: %v", err)) - return fmt.Errorf("failed to open config file: %v", err) + p.logger.Error(fmt.Sprintf("Failed to get config: %v", err)) + return err } - defer file.Close() - - decoder := json.NewDecoder(file) - if err := decoder.Decode(&p.config); err != nil { - p.logger.Error(fmt.Sprintf("Failed to decode config: %v", err)) - return fmt.Errorf("failed to decode config: %v", err) - } - + p.config = *conf p.logger.Debug("Config loaded successfully") return nil } diff --git a/backend/db.go b/backend/db.go new file mode 100644 index 0000000..ef3acd0 --- /dev/null +++ b/backend/db.go @@ -0,0 +1,347 @@ +package main + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + _ "github.com/mattn/go-sqlite3" +) + +type DB struct { + *sql.DB +} + +func NewDB() (*DB, error) { + exePath, err := os.Executable() + if err != nil { + return nil, fmt.Errorf("failed to get executable path: %v", err) + } + + dbpath := filepath.Join(filepath.Dir(exePath), "data", "store.db") + _, err = os.Stat(dbpath) + if os.IsNotExist(err) { + // If it doesn't exist, create it + _, err := os.Create(dbpath) + if err != nil { + return nil, err + } + } + + db, err := sql.Open("sqlite3", dbpath) + if err != nil { + return nil, err + } + if err = db.Ping(); err != nil { + return nil, err + } + return &DB{db}, nil +} + +func (db *DB) InitSchema(logger *Logger) error { + // Drop tables if they exist + // logger.Debug("Dropping tables if they exist...") + // _, dropErr := db.Exec(`DROP TABLE IF EXISTS configs`) + // if dropErr != nil { + // return dropErr + // } + // _, dropErr = db.Exec(`DROP TABLE IF EXISTS script_configs`) + // if dropErr != nil { + // return dropErr + // } + // _, dropErr = db.Exec(`DROP TABLE IF EXISTS sensor_configs`) + // if dropErr != nil { + // return dropErr + // } + + // Create tables + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + broker_address TEXT NOT NULL, + username TEXT, + password TEXT, + client_id TEXT, + topic TEXT, + log_level TEXT, + script_timeout INTEGER, + created_at DATETIME, + updated_at DATETIME + ); + + CREATE TABLE IF NOT EXISTS script_configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + script_path TEXT NOT NULL, + run_as_user BOOLEAN, + script_timeout INTEGER, + created_at DATETIME, + updated_at DATETIME + ); + + CREATE TABLE IF NOT EXISTS sensor_configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + enabled BOOLEAN, + interval INTEGER, + sensor_topic TEXT, + created_at DATETIME, + updated_at DATETIME + ); + `) + + if err != nil { + return err + } + // Check if the default data already exists + var defaultDataExists bool + err = db.QueryRow("SELECT id FROM configs LIMIT 1").Scan(&defaultDataExists) + logger.Debug(fmt.Sprintf("Default data exists: %v", defaultDataExists)) + if err != nil && err != sql.ErrNoRows { + logger.Error(fmt.Sprintf("Failed to check if default data exists: %v", err)) + return err + } + if !defaultDataExists { + // Add default data if it doesn't exist + _, err = db.Exec(` + INSERT INTO configs (id, broker_address, username, password, client_id, topic, log_level, script_timeout, created_at, updated_at) + VALUES (1, 'tcp://0.0.0.0:1883', 'your_username', 'your_password', 'my-windows-automation-service', 'windows/commands', 'debug', 300, '2023-07-01 12:00:00', '2023-07-01 12:00:00'); + + INSERT INTO sensor_configs (id, name, enabled, interval, sensor_topic, created_at, updated_at) + VALUES (1, 'cpu_usage', false, 60, 'windows/sensors/cpu_usage', '2023-07-01 12:00:00', '2023-07-01 12:00:00'); + `) + if err != nil { + return err + } + } + err = db.AddScriptsFromDir() + return err +} + +func (db *DB) AddScriptsFromDir() error { + exePath, err := os.Executable() + if err != nil { + return fmt.Errorf("failed to get executable path: %v", err) + } + dir := filepath.Join(filepath.Dir(exePath), "scripts") + + files, err := os.ReadDir(dir) + if err != nil { + return fmt.Errorf("failed to read directory: %v", err) + } + + for _, file := range files { + if file.IsDir() { + continue + } + + // Check if db has script with same script path + var script ScriptConfig + err := db.QueryRow("SELECT id, name, script_path, run_as_user, script_timeout, created_at, updated_at FROM script_configs WHERE script_path = ?", file.Name()).Scan(&script) + // split file name into name and extension + filename := strings.Split(file.Name(), ".")[0] + + if err == sql.ErrNoRows { + // If not, add it + script := ScriptConfig{ + Name: filename, + ScriptPath: file.Name(), + RunAsUser: true, + ScriptTimeout: 300, + } + err = db.CreateScriptConfig(&script) + if err != nil { + return fmt.Errorf("failed to create script config: %v", err) + } + } + } + return nil +} + +func (db *DB) GetConfig() (*Config, error) { + var configModel ConfigModel + + err := db.QueryRow("SELECT id, broker_address, username, password, client_id, topic, log_level, script_timeout, created_at, updated_at FROM configs ORDER BY id DESC LIMIT 1").Scan( + &configModel.ID, + &configModel.BrokerAddress, + &configModel.Username, + &configModel.Password, + &configModel.ClientID, + &configModel.Topic, + &configModel.LogLevel, + &configModel.ScriptTimeout, + &configModel.CreatedAt, + &configModel.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to get config: %v", err) + } + + configsScript, err := db.GetScriptConfigs() + if err != nil { + return nil, fmt.Errorf("failed to get script configs: %v", err) + } + configsScriptArray := make(map[string]ScriptConfig) + for _, config := range *configsScript { + configsScriptArray[config.Name] = config + } + + configsSensor, err := db.GetSensorConfigs() + if err != nil { + return nil, fmt.Errorf("failed to get sensor configs: %v", err) + } + configsSensorArray := make(map[string]SensorConfig) + for _, config := range *configsSensor { + configsSensorArray[config.SensorTopic] = config + } + + config := Config{ + ID: configModel.ID, + BrokerAddress: configModel.BrokerAddress, + Username: configModel.Username, + Password: configModel.Password, + ClientID: configModel.ClientID, + Topic: configModel.Topic, + LogLevel: configModel.LogLevel, + ScriptTimeout: configModel.ScriptTimeout, + SensorConfigEnabled: false, + Commands: configsScriptArray, + Sensors: configsSensorArray, + } + + return &config, nil +} + +func (db *DB) GetScriptConfigs() (*ScriptConfigs, error) { + rows, err := db.Query("SELECT id, name, script_path, run_as_user, script_timeout, created_at, updated_at FROM script_configs ORDER BY id DESC") + if err != nil { + return nil, fmt.Errorf("failed to query script configs: %v", err) + } + defer rows.Close() + + var scriptConfigs ScriptConfigs + for rows.Next() { + var sc ScriptConfig + err := rows.Scan(&sc.ID, &sc.Name, &sc.ScriptPath, &sc.RunAsUser, &sc.ScriptTimeout, &sc.CreatedAt, &sc.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to scan script config: %v", err) + } + scriptConfigs = append(scriptConfigs, sc) + } + return &scriptConfigs, nil +} + +func (db *DB) GetSensorConfigs() (*SensorConfigs, error) { + rows, err := db.Query("SELECT id, name, enabled, interval, sensor_topic, created_at, updated_at FROM sensor_configs ORDER BY id DESC") + if err != nil { + return nil, fmt.Errorf("failed to query sensor configs: %v", err) + } + defer rows.Close() + + var sensorConfigs SensorConfigs + for rows.Next() { + var sc SensorConfig + err := rows.Scan(&sc.ID, &sc.Name, &sc.Enabled, &sc.Interval, &sc.SensorTopic, &sc.CreatedAt, &sc.UpdatedAt) + if err != nil { + return nil, fmt.Errorf("failed to scan sensor config: %v", err) + } + sensorConfigs = append(sensorConfigs, sc) + } + return &sensorConfigs, nil +} + +func (db *DB) SaveConfig(config *Config) error { + now := time.Now() + _, err := db.Exec(` + INSERT INTO configs ( + broker_address, username, password, client_id, topic, + log_level, script_timeout, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + config.BrokerAddress, config.Username, config.Password, + config.ClientID, config.Topic, config.LogLevel, + config.ScriptTimeout, now, now, + ) + return err +} + +func (db *DB) UpdateConfig(config *Config) error { + now := time.Now() + _, err := db.Exec(` + UPDATE configs SET + broker_address = ?, username = ?, password = ?, client_id = ?, topic = ?, + log_level = ?, script_timeout = ?, updated_at = ? + WHERE id = ?`, + config.BrokerAddress, config.Username, config.Password, + config.ClientID, config.Topic, config.LogLevel, + config.ScriptTimeout, now, + config.ID, + ) + return err +} + +func (db *DB) GetSensorConfig(id int64) (*SensorConfig, error) { + var sensorConfig SensorConfig + err := db.QueryRow("SELECT id, name, enabled, interval, sensor_topic, created_at, updated_at FROM sensor_configs WHERE id = ? ORDER BY id DESC LIMIT 1", id).Scan( + &sensorConfig.ID, + &sensorConfig.Name, + &sensorConfig.Enabled, + &sensorConfig.Interval, + &sensorConfig.SensorTopic, + &sensorConfig.CreatedAt, + &sensorConfig.UpdatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to get sensor config: %v", err) + } + return &sensorConfig, nil +} + +func (db *DB) UpdateSensorConfig(sensorConfig *SensorConfig) error { + _, err := db.Exec(` + UPDATE sensor_configs SET + name = ?, enabled = ?, interval = ?, sensor_topic = ?, updated_at = ? + WHERE id = ?`, + sensorConfig.Name, + sensorConfig.Enabled, + sensorConfig.Interval, + sensorConfig.SensorTopic, + time.Now(), + sensorConfig.ID, + ) + return err +} + +func (db *DB) CreateSensorConfig(sensorConfig *SensorConfig) error { + now := time.Now() + _, err := db.Exec(` + INSERT INTO sensor_configs ( + name, enabled, interval, sensor_topic, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?)`, + sensorConfig.Name, + sensorConfig.Enabled, + sensorConfig.Interval, + sensorConfig.SensorTopic, + now, + now, + ) + return err +} + +func (db *DB) CreateScriptConfig(scriptConf *ScriptConfig) error { + now := time.Now() + _, err := db.Exec(` + INSERT INTO script_configs ( + name, script_path, run_as_user, script_timeout, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?)`, + scriptConf.Name, + scriptConf.ScriptPath, + scriptConf.RunAsUser, + scriptConf.ScriptTimeout, + now, + now, + ) + return err +} diff --git a/backend/http_server.go b/backend/http_server.go index a5618d2..32ce3a5 100644 --- a/backend/http_server.go +++ b/backend/http_server.go @@ -25,6 +25,7 @@ func (p *program) startHTTPServer() { r.HandleFunc("/api/config", p.handleUpdateConfig).Methods("POST") r.HandleFunc("/api/scripts", p.handleListScripts).Methods("GET") r.HandleFunc("/api/scripts", p.handleAddScript).Methods("POST") + r.HandleFunc("/api/restart", p.handleRestartService).Methods("POST") // Serve static files (our UI) - this will be added at build time from our Nuxt frontend r.PathPrefix("/").Handler(http.FileServer(http.Dir(staticPath))) @@ -55,9 +56,20 @@ func (p *program) handleGetConfig(w http.ResponseWriter, r *http.Request) { func (p *program) handleUpdateConfig(w http.ResponseWriter, r *http.Request) { p.logger.Debug("Handling /api/config POST request") var newConfig Config - json.NewDecoder(r.Body).Decode(&newConfig) - // Write new config to file - // This will later be saved in a db + err := json.NewDecoder(r.Body).Decode(&newConfig) + if err != nil { + p.logger.Error(fmt.Sprintf("Failed to decode config: %v", err)) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + p.config = newConfig + err = p.db.UpdateConfig(&p.config) + if err != nil { + p.logger.Error(fmt.Sprintf("Failed to save config: %v", err)) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) } func (p *program) handleListScripts(w http.ResponseWriter, r *http.Request) { @@ -70,3 +82,14 @@ func (p *program) handleAddScript(w http.ResponseWriter, r *http.Request) { p.logger.Debug("Handling /api/scripts POST request") // Logic to add new powershell scripts } + +func (p *program) handleRestartService(w http.ResponseWriter, r *http.Request) { + p.logger.Debug("Handling /api/restart POST request") + err := p.restartService() + if err != nil { + p.logger.Error(fmt.Sprintf("Failed to restart service: %v", err)) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusOK) +} diff --git a/backend/models.go b/backend/models.go new file mode 100644 index 0000000..292945b --- /dev/null +++ b/backend/models.go @@ -0,0 +1,40 @@ +package main + +import ( + "time" +) + +type ConfigModel struct { + ID int64 `db:"id"` + BrokerAddress string `db:"broker_address"` + Username string `db:"username"` + Password string `db:"password"` + ClientID string `db:"client_id"` + Topic string `db:"topic"` + LogLevel string `db:"log_level"` + ScriptTimeout int `db:"script_timeout"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +type ScriptConfig struct { + ID int64 `db:"id" json:"id"` + Name string `db:"name" json:"name"` + ScriptPath string `db:"script_path" json:"script_path"` + RunAsUser bool `db:"run_as_user" json:"run_as_user"` + ScriptTimeout int `db:"script_timeout" json:"script_timeout"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} +type ScriptConfigs []ScriptConfig + +type SensorConfig struct { + ID int64 `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Enabled bool `db:"enabled" json:"enabled"` + Interval int `db:"interval" json:"interval"` + SensorTopic string `db:"sensor_topic" json:"sensor_topic"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} +type SensorConfigs []SensorConfig diff --git a/backend/mqtt.go b/backend/mqtt.go index 4461142..0476b9b 100644 --- a/backend/mqtt.go +++ b/backend/mqtt.go @@ -1,7 +1,6 @@ package main import ( - "encoding/json" "fmt" "path/filepath" "runtime/debug" @@ -83,29 +82,32 @@ func (p *program) publishResponse(client mqtt.Client, message string) { } func (p *program) publishSensorData() { - ticker := time.NewTicker(time.Duration(p.config.SensorConfig.Interval) * time.Second) - defer ticker.Stop() - - for range ticker.C { - sensorData, err := collectSensorData() - if err != nil { - p.logger.Error(fmt.Sprintf("Failed to collect sensor data: %v", err)) - continue - } - - jsonData, err := json.Marshal(sensorData) - if err != nil { - p.logger.Error(fmt.Sprintf("Failed to marshal sensor data: %v", err)) - continue - } - - token := p.mqttClient.Publish(p.config.SensorConfig.SensorTopic, 0, false, jsonData) - if token.Wait() && token.Error() != nil { - p.logger.Error(fmt.Sprintf("Failed to publish sensor data: %v", token.Error())) - } else { - p.logger.Debug("Successfully published sensor data") - } - } + // TODO: Uncomment this when sensors are implemented + // Must be able to manage multiple sensors independently + + // ticker := time.NewTicker(time.Duration(p.config.SensorConfig.Interval) * time.Second) + // defer ticker.Stop() + + // for range ticker.C { + // sensorData, err := collectSensorData() + // if err != nil { + // p.logger.Error(fmt.Sprintf("Failed to collect sensor data: %v", err)) + // continue + // } + + // jsonData, err := json.Marshal(sensorData) + // if err != nil { + // p.logger.Error(fmt.Sprintf("Failed to marshal sensor data: %v", err)) + // continue + // } + + // token := p.mqttClient.Publish(p.config.SensorConfig.SensorTopic, 0, false, jsonData) + // if token.Wait() && token.Error() != nil { + // p.logger.Error(fmt.Sprintf("Failed to publish sensor data: %v", token.Error())) + // } else { + // p.logger.Debug("Successfully published sensor data") + // } + // } } func (p *program) setupMQTTClient() { @@ -123,7 +125,7 @@ func (p *program) setupMQTTClient() { p.mqttClient = mqtt.NewClient(opts) - if p.config.SensorConfig.Enabled { + if p.config.SensorConfigEnabled { go p.publishSensorData() } } diff --git a/backend/service.go b/backend/service.go index 20cdd59..c4da773 100644 --- a/backend/service.go +++ b/backend/service.go @@ -21,6 +21,7 @@ type program struct { logger *Logger scriptDir string router *mux.Router + db *DB } func newProgram() (*program, error) { @@ -36,6 +37,20 @@ func newProgram() (*program, error) { return nil, fmt.Errorf("failed to get executable path: %v", err) } + // Init DB + p.db, err = NewDB() + if err != nil { + p.logger.Error(fmt.Sprintf("Failed to create database: %v", err)) + return nil, err + } + + // Init Schema + err = p.db.InitSchema(p.logger) + if err != nil { + p.logger.Error(fmt.Sprintf("Failed to initialize database schema: %v", err)) + return nil, err + } + // Set scripts directory p.scriptDir = filepath.Join(filepath.Dir(exePath), "scripts") @@ -152,3 +167,20 @@ func (p *program) executeScript(scriptPath string, runAsUser bool) (string, erro return p.runAsLocalSystem(scriptPath) } } + +func (p *program) restartService() error { + p.logger.Debug("Restarting service") + err := p.Stop(nil) + if err != nil { + return fmt.Errorf("failed to stop service: %v", err) + } + // clear mqtt client + p.mqttClient = nil + time.Sleep(time.Second * 5) + p.logger.Debug("Service stopped, restarting...") + err = p.Start(nil) + if err != nil { + return fmt.Errorf("failed to start service: %v", err) + } + return nil +} diff --git a/build_and_deploy.ps1 b/build_and_deploy.ps1 index 4220566..3de32c2 100644 --- a/build_and_deploy.ps1 +++ b/build_and_deploy.ps1 @@ -6,7 +6,7 @@ Stop-Service -Name "WinSenseConnect" -ErrorAction SilentlyContinue # Build the Go program Write-Host "Building the Go program..." Set-Location .\backend -go build -o ..\WinSenseConnect.exe +$env:CGO_ENABLED=1; go build -o ..\WinSenseConnect.exe if ($LASTEXITCODE -ne 0) { Write-Host "Build failed. Exiting." diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 8377154..829389c 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -5,4 +5,45 @@ html, body, #__nuxt { @apply text-slate-50 font-sans h-full; background: rgb(15, 23, 42); +} + +/* Input Styles */ +input, select { + @apply bg-slate-800 text-white rounded-md py-2 px-4 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50; +} + +.btn-primary { + @apply bg-primary-500 text-white font-semibold rounded-md py-2 px-8 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 focus:ring-offset-primary-50; +} + +.btn-secondary { + @apply bg-slate-200 text-slate-600 rounded-md py-2 px-4 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:ring-offset-slate-50; +} + +/* Form Control */ +.form-control { + @apply mb-4 flex flex-col gap-2; +} + +/* Table */ +.table { + @apply table-auto w-full text-left text-sm text-slate-500; +} +.table thead { + @apply text-xs font-semibold text-slate-400 uppercase bg-slate-800; +} +.table tbody tr { + @apply border-b border-slate-700; +} +.table tbody tr:last-child { + @apply border-b-0; +} +.table tbody td { + @apply py-2 px-4; +} +.table tbody td:first-child { + @apply pl-0; +} +.table tbody td:last-child { + @apply pr-0; } \ No newline at end of file diff --git a/frontend/components/Sidebar.vue b/frontend/components/Sidebar.vue index 40360be..2f9ab9b 100644 --- a/frontend/components/Sidebar.vue +++ b/frontend/components/Sidebar.vue @@ -9,7 +9,7 @@ Scripts - + MQTT diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index 3b492c0..bbc0345 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -5,7 +5,7 @@
-
+
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 33e23f8..a4ed710 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,16 +1,17 @@ { - "name": "nuxt-app", + "name": "mqtt-win-control", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "nuxt-app", + "name": "mqtt-win-control", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.12.1", "nuxt": "^3.13.0", "vue": "latest", - "vue-router": "latest" + "vue-router": "latest", + "vue3-toastify": "^0.2.2" } }, "node_modules/@alloc/quick-lru": { @@ -10725,6 +10726,28 @@ "vue": "^3.2.0" } }, + "node_modules/vue3-toastify": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/vue3-toastify/-/vue3-toastify-0.2.2.tgz", + "integrity": "sha512-D8pmIp2UeU8MU1OY7GktA70HviZ38b1RagN82P7tFu3abUD86w+PjfmbdRch4QVtjVxK+eqKLvi5cXJRndwJfw==", + "license": "MIT", + "workspaces": [ + "docs", + "playground" + ], + "engines": { + "node": ">=18.18.0", + "npm": ">=9.0.0" + }, + "peerDependencies": { + "vue": ">=3.2.0" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 27b1250..f3368e5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,6 +13,7 @@ "@nuxtjs/tailwindcss": "^6.12.1", "nuxt": "^3.13.0", "vue": "latest", - "vue-router": "latest" + "vue-router": "latest", + "vue3-toastify": "^0.2.2" } } diff --git a/frontend/pages/config/mqtt.vue b/frontend/pages/config/mqtt.vue index 41a40c8..a3511b6 100644 --- a/frontend/pages/config/mqtt.vue +++ b/frontend/pages/config/mqtt.vue @@ -1 +1,91 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/frontend/plugins/toast.client.js b/frontend/plugins/toast.client.js new file mode 100644 index 0000000..cc0bc6f --- /dev/null +++ b/frontend/plugins/toast.client.js @@ -0,0 +1,10 @@ +import Vue3Toastify, { toast } from 'vue3-toastify'; +import 'vue3-toastify/dist/index.css'; + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(Vue3Toastify, { autoClose: 2000 }); + + return { + provide: { toast }, + }; +}); \ No newline at end of file diff --git a/go.mod b/go.mod index e24f445..f4cfefd 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gorilla/websocket v1.5.3 // indirect github.com/kardianos/service v1.2.2 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/mattn/go-sqlite3 v1.14.24 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/rs/cors v1.11.1 // indirect github.com/shirou/gopsutil/v4 v4.24.9 // indirect diff --git a/go.sum b/go.sum index ee174a4..b620c6a 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=