feat: login with username or email + change password
Backend: - Login handler accepts username OR email via the same field - New PUT /auth/me/password endpoint (verifies current password, requires min 8 chars for new password) Frontend: - Login form shows EMAIL OR USERNAME field (single field for both) - Auth store updated with changePassword function - UserSettings has new CHANGE PASSWORD section
This commit is contained in:
@@ -38,6 +38,7 @@ func (h *Handler) RegisterPublicRoutes(r chi.Router) {
|
||||
func (h *Handler) RegisterProtectedRoutes(r chi.Router) {
|
||||
r.Get("/auth/me", h.Me)
|
||||
r.Patch("/auth/me", h.UpdateProfile)
|
||||
r.Put("/auth/me/password", h.ChangePassword)
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
@@ -49,6 +50,7 @@ type registerRequest struct {
|
||||
|
||||
type loginRequest struct {
|
||||
Email string `json:"email"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -59,6 +61,11 @@ type updateProfileRequest struct {
|
||||
StatusText string `json:"status_text"`
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
CurrentPassword string `json:"current_password"`
|
||||
NewPassword string `json:"new_password"`
|
||||
}
|
||||
|
||||
type UserProfile struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
@@ -150,10 +157,19 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
login := strings.TrimSpace(req.Email)
|
||||
if login == "" {
|
||||
login = strings.TrimSpace(req.Username)
|
||||
}
|
||||
if login == "" || req.Password == "" {
|
||||
http.Error(w, `{"error":"email/username and password required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
var userID, hash string
|
||||
err := h.db.QueryRowContext(r.Context(), `
|
||||
SELECT id, password_hash FROM users WHERE email = $1
|
||||
`, req.Email).Scan(&userID, &hash)
|
||||
SELECT id, password_hash FROM users WHERE email = $1 OR username = $1
|
||||
`, login).Scan(&userID, &hash)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"invalid credentials"}`, http.StatusUnauthorized)
|
||||
return
|
||||
@@ -295,3 +311,58 @@ func (h *Handler) getProfile(ctx context.Context, userID string) (UserProfile, e
|
||||
func (h *Handler) setSessionCookie(w http.ResponseWriter, token string) {
|
||||
SetSessionCookie(w, h.cfg.Session.CookieName, token, h.cfg.Session.Duration)
|
||||
}
|
||||
|
||||
// ChangePassword handles PUT /auth/me/password.
|
||||
func (h *Handler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var req changePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if req.CurrentPassword == "" || req.NewPassword == "" {
|
||||
http.Error(w, `{"error":"current and new password required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.NewPassword) < 8 {
|
||||
http.Error(w, `{"error":"password must be at least 8 characters"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify current password
|
||||
var currentHash string
|
||||
err := h.db.QueryRowContext(r.Context(), "SELECT password_hash FROM users WHERE id = $1", userID).Scan(¤tHash)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"user not found"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ok, err = VerifyPassword(req.CurrentPassword, currentHash)
|
||||
if err != nil || !ok {
|
||||
http.Error(w, `{"error":"current password is incorrect"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
newHash, err := HashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = h.db.ExecContext(r.Context(), "UPDATE users SET password_hash = $1 WHERE id = $2", newHash, userID)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"failed to update password"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"message": "password updated"})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user