feat: add password change and session invalidation
This commit is contained in:
@@ -163,6 +163,11 @@ func (a *App) adminUpdateUser(c *gin.Context) {
|
|||||||
if err := tx.Save(&u).Error; err != nil {
|
if err := tx.Save(&u).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if in.Password != nil {
|
||||||
|
if err := tx.Where("user_id = ?", u.ID).Delete(&Session{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
if in.Role != nil {
|
if in.Role != nil {
|
||||||
if err := tx.Where("user_id = ?", u.ID).Delete(&UserPermission{}).Error; err != nil {
|
if err := tx.Where("user_id = ?", u.ID).Delete(&UserPermission{}).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionCookie = "am_session"
|
const sessionCookie = "am_session"
|
||||||
@@ -144,6 +145,8 @@ func (a *App) login(c *gin.Context) {
|
|||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "session generation failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "session generation failed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Keep the session table bounded without requiring a background scheduler.
|
||||||
|
a.db.Where("expires_at <= ?", time.Now()).Delete(&Session{})
|
||||||
session := Session{UserID: user.ID, TokenHash: hashToken(token), ExpiresAt: time.Now().Add(7 * 24 * time.Hour)}
|
session := Session{UserID: user.ID, TokenHash: hashToken(token), ExpiresAt: time.Now().Add(7 * 24 * time.Hour)}
|
||||||
if err := a.db.Create(&session).Error; err != nil {
|
if err := a.db.Create(&session).Error; err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "session creation failed"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "session creation failed"})
|
||||||
@@ -162,6 +165,40 @@ func (a *App) logout(c *gin.Context) {
|
|||||||
c.Status(http.StatusNoContent)
|
c.Status(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) changePassword(c *gin.Context) {
|
||||||
|
user := c.MustGet("user").(*User)
|
||||||
|
var in struct {
|
||||||
|
CurrentPassword string `json:"currentPassword"`
|
||||||
|
NewPassword string `json:"newPassword"`
|
||||||
|
}
|
||||||
|
if c.BindJSON(&in) != nil || in.CurrentPassword == "" || len(in.NewPassword) < 8 {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "currentPassword and newPassword(8+) required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var stored User
|
||||||
|
if err := a.db.First(&stored, user.ID).Error; err != nil || checkPassword(stored.PasswordHash, in.CurrentPassword) != nil {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "current password is incorrect"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hash, err := hashPassword(in.NewPassword)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "password hashing failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := a.db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Model(&User{}).Where("id = ?", user.ID).Update("password_hash", hash).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Where("user_id = ?", user.ID).Delete(&Session{}).Error
|
||||||
|
}); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "password change failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.SetCookie(c.Writer, &http.Cookie{Name: sessionCookie, Value: "", Path: "/", MaxAge: -1, HttpOnly: true, SameSite: http.SameSiteLaxMode})
|
||||||
|
a.audit(user.ID, "auth.password.change", "user", fmt.Sprint(user.ID), "password changed; sessions revoked")
|
||||||
|
c.Status(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) me(c *gin.Context) {
|
func (a *App) me(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"user": a.currentUser(c)})
|
c.JSON(http.StatusOK, gin.H{"user": a.currentUser(c)})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ func main() {
|
|||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
api.POST("/auth/login", a.login)
|
api.POST("/auth/login", a.login)
|
||||||
api.POST("/auth/logout", a.requireAuth, a.logout)
|
api.POST("/auth/logout", a.requireAuth, a.logout)
|
||||||
|
api.POST("/auth/password", a.requireAuth, a.changePassword)
|
||||||
api.GET("/auth/me", a.requireAuth, a.me)
|
api.GET("/auth/me", a.requireAuth, a.me)
|
||||||
auth := api.Group("", a.requireAuth)
|
auth := api.Group("", a.requireAuth)
|
||||||
auth.GET("/search", a.requirePermission(PermDownload), a.search)
|
auth.GET("/search", a.requirePermission(PermDownload), a.search)
|
||||||
|
|||||||
Reference in New Issue
Block a user