First pass of authentication

This commit is contained in:
Leon Mika 2026-02-25 22:04:47 +11:00
parent c943864edc
commit 01c6e9de87
15 changed files with 311 additions and 42 deletions

View file

@ -1,6 +1,7 @@
root = "." root = "."
testdata_dir = "testdata" testdata_dir = "testdata"
tmp_dir = "build/tmp" tmp_dir = "build/tmp"
env_files = [".env"]
[build] [build]
args_bin = [] args_bin = []

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ node_modules/
static/assets/ static/assets/
# Local Netlify folder # Local Netlify folder
.netlify .netlify
.env

26
config/config.go Normal file
View file

@ -0,0 +1,26 @@
package config
import (
"fmt"
"github.com/Netflix/go-env"
)
type Config struct {
DataDir string `env:"DATA_DIR"`
SiteDomain string `env:"SITE_DOMAIN"`
LoginLocked bool `env:"LOGIN_LOCKED,default=false"`
Env string `env:"ENV,default=prod"`
}
func LoadConfig() (Config, error) {
cfg := Config{}
if _, err := env.UnmarshalFromEnviron(&cfg); err != nil {
return Config{}, fmt.Errorf("failed to load config: %w", err)
}
return cfg, nil
}
func (c Config) IsProd() bool {
return c.Env != "dev"
}

19
go.mod
View file

@ -18,6 +18,7 @@ require (
github.com/Azure/go-autorest/autorest/date v0.2.0 // indirect github.com/Azure/go-autorest/autorest/date v0.2.0 // indirect
github.com/Azure/go-autorest/logger v0.1.0 // indirect github.com/Azure/go-autorest/logger v0.1.0 // indirect
github.com/Azure/go-autorest/tracing v0.5.0 // indirect github.com/Azure/go-autorest/tracing v0.5.0 // indirect
github.com/Netflix/go-env v0.1.2 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
@ -37,21 +38,23 @@ require (
github.com/go-openapi/swag v0.19.12 // indirect github.com/go-openapi/swag v0.19.12 // indirect
github.com/go-openapi/validate v0.20.0 // indirect github.com/go-openapi/validate v0.20.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-stack/stack v1.8.0 // indirect
github.com/gofiber/fiber/v3 v3.0.0 // indirect github.com/gofiber/fiber/v3 v3.1.0 // indirect
github.com/gofiber/schema v1.6.0 // indirect github.com/gofiber/schema v1.7.0 // indirect
github.com/gofiber/storage/sqlite3/v2 v2.2.3 // indirect
github.com/gofiber/template v1.8.3 // indirect github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/template/html/v3 v3.0.2 // indirect github.com/gofiber/template/html/v3 v3.0.2 // indirect
github.com/gofiber/template/v2 v2.1.0 // indirect github.com/gofiber/template/v2 v2.1.0 // indirect
github.com/gofiber/utils/v2 v2.0.0 // indirect github.com/gofiber/utils/v2 v2.0.2 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.3 // indirect
github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect github.com/lmika/gopkgs v0.0.0-20240408110817-a02f6fc67d1f // indirect
github.com/mailru/easyjson v0.7.6 // indirect github.com/mailru/easyjson v0.7.6 // indirect
github.com/matoous/go-nanoid/v2 v2.1.0 // indirect github.com/matoous/go-nanoid/v2 v2.1.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.33 // indirect
github.com/mitchellh/mapstructure v1.4.0 // indirect github.com/mitchellh/mapstructure v1.4.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/netlify/open-api/v2 v2.49.1 // indirect github.com/netlify/open-api/v2 v2.49.1 // indirect
@ -67,11 +70,11 @@ require (
go.mongodb.org/mongo-driver v1.4.4 // indirect go.mongodb.org/mongo-driver v1.4.4 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.50.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.67.6 // indirect modernc.org/libc v1.67.6 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect

23
go.sum
View file

@ -31,6 +31,8 @@ github.com/Azure/go-autorest/tracing v0.5.0 h1:TRn4WjSnkcSy5AEG3pnbtFSwNtwzjr4VY
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Netflix/go-env v0.1.2 h1:0DRoLR9lECQ9Zqvkswuebm3jJ/2enaDX6Ei8/Z+EnK0=
github.com/Netflix/go-env v0.1.2/go.mod h1:WlIhYi++8FlKNJtrop1mjXYAJMzv1f43K4MqCoh0yGE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
@ -207,8 +209,14 @@ github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/V
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw= github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/gofiber/fiber/v3 v3.0.0 h1:GPeCG8X60L42wLKrzgeewDHBr6pE6veAvwaXsqD3Xjk= github.com/gofiber/fiber/v3 v3.0.0 h1:GPeCG8X60L42wLKrzgeewDHBr6pE6veAvwaXsqD3Xjk=
github.com/gofiber/fiber/v3 v3.0.0/go.mod h1:kVZiO/AwyT5Pq6PgC8qRCJ+j/BHrMy5jNw1O9yH38aY= github.com/gofiber/fiber/v3 v3.0.0/go.mod h1:kVZiO/AwyT5Pq6PgC8qRCJ+j/BHrMy5jNw1O9yH38aY=
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY= github.com/gofiber/schema v1.6.0 h1:rAgVDFwhndtC+hgV7Vu5ItQCn7eC2mBA4Eu1/ZTiEYY=
github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s= github.com/gofiber/schema v1.6.0/go.mod h1:WNZWpQx8LlPSK7ZaX0OqOh+nQo/eW2OevsXs1VZfs/s=
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/storage/sqlite3/v2 v2.2.3 h1:m3n80wUewnB5ruAV3Qq0mzIS+bwBrYYETo4N+fvBoow=
github.com/gofiber/storage/sqlite3/v2 v2.2.3/go.mod h1:F1w9BpQtU7BD5cCjlQnFIEjWHUaAcm9Hh5fuCpfG/OE=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc= github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8= github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v3 v3.0.2 h1:/Fh8UcEsB4uhf1QWNbYaAOwXxSORebJ2zXkb5tgG/TI= github.com/gofiber/template/html/v3 v3.0.2 h1:/Fh8UcEsB4uhf1QWNbYaAOwXxSORebJ2zXkb5tgG/TI=
@ -217,6 +225,8 @@ github.com/gofiber/template/v2 v2.1.0 h1:vrLY6uEW2HdioJm6J5FGUpYZuapVQhHciNz21XQ
github.com/gofiber/template/v2 v2.1.0/go.mod h1:ohgpR/Ng90nJbK+IyNzrgR/XpnBNt862/oTF5G7SAmE= github.com/gofiber/template/v2 v2.1.0/go.mod h1:ohgpR/Ng90nJbK+IyNzrgR/XpnBNt862/oTF5G7SAmE=
github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM= github.com/gofiber/utils/v2 v2.0.0 h1:SCC3rpsEDWupFSHtc0RKxg/BKgV0s1qKfZg9Jv6D0sM=
github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE= github.com/gofiber/utils/v2 v2.0.0/go.mod h1:xF9v89FfmbrYqI/bQUGN7gR8ZtXot2jxnZvmAUtiavE=
github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=
github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
@ -302,6 +312,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
@ -335,6 +347,8 @@ github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stg
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
@ -481,6 +495,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -535,6 +551,8 @@ golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -582,6 +600,8 @@ golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -595,6 +615,8 @@ golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -629,6 +651,7 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

62
handlers/login.go Normal file
View file

@ -0,0 +1,62 @@
package handlers
import (
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/session"
"lmika.dev/lmika/weiro/config"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/services/auth"
)
type LoginHandler struct {
Config config.Config
AuthService *auth.Service
}
func (lh *LoginHandler) Login(c fiber.Ctx) error {
if lh.Config.LoginLocked {
return c.Status(fiber.StatusForbidden).SendString("Login is locked")
}
loginChallenge := models.NewNanoID()
sess := session.FromContext(c)
sess.Set("_login_challenge", loginChallenge)
c.Render("login/login", fiber.Map{
"challenge": loginChallenge,
}, "layouts/bare")
return nil
}
func (lh *LoginHandler) DoLogin(c fiber.Ctx) error {
var req struct {
Username string `form:"username"`
Password string `form:"password"`
LoginChallenge string `form:"_login_challenge"`
}
if req.Username == "" || req.Password == "" {
return c.Status(fiber.StatusBadRequest).SendString("Username and password are required")
}
sess := session.FromContext(c)
challenge, _ := sess.Get("_login_challenge").(string)
if challenge == req.LoginChallenge {
return c.Redirect().To("/login")
}
user, err := lh.AuthService.Login(c.Context(), req.Username, req.Password)
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to login")
}
if err := sess.Regenerate(); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to login")
}
sess.Set("user_id", user.ID)
return c.Redirect().To("/")
}

View file

@ -1,24 +1,27 @@
package middleware package middleware
import ( import (
"log"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/session"
"lmika.dev/lmika/weiro/models" "lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/services/auth"
) )
func AuthUser() func(c fiber.Ctx) error { func AuthUser(auth *auth.Service) func(c fiber.Ctx) error {
return func(c fiber.Ctx) error { return func(c fiber.Ctx) error {
// TEMP - Actually do the auth here sess := session.FromContext(c)
user := models.User{ userID, _ := sess.Get("user_id").(int64)
ID: 1, if userID == 0 {
Username: "testuser", return c.Redirect().To("/login")
TimeZone: "Australia/Melbourne", }
user, err := auth.GetUser(c.Context(), userID)
if err != nil {
return c.Redirect().To("/login")
} }
c.Locals("user", user) c.Locals("user", user)
c.SetContext(models.WithUser(c.Context(), user)) c.SetContext(models.WithUser(c.Context(), user))
log.Printf("User %s authenticated", user.Username)
return c.Next() return c.Next()
} }

72
main.go
View file

@ -5,46 +5,47 @@ import (
"html" "html"
"html/template" "html/template"
"log" "log"
"path/filepath"
"strings" "strings"
"time"
"github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/extractors"
"github.com/gofiber/fiber/v3/middleware/session"
"github.com/gofiber/fiber/v3/middleware/static" "github.com/gofiber/fiber/v3/middleware/static"
"github.com/gofiber/storage/sqlite3/v2"
fiber_html "github.com/gofiber/template/html/v3" fiber_html "github.com/gofiber/template/html/v3"
"github.com/gofiber/utils/v2"
"github.com/yuin/goldmark" "github.com/yuin/goldmark"
"github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/extension"
"lmika.dev/lmika/weiro/config"
"lmika.dev/lmika/weiro/handlers" "lmika.dev/lmika/weiro/handlers"
"lmika.dev/lmika/weiro/handlers/middleware" "lmika.dev/lmika/weiro/handlers/middleware"
"lmika.dev/lmika/weiro/providers/db" "lmika.dev/lmika/weiro/providers/db"
"lmika.dev/lmika/weiro/services/auth"
"lmika.dev/lmika/weiro/services/posts" "lmika.dev/lmika/weiro/services/posts"
"lmika.dev/lmika/weiro/services/publisher" "lmika.dev/lmika/weiro/services/publisher"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
) )
func main() { func main() {
dbp, err := db.New("build/weiro.db") cfg, err := config.LoadConfig()
if err != nil {
log.Fatal(err)
}
dbp, err := db.New(filepath.Join(cfg.DataDir, "weiro.db"))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer dbp.Close() defer dbp.Close()
authSvc := auth.New(dbp)
publisherSvc := publisher.New(dbp) publisherSvc := publisher.New(dbp)
publisherQueue := publisher.NewQueue(publisherSvc) publisherQueue := publisher.NewQueue(publisherSvc)
publisherQueue.Start(context.Background()) publisherQueue.Start(context.Background())
postService := posts.New(dbp, publisherQueue) postService := posts.New(dbp, publisherQueue)
//user, err := dbp.SelectUserByUsername(context.Background(), "testuser")
//if err != nil {
// user = models.User{
// Username: "testuser",
// PasswordHashed: []byte("changeme"),
// }
// if err := dbp.SaveUser(context.Background(), &user); err != nil {
// log.Fatal(err)
// }
//}
fiberTemplate := fiber_html.New("./views", ".html") fiberTemplate := fiber_html.New("./views", ".html")
fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y } fiberTemplate.Funcmap["sub"] = func(x, y int) int { return x - y }
fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML { fiberTemplate.Funcmap["markdown"] = func() func(s string) template.HTML {
@ -60,16 +61,57 @@ func main() {
} }
}() }()
// Initialize custom config
store := sqlite3.New(sqlite3.Config{
Database: filepath.Join(cfg.DataDir, "./fiber.db"),
Table: "fiber_storage",
Reset: false,
GCInterval: 10 * time.Second,
MaxOpenConns: 100,
MaxIdleConns: 100,
ConnMaxLifetime: 1 * time.Second,
})
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
Views: fiberTemplate, Views: fiberTemplate,
ViewsLayout: "layouts/main", ViewsLayout: "layouts/main",
PassLocalsToViews: true, PassLocalsToViews: true,
}) })
app.Use(session.New(session.Config{
// Storage
Storage: store,
siteGroup := app.Group("/sites/:siteID", middleware.AuthUser(), middleware.RequiresSite(dbp)) // Security
CookieSecure: cfg.IsProd(),
CookieSameSite: "Lax",
// Session Management
IdleTimeout: 24 * time.Hour, // Inactivity timeout
AbsoluteTimeout: 7 * 24 * time.Hour, // Maximum session duration
// Cookie Settings
CookiePath: "/",
CookieDomain: cfg.SiteDomain,
CookieSessionOnly: false, // Persist across browser restarts
// Session ID
Extractor: extractors.FromCookie("__wro-session_id"),
KeyGenerator: utils.SecureToken,
// Error Handling
ErrorHandler: func(c fiber.Ctx, err error) {
log.Printf("Session error: %v", err)
},
}))
lh := handlers.LoginHandler{Config: cfg, AuthService: authSvc}
ph := handlers.PostsHandler{PostService: postService} ph := handlers.PostsHandler{PostService: postService}
app.Get("/login", lh.Login)
app.Post("/login", lh.Login)
siteGroup := app.Group("/sites/:siteID", middleware.AuthUser(authSvc), middleware.RequiresSite(dbp))
siteGroup.Get("/posts", ph.Index) siteGroup.Get("/posts", ph.Index)
siteGroup.Get("/posts/new", ph.New) siteGroup.Get("/posts/new", ph.New)
siteGroup.Get("/posts/:postID", ph.Edit) siteGroup.Get("/posts/:postID", ph.Edit)

View file

@ -1,6 +1,10 @@
package models package models
import "time" import (
"time"
"golang.org/x/crypto/bcrypt"
)
type User struct { type User struct {
ID int64 ID int64
@ -9,6 +13,16 @@ type User struct {
TimeZone string TimeZone string
} }
func (u *User) SetPassword(pwd string) {
bcrypted, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
u.PasswordHashed = bcrypted
}
func (u User) CheckPassword(pwd string) bool {
err := bcrypt.CompareHashAndPassword(u.PasswordHashed, []byte(pwd))
return err == nil
}
func (u User) FormatTime(t time.Time) string { func (u User) FormatTime(t time.Time) string {
if loc := getLocation(u.TimeZone); loc != nil { if loc := getLocation(u.TimeZone); loc != nil {
return t.In(loc).Format("2006-01-02 15:04:05") return t.In(loc).Format("2006-01-02 15:04:05")

View file

@ -25,8 +25,19 @@ func (q *Queries) InsertUser(ctx context.Context, arg InsertUserParams) (int64,
return id, err return id, err
} }
const selectUserByID = `-- name: SelectUserByID :one
SELECT id, username, password FROM users WHERE id = ? LIMIT 1
`
func (q *Queries) SelectUserByID(ctx context.Context, id int64) (User, error) {
row := q.db.QueryRowContext(ctx, selectUserByID, id)
var i User
err := row.Scan(&i.ID, &i.Username, &i.Password)
return i, err
}
const selectUserByUsername = `-- name: SelectUserByUsername :one const selectUserByUsername = `-- name: SelectUserByUsername :one
SELECT id, username, password FROM users WHERE username = ? SELECT id, username, password FROM users WHERE username = ? LIMIT 1
` `
func (q *Queries) SelectUserByUsername(ctx context.Context, username string) (User, error) { func (q *Queries) SelectUserByUsername(ctx context.Context, username string) (User, error) {

View file

@ -14,16 +14,16 @@ func (db *Provider) SelectUserByUsername(ctx context.Context, username string) (
return models.User{}, err return models.User{}, err
} }
pwdBytes, err := base64.StdEncoding.DecodeString(res.Password) return dbUserToUser(res)
}
func (db *Provider) SelectUserByID(ctx context.Context, userID int64) (models.User, error) {
res, err := db.queries.SelectUserByID(ctx, userID)
if err != nil { if err != nil {
return models.User{}, err return models.User{}, err
} }
return models.User{ return dbUserToUser(res)
ID: res.ID,
Username: res.Username,
PasswordHashed: pwdBytes,
}, nil
} }
func (db *Provider) SaveUser(ctx context.Context, user *models.User) error { func (db *Provider) SaveUser(ctx context.Context, user *models.User) error {
@ -47,3 +47,16 @@ func (db *Provider) SaveUser(ctx context.Context, user *models.User) error {
Password: hashedPassword, Password: hashedPassword,
}) })
} }
func dbUserToUser(res sqlgen.User) (models.User, error) {
pwdBytes, err := base64.StdEncoding.DecodeString(res.Password)
if err != nil {
return models.User{}, err
}
return models.User{
ID: res.ID,
Username: res.Username,
PasswordHashed: pwdBytes,
}, nil
}

36
services/auth/service.go Normal file
View file

@ -0,0 +1,36 @@
package auth
import (
"context"
"emperror.dev/errors"
"lmika.dev/lmika/weiro/models"
"lmika.dev/lmika/weiro/providers/db"
)
type Service struct {
db *db.Provider
}
func New(db *db.Provider) *Service {
return &Service{
db: db,
}
}
func (s *Service) Login(ctx context.Context, username, password string) (models.User, error) {
user, err := s.db.SelectUserByUsername(ctx, username)
if err != nil {
return models.User{}, err
}
if !user.CheckPassword(password) {
return models.User{}, errors.New("invalid password")
}
return user, nil
}
func (s *Service) GetUser(ctx context.Context, userID int64) (models.User, error) {
return s.db.SelectUserByID(ctx, userID)
}

View file

@ -1,5 +1,8 @@
-- name: SelectUserByUsername :one -- name: SelectUserByUsername :one
SELECT * FROM users WHERE username = ?; SELECT * FROM users WHERE username = ? LIMIT 1;
-- name: SelectUserByID :one
SELECT * FROM users WHERE id = ? LIMIT 1;
-- name: InsertUser :one -- name: InsertUser :one
INSERT INTO users (username, password) VALUES (?, ?) RETURNING id; INSERT INTO users (username, password) VALUES (?, ?) RETURNING id;

12
views/layouts/bare.html Normal file
View file

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Weiro</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/assets/main.css">
</head>
<body class="min-vh-100 d-flex flex-column">
{{ embed }}
</body>
</html>

19
views/login/login.html Normal file
View file

@ -0,0 +1,19 @@
<div class="mx-auto p-2" style="width: 400px; margin-block-start: 100px;">
<div class="text-center mb-3">
<h3>Weiro Login</h3>
</div>
<input type="hidden" name="_login_challenge" value="{{ .challenge }}">
<form action="/login" method="post">
<div class="mb-2">
<label for="login_username" class="form-label">Login</label>
<input type="email" class="form-control" id="login_username">
</div>
<div class="mb-3">
<label for="login_password" class="form-label">Password</label>
<input type="password" class="form-control" id="login_password">
</div>
<div class="mb-3 text-end">
<input type="submit" class="btn btn-primary" value="Login">
</div>
</form>
</div>