Files
dumpsterChat/internal/auth/email_handlers.go
T

223 lines
6.4 KiB
Go

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 := "http://" + h.cfg.Host
if h.cfg.Port != "80" && h.cfg.Port != "443" {
appURL += ":" + h.cfg.Port
}
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 := "http://" + h.cfg.Host
if h.cfg.Port != "80" && h.cfg.Port != "443" {
appURL += ":" + h.cfg.Port
}
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)
}