feat(users): Multi-User-Management — DB-backed Login-Accounts + UI
- internal/services/users: Repo mit CRUD, bcrypt (cost 12), Upsert für setup-store-Admin-Migration, RecordLogin - internal/handlers/users: GET/POST/PUT /users, POST /users/:id/password, DELETE /users/:id; Schutz gegen Selbst-Löschung - auth.go Login: DB-Nutzer first, Fallback auf setup-store-Admin; bei erfolgreichem Fallback wird der Admin per Upsert in die DB migriert (kein manueller Eingriff nötig) - management-ui: /users-Seite mit Tabelle, Anlegen-, Bearbeiten-, Passwort-setzen- und Löschen-Modals; "You"-Badge für eigenen Account - Sidebar + Route + i18n (de/en) ergänzt Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,16 +12,20 @@ import (
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/audit"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/session"
|
||||
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
|
||||
usersvc "git.netcell-it.de/projekte/edgeguard-native/internal/services/users"
|
||||
)
|
||||
|
||||
// AuthHandler exposes login / me / logout. v1 verifies against the
|
||||
// setup-store (single admin); admin_users-table support comes when the
|
||||
// users repo lands.
|
||||
// AuthHandler exposes login / me / logout.
|
||||
// Login checks the DB users table first; falls back to the setup-store
|
||||
// admin for backwards compatibility. On a successful setup-store login
|
||||
// the account is auto-migrated into the DB (Upsert) so it shows up in
|
||||
// user management from that point on.
|
||||
type AuthHandler struct {
|
||||
Setup *setup.Store
|
||||
Signer *session.Signer
|
||||
Audit *audit.Repo
|
||||
NodeID string
|
||||
Users *usersvc.Repo // optional — nil on first boot before DB is ready
|
||||
}
|
||||
|
||||
func NewAuthHandler(s *setup.Store, sig *session.Signer) *AuthHandler {
|
||||
@@ -36,6 +40,12 @@ func (h *AuthHandler) WithAudit(a *audit.Repo, nodeID string) *AuthHandler {
|
||||
return h
|
||||
}
|
||||
|
||||
// WithUsers injects the users repo so Login can verify against the DB.
|
||||
func (h *AuthHandler) WithUsers(u *usersvc.Repo) *AuthHandler {
|
||||
h.Users = u
|
||||
return h
|
||||
}
|
||||
|
||||
// Register mounts /auth/login + /logout (public) and /auth/me
|
||||
// (gated by requireAuth, passed in as a per-route middleware).
|
||||
func (h *AuthHandler) Register(rg *gin.RouterGroup, requireAuth gin.HandlerFunc) {
|
||||
@@ -73,13 +83,44 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
response.Err(c, http.StatusServiceUnavailable, errors.New("setup_required"))
|
||||
return
|
||||
}
|
||||
if !strings.EqualFold(st.AdminEmail, strings.TrimSpace(req.Email)) ||
|
||||
!st.VerifyAdminPassword(req.Password) {
|
||||
response.Unauthorized(c, errors.New("invalid_credentials"))
|
||||
return
|
||||
|
||||
email := strings.TrimSpace(req.Email)
|
||||
actor, role := "", "admin"
|
||||
|
||||
// 1. Try DB users table first.
|
||||
if h.Users != nil {
|
||||
u, hash, dbErr := h.Users.FindByEmail(c.Request.Context(), email)
|
||||
if dbErr == nil {
|
||||
if !u.Active {
|
||||
response.Unauthorized(c, errors.New("account_disabled"))
|
||||
return
|
||||
}
|
||||
if !usersvc.VerifyPassword(hash, req.Password) {
|
||||
response.Unauthorized(c, errors.New("invalid_credentials"))
|
||||
return
|
||||
}
|
||||
actor = u.Email
|
||||
role = u.Role
|
||||
h.Users.RecordLogin(c.Request.Context(), u.ID)
|
||||
}
|
||||
}
|
||||
|
||||
raw, tok, err := h.Signer.IssueWithRole(st.AdminEmail, "admin")
|
||||
// 2. Fallback: setup-store admin (backwards compat for pre-DB installs).
|
||||
if actor == "" {
|
||||
if !strings.EqualFold(st.AdminEmail, email) || !st.VerifyAdminPassword(req.Password) {
|
||||
response.Unauthorized(c, errors.New("invalid_credentials"))
|
||||
return
|
||||
}
|
||||
actor = st.AdminEmail
|
||||
role = "admin"
|
||||
// Auto-migrate: insert the setup-store admin into the DB so it
|
||||
// shows up in user management from this point on.
|
||||
if h.Users != nil {
|
||||
_, _ = h.Users.Upsert(c.Request.Context(), st.AdminEmail, req.Password, "admin", true)
|
||||
}
|
||||
}
|
||||
|
||||
raw, tok, err := h.Signer.IssueWithRole(actor, role)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user