Compare commits

...
10 Commits
Author SHA1 Message Date
odonax 93ca9ea33b Added session cookie and logout for admin 2026-05-20 15:57:09 +02:00
odonax 4d53a6b471 Added skeleton for basic admin auth system 2026-05-16 17:01:34 +02:00
odonax d5c4cfa9d0 Switch configuration file from json to toml 2026-04-30 06:53:36 +02:00
odonax 9f6c5b2880 Delete another temp file that shouldn't exist 2026-04-30 06:14:54 +02:00
odonax 814949bfb0 Get rid of a swap file which does not belong 2026-04-29 22:09:07 +02:00
odonax 64e5c26bc0 Implemented configuration file functionality
For site name, port number and data directory
2026-04-29 22:08:23 +02:00
odonax 267383a45b Updated gitignore to ignore configuration file 2026-04-29 22:07:30 +02:00
odonax 9dbc6dc414 Added database initialization 2026-04-29 21:50:44 +02:00
odonax 19e66e93d6 Swapped 'project' with 'listing' in database
Done for clarity
2026-04-29 21:48:29 +02:00
odonax 7a5e6a8e79 Updated gitignore to ignore Kate's backup files 2026-04-29 21:47:44 +02:00
14 changed files with 344 additions and 16 deletions
+4
View File
@@ -1 +1,5 @@
data/ data/
config.toml
*~
*.kate-swp
.*.kate-swp
BIN
View File
Binary file not shown.
+21
View File
@@ -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 == ""
}
+69
View File
@@ -0,0 +1,69 @@
package auth
import (
"crypto/rand"
"encoding/hex"
"net/http"
"sync"
"time"
)
var (
sessions = map[string]time.Time{}
mu sync.Mutex
)
func GenerateSession() (string, error) {
bytes := make([]byte, 32)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
token := hex.EncodeToString(bytes)
mu.Lock()
sessions[token] = time.Now()
mu.Unlock()
return token, nil
}
func IsLoggedIn(r *http.Request) bool {
cookie, err := r.Cookie("session")
if err != nil {
return false
}
mu.Lock()
_, exists := sessions[cookie.Value]
mu.Unlock()
return exists
}
func SetSessionCookie(w http.ResponseWriter, token string) {
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: token,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
})
}
func ClearSession(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err == nil {
mu.Lock()
delete(sessions, cookie.Value)
mu.Unlock()
}
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
})
}
+57
View File
@@ -0,0 +1,57 @@
package config
import (
"log"
"os"
"github.com/BurntSushi/toml"
)
type Config struct {
SiteName string `toml:"site_name"`
Port string `toml:"port"`
DataDir string `toml:"data_dir"`
AdminUser string `toml:"admin_user"`
AdminPasswordHash string `toml:"admin_password_hash"`
}
func Load(path string) Config {
configData := Config{
SiteName: "Unnamed Download Listing",
Port: ":8080",
DataDir: "data",
}
// Overwrite the default configuration (configData) with data from the file at 'path'
_, err := toml.DecodeFile(path, &configData)
if err != nil {
log.Println("No config file found, using defaults. Consider setting up a Site Name at the very least!")
}
return configData
}
func Save(path string, configData Config) error {
file, err := os.Create(path)
if err != nil {
log.Println("An error occured while trying to save configuration data.")
return err
}
defer file.Close()
file.WriteString("site_name = \"" + configData.SiteName + "\"\n")
file.WriteString("port = \"" + configData.Port + "\"\n")
file.WriteString("data_dir = \"" + configData.DataDir + "\"\n")
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\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")
file.WriteString("# Your uploaded content will stay intact.\n")
}
return nil
}
Binary file not shown.
+7 -7
View File
@@ -23,7 +23,7 @@ func Open(path string) *sql.DB {
func createTables(db *sql.DB) { func createTables(db *sql.DB) {
query := ` query := `
CREATE TABLE IF NOT EXISTS project ( CREATE TABLE IF NOT EXISTS listing (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE,
@@ -35,13 +35,13 @@ func createTables(db *sql.DB) {
CREATE TABLE IF NOT EXISTS release ( CREATE TABLE IF NOT EXISTS release (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL, listing_id INTEGER NOT NULL,
version TEXT NOT NULL, version TEXT NOT NULL,
title TEXT, title TEXT,
changelog TEXT, changelog TEXT,
is_latest INTEGER DEFAULT 0, is_latest INTEGER DEFAULT 0,
published_at DATETIME DEFAULT CURRENT_TIMESTAMP, published_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (project_id) REFERENCES project(id) FOREIGN KEY (listing_id) REFERENCES listing(id)
); );
CREATE TABLE IF NOT EXISTS asset ( CREATE TABLE IF NOT EXISTS asset (
@@ -59,21 +59,21 @@ func createTables(db *sql.DB) {
CREATE TABLE IF NOT EXISTS page ( CREATE TABLE IF NOT EXISTS page (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL, listing_id INTEGER NOT NULL,
slug TEXT NOT NULL, slug TEXT NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
body TEXT, body TEXT,
sort_order INTEGER DEFAULT 0, sort_order INTEGER DEFAULT 0,
FOREIGN KEY (project_id) REFERENCES project(id) FOREIGN KEY (listing_id) REFERENCES listing(id)
); );
CREATE TABLE IF NOT EXISTS screenshot ( CREATE TABLE IF NOT EXISTS screenshot (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL, listing_id INTEGER NOT NULL,
filename TEXT NOT NULL, filename TEXT NOT NULL,
caption TEXT, caption TEXT,
sort_order INTEGER DEFAULT 0, sort_order INTEGER DEFAULT 0,
FOREIGN KEY (project_id) REFERENCES project(id) FOREIGN KEY (listing_id) REFERENCES listing(id)
); );
` `
BIN
View File
Binary file not shown.
+6 -1
View File
@@ -2,4 +2,9 @@ module 192.168.1.180/odonax/filehost-athome
go 1.26.2 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
+4
View File
@@ -1,2 +1,6 @@
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 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ= 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=
+95
View File
@@ -0,0 +1,95 @@
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
}
token, err := auth.GenerateSession()
if err != nil {
tmpl.Execute(w, map[string]string{
"SiteName": Cfg.SiteName,
"Error": "Something went wrong. Please try again.",
})
return
}
auth.SetSessionCookie(w, token)
http.Redirect(w, r, "/admin", http.StatusSeeOther)
}
}
func Logout() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth.ClearSession(w, r)
http.Redirect(w, r, "/", http.StatusSeeOther)
}
}
+30 -8
View File
@@ -4,22 +4,44 @@ import (
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
"os"
"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() { func main() {
tmpl, err := template.ParseFiles("templates/home.html") cfgPath := "config.toml"
if err != nil { cfg := config.Load(cfgPath)
log.Fatal("Failed to load template:", err)
} handlers.Cfg = &cfg
handlers.CfgPath = cfgPath
os.MkdirAll(cfg.DataDir+"/files", 0755)
db := database.Open(cfg.DataDir+"/hub.db")
defer db.Close()
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) { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
data := map[string]string{ data := map[string]string{
"Name": "Filehost at home", "Name": cfg.SiteName,
"Description": "This is where you'll own your files and share them freely.", "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") http.HandleFunc("/setup", handlers.Setup(tmpl.Lookup("setup.html")))
log.Fatal(http.ListenAndServe(":8080", nil)) http.HandleFunc("/login", handlers.Login(tmpl.Lookup("login.html")))
http.HandleFunc("/logout", handlers.Logout())
log.Printf("Starting filehost on %s\n", cfg.Port)
log.Fatal(http.ListenAndServe(cfg.Port, nil))
} }
+23
View File
@@ -0,0 +1,23 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log in - {{ .SiteName }}</title>
</head>
<body>
<h1>Log in</h1>
{{ if .Error }}
<p><strong>{{ .Error }}</strong></p>
{{ end }}
<form method="POST" action="/login">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
<button type="submit">Log in</button>
</form>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Set up your filehost</title>
</head>
<body>
<h1>Welcome! It's time to get you set up.</h1>
<p>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.</p>
<p>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.</p>
{{ if .Error }}
<p><strong>{{ .Error }}</strong></p>
{{ end }}
<form method="POST" action="/setup">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
<label for="confirm">Confirm password</label>
<input type="password" id="confirm" name="confirm" required>
<button type="submit">Create account</button>
</form>
</body>
</html>