feat: allow password change
All checks were successful
ci/woodpecker/tag/release Pipeline was successful

This commit is contained in:
Felipe M 2025-04-21 15:44:45 +02:00
parent ece8280358
commit 6aedfc794f
Signed by: fmartingr
GPG key ID: CCFBC5637D4000A8
4 changed files with 203 additions and 22 deletions

View file

@ -2,6 +2,8 @@ package admin
import ( import (
"embed" "embed"
"encoding/gob"
"fmt"
"html/template" "html/template"
"net/http" "net/http"
"strconv" "strconv"
@ -28,6 +30,11 @@ type FlashMessage struct {
Message string Message string
} }
func init() {
// Register the FlashMessage type with gob package for session serialization
gob.Register(FlashMessage{})
}
// TemplateData holds data for rendering templates // TemplateData holds data for rendering templates
type TemplateData struct { type TemplateData struct {
User *model.User User *model.User
@ -52,8 +59,13 @@ type Admin struct {
// New creates a new Admin instance // New creates a new Admin instance
func New(cfg *config.Config, database *db.Database) *Admin { func New(cfg *config.Config, database *db.Database) *Admin {
// Create session store // Create session store with appropriate options
store := sessions.NewCookieStore([]byte(cfg.SecretKey)) store := sessions.NewCookieStore([]byte(cfg.SecretKey))
store.Options = &sessions.Options{
Path: "/admin",
MaxAge: 3600 * 24 * 7, // 1 week
HttpOnly: true,
}
// Load templates // Load templates
templates := make(map[string]*template.Template) templates := make(map[string]*template.Template)
@ -79,6 +91,7 @@ func New(cfg *config.Config, database *db.Database) *Admin {
templateFiles := []string{ templateFiles := []string{
"index.html", "index.html",
"login.html", "login.html",
"change_password.html",
"channel_list.html", "channel_list.html",
"channel_detail.html", "channel_detail.html",
"plugin_list.html", "plugin_list.html",
@ -122,6 +135,7 @@ func (a *Admin) RegisterRoutes(mux *http.ServeMux) {
mux.HandleFunc("/admin/", a.handleIndex) mux.HandleFunc("/admin/", a.handleIndex)
mux.HandleFunc("/admin/login", a.handleLogin) mux.HandleFunc("/admin/login", a.handleLogin)
mux.HandleFunc("/admin/logout", a.handleLogout) mux.HandleFunc("/admin/logout", a.handleLogout)
mux.HandleFunc("/admin/change-password", a.handleChangePassword)
mux.HandleFunc("/admin/plugins", a.handlePluginList) mux.HandleFunc("/admin/plugins", a.handlePluginList)
mux.HandleFunc("/admin/channels", a.handleChannelList) mux.HandleFunc("/admin/channels", a.handleChannelList)
mux.HandleFunc("/admin/channels/", a.handleChannelDetail) mux.HandleFunc("/admin/channels/", a.handleChannelDetail)
@ -131,7 +145,11 @@ func (a *Admin) RegisterRoutes(mux *http.ServeMux) {
// getCurrentUser gets the current user from the session // getCurrentUser gets the current user from the session
func (a *Admin) getCurrentUser(r *http.Request) *model.User { func (a *Admin) getCurrentUser(r *http.Request) *model.User {
session, _ := a.store.Get(r, sessionKey) session, err := a.store.Get(r, sessionKey)
if err != nil {
fmt.Printf("Error getting session for user retrieval: %v\n", err)
return nil
}
// Check if user is logged in // Check if user is logged in
userID, ok := session.Values["user_id"].(int64) userID, ok := session.Values["user_id"].(int64)
@ -142,6 +160,7 @@ func (a *Admin) getCurrentUser(r *http.Request) *model.User {
// Get user from database // Get user from database
user, err := a.db.GetUserByID(userID) user, err := a.db.GetUserByID(userID)
if err != nil { if err != nil {
fmt.Printf("Error retrieving user from database: %v\n", err)
return nil return nil
} }
@ -150,32 +169,63 @@ func (a *Admin) getCurrentUser(r *http.Request) *model.User {
// isLoggedIn checks if the user is logged in // isLoggedIn checks if the user is logged in
func (a *Admin) isLoggedIn(r *http.Request) bool { func (a *Admin) isLoggedIn(r *http.Request) bool {
session, _ := a.store.Get(r, sessionKey) session, err := a.store.Get(r, sessionKey)
if err != nil {
fmt.Printf("Error getting session for login check: %v\n", err)
return false
}
return session.Values["logged_in"] == true return session.Values["logged_in"] == true
} }
// addFlash adds a flash message to the session // addFlash adds a flash message to the session
func (a *Admin) addFlash(w http.ResponseWriter, r *http.Request, message string, category string) { func (a *Admin) addFlash(w http.ResponseWriter, r *http.Request, message string, category string) {
session, _ := a.store.Get(r, sessionKey) session, err := a.store.Get(r, sessionKey)
if err != nil {
// If there's an error getting the session, create a new one
session = sessions.NewSession(a.store, sessionKey)
session.Options = &sessions.Options{
Path: "/admin",
MaxAge: 3600 * 24 * 7, // 1 week
HttpOnly: true,
}
}
// Add flash message // Map internal categories to Bootstrap alert classes
flashes := session.Flashes() alertClass := category
if flashes == nil { switch category {
flashes = make([]interface{}, 0) case "success":
alertClass = "success"
case "danger":
alertClass = "danger"
case "warning":
alertClass = "warning"
case "info":
alertClass = "info"
default:
alertClass = "info"
} }
flash := FlashMessage{ flash := FlashMessage{
Category: category, Category: alertClass,
Message: message, Message: message,
} }
session.AddFlash(flash) session.AddFlash(flash)
session.Save(r, w) err = session.Save(r, w)
if err != nil {
// Log the error or handle it appropriately
fmt.Printf("Error saving session: %v\n", err)
}
} }
// getFlashes gets all flash messages from the session // getFlashes gets all flash messages from the session
func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessage { func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessage {
session, _ := a.store.Get(r, sessionKey) session, err := a.store.Get(r, sessionKey)
if err != nil {
// If there's an error getting the session, return an empty slice
fmt.Printf("Error getting session for flashes: %v\n", err)
return []FlashMessage{}
}
// Get flash messages // Get flash messages
flashes := session.Flashes() flashes := session.Flashes()
@ -188,7 +238,10 @@ func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessag
} }
// Save session to clear flashes // Save session to clear flashes
session.Save(r, w) err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session after getting flashes: %v\n", err)
}
return messages return messages
} }
@ -299,10 +352,19 @@ func (a *Admin) handleLogin(w http.ResponseWriter, r *http.Request) {
// handleLogout handles the logout route // handleLogout handles the logout route
func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) { func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) {
// Clear session // Clear session
session, _ := a.store.Get(r, sessionKey) session, err := a.store.Get(r, sessionKey)
if err != nil {
fmt.Printf("Error getting session for logout: %v\n", err)
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
session.Values = make(map[interface{}]interface{}) session.Values = make(map[interface{}]interface{})
session.Options.MaxAge = -1 // Delete session session.Options.MaxAge = -1 // Delete session
session.Save(r, w) err = session.Save(r, w)
if err != nil {
fmt.Printf("Error saving session for logout: %v\n", err)
}
a.addFlash(w, r, "You were logged out", "success") a.addFlash(w, r, "You were logged out", "success")
@ -310,6 +372,74 @@ func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther) http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
} }
// handleChangePassword handles the change password route
func (a *Admin) handleChangePassword(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in
if !a.isLoggedIn(r) {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Get current user
user := a.getCurrentUser(r)
if user == nil {
http.Redirect(w, r, "/admin/login", http.StatusSeeOther)
return
}
// Handle form submission
if r.Method == http.MethodPost {
// Parse form
if err := r.ParseForm(); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Get form values
currentPassword := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_password")
// Validate current password
_, err := a.db.CheckCredentials(user.Username, currentPassword)
if err != nil {
a.addFlash(w, r, "Current password is incorrect", "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
// Validate new password and confirmation
if newPassword == "" {
a.addFlash(w, r, "New password cannot be empty", "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
if newPassword != confirmPassword {
a.addFlash(w, r, "New passwords do not match", "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
// Update password
if err := a.db.UpdateUserPassword(user.ID, newPassword); err != nil {
a.addFlash(w, r, "Failed to update password: "+err.Error(), "danger")
http.Redirect(w, r, "/admin/change-password", http.StatusSeeOther)
return
}
// Success
a.addFlash(w, r, "Password changed successfully", "success")
http.Redirect(w, r, "/admin/", http.StatusSeeOther)
return
}
// Render change password template
a.render(w, r, "change_password.html", TemplateData{
Title: "Change Password",
})
}
// handlePluginList handles the plugin list route // handlePluginList handles the plugin list route
func (a *Admin) handlePluginList(w http.ResponseWriter, r *http.Request) { func (a *Admin) handlePluginList(w http.ResponseWriter, r *http.Request) {
// Check if user is logged in // Check if user is logged in

View file

@ -28,8 +28,10 @@
<a href="/admin/login">Log in</a> <a href="/admin/login">Log in</a>
{{else}} {{else}}
<div class="d-none d-xl-block pl-2"> <div class="d-none d-xl-block pl-2">
<div>{{.User.Username}} - <a class="mt-1 small" <div>{{.User.Username}} -
href="/admin/logout">Log out</a></div> <a class="mt-1 small" href="/admin/change-password">Change Password</a> |
<a class="mt-1 small" href="/admin/logout">Log out</a>
</div>
</div> </div>
</a> </a>
{{end}} {{end}}
@ -100,14 +102,14 @@
{{end}} {{end}}
</div> </div>
{{range .Flash}} <div class="container-xl mt-3">
<div class="card"> {{range .Flash}}
<div class="card-status-top bg-{{.Category}}"></div> <div class="alert alert-{{.Category}} alert-dismissible" role="alert">
<div class="card-body"> {{.Message}}
<p>{{.Message}}</p> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div> </div>
{{end}}
</div> </div>
{{end}}
<div class="content"> <div class="content">
<div class="container-xl"> <div class="container-xl">

View file

@ -0,0 +1,30 @@
{{define "content"}}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h3 class="card-title">Change Password</h3>
</div>
<div class="card-body">
<form method="post" action="/admin/change-password">
<div class="mb-3">
<label class="form-label">Current Password</label>
<input type="password" name="current_password" class="form-control" placeholder="Current Password" required>
</div>
<div class="mb-3">
<label class="form-label">New Password</label>
<input type="password" name="new_password" class="form-control" placeholder="New Password" required>
</div>
<div class="mb-3">
<label class="form-label">Confirm New Password</label>
<input type="password" name="confirm_password" class="form-control" placeholder="Confirm New Password" required>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary">Change Password</button>
</div>
</form>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -572,6 +572,25 @@ func (d *Database) CheckCredentials(username, password string) (*model.User, err
}, nil }, nil
} }
// UpdateUserPassword updates a user's password
func (d *Database) UpdateUserPassword(userID int64, newPassword string) error {
// Hash the new password
hashedPassword, err := hashPassword(newPassword)
if err != nil {
return err
}
// Update the user's password
query := `
UPDATE users
SET password = ?
WHERE id = ?
`
_, err = d.db.Exec(query, hashedPassword, userID)
return err
}
// Helper function to hash password // Helper function to hash password
func hashPassword(password string) (string, error) { func hashPassword(password string) (string, error) {
// Use bcrypt for secure password hashing // Use bcrypt for secure password hashing