forgejo-tickets/internal/handlers/admin/users.go

266 lines
7.7 KiB
Go

package admin
import (
"crypto/rand"
"encoding/hex"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/mattnite/forgejo-tickets/internal/forgejo"
"github.com/mattnite/forgejo-tickets/internal/models"
"github.com/rs/zerolog/log"
)
type UserHandler struct {
deps Dependencies
}
func (h *UserHandler) List(c *gin.Context) {
var users []models.User
if err := h.deps.DB.Order("created_at DESC").Limit(100).Find(&users).Error; err != nil {
log.Error().Err(err).Msg("list users error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to load users")
return
}
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/list", map[string]interface{}{
"Users": users,
})
}
func (h *UserHandler) Detail(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid user ID")
return
}
var user models.User
if err := h.deps.DB.First(&user, "id = ?", userID).Error; err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusNotFound, "User not found")
return
}
// Load user's ticket mappings with repo info
var tickets []models.Ticket
h.deps.DB.Preload("Repo").Where("user_id = ?", user.ID).Order("created_at DESC").Limit(50).Find(&tickets)
// Group by repo and fetch issue details from Forgejo
type ticketView struct {
ID uuid.UUID
Title string
Status string
RepoName string
CreatedAt interface{}
}
var ticketViews []ticketView
type repoGroup struct {
repo models.Repo
tickets []models.Ticket
}
repoMap := map[uuid.UUID]*repoGroup{}
for _, t := range tickets {
if _, ok := repoMap[t.RepoID]; !ok {
repoMap[t.RepoID] = &repoGroup{repo: t.Repo}
}
repoMap[t.RepoID].tickets = append(repoMap[t.RepoID].tickets, t)
}
for _, group := range repoMap {
issues, err := h.deps.ForgejoClient.ListIssues(group.repo.ForgejoOwner, group.repo.ForgejoRepo, "all", "")
if err != nil {
log.Error().Err(err).Str("repo", group.repo.Name).Msg("forgejo list issues error")
for _, t := range group.tickets {
ticketViews = append(ticketViews, ticketView{
ID: t.ID,
Title: "Unable to load",
Status: "open",
RepoName: group.repo.Name,
CreatedAt: t.CreatedAt,
})
}
continue
}
issueByNumber := map[int64]*forgejo.Issue{}
for i := range issues {
issueByNumber[issues[i].Number] = &issues[i]
}
for _, t := range group.tickets {
if issue, ok := issueByNumber[t.ForgejoIssueNumber]; ok {
ticketViews = append(ticketViews, ticketView{
ID: t.ID,
Title: issue.Title,
Status: forgejo.DeriveStatus(issue),
RepoName: group.repo.Name,
CreatedAt: t.CreatedAt,
})
}
}
}
// Load all repos and user's assigned repo IDs
var allRepos []models.Repo
h.deps.DB.Where("active = ?", true).Order("name ASC").Find(&allRepos)
var userRepos []models.UserRepo
h.deps.DB.Where("user_id = ?", user.ID).Find(&userRepos)
assignedRepoIDs := make(map[string]bool)
for _, ur := range userRepos {
assignedRepoIDs[ur.RepoID.String()] = true
}
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/detail", map[string]interface{}{
"User": user,
"Tickets": ticketViews,
"AllRepos": allRepos,
"AssignedRepoIDs": assignedRepoIDs,
})
}
func (h *UserHandler) NewForm(c *gin.Context) {
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/new", nil)
}
func (h *UserHandler) Create(c *gin.Context) {
name := strings.TrimSpace(c.PostForm("name"))
email := strings.TrimSpace(c.PostForm("email"))
if name == "" || email == "" {
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/new", map[string]interface{}{
"Error": "Name and email are required",
"Name": name,
"Email": email,
})
return
}
tempPassBytes := make([]byte, 12)
rand.Read(tempPassBytes)
tempPassword := hex.EncodeToString(tempPassBytes)[:16]
user, err := h.deps.Auth.CreateUserWithPassword(c.Request.Context(), email, tempPassword, name, true, true)
if err != nil {
if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique") {
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/new", map[string]interface{}{
"Error": "A user with this email already exists",
"Name": name,
"Email": email,
})
} else {
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/new", map[string]interface{}{
"Error": "Failed to create user: " + err.Error(),
"Name": name,
"Email": email,
})
}
return
}
if err := h.deps.EmailClient.SendWelcomeEmail(email, name, tempPassword); err != nil {
log.Error().Err(err).Msg("send welcome email error")
}
c.Redirect(http.StatusSeeOther, "/users/"+user.ID.String())
}
func (h *UserHandler) PendingList(c *gin.Context) {
var users []models.User
if err := h.deps.DB.Where("email_verified = ? AND approved = ?", true, false).Order("created_at DESC").Find(&users).Error; err != nil {
log.Error().Err(err).Msg("list pending users error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to load pending users")
return
}
h.deps.Renderer.Render(c.Writer, c.Request, "admin/users/pending", map[string]interface{}{
"Users": users,
})
}
func (h *UserHandler) Approve(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid user ID")
return
}
var user models.User
if err := h.deps.DB.First(&user, "id = ?", userID).Error; err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusNotFound, "User not found")
return
}
h.deps.DB.Model(&user).Update("approved", true)
if err := h.deps.EmailClient.SendAccountApprovedEmail(user.Email, user.Name); err != nil {
log.Error().Err(err).Msg("send approval email error")
}
redirectURL := "/users/pending?" + url.Values{
"flash": {"User " + user.Email + " has been approved"},
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
}
func (h *UserHandler) Reject(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid user ID")
return
}
if err := h.deps.DB.Delete(&models.User{}, "id = ?", userID).Error; err != nil {
log.Error().Err(err).Msg("delete user error")
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusInternalServerError, "Failed to reject user")
return
}
redirectURL := "/users/pending?" + url.Values{
"flash": {"User request has been rejected"},
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
}
func (h *UserHandler) UpdateRepos(c *gin.Context) {
userID, err := uuid.Parse(c.Param("id"))
if err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusBadRequest, "Invalid user ID")
return
}
var user models.User
if err := h.deps.DB.First(&user, "id = ?", userID).Error; err != nil {
h.deps.Renderer.RenderError(c.Writer, c.Request, http.StatusNotFound, "User not found")
return
}
// Get selected repo IDs from form
repoIDs := c.PostFormArray("repo_ids")
// Delete existing assignments
h.deps.DB.Where("user_id = ?", userID).Delete(&models.UserRepo{})
// Create new assignments
for _, idStr := range repoIDs {
repoID, err := uuid.Parse(idStr)
if err != nil {
continue
}
h.deps.DB.Create(&models.UserRepo{UserID: userID, RepoID: repoID})
}
redirectURL := "/users/" + userID.String() + "?" + url.Values{
"flash": {"Project assignments updated"},
"flash_type": {"success"},
}.Encode()
c.Redirect(http.StatusSeeOther, redirectURL)
}