feat(auth): block panel with default admin/admin credentials and guide credential change

checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
This commit is contained in:
farhadh
2026-05-11 21:09:48 +02:00
parent ce88b0b432
commit 56ce6073ce
8 changed files with 228 additions and 42 deletions
+14 -2
View File
@@ -39,7 +39,7 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.GET("/logout", a.logout)
g.GET("/logout", a.logoutGet)
// Public CSRF endpoint — the SPA login page (served by Vite in
// dev or by serveDistPage in prod) needs a token to POST /login,
// but the panel-side /panel/csrf-token sits behind checkLogin.
@@ -48,6 +48,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/csrf-token", a.csrfToken)
g.POST("/login", middleware.CSRFMiddleware(), a.login)
g.POST("/logout", middleware.CSRFMiddleware(), a.logout)
g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
}
@@ -130,7 +131,9 @@ func (a *IndexController) login(c *gin.Context) {
}
logger.Infof("%s logged in successfully", safeUser)
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
jsonMsgObj(c, I18nWeb(c, "pages.login.toasts.successLogin"), gin.H{
"mustChangeCredentials": user.Username == "admin" && form.Password == "admin",
}, nil)
}
func loginFailureReason(err error) string {
@@ -150,9 +153,18 @@ func (a *IndexController) logout(c *gin.Context) {
logger.Warning("Unable to clear session on logout:", err)
}
c.Header("Cache-Control", "no-store")
if isAjax(c) {
jsonMsg(c, "", nil)
return
}
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
func (a *IndexController) logoutGet(c *gin.Context) {
c.Header("Allow", http.MethodPost)
c.AbortWithStatus(http.StatusMethodNotAllowed)
}
// csrfToken returns the session CSRF token. Public — the login page
// needs a token before authenticating.
func (a *IndexController) csrfToken(c *gin.Context) {