diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..539f42f --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,21 @@ +package auth + +import ( + "golang.org/x/crypto/bcrypt" +) + +func HashPassword(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hash), nil +} + +func CheckPassword(hash, password string) bool { + return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil +} + +func NeedsSetup(adminUser string) bool { + return adminUser == "" +} diff --git a/config/config.go b/config/config.go index 3b1cf7f..4393541 100644 --- a/config/config.go +++ b/config/config.go @@ -18,7 +18,7 @@ type Config struct { func Load(path string) Config { configData := Config{ SiteName: "Unnamed Download Listing", - Port: "8080", + Port: ":8080", DataDir: "data", } @@ -46,7 +46,7 @@ func Save(path string, configData Config) error { if configData.AdminUser != "" { file.WriteString("admin_user = \"" + configData.AdminUser + "\"\n") file.WriteString("admin_password_hash = \"" + configData.AdminPasswordHash + "\"\n") - file.WriteString("# This admin_password_hash above is a hashed password, not your actual\"") + file.WriteString("# This admin_password_hash above is a hashed password, not your actual\n") file.WriteString("# written password. You can't change your admin password here.\n") file.WriteString("# If you've lost your password, delete admin_user and admin_hash\n") file.WriteString("# from this file and restart the app to create a new account.\n") diff --git a/filehost-athome b/filehost-athome new file mode 100755 index 0000000..7373929 Binary files /dev/null and b/filehost-athome differ diff --git a/go.mod b/go.mod index 79e2773..c6b729b 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module 192.168.1.180/odonax/filehost-athome go 1.26.2 -require github.com/mattn/go-sqlite3 v1.14.44 +require ( + github.com/mattn/go-sqlite3 v1.14.44 + golang.org/x/crypto v0.51.0 +) -require github.com/BurntSushi/toml v1.6.0 // indirect +require github.com/BurntSushi/toml v1.6.0 diff --git a/go.sum b/go.sum index e5adf48..e1a878a 100644 --- a/go.sum +++ b/go.sum @@ -2,3 +2,5 @@ github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8= github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= diff --git a/handlers/auth.go b/handlers/auth.go new file mode 100644 index 0000000..37bf03a --- /dev/null +++ b/handlers/auth.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "html/template" + "net/http" + + "192.168.1.180/odonax/filehost-athome/auth" + "192.168.1.180/odonax/filehost-athome/config" +) + +var Cfg *config.Config +var CfgPath string + +func Setup(tmpl *template.Template) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if !auth.NeedsSetup(Cfg.AdminUser) { + http.Redirect(w, r, "/", http.StatusSeeOther) + return + } + + if r.Method == "GET" { + tmpl.Execute(w, nil) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + confirm := r.FormValue("confirm") + + if username == "" || password == "" { + tmpl.Execute(w, map[string]string{"Error": "Please fill in all fields."}) + return + } + + if password != confirm { + tmpl.Execute(w, map[string]string{"Error": "Passwords do not match."}) + return + } + + hash, err := auth.HashPassword(password) + if err != nil { + tmpl.Execute(w, map[string]string{"Error": "Something went wrong. Refresh the page and try again. If the issue persists, contact the developer of this application and tell them what happened."}) + return + } + + Cfg.AdminUser = username + Cfg.AdminPasswordHash = hash + config.Save(CfgPath, *Cfg) + + http.Redirect(w, r, "/admin", http.StatusSeeOther) + } +} + +func Login(tmpl *template.Template) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if auth.NeedsSetup(Cfg.AdminUser) { + http.Redirect(w, r, "/setup", http.StatusSeeOther) + return + } + + if r.Method == "GET" { + tmpl.Execute(w, map[string]string{"SiteName": Cfg.SiteName}) + return + } + + username := r.FormValue("username") + password := r.FormValue("password") + + if username != Cfg.AdminUser || !auth.CheckPassword(Cfg.AdminPasswordHash, password) { + tmpl.Execute(w, map[string]string{ + "SiteName": Cfg.SiteName, + "Error": "Invalid credentials.", + }) + return + } + + // TODO: create a session so the admin stays logged in + http.Redirect(w, r, "/admin", http.StatusSeeOther) + } +} diff --git a/main.go b/main.go index 9129385..bd6576b 100644 --- a/main.go +++ b/main.go @@ -8,29 +8,39 @@ import ( "192.168.1.180/odonax/filehost-athome/config" "192.168.1.180/odonax/filehost-athome/database" + "192.168.1.180/odonax/filehost-athome/handlers" ) func main() { - cfg := config.Load("config.toml") + cfgPath := "config.toml" + cfg := config.Load(cfgPath) + + handlers.Cfg = &cfg + handlers.CfgPath = cfgPath os.MkdirAll(cfg.DataDir+"/files", 0755) db := database.Open(cfg.DataDir+"/hub.db") defer db.Close() - tmpl, err := template.ParseFiles("templates/home.html") - if err != nil { - log.Fatal("Failed to load template:", err) - } + tmpl := template.Must(template.ParseGlob("templates/*.html")) + + // tmpl, err := template.ParseFiles("templates/home.html") + // if err != nil { + // log.Fatal("Failed to load template:", err) + // } http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { data := map[string]string{ "Name": cfg.SiteName, "Description": "This is where you'll own your files and share them freely.", } - tmpl.Execute(w, data) + tmpl.ExecuteTemplate(w, "home.html", data) }) - log.Println("Starting filehost on :8080") - log.Fatal(http.ListenAndServe(":8080", nil)) + http.HandleFunc("/setup", handlers.Setup(tmpl.Lookup("setup.html"))) + http.HandleFunc("/login", handlers.Login(tmpl.Lookup("login.html"))) + + log.Printf("Starting filehost on %s\n", cfg.Port) + log.Fatal(http.ListenAndServe(cfg.Port, nil)) } diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..8f78bfd --- /dev/null +++ b/templates/login.html @@ -0,0 +1,23 @@ + + + + + + Log in - {{ .SiteName }} + + +

Log in

+ {{ if .Error }} +

{{ .Error }}

+ {{ end }} +
+ + + + + + + +
+ + diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 0000000..812262b --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,28 @@ + + + + + + Set up your filehost + + +

Welcome! It's time to get you set up.

+

Create your admin account to start managing your site. Consider picking a username different than 'admin' or your standard internet alias to make it harder for people to guess your admin credentials.

+

If you ever lose your credentials or your admin account is compromised, you can reset it by editing the 'config.toml' file and removing the credentials there. You can then recreate your admin account without losing your listings.

+ {{ if .Error }} +

{{ .Error }}

+ {{ end }} +
+ + + + + + + + + + +
+ +