From 3d9c7109858635781240032568853ac4572dd1c8 Mon Sep 17 00:00:00 2001 From: Daniel Eder Date: Mon, 17 Feb 2025 19:53:43 +0100 Subject: [PATCH] authentication with username & password --- go.mod | 9 ++- go.sum | 4 + main.go | 183 ++++++++++++++++++++++++++++++++++++++++--- templates/login.html | 18 +++++ 4 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 templates/login.html diff --git a/go.mod b/go.mod index fb562ff..03c0c3e 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,13 @@ module tms -go 1.22.0 +go 1.23 + +toolchain go1.23.6 + +require golang.org/x/crypto v0.33.0 require ( + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect github.com/mattn/go-sqlite3 v1.14.24 // indirect - golang.org/x/crypto v0.33.0 ) diff --git a/go.sum b/go.sum index c2beb89..88c0aad 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= diff --git a/main.go b/main.go index c7f938f..f6093a6 100644 --- a/main.go +++ b/main.go @@ -5,9 +5,11 @@ import ( "embed" "fmt" "html/template" + "log" "net/http" "time" + "github.com/gorilla/sessions" "golang.org/x/crypto/bcrypt" _ "github.com/mattn/go-sqlite3" @@ -19,6 +21,9 @@ var templatesFS embed.FS //go:embed schema.sql var schemaSQL string +// Store for sessions (adjust the key to something secure) +var store = sessions.NewCookieStore([]byte("your-secret-key")) + type Trainer struct { ID int Name string @@ -64,13 +69,15 @@ func initDB() { var err error db, err = sql.Open("sqlite3", "training.db") if err != nil { - panic(err) + log.Fatalf("Error opening database: %v", err) } _, err = db.Exec(schemaSQL) if err != nil { - panic(err) + log.Fatalf("Error initializing database schema: %v", err) } + + log.Println("Database initialized successfully") } var tmpl *template.Template @@ -79,26 +86,33 @@ func initTemplates() { var err error tmpl, err = template.ParseFS(templatesFS, "templates/*.html") if err != nil { - panic(fmt.Sprintf("Error parsing templates: %v", err)) + log.Fatalf("Error parsing templates: %v", err) } + + log.Println("Templates loaded successfully") } func homeHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Handling home page request") err := tmpl.ExecuteTemplate(w, "home.html", nil) if err != nil { + log.Printf("Error in templating page home.html: %v", err) http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) } } func trainingAreaListHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Fetching training areas and trainers") areas, err := fetchTrainingAreasWithTrainers() if err != nil { + log.Printf("Error fetching training areas: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } trainers, err := fetchTrainers() if err != nil { + log.Printf("Error fetching trainers: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } @@ -113,12 +127,16 @@ func trainingAreaListHandler(w http.ResponseWriter, r *http.Request) { err = tmpl.ExecuteTemplate(w, "training_area_list.html", data) if err != nil { + log.Printf("Error in templating page training_area_list.html: %v", err) http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) return } + + log.Println("Training areas and trainers displayed successfully") } func fetchTrainingAreasWithTrainers() ([]TrainingArea, error) { + log.Println("Executing query to fetch training areas with trainers") rows, err := db.Query(` SELECT ta.id, ta.name, t.id, t.name, t.email FROM training_areas ta @@ -126,6 +144,7 @@ func fetchTrainingAreasWithTrainers() ([]TrainingArea, error) { LEFT JOIN trainers t ON tta.trainer_id = t.id ORDER BY ta.id, t.name`) if err != nil { + log.Printf("Database error while fetching training areas: %v", err) return nil, err } defer rows.Close() @@ -141,6 +160,7 @@ func fetchTrainingAreasWithTrainers() ([]TrainingArea, error) { err := rows.Scan(&areaID, &areaName, &trainerID, &trainerName, &trainerEmail) if err != nil { + log.Printf("Error scanning row for training area: %v", err) return nil, err } @@ -164,55 +184,67 @@ func fetchTrainingAreasWithTrainers() ([]TrainingArea, error) { areas = append(areas, *area) } + log.Printf("Found %d training areas", len(areas)) return areas, nil } func assignTrainerToAreaHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Handling trainer assignment to training area") if r.Method == http.MethodPost { trainerID := r.FormValue("trainer_id") areaID := r.FormValue("training_area_id") if trainerID == "" || areaID == "" { + log.Println("Missing trainer or training area ID") http.Error(w, "Trainer and Training Area are required", http.StatusBadRequest) return } _, err := db.Exec("INSERT OR IGNORE INTO trainer_training_areas (trainer_id, training_area_id) VALUES (?, ?)", trainerID, areaID) if err != nil { + log.Printf("Error assigning trainer to area: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } + log.Println("Trainer assigned to area successfully") http.Redirect(w, r, "/training-areas", http.StatusSeeOther) } } func addTrainingAreaHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Handling request to add a new training area") if r.Method == http.MethodPost { name := r.FormValue("name") if name == "" { + log.Println("Training area name is required") http.Error(w, "Name is required", http.StatusBadRequest) return } _, err := db.Exec("INSERT INTO training_areas (name) VALUES (?)", name) if err != nil { + log.Printf("Error adding training area: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } + log.Println("New training area added successfully") http.Redirect(w, r, "/training-areas", http.StatusSeeOther) return } err := tmpl.ExecuteTemplate(w, "add_training_area.html", nil) if err != nil { + log.Printf("Error in templating page add_training_area.html: %v", err) http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) } } func fetchTrainings() ([]Training, error) { + log.Println("Fetching trainings from the database") rows, err := db.Query("SELECT id, title, training_type, mode, start, end, status FROM trainings") if err != nil { + log.Printf("Error fetching trainings: %v", err) return nil, err } defer rows.Close() @@ -223,31 +255,39 @@ func fetchTrainings() ([]Training, error) { var start, end string err := rows.Scan(&t.ID, &t.Title, &t.TrainingType, &t.Mode, &start, &end, &t.Status) if err != nil { + log.Printf("Error scanning row for training: %v", err) return nil, err } t.Start, _ = time.Parse("2006-01-02 15:04:05", start) t.End, _ = time.Parse("2006-01-02 15:04:05", end) trainings = append(trainings, t) } + + log.Printf("Found %d trainings", len(trainings)) return trainings, nil } func trainingListHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Handling training list request") trainings, err := fetchTrainings() if err != nil { + log.Printf("Error fetching trainings: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } err = tmpl.ExecuteTemplate(w, "training_list.html", trainings) if err != nil { + log.Printf("Error in templating page training_list.html: %v", err) http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) } } func fetchTrainers() ([]Trainer, error) { + log.Println("Fetching trainers from the database") rows, err := db.Query("SELECT id, name, email FROM trainers") if err != nil { + log.Printf("Error fetching trainers: %v", err) return nil, err } defer rows.Close() @@ -257,52 +297,63 @@ func fetchTrainers() ([]Trainer, error) { var t Trainer err := rows.Scan(&t.ID, &t.Name, &t.Email) if err != nil { + log.Printf("Error scanning row for trainer: %v", err) return nil, err } trainers = append(trainers, t) } + + log.Printf("Found %d trainers", len(trainers)) return trainers, nil } func trainerListHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Handling trainer list request") trainers, err := fetchTrainers() if err != nil { + log.Printf("Error fetching trainers: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } err = tmpl.ExecuteTemplate(w, "trainer_list.html", trainers) if err != nil { + log.Printf("Error in templating page trainer_list.html: %v", err) http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) } } func addTrainerHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Handling request to add a new trainer") if r.Method == http.MethodPost { name := r.FormValue("name") email := r.FormValue("email") if name == "" || email == "" { + log.Println("Trainer name and email are required") http.Error(w, "Name and Email are required", http.StatusBadRequest) return } _, err := db.Exec("INSERT INTO trainers (name, email) VALUES (?, ?)", name, email) if err != nil { + log.Printf("Error adding trainer: %v", err) http.Error(w, "Database error", http.StatusInternalServerError) return } + log.Println("New trainer added successfully") http.Redirect(w, r, "/trainers", http.StatusSeeOther) return } err := tmpl.ExecuteTemplate(w, "add_trainer.html", nil) if err != nil { + log.Printf("Error in templating page add_trainer.html: %v", err) http.Error(w, fmt.Sprintf("Error in templating page %v", err), http.StatusInternalServerError) } - } func hashPassword(password string) (string, error) { + log.Println("Hashing password") bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(bytes), err } @@ -367,13 +418,114 @@ func redirectIfNoAdmin(next http.Handler) http.Handler { http.Redirect(w, r, "/setup-admin", http.StatusSeeOther) return } + + session, err := store.Get(r, "user-session") + if err != nil || session.Values["username"] == nil { + http.Redirect(w, r, "/login", http.StatusSeeOther) + return + } + next.ServeHTTP(w, r) }) } -func main() { - initDB() - initTemplates() +func checkPasswordHash(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) + return err == nil +} + +// / loginHandler handles the user login +func loginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Show the login page + log.Println("Rendering login page.") + err := tmpl.ExecuteTemplate(w, "login.html", nil) + if err != nil { + log.Println("Error rendering login template:", err) + http.Error(w, "Template rendering error", http.StatusInternalServerError) + } + return + } + + if r.Method == http.MethodPost { + username := r.FormValue("username") + password := r.FormValue("password") + + log.Printf("Received login attempt for username: %s", username) + + // Query the admin table for the password hash + var storedHash string + err := db.QueryRow("SELECT password FROM admins WHERE email = $1", username).Scan(&storedHash) + if err != nil { + if err == sql.ErrNoRows { + // User doesn't exist + log.Println("Invalid username or password: no matching user found.") + http.Error(w, "Invalid username or password", http.StatusUnauthorized) + } else { + // Database error + log.Println("Error querying database:", err) + http.Error(w, "Database error", http.StatusInternalServerError) + } + return + } + + // Check if the provided password matches the stored hash + if !checkPasswordHash(password, storedHash) { + log.Println("Invalid username or password: password mismatch.") + http.Error(w, "Invalid username or password", http.StatusUnauthorized) + return + } + + // Password is correct; set up a session + session, err := store.Get(r, "user-session") + if err != nil { + log.Println("Error creating session:", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // Store username in the session + session.Values["username"] = username + err = session.Save(r, w) + if err != nil { + log.Println("Error saving session:", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + log.Printf("User '%s' successfully logged in.", username) + + // Redirect to the home page (or dashboard) + http.Redirect(w, r, "/", http.StatusSeeOther) + } +} + +// logoutHandler logs the user out by destroying their session +func logoutHandler(w http.ResponseWriter, r *http.Request) { + log.Println("Logging out user.") + session, err := store.Get(r, "user-session") + if err != nil { + log.Println("Error fetching session:", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + // Clear the session + session.Options.MaxAge = -1 + err = session.Save(r, w) + if err != nil { + log.Println("Error saving session:", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + log.Println("User successfully logged out.") + + // Redirect to the login page or home page + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +func initServerHandlers() { http.Handle("/", redirectIfNoAdmin(http.HandlerFunc(homeHandler))) http.Handle("/trainings", redirectIfNoAdmin(http.HandlerFunc(trainingListHandler))) http.Handle("/trainers", redirectIfNoAdmin(http.HandlerFunc(trainerListHandler))) @@ -383,7 +535,18 @@ func main() { http.Handle("/training-areas/assign-trainer", redirectIfNoAdmin(http.HandlerFunc(assignTrainerToAreaHandler))) http.HandleFunc("/setup-admin", setupAdminHandler) - - fmt.Println("Server running on http://localhost:8080") - http.ListenAndServe(":8080", nil) + http.HandleFunc("/login", loginHandler) + http.HandleFunc("/logout", logoutHandler) +} + +func main() { + initDB() + initTemplates() + initServerHandlers() + + log.Println("Starting server on :8080") + err := http.ListenAndServe(":8080", nil) + if err != nil { + log.Fatalf("Error starting server: %v", err) + } } diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..7db5e1a --- /dev/null +++ b/templates/login.html @@ -0,0 +1,18 @@ + + +{{template "head.html" .}} + + +{{template "header.html" .}} +

Please log in to continue

+
+ +

+ + +

+ + +
+ +