fix up git
This commit is contained in:
322
server/internal/handlers/handlers_github_repos.go
Normal file
322
server/internal/handlers/handlers_github_repos.go
Normal file
@@ -0,0 +1,322 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/internal/middleware"
|
||||
"server/internal/models"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type githubRepoListItem struct {
|
||||
Key string `json:"key"`
|
||||
OwnerLogin string `json:"ownerLogin"`
|
||||
RepoName string `json:"repoName"`
|
||||
RepoHTMLURL string `json:"repoHtmlUrl"`
|
||||
Private bool `json:"private"`
|
||||
PublishMode string `json:"publishMode,omitempty"`
|
||||
ExamRoomID *uint `json:"examRoomId,omitempty"`
|
||||
ExamRoomName string `json:"examRoomName,omitempty"`
|
||||
StudentLabel string `json:"studentLabel,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
FromSimpleCare bool `json:"fromSimpleCare"`
|
||||
}
|
||||
|
||||
func githubDeleteRepo(token, owner, repo string) error {
|
||||
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s", url.PathEscape(owner), url.PathEscape(repo))
|
||||
req, err := http.NewRequest(http.MethodDelete, apiURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode >= 300 {
|
||||
raw, _ := io.ReadAll(resp.Body)
|
||||
msg := strings.TrimSpace(string(raw))
|
||||
var errBody struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
_ = json.Unmarshal(raw, &errBody)
|
||||
if m := strings.TrimSpace(errBody.Message); m != "" {
|
||||
msg = m
|
||||
}
|
||||
if msg == "" {
|
||||
msg = fmt.Sprintf("HTTP %d", resp.StatusCode)
|
||||
}
|
||||
if resp.StatusCode == http.StatusForbidden {
|
||||
return fmt.Errorf("%s — token thiếu quyền delete_repo; ngắt kết nối rồi kết nối lại GitHub", msg)
|
||||
}
|
||||
return fmt.Errorf("%s", msg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func githubListUserRepos(token string) ([]struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Private bool `json:"private"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}, error) {
|
||||
var all []struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Private bool `json:"private"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
for page := 1; page <= 10; page++ {
|
||||
apiURL := fmt.Sprintf("https://api.github.com/user/repos?per_page=100&page=%d&sort=updated&direction=desc", page)
|
||||
req, err := http.NewRequest(http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var batch []struct {
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Private bool `json:"private"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&batch); err != nil {
|
||||
resp.Body.Close()
|
||||
return nil, err
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("GitHub API lỗi (%d)", resp.StatusCode)
|
||||
}
|
||||
all = append(all, batch...)
|
||||
if len(batch) < 100 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return all, nil
|
||||
}
|
||||
|
||||
type repoMeta struct {
|
||||
PublishMode string
|
||||
ExamRoomID *uint
|
||||
ExamRoomName string
|
||||
StudentLabel string
|
||||
}
|
||||
|
||||
func repoMetaKey(owner, name string) string {
|
||||
return strings.ToLower(owner) + "/" + strings.ToLower(name)
|
||||
}
|
||||
|
||||
func buildRepoMetaMap(db *gorm.DB, staffID uint) map[string]repoMeta {
|
||||
out := map[string]repoMeta{}
|
||||
var tracked []models.StaffGitHubRepo
|
||||
_ = db.Where("staff_id = ?", staffID).Find(&tracked).Error
|
||||
roomNames := map[uint]string{}
|
||||
for _, t := range tracked {
|
||||
key := repoMetaKey(t.OwnerLogin, t.RepoName)
|
||||
meta := repoMeta{
|
||||
PublishMode: t.PublishMode,
|
||||
ExamRoomID: t.ExamRoomID,
|
||||
StudentLabel: t.StudentLabel,
|
||||
}
|
||||
if t.ExamRoomID != nil {
|
||||
rid := *t.ExamRoomID
|
||||
if name, ok := roomNames[rid]; ok {
|
||||
meta.ExamRoomName = name
|
||||
} else {
|
||||
var room models.ExamRoom
|
||||
if err := db.Select("name").First(&room, rid).Error; err == nil {
|
||||
roomNames[rid] = room.Name
|
||||
meta.ExamRoomName = room.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
out[key] = meta
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func recordStaffGitHubRepo(db *gorm.DB, staffID uint, owner, repoName, htmlURL, publishMode, studentLabel string, examRoomID, submissionID *uint) {
|
||||
owner = strings.TrimSpace(owner)
|
||||
repoName = strings.TrimSpace(repoName)
|
||||
if staffID == 0 || owner == "" || repoName == "" || htmlURL == "" {
|
||||
return
|
||||
}
|
||||
_ = db.Where("staff_id = ? AND owner_login = ? AND repo_name = ?", staffID, owner, repoName).
|
||||
Delete(&models.StaffGitHubRepo{}).Error
|
||||
row := models.StaffGitHubRepo{
|
||||
StaffID: staffID,
|
||||
OwnerLogin: owner,
|
||||
RepoName: repoName,
|
||||
RepoHTMLURL: htmlURL,
|
||||
ExamRoomID: examRoomID,
|
||||
SubmissionID: submissionID,
|
||||
StudentLabel: studentLabel,
|
||||
PublishMode: publishMode,
|
||||
}
|
||||
_ = db.Create(&row).Error
|
||||
}
|
||||
|
||||
// GET /api/auth/github/repos — toàn bộ repo trên tài khoản GitHub đã kết nối
|
||||
func GitHubListReposHandler(db *gorm.DB) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
staffID := middleware.StaffIDFromCtx(c)
|
||||
token, ghLogin, err := loadStaffGitHubToken(db, staffID)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "Chưa kết nối GitHub"})
|
||||
}
|
||||
ghRepos, err := githubListUserRepos(token)
|
||||
if err != nil {
|
||||
return c.Status(502).JSON(fiber.Map{"error": "Không tải repo từ GitHub: " + err.Error()})
|
||||
}
|
||||
metaMap := buildRepoMetaMap(db, staffID)
|
||||
out := make([]githubRepoListItem, 0, len(ghRepos))
|
||||
for _, r := range ghRepos {
|
||||
parts := strings.SplitN(r.FullName, "/", 2)
|
||||
owner := ghLogin
|
||||
if len(parts) == 2 {
|
||||
owner = parts[0]
|
||||
}
|
||||
key := owner + "/" + r.Name
|
||||
item := githubRepoListItem{
|
||||
Key: key,
|
||||
OwnerLogin: owner,
|
||||
RepoName: r.Name,
|
||||
RepoHTMLURL: r.HTMLURL,
|
||||
Private: r.Private,
|
||||
CreatedAt: r.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: r.UpdatedAt.Format(time.RFC3339),
|
||||
FromSimpleCare: strings.Contains(strings.ToLower(r.Description), "simple care"),
|
||||
}
|
||||
if meta, ok := metaMap[repoMetaKey(owner, r.Name)]; ok {
|
||||
item.PublishMode = meta.PublishMode
|
||||
item.ExamRoomID = meta.ExamRoomID
|
||||
item.ExamRoomName = meta.ExamRoomName
|
||||
item.StudentLabel = meta.StudentLabel
|
||||
item.FromSimpleCare = true
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return c.JSON(fiber.Map{"data": out, "total": len(out), "githubLogin": ghLogin})
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/auth/github/repos/bulk-delete — repos: ["owner/name", ...]
|
||||
func GitHubBulkDeleteReposHandler(db *gorm.DB) fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
staffID := middleware.StaffIDFromCtx(c)
|
||||
token, _, err := loadStaffGitHubToken(db, staffID)
|
||||
if err != nil {
|
||||
return c.Status(401).JSON(fiber.Map{"error": "Chưa kết nối GitHub"})
|
||||
}
|
||||
var authRow models.StaffGitHubAuth
|
||||
_ = db.First(&authRow, "staff_id = ?", staffID).Error
|
||||
if !githubScopeAllowsDelete(authRow.Scope) {
|
||||
return c.Status(403).JSON(fiber.Map{
|
||||
"error": "Token GitHub chưa có quyền xóa repo (delete_repo). Ngắt kết nối rồi bấm Kết nối lại GitHub.",
|
||||
})
|
||||
}
|
||||
var req struct {
|
||||
Repos []string `json:"repos"`
|
||||
IDs []uint `json:"ids"` // legacy
|
||||
}
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Invalid body"})
|
||||
}
|
||||
targets := make([]struct{ owner, name string }, 0)
|
||||
for _, raw := range req.Repos {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(raw, "/", 2)
|
||||
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
||||
continue
|
||||
}
|
||||
targets = append(targets, struct{ owner, name string }{owner: parts[0], name: parts[1]})
|
||||
}
|
||||
if len(targets) == 0 && len(req.IDs) > 0 {
|
||||
var rows []models.StaffGitHubRepo
|
||||
_ = db.Where("staff_id = ? AND id IN ?", staffID, req.IDs).Find(&rows).Error
|
||||
for _, row := range rows {
|
||||
targets = append(targets, struct{ owner, name string }{owner: row.OwnerLogin, name: row.RepoName})
|
||||
}
|
||||
}
|
||||
if len(targets) == 0 {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "Chọn ít nhất một repo"})
|
||||
}
|
||||
deleted := 0
|
||||
var failures []string
|
||||
for _, t := range targets {
|
||||
if err := githubDeleteRepo(token, t.owner, t.name); err != nil {
|
||||
failures = append(failures, t.name+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
var row models.StaffGitHubRepo
|
||||
if err := db.Where("staff_id = ? AND owner_login = ? AND repo_name = ?", staffID, t.owner, t.name).First(&row).Error; err == nil {
|
||||
_ = db.Delete(&row).Error
|
||||
clearLinkedGitURLs(db, row)
|
||||
} else {
|
||||
clearLinkedGitURLsByName(db, t.owner, t.name)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
return c.JSON(fiber.Map{
|
||||
"ok": deleted > 0,
|
||||
"deleted": deleted,
|
||||
"failures": failures,
|
||||
"message": fmt.Sprintf("Đã xóa %d repo trên GitHub", deleted),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func clearLinkedGitURLs(db *gorm.DB, row models.StaffGitHubRepo) {
|
||||
url := strings.TrimSpace(row.RepoHTMLURL)
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
if row.ExamRoomID != nil && row.PublishMode == models.GitPublishModeRoom {
|
||||
_ = db.Model(&models.ExamRoom{}).Where("id = ? AND git_repo_url = ?", *row.ExamRoomID, url).
|
||||
Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
|
||||
}
|
||||
if row.SubmissionID != nil {
|
||||
_ = db.Model(&models.ExamSubmission{}).Where("id = ? AND git_repo_url = ?", *row.SubmissionID, url).
|
||||
Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
|
||||
}
|
||||
}
|
||||
|
||||
func clearLinkedGitURLsByName(db *gorm.DB, owner, repoName string) {
|
||||
prefix := fmt.Sprintf("https://github.com/%s/%s", owner, repoName)
|
||||
_ = db.Model(&models.ExamRoom{}).Where("git_repo_url LIKE ?", prefix+"%").
|
||||
Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
|
||||
_ = db.Model(&models.ExamSubmission{}).Where("git_repo_url LIKE ?", prefix+"%").
|
||||
Updates(map[string]any{"git_repo_url": "", "git_publish_url": ""}).Error
|
||||
}
|
||||
Reference in New Issue
Block a user