Simple and fast dashboard with a go backend and a mostly HTML+CSS frontend with a small amount of JS.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

154 lines
4.5 KiB

Copyright 2021 Arsen Musayelyan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
See the License for the specific language governing permissions and
limitations under the License.
package main
import (
flag ""
// Set global logger to ConsoleWriter
var Log = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
type App struct {
Route *mux.Router
Templates map[string]*template.Template
Session *gormstore.Store
Config Conf
// Create new empty map to store templates
var templates = map[string]*template.Template{}
func main() {
// Create command-line flags
addr := flag.IPP("addr", "a", net.ParseIP(""), "Bind address for HTTP server")
port := flag.IntP("port", "p", 8080, "Bind port for HTTP server")
config := flag.StringP("config", "c", "simpledash.toml", "TOML config file")
hash := flag.String("hash", "", "Generate new bcrypt password hash")
flag.ErrHelp = errors.New("simpledash: help requested")
// Parse flags
if *hash != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(*hash), bcrypt.DefaultCost)
if err != nil {
Log.Fatal().Err(err).Msg("Error creating bcrypt hash")
// Create new router
router := mux.NewRouter().StrictSlash(true)
// Create OS-specific glob for all templates
path := filepath.Join("resources", "templates", "*.html")
// Create OS-specific glob for all card templates
cardGlob := filepath.Join("resources", "templates", "cards", "*.html")
// Get all template paths
tmplMatches, _ := filepath.Glob(path)
cardMatches, _ := filepath.Glob(cardGlob)
matches := append(tmplMatches, cardMatches...)
// For each template path
for _, match := range matches {
// Get name of file without path or extension
fileName := strings.TrimSuffix(filepath.Base(match), filepath.Ext(match))
// If file is called base
if fileName == "base" {
// Skip
var err error
// Parse detected template and base template, add to templates map
templates[fileName], err = template.New(
"resources/templates/base.html", match)
if err != nil {
Log.Fatal().Str("template", fileName).Err(err).Msg("Error parsing template")
// Open sqlite database called sessions.db for storing sessions
sessionDB, _ := gorm.Open(sqlite.Open("sessions.db"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
// Create session store from database
sessionStore := gormstore.New(sessionDB, []byte(""))
// Create channel to stop periodic cleanup
quitCleanup := make(chan struct{})
// Clean up expired sessions every hour
go sessionStore.PeriodicCleanup(1*time.Hour, quitCleanup)
// Open config file
configFile, err := os.Open(filepath.Clean(*config))
if err != nil {
Log.Fatal().Err(err).Msg("Error opening config file")
// Create new TOML decoder
dec := toml.NewDecoder(configFile)
// Create new nil variable to store decoded config
var decodedConf Conf
// Decode config into variable
err = dec.Decode(&decodedConf)
if err != nil {
Log.Fatal().Err(err).Msg("Error decoding config file")
// Register HTTP routes
Route: router,
Templates: templates,
Session: sessionStore,
Config: decodedConf,
// Create string address from flag values
strAddr := fmt.Sprint(*addr, ":", *port)
// Create listener on IPv4 using address created above
ln, err := net.Listen("tcp4", strAddr)
if err != nil {
Log.Fatal().Err(err).Msg("Error creating listener")
// Log HTTP server start
Log.Info().Str("addr", strAddr).Msg("Starting HTTP server")
// Start HTTP server using previously-created router and listener
err = http.Serve(ln, router)
if err != nil {
Log.Fatal().Err(err).Msg("Error while serving")