From bf466b26ac93afc55fa4f23e320521e46ff0761c Mon Sep 17 00:00:00 2001
From: Zoe <62722391+juls0730@users.noreply.github.com>
Date: Wed, 9 Apr 2025 04:48:38 -0500
Subject: [PATCH] Better docs, bug fixes, and config structure
The project now uses a more sensible config structure to allow for
actual defaults. Passport now has NO reliance on javascript whatsoever,
and can be used identically without javascript enabled.
---
.env.example | 3 +-
README.md | 42 +++++++---
go.mod | 1 +
go.sum | 2 +
main.go | 166 ++++++++++++++++++++++++++++----------
styles/main.css | 4 +-
templates/views/index.hbs | 33 +++-----
zqdgr.config.json | 4 +-
8 files changed, 174 insertions(+), 81 deletions(-)
diff --git a/.env.example b/.env.example
index d802a0c..c28ea21 100644
--- a/.env.example
+++ b/.env.example
@@ -1,7 +1,8 @@
+PASSPORT_ENABLE_WEATHER=true
OPENWEATHER_API_KEY=1234567890
OPENWEATHER_LAT=34.052235
OPENWEATHER_LON=-118.243683
OPENWEATHER_UPDATE_INTERVAL=15
PASSPORT_ADMIN_USERNAME=admin
PASSPORT_ADMIN_PASSWORD=P@ssw0rd
-PASSPORT_SEARCH_PROVIDER=https://google.com/search?q=%s
+PASSPORT_SEARCH_PROVIDER=https://google.com/search
\ No newline at end of file
diff --git a/README.md b/README.md
index 1242328..82d7066 100644
--- a/README.md
+++ b/README.md
@@ -15,29 +15,45 @@ Passport is a simple, fast, and lightweight web dashboard/new tab replacement.
- [sqlite3](https://www.sqlite.org/download.html)
- [TailwdinCSS CLI](https://github.com/tailwindlabs/tailwindcss/releases/latest)
-### Usage
+## Usage
1. Clone the repository
2. Configure the `.env` file, an example is provided in the `.env.example` file, see below for every available environment variable
4. Deploy `passport` to your web server
5. profit
-#### Configuration
+### Configuration
-| Environment Variable | Description | Required | Default |
+#### Passport configuration
+
+| Environment Variable | Description | Required | Default |
| --- | --- | --- | --- |
-| `PASSPORT_DEV_MODE` | Enables dev mode | false | false |
-| `PASSPORT_ENABLE_WEATHER` | Enables weather data, requires an OpenWeather API key | false | true |
-| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, requires an UptimeRobot API key | false | true |
+| `PASSPORT_DEV_MODE` | Enables dev mode | false | false |
+| `PASSPORT_ENABLE_PREFORK` | Enables preforking | false | false |
+| `PASSPORT_ENABLE_WEATHER` | Enables weather data, see [Weather configuration](#weather-configuration) | false | false |
+| `PASSPORT_ENABLE_UPTIME` | Enables uptime data, see [Uptime configuration](#uptime-configuration) | false | false |
| `PASSPORT_ADMIN_USERNAME` | The username for the admin dashboard | true |
| `PASSPORT_ADMIN_PASSWORD` | The password for the admin dashboard | true |
-| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar | true |
-| `OPENWEATHER_API_KEY` | The OpenWeather API key | if enabled |
-| `OPENWEATHER_LAT` | The latitude of your location | if enabled |
-| `OPENWEATHER_LON` | The longitude of your location | if enabled |
-| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
-| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | if enabled |
-| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
+| `PASSPORT_SEARCH_PROVIDER` | The search provider to use for the search bar, without any query parameters | true |
+| `PASSPORT_SEARCH_PROVIDER_QUERY_PARAM` | The query parameter to use for the search provider, e.g. `q` for most providers |false | q |
+
+#### Weather configuration
+
+| Environment Variable | Description | Required | Default |
+| --- | --- | --- | --- |
+| `OPENWEATHER_PROVIDER` | The weather provider to use, currently only `openweathermap` is supported | true | openweathermap |
+| `OPENWEATHER_API_KEY` | The OpenWeather API key | if enabled | |
+| `OPENWEATHER_TEMP_UNITS` | The temperature units to use, either `metric` or `imperial` | false | metric |
+| `OPENWEATHER_LAT` | The latitude of your location | if enabled | |
+| `OPENWEATHER_LON` | The longitude of your location | if enabled | |
+| `OPENWEATHER_UPDATE_INTERVAL` | The interval in minutes to update the weather data | false | 15 |
+
+#### Uptime configuration
+
+| Environment Variable | Description | Required | Default |
+| --- | --- | --- | --- |
+| `UPTIMEROBOT_API_KEY` | The UptimeRobot API key | if enabled | |
+| `UPTIMEROBOT_UPDATE_INTERVAL` | The interval in seconds to update the uptime data | false | 300 |
### Adding links and categories
diff --git a/go.mod b/go.mod
index 11423fe..a8ed7a8 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/juls0730/passport
go 1.23.2
require (
+ github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/gofiber/schema v1.2.0 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
diff --git a/go.sum b/go.sum
index 4e1a827..fe828c0 100644
--- a/go.sum
+++ b/go.sum
@@ -2,6 +2,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
+github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/main.go b/main.go
index c623ff6..37d4c03 100644
--- a/main.go
+++ b/main.go
@@ -20,11 +20,11 @@ import (
"net/http"
"os"
"path/filepath"
- "strconv"
"strings"
"sync"
"time"
+ "github.com/caarlos0/env/v11"
"github.com/chai2010/webp"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/helmet"
@@ -67,14 +67,88 @@ var (
insertLinkStmt *sql.Stmt
)
+type WeatherProvider string
+
+const (
+ OpenWeatherMap WeatherProvider = "openweathermap"
+)
+
+type WeatherConfig struct {
+ Provider WeatherProvider `env:"OPENWEATHER_PROVIDER" envDefault:"openweathermap"`
+ OpenWeather struct {
+ APIKey string `env:"OPENWEATHER_API_KEY"`
+ Units string `env:"OPENWEATHER_TEMP_UNITS" envDefault:"metric"`
+ Lat float64 `env:"OPENWEATHER_LAT"`
+ Lon float64 `env:"OPENWEATHER_LON"`
+ }
+ UpdateInterval int `env:"OPENWEATHER_UPDATE_INTERVAL" envDefault:"15"`
+}
+
+type UptimeConfig struct {
+ APIKey string `env:"UPTIMEROBOT_API_KEY"`
+ UpdateInterval int `env:"UPTIMEROBOT_UPDATE_INTERVAL" envDefault:"300"`
+}
+
+type Config struct {
+ DevMode bool `env:"PASSPORT_DEV_MODE" envDefault:"false"`
+ Prefork bool `env:"PASSPORT_ENABLE_PREFORK" envDefault:"false"`
+
+ WeatherEnabled bool `env:"PASSPORT_ENABLE_WEATHER" envDefault:"false"`
+ Weather *WeatherConfig
+
+ UptimeEnabled bool `env:"PASSPORT_ENABLE_UPTIME" envDefault:"false"`
+ Uptime *UptimeConfig
+
+ Admin struct {
+ Username string `env:"PASSPORT_ADMIN_USERNAME"`
+ Password string `env:"PASSPORT_ADMIN_PASSWORD"`
+ }
+
+ SearchProvider struct {
+ URL string `env:"PASSPORT_SEARCH_PROVIDER"`
+ Query string `env:"PASSPORT_SEARCH_PROVIDER_QUERY_PARAM" envDefault:"q"`
+ }
+}
+
+func ParseConfig() (*Config, error) {
+ config := Config{}
+
+ err := env.Parse(&config)
+ if err != nil {
+ return nil, err
+ }
+
+ if config.WeatherEnabled {
+ config.Weather = &WeatherConfig{}
+ if err := env.Parse(config.Weather); err != nil {
+ return nil, err
+ }
+ }
+
+ if config.UptimeEnabled {
+ config.Uptime = &UptimeConfig{}
+ if err := env.Parse(config.Uptime); err != nil {
+ return nil, err
+ }
+ }
+
+ return &config, nil
+}
+
type App struct {
- *WeatherCache
+ *Config
*CategoryManager
+ *WeatherCache
*UptimeManager
db *sql.DB
}
func NewApp(dbPath string) (*App, error) {
+ config, err := ParseConfig()
+ if err != nil {
+ return nil, err
+ }
+
db, err := sql.Open("sqlite3", dbPath)
if err != nil {
return nil, err
@@ -96,16 +170,17 @@ func NewApp(dbPath string) (*App, error) {
}
var weatherCache *WeatherCache
- if os.Getenv("PASSPORT_ENABLE_WEATHER") != "false" {
- weatherCache = NewWeatherCache()
+ if config.WeatherEnabled {
+ weatherCache = NewWeatherCache(config.Weather)
}
var uptimeManager *UptimeManager
- if os.Getenv("PASSPORT_ENABLE_UPTIME") != "false" {
- uptimeManager = NewUptimeManager()
+ if config.UptimeEnabled {
+ uptimeManager = NewUptimeManager(config.Uptime)
}
return &App{
+ Config: config,
WeatherCache: weatherCache,
CategoryManager: categoryManager,
UptimeManager: uptimeManager,
@@ -127,21 +202,21 @@ type UptimeManager struct {
apiKey string
}
-func NewUptimeManager() *UptimeManager {
- if os.Getenv("UPTIMEROBOT_API_KEY") == "" {
+func NewUptimeManager(config *UptimeConfig) *UptimeManager {
+ if config.APIKey == "" {
log.Fatalln("UptimeRobot API Key is required!")
return nil
}
- updateInterval, err := strconv.Atoi(os.Getenv("UPTIMEROBOT_UPDATE_INTERVAL"))
- if err != nil || updateInterval < 1 {
+ updateInterval := config.UpdateInterval
+ if updateInterval < 1 {
updateInterval = 300
}
uptimeManager := &UptimeManager{
updateChan: make(chan struct{}),
updateInterval: updateInterval,
- apiKey: os.Getenv("UPTIMEROBOT_API_KEY"),
+ apiKey: config.APIKey,
sites: []UptimeRobotSite{},
}
@@ -228,22 +303,27 @@ type WeatherCache struct {
tempUnits string
updateInterval int
apiKey string
- lat string
- lon string
+ lat float64
+ lon float64
}
-func NewWeatherCache() *WeatherCache {
- if os.Getenv("OPENWEATHER_API_KEY") == "" || os.Getenv("OPENWEATHER_LAT") == "" || os.Getenv("OPENWEATHER_LON") == "" {
- log.Fatalln("OpenWeather API Key, and your latitude and longitude are required!")
+func NewWeatherCache(config *WeatherConfig) *WeatherCache {
+ if config.Provider != OpenWeatherMap {
+ log.Fatalln("Only OpenWeatherMap is supported!")
return nil
}
- updateInterval, err := strconv.Atoi(os.Getenv("OPENWEATHER_UPDATE_INTERVAL"))
- if err != nil || updateInterval < 1 {
+ if config.OpenWeather.APIKey == "" {
+ log.Fatalln("An API Key required for OpenWeather!")
+ return nil
+ }
+
+ updateInterval := config.UpdateInterval
+ if updateInterval < 1 {
updateInterval = 15
}
- units := os.Getenv("OPENWEATHER_TEMP_UNITS")
+ units := config.OpenWeather.Units
if units == "" {
units = "metric"
}
@@ -253,9 +333,9 @@ func NewWeatherCache() *WeatherCache {
updateChan: make(chan struct{}),
tempUnits: units,
updateInterval: updateInterval,
- apiKey: os.Getenv("OPENWEATHER_API_KEY"),
- lat: os.Getenv("OPENWEATHER_LAT"),
- lon: os.Getenv("OPENWEATHER_LON"),
+ apiKey: config.OpenWeather.APIKey,
+ lat: config.OpenWeather.Lat,
+ lon: config.OpenWeather.Lon,
}
go cache.weatherWorker()
@@ -286,7 +366,7 @@ func (c *WeatherCache) weatherWorker() {
}
func (c *WeatherCache) updateWeather() {
- url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%s&lon=%s&appid=%s&units=%s",
+ url := fmt.Sprintf("https://api.openweathermap.org/data/2.5/weather?lat=%f&lon=%f&appid=%s&units=%s",
c.lat, c.lon, c.apiKey, c.tempUnits)
resp, err := http.Get(url)
@@ -549,17 +629,17 @@ func (manager *CategoryManager) DeleteLink(id any) error {
}
var WeatherIcons = map[string]string{
- "clear-day": ``,
- "clear-night": ``,
- "partly-cloudy-day": ``,
- "partly-cloudy-night": ``,
- "mostly-cloudy-day": ``,
- "mostly-cloudy-night": ``,
- "light-rain": ``,
- "rain": ``,
- "thunder": ``,
- "snow": ``,
- "mist": ``,
+ "clear-day": ``,
+ "clear-night": ``,
+ "partly-cloudy-day": ``,
+ "partly-cloudy-night": ``,
+ "mostly-cloudy-day": ``,
+ "mostly-cloudy-night": ``,
+ "light-rain": ``,
+ "rain": ``,
+ "thunder": ``,
+ "snow": ``,
+ "mist": ``,
}
func getWeatherIcon(iconId string) string {
@@ -593,7 +673,7 @@ func getWeatherIcon(iconId string) string {
func init() {
if err := godotenv.Load(); err != nil {
- fmt.Println("No .env file found")
+ fmt.Println("No .env file found, using default values")
}
}
@@ -630,7 +710,7 @@ func main() {
})
engine.AddFunc("devContent", func() string {
- if os.Getenv("PASSPORT_DEV_MODE") == "true" {
+ if app.Config.DevMode {
return devContent
}
return ""
@@ -658,11 +738,12 @@ func main() {
router.Get("/", func(c fiber.Ctx) error {
renderData := fiber.Map{
- "SearchProvider": os.Getenv("PASSPORT_SEARCH_PROVIDER"),
- "Categories": app.CategoryManager.Categories,
+ "SearchProviderURL": app.Config.SearchProvider.URL,
+ "SearchParam": app.Config.SearchProvider.Query,
+ "Categories": app.CategoryManager.Categories,
}
- if os.Getenv("PASSPORT_ENABLE_WEATHER") != "false" {
+ if app.Config.WeatherEnabled {
weather := app.WeatherCache.GetWeather()
renderData["WeatherData"] = fiber.Map{
@@ -672,7 +753,7 @@ func main() {
}
}
- if os.Getenv("PASSPORT_ENABLE_UPTIME") != "false" {
+ if app.Config.UptimeEnabled {
renderData["UptimeData"] = app.UptimeManager.getUptime()
}
@@ -702,7 +783,8 @@ func main() {
return err
}
- if loginData.Username != os.Getenv("PASSPORT_ADMIN_USERNAME") || loginData.Password != os.Getenv("PASSPORT_ADMIN_PASSWORD") {
+ // possible vulnerable to timing attacks
+ if loginData.Username != app.Config.Admin.Username || loginData.Password != app.Config.Admin.Password {
return c.Status(http.StatusUnauthorized).JSON(fiber.Map{"message": "Invalid username or password"})
}
@@ -942,6 +1024,6 @@ func main() {
}
router.Listen(":3000", fiber.ListenConfig{
- EnablePrefork: os.Getenv("PASSPORT_ENABLE_PREFORK") == "true",
+ EnablePrefork: app.Config.Prefork,
})
}
diff --git a/styles/main.css b/styles/main.css
index 71503d6..c328a8e 100644
--- a/styles/main.css
+++ b/styles/main.css
@@ -7,7 +7,7 @@
}
:root {
- --default-font-family: "Instrument Sans", ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
+ --default-font-family: "Instrument Sans", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
html {
@@ -25,7 +25,7 @@ h6 {
}
h1 {
- font-size: clamp(48px, 10vw, 64px);
+ font-size: clamp(42px, 10vw, 64px);
}
h2 {
diff --git a/templates/views/index.hbs b/templates/views/index.hbs
index b5e8142..adb3ef8 100644
--- a/templates/views/index.hbs
+++ b/templates/views/index.hbs
@@ -1,7 +1,7 @@
-
+
- {{#if WeatherData}}
+ {{#if WeatherData}}
{{{WeatherData.Icon}}}
@@ -11,10 +11,10 @@
{{WeatherData.Desc}}
- {{/if}}
+ {{/if}}
- {{#if UptimeData}}
+ {{#if UptimeData}}
{{#each UptimeData}}
@@ -33,12 +33,12 @@
{{/each}}
- {{/if}}
+ {{/if}}
-
Passport
-
+
@@ -89,15 +91,4 @@
{{/each}}
-
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/zqdgr.config.json b/zqdgr.config.json
index c9901a9..c9a84c1 100644
--- a/zqdgr.config.json
+++ b/zqdgr.config.json
@@ -1,6 +1,6 @@
{
"name": "passport",
- "version": "0.0.1",
+ "version": "0.2.0",
"description": "Passport is a simple, lightweight, and fast dashboard/new tab page for your browser.",
"author": "juls0730",
"license": "BSL-1.0",
@@ -13,5 +13,5 @@
"dev": "go generate; PASSPORT_DEV_MODE=true go run main.go",
"build": "go generate && go build -tags netgo,prod -o passport"
},
- "pattern": "**/*.go,templates/views/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}"
+ "pattern": "**/*.go,templates/views/**/*.hbs,styles/**/*.css,assets/**/*.{svg,png,jpg,jpeg,webp,woff2,ttf,otf,eot,ico,gif,webp}"
}
\ No newline at end of file