This commit is contained in:
Bronku 2025-08-06 19:24:23 +02:00
parent 1ae02b3bc6
commit 8c640f7b6b
13 changed files with 37 additions and 303 deletions

View file

@ -1,84 +0,0 @@
package auth
import (
"errors"
"log"
"net/http"
"time"
"github.com/Bronku/iroon/crypto"
"github.com/Bronku/iroon/models"
"gorm.io/gorm"
)
type Config struct {
SessionExpiration time.Duration
}
type Authenticator struct {
db *gorm.DB
config Config
}
func New(db *gorm.DB, config Config) Authenticator {
err := db.AutoMigrate(&models.User{}, &models.Token{})
if err != nil {
log.Fatal("failed to initialize authenticator", err)
}
return Authenticator{db, config}
}
var ErrUserNotFound = errors.New("user with this login doesn't exist")
var ErrWrongCredentials = errors.New("wrong credentials")
// #todo only used once, maybe remove it entirely later
func (a *Authenticator) verifyCredentials(login, password string) error {
var user models.User
result := a.db.First(&user, "login = ?", login)
if result.Error != nil {
return ErrUserNotFound
}
hash := crypto.PasswordHash(password, user.Salt)
if hash == user.Password {
return nil
}
return ErrWrongCredentials
}
func (a *Authenticator) Middleware(inner http.Handler) http.Handler {
handler := http.NewServeMux()
handler.HandleFunc("GET /login", getLogin)
handler.HandleFunc("POST /login", a.login)
handler.HandleFunc("GET /logout", a.logout)
handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, err := a.getSession(r)
if err != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
inner.ServeHTTP(w, r)
})
return handler
}
var ErrUsernameTaken = errors.New("username taken")
func (a *Authenticator) AddUser(login, password string) error {
var user models.User
result := a.db.First(&user, "login = ?", login)
if !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return ErrUsernameTaken
}
salt := crypto.GenerateKey()
hash := crypto.PasswordHash(password, salt)
result = a.db.Save(&models.User{Login: login, Password: hash, Salt: salt})
return result.Error
}

View file

@ -1,17 +0,0 @@
package auth
import (
"fmt"
"net/http"
_ "embed"
)
//go:embed templates/login.html
var loginHTML string
// returns a simple login page
func getLogin(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("content-type", "text/html")
_, _ = fmt.Fprint(w, loginHTML)
}

View file

@ -1,81 +0,0 @@
package auth
import (
"errors"
"net/http"
"time"
"github.com/Bronku/iroon/crypto"
"github.com/Bronku/iroon/models"
)
var ErrNoCookie = errors.New("session cookie not found")
var ErrSessionNotFound = errors.New("session not found")
var ErrSessionExpired = errors.New("session has expired")
func (a *Authenticator) getSession(r *http.Request) (models.Token, error) {
cookie, err := r.Cookie("token")
if err != nil {
return models.Token{}, ErrNoCookie
}
var session models.Token
result := a.db.First(&session, "token = ?", cookie.Value)
if result.Error != nil {
return models.Token{}, ErrSessionNotFound
}
if time.Since(session.Expiration) > 0 {
a.db.Delete(&session)
return models.Token{}, ErrSessionExpired
}
return session, nil
}
func (a *Authenticator) login(w http.ResponseWriter, r *http.Request) {
_ = r.ParseForm()
login := r.PostFormValue("login")
password := r.PostFormValue("password")
if a.verifyCredentials(login, password) != nil {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
cookie := a.newSession(login)
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusFound)
}
func (a *Authenticator) logout(w http.ResponseWriter, r *http.Request) {
cookie := http.Cookie{
Name: "Token",
Value: "nil",
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
}
http.SetCookie(w, &cookie)
http.Redirect(w, r, "/", http.StatusFound)
c, err := r.Cookie("token")
if err != nil {
return
}
a.db.Delete(&models.Token{}, c.Value)
}
func (a *Authenticator) newSession(user string) http.Cookie {
key := crypto.GenerateKey()
session := models.Token{
User: user,
Expiration: time.Now().Add(a.config.SessionExpiration),
Token: key,
}
a.db.Create(&session)
cookie := http.Cookie{
Name: "token",
Value: key,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/",
// Secure: true, #todo
}
return cookie
}

View file

@ -1,16 +0,0 @@
<header>
<h1>Login</h1>
</header>
<main>
<form method="post">
<label>Login</label>
<label>
<input type="text" name="login" />
</label>
<label>Password</label>
<label>
<input type="password" name="password" />
</label>
<button type="submit">login</button>
</form>
</main>

View file

@ -1,11 +1,6 @@
[Database]
File = "foo.db"
[Authentication]
DefaultLogin = "admin"
DefaultPassword = "secret"
SessionExpiration = "24h"
[Server]
Addr = ":8080"
ReadHeaderTimeout = "3s"

View file

@ -1,25 +0,0 @@
package crypto
import (
"crypto/rand"
"encoding/base64"
"golang.org/x/crypto/argon2"
)
func PasswordHash(password, salt string) string {
var time uint32 = 3
var memory uint32 = 32 * 1024
var threads uint8 = 4
var length uint32 = 32
return string(argon2.Key([]byte(password), []byte(salt), time, memory, threads, length))
}
func GenerateKey() string {
key := [32]byte{}
// rand.Read always panics when encountering an error, so checking it is pointless, as the program has already panicked
_, _ = rand.Read(key[:])
return base64.StdEncoding.EncodeToString(key[:])
}

2
go.mod
View file

@ -12,8 +12,6 @@ require (
)
require (
golang.org/x/crypto v0.36.0
golang.org/x/sys v0.31.0 // indirect
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.30.0
)

4
go.sum
View file

@ -6,10 +6,6 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=

View file

@ -1,13 +0,0 @@
package logging
import (
"fmt"
"net/http"
)
func ErrorPage(err error, status int) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
_, _ = fmt.Fprint(w, err.Error())
}
}

View file

@ -1,15 +0,0 @@
package logging
import (
"log"
"net/http"
"time"
)
func Middleware(in http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
in.ServeHTTP(w, r)
log.Println(r.Method, r.URL.String(), time.Since(start))
})
}

38
main.go
View file

@ -2,20 +2,12 @@ package main
import (
_ "embed"
"errors"
"log"
"net/http"
"os"
"time"
"github.com/Bronku/iroon/auth"
"github.com/Bronku/iroon/logging"
"github.com/Bronku/iroon/models"
"github.com/Bronku/iroon/server"
"github.com/BurntSushi/toml"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type config struct {
@ -23,12 +15,6 @@ type config struct {
File string
}
Authentication struct {
auth.Config
DefaultLogin string
DefaultPassword string
}
Server struct {
Addr string
ReadHeaderTimeout time.Duration
@ -51,34 +37,14 @@ func main() {
}
// load database
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
IgnoreRecordNotFoundError: true},
)
db, err := gorm.Open(sqlite.Open(conf.Database.File), &gorm.Config{
Logger: newLogger,
})
db, err := store.loadStore(conf.Database.File)
if err != nil {
log.Fatal("failed to connect database")
}
err = db.AutoMigrate(&models.Order{}, &models.OrderItem{}, &models.Product{})
if err != nil {
log.Fatal("couldn't migrate database")
}
// load auth
authenticator := auth.New(db, conf.Authentication.Config)
err = authenticator.AddUser(conf.Authentication.DefaultLogin, conf.Authentication.DefaultPassword)
if err != nil && !errors.Is(err, auth.ErrUsernameTaken) {
log.Println("add user:", err)
log.Panic(err)
}
// load server
h := server.New(db)
var handler http.Handler = h
handler = authenticator.Middleware(handler)
handler = logging.Middleware(handler)
log.Println("starting server")
// start server

View file

@ -2,11 +2,10 @@ package server
import (
"embed"
"fmt"
"html/template"
"log"
"net/http"
"github.com/Bronku/iroon/logging"
)
//go:embed templates/*
@ -28,14 +27,14 @@ func (h *Server) loadTemplates() {
func (h *Server) render(fetch fetcher, templateFile string, templateEntry string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
data, code, err := fetch(r)
data, _, err := fetch(r)
if err != nil {
logging.ErrorPage(err, code).ServeHTTP(w, r)
fmt.Fprint(w, err)
return
}
err = h.tmpl[templateFile].ExecuteTemplate(w, templateEntry, data)
if err != nil {
logging.ErrorPage(err, http.StatusInternalServerError).ServeHTTP(w, r)
fmt.Fprint(w, err)
return
}
w.Header().Set("content-type", "text/html")

31
store/store.go Normal file
View file

@ -0,0 +1,31 @@
package store
import (
"fmt"
"log"
"os"
"github.com/Bronku/iroon/models"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func loadStore(dsn string) (*gorm.DB, error) {
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
IgnoreRecordNotFoundError: true},
)
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
return db, fmt.Errorf("error connecting to database %w", err)
}
err = db.AutoMigrate(&models.Order{}, &models.OrderItem{}, &models.Product{})
if err != nil {
return db, fmt.Errorf("error migrating database %w", err)
}
return db, nil
}