package main import ( "net/http" "time" "github.com/gin-gonic/gin" "gorm.io/gorm" ) type registerDeviceRequest struct { Token string `json:"token" binding:"required"` Platform string `json:"platform"` } func messageJSON(m Message) gin.H { return gin.H{ "id": m.ID, "conversation_id": m.ConversationID, "sender_id": m.SenderID, "sender_mode": m.SenderMode, "text": m.Text, "retracted": m.Retracted, "created_at": m.CreatedAt, } } func registerBRoutes(r *gin.Engine, db *gorm.DB) { // Minimal HTML dashboard for operators (roadmap B). JSON stays on /admin/metrics. r.GET("/admin/dashboard", func(c *gin.Context) { if !requireAdmin(c) { return } c.Header("Content-Type", "text/html; charset=utf-8") c.String(http.StatusOK, adminDashboardHTML) }) r.POST("/users/:id/device-tokens", func(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } if !requireSelf(c, db, userID) { return } var req registerDeviceRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"detail": err.Error()}) return } platform := req.Platform if platform == "" { platform = "android" } var existing DeviceToken err := db.Where("token = ?", req.Token).First(&existing).Error if err == nil { existing.UserID = userID existing.Platform = platform existing.UpdatedAt = time.Now() db.Save(&existing) c.JSON(http.StatusOK, gin.H{"id": existing.ID, "token": existing.Token, "platform": existing.Platform}) return } row := DeviceToken{UserID: userID, Token: req.Token, Platform: platform} if err := db.Create(&row).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"detail": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"id": row.ID, "token": row.Token, "platform": row.Platform}) }) r.GET("/users/:id/device-tokens", func(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } if !requireSelf(c, db, userID) { return } var tokens []DeviceToken db.Where("user_id = ?", userID).Order("id desc").Find(&tokens) out := make([]gin.H, 0, len(tokens)) for _, t := range tokens { out = append(out, gin.H{ "id": t.ID, "platform": t.Platform, "token_tail": trimToken(t.Token), "updated_at": t.UpdatedAt, }) } c.JSON(http.StatusOK, gin.H{"device_tokens": out}) }) // Multi-device awareness: list active sessions for the authenticated user. r.GET("/users/:id/sessions", func(c *gin.Context) { userID, ok := parseUintParam(c, "id") if !ok { return } actor, ok := currentUser(c, db, true) if !ok { return } if actor.ID != userID { c.JSON(http.StatusForbidden, gin.H{"detail": "can only list your own sessions"}) return } var sessions []Session db.Where("user_id = ? AND expires_at > ?", userID, time.Now()).Order("id desc").Find(&sessions) out := make([]gin.H, 0, len(sessions)) current := bearerToken(c) for _, s := range sessions { out = append(out, gin.H{ "id": s.ID, "created_at": s.CreatedAt, "expires_at": s.ExpiresAt, "is_current": s.Token == current, }) } c.JSON(http.StatusOK, gin.H{"sessions": out}) }) } func trimToken(token string) string { if len(token) <= 8 { return "****" } return "…" + token[len(token)-6:] } const adminDashboardHTML = `
Bearer ADMIN_API_TOKEN 으로 /admin/metrics 를 불러옵니다. 생성 지연·오류율은 프로세스 메모리 샘플입니다.
loading…`