feat: allow password change
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
All checks were successful
ci/woodpecker/tag/release Pipeline was successful
This commit is contained in:
parent
ece8280358
commit
6aedfc794f
4 changed files with 203 additions and 22 deletions
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
30
internal/admin/templates/change_password.html
Normal file
30
internal/admin/templates/change_password.html
Normal 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}}
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue