This commit is contained in:
2026-06-30 15:53:01 +07:00
parent 595aebd8a5
commit 74b540cac2
19 changed files with 1719 additions and 252 deletions

View File

@@ -49,6 +49,7 @@ func AutoMigrate(db *gorm.DB) error {
&models.ClassCourse{},
&models.ClassAllowedApp{},
&models.AppPoolEntry{},
&models.AppTemplate{},
&models.WifiPoolEntry{},
&models.AcceptedWifi{},
&models.StudentSession{},

View File

@@ -0,0 +1,119 @@
package handlers
import (
"strings"
"server/internal/models"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
)
// GET /api/app-templates
func ListAppTemplatesHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var rows []models.AppTemplate
if err := db.Order("name asc").Find(&rows).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"data": rows})
}
}
// POST /api/app-templates
func CreateAppTemplateHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var req struct {
Name string `json:"name"`
Description string `json:"description"`
Keywords string `json:"keywords"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
name := strings.TrimSpace(req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name required"})
}
keywords := normalizeKeywordCSV(req.Keywords)
if keywords == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "keywords required"})
}
row := models.AppTemplate{
Name: name,
Description: strings.TrimSpace(req.Description),
Keywords: keywords,
}
if err := db.Create(&row).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "data": row})
}
}
// PATCH /api/app-templates/:id
func UpdateAppTemplateHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
var row models.AppTemplate
if err := db.First(&row, c.Params("id")).Error; err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "not found"})
}
var req struct {
Name *string `json:"name"`
Description *string `json:"description"`
Keywords *string `json:"keywords"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid payload"})
}
if req.Name != nil {
name := strings.TrimSpace(*req.Name)
if name == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "name required"})
}
row.Name = name
}
if req.Description != nil {
row.Description = strings.TrimSpace(*req.Description)
}
if req.Keywords != nil {
keywords := normalizeKeywordCSV(*req.Keywords)
if keywords == "" {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "keywords required"})
}
row.Keywords = keywords
}
if err := db.Save(&row).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true, "data": row})
}
}
// DELETE /api/app-templates/:id
func DeleteAppTemplateHandler(db *gorm.DB) fiber.Handler {
return func(c *fiber.Ctx) error {
if err := db.Delete(&models.AppTemplate{}, c.Params("id")).Error; err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"ok": true})
}
}
func normalizeKeywordCSV(raw string) string {
parts := strings.Split(raw, ",")
seen := make(map[string]struct{})
var out []string
for _, p := range parts {
kw := strings.ToLower(strings.TrimSpace(p))
if kw == "" {
continue
}
if _, ok := seen[kw]; ok {
continue
}
seen[kw] = struct{}{}
out = append(out, kw)
}
return strings.Join(out, ", ")
}

View File

@@ -122,6 +122,18 @@ type AppPoolEntry struct {
func (AppPoolEntry) TableName() string { return "app_pool" }
// AppTemplate — khung ứng dụng (bộ keyword whitelist) dùng chung cho lớp / phòng thi
type AppTemplate struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
Name string `gorm:"column:name;size:120;not null" json:"name"`
Description string `gorm:"column:description;type:text" json:"description"`
Keywords string `gorm:"column:keywords;type:text;not null" json:"keywords"`
}
func (AppTemplate) TableName() string { return "app_templates" }
// WifiPoolEntry — kho WiFi phát hiện từ máy sinh viên (SSID + BSSID điểm phát)
type WifiPoolEntry struct {
ID uint `gorm:"primaryKey" json:"id"`