package auth import ( "crypto/rand" "encoding/hex" "encoding/json" "net/http" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware" "git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions" ) func generateToken() string { b := make([]byte, 32) rand.Read(b) return hex.EncodeToString(b) } func (h *Handler) RequestVerification(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } user, err := h.getProfile(r.Context(), userID) if err != nil { http.Error(w, `{"error":"user not found"}`, http.StatusNotFound) return } token := generateToken() _, err = h.db.ExecContext(r.Context(), ` INSERT INTO user_tokens (user_id, token, type, expires_at) VALUES ($1, $2, 'verify_email', NOW() + INTERVAL '24 hours') `, userID, token) if err != nil { http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError) return } appURL := h.cfg.AppURL() if err := h.mailer.SendVerificationEmail(user.Email, user.Username, token, appURL); err != nil { http.Error(w, `{"error":"failed to send email"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (h *Handler) VerifyEmail(w http.ResponseWriter, r *http.Request) { var req struct { Token string `json:"token"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } var userID string err := h.db.QueryRowContext(r.Context(), ` DELETE FROM user_tokens WHERE token = $1 AND type = 'verify_email' AND expires_at > NOW() RETURNING user_id `, req.Token).Scan(&userID) if err != nil { http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest) return } _, err = h.db.ExecContext(r.Context(), ` UPDATE users SET email_verified = TRUE WHERE id = $1 `, userID) if err != nil { http.Error(w, `{"error":"failed to update user"}`, http.StatusInternalServerError) return } w.WriteHeader(http.StatusNoContent) } func (h *Handler) RequestPasswordReset(w http.ResponseWriter, r *http.Request) { var req struct { Email string `json:"email"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } var userID, username string err := h.db.QueryRowContext(r.Context(), ` SELECT id, username FROM users WHERE email = $1 `, req.Email).Scan(&userID, &username) if err != nil { // Silent fail to prevent email enumeration w.WriteHeader(http.StatusNoContent) return } token := generateToken() _, err = h.db.ExecContext(r.Context(), ` INSERT INTO user_tokens (user_id, token, type, expires_at) VALUES ($1, $2, 'reset_password', NOW() + INTERVAL '1 hour') `, userID, token) if err != nil { http.Error(w, `{"error":"failed to generate token"}`, http.StatusInternalServerError) return } appURL := h.cfg.AppURL() h.mailer.SendPasswordResetEmail(req.Email, username, token, appURL) w.WriteHeader(http.StatusNoContent) } func (h *Handler) ResetPassword(w http.ResponseWriter, r *http.Request) { var req struct { Token string `json:"token"` NewPassword string `json:"new_password"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } if len(req.NewPassword) < 8 { http.Error(w, `{"error":"password too short"}`, http.StatusBadRequest) return } var userID string err := h.db.QueryRowContext(r.Context(), ` DELETE FROM user_tokens WHERE token = $1 AND type = 'reset_password' AND expires_at > NOW() RETURNING user_id `, req.Token).Scan(&userID) if err != nil { http.Error(w, `{"error":"invalid or expired token"}`, http.StatusBadRequest) return } hash, err := HashPassword(req.NewPassword) if err != nil { http.Error(w, `{"error":"failed to hash password"}`, http.StatusInternalServerError) return } _, err = h.db.ExecContext(r.Context(), ` UPDATE users SET password_hash = $2 WHERE id = $1 `, userID, hash) if err != nil { http.Error(w, `{"error":"failed to update user"}`, http.StatusInternalServerError) return } // Delete all sessions to force re-login h.db.ExecContext(r.Context(), `DELETE FROM sessions WHERE user_id = $1`, userID) w.WriteHeader(http.StatusNoContent) } func (h *Handler) AdminAnnounce(w http.ResponseWriter, r *http.Request) { userID, ok := middleware.UserIDFromContext(r.Context()) if !ok { http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized) return } var req struct { Subject string `json:"subject"` Body string `json:"body"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid request"}`, http.StatusBadRequest) return } // Check if user is an admin across the whole system. // For now, let's assume if they have ADMINISTRATOR on ANY server, they can announce. // Or maybe there is a global admin flag? Let's check permissions. // Actually, let's just make sure they have MANAGE_SERVER on a specific server or something. // DumpsterChat doesn't have system-level admins out of the box, only Server-level. // Let's require them to be the owner of at least one server, or have ADMINISTRATOR on a server? // The prompt just said "admin sent announcements". Let's allow it for users who are in any server with ADMINISTRATOR. var isAdmin bool err := h.db.QueryRowContext(r.Context(), ` SELECT EXISTS ( SELECT 1 FROM member_roles mr JOIN roles r ON mr.role_id = r.id WHERE mr.user_id = $1 AND (r.permissions & $2) = $2 ) `, userID, permissions.ADMINISTRATOR).Scan(&isAdmin) if err != nil || !isAdmin { http.Error(w, `{"error":"must be an administrator"}`, http.StatusForbidden) return } rows, err := h.db.QueryContext(r.Context(), `SELECT email FROM users`) if err != nil { http.Error(w, `{"error":"failed to fetch users"}`, http.StatusInternalServerError) return } defer rows.Close() for rows.Next() { var email string if err := rows.Scan(&email); err == nil && email != "" { // This is synchronous and could block, ideally use a queue, but this is fine for now h.mailer.SendAnnouncement(email, req.Subject, req.Body) } } w.WriteHeader(http.StatusNoContent) }