diff --git a/internal/admin/admin.go b/internal/admin/admin.go index d590995..045d980 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -2,6 +2,8 @@ package admin import ( "embed" + "encoding/gob" + "fmt" "html/template" "net/http" "strconv" @@ -28,6 +30,11 @@ type FlashMessage struct { Message string } +func init() { + // Register the FlashMessage type with gob package for session serialization + gob.Register(FlashMessage{}) +} + // TemplateData holds data for rendering templates type TemplateData struct { User *model.User @@ -52,8 +59,13 @@ type Admin struct { // New creates a new Admin instance 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.Options = &sessions.Options{ + Path: "/admin", + MaxAge: 3600 * 24 * 7, // 1 week + HttpOnly: true, + } // Load templates templates := make(map[string]*template.Template) @@ -79,6 +91,7 @@ func New(cfg *config.Config, database *db.Database) *Admin { templateFiles := []string{ "index.html", "login.html", + "change_password.html", "channel_list.html", "channel_detail.html", "plugin_list.html", @@ -122,6 +135,7 @@ func (a *Admin) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("/admin/", a.handleIndex) mux.HandleFunc("/admin/login", a.handleLogin) mux.HandleFunc("/admin/logout", a.handleLogout) + mux.HandleFunc("/admin/change-password", a.handleChangePassword) mux.HandleFunc("/admin/plugins", a.handlePluginList) mux.HandleFunc("/admin/channels", a.handleChannelList) 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 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 userID, ok := session.Values["user_id"].(int64) @@ -142,6 +160,7 @@ func (a *Admin) getCurrentUser(r *http.Request) *model.User { // Get user from database user, err := a.db.GetUserByID(userID) if err != nil { + fmt.Printf("Error retrieving user from database: %v\n", err) return nil } @@ -150,32 +169,63 @@ func (a *Admin) getCurrentUser(r *http.Request) *model.User { // isLoggedIn checks if the user is logged in 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 } // addFlash adds a flash message to the session 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 - flashes := session.Flashes() - if flashes == nil { - flashes = make([]interface{}, 0) + // Map internal categories to Bootstrap alert classes + alertClass := category + switch category { + case "success": + alertClass = "success" + case "danger": + alertClass = "danger" + case "warning": + alertClass = "warning" + case "info": + alertClass = "info" + default: + alertClass = "info" } flash := FlashMessage{ - Category: category, + Category: alertClass, Message: message, } 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 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 flashes := session.Flashes() @@ -188,7 +238,10 @@ func (a *Admin) getFlashes(w http.ResponseWriter, r *http.Request) []FlashMessag } // 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 } @@ -299,10 +352,19 @@ func (a *Admin) handleLogin(w http.ResponseWriter, r *http.Request) { // handleLogout handles the logout route func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) { // 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.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") @@ -310,6 +372,74 @@ func (a *Admin) handleLogout(w http.ResponseWriter, r *http.Request) { 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 func (a *Admin) handlePluginList(w http.ResponseWriter, r *http.Request) { // Check if user is logged in diff --git a/internal/admin/templates/_base.html b/internal/admin/templates/_base.html index d056ab5..4a414e3 100644 --- a/internal/admin/templates/_base.html +++ b/internal/admin/templates/_base.html @@ -28,8 +28,10 @@ Log in {{else}}
-
{{.User.Username}} - Log out
+
{{.User.Username}} - + Change Password | + Log out +
{{end}} @@ -100,14 +102,14 @@ {{end}} - {{range .Flash}} -
-
-
-

{{.Message}}

+
+ {{range .Flash}} + + {{end}}
- {{end}}
diff --git a/internal/admin/templates/change_password.html b/internal/admin/templates/change_password.html new file mode 100644 index 0000000..eed3dc5 --- /dev/null +++ b/internal/admin/templates/change_password.html @@ -0,0 +1,30 @@ +{{define "content"}} +
+
+
+
+

Change Password

+
+
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+
+
+
+{{end}} \ No newline at end of file diff --git a/internal/db/db.go b/internal/db/db.go index e1c51e0..8cdce4a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -572,6 +572,25 @@ func (d *Database) CheckCredentials(username, password string) (*model.User, err }, 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 func hashPassword(password string) (string, error) { // Use bcrypt for secure password hashing