257 lines
7.5 KiB
Go
257 lines
7.5 KiB
Go
package admin
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"github.com/mattnite/forgejo-tickets/internal/forgejo"
|
|
"github.com/mattnite/forgejo-tickets/internal/middleware"
|
|
"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")
|
|
}
|
|
|
|
middleware.SetFlash(c, "success", "User "+user.Email+" has been approved")
|
|
c.Redirect(http.StatusSeeOther, "/users/pending")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
middleware.SetFlash(c, "success", "User request has been rejected")
|
|
c.Redirect(http.StatusSeeOther, "/users/pending")
|
|
}
|
|
|
|
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})
|
|
}
|
|
|
|
middleware.SetFlash(c, "success", "Project assignments updated")
|
|
c.Redirect(http.StatusSeeOther, "/users/"+userID.String())
|
|
}
|