Phase 2 completed: Stream Server Auth, Database and Business API

This commit is contained in:
2026-03-16 15:54:41 +08:00
commit c84dd6ea36
16 changed files with 757 additions and 0 deletions

96
internal/api/auth.go Normal file
View File

@@ -0,0 +1,96 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"hightube/internal/db"
"hightube/internal/model"
"hightube/internal/utils"
)
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if user exists
var existingUser model.User
if err := db.DB.Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "Username already exists"})
return
}
// Hash password
hashedPassword, err := utils.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
return
}
// Create user
user := model.User{
Username: req.Username,
Password: hashedPassword,
}
if err := db.DB.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
return
}
// Create a default live room for the new user
room := model.Room{
UserID: user.ID,
Title: user.Username + "'s Live Room",
StreamKey: utils.GenerateStreamKey(),
IsActive: false,
}
if err := db.DB.Create(&room).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create room for user"})
return
}
c.JSON(http.StatusCreated, gin.H{"message": "User registered successfully", "user_id": user.ID})
}
func Login(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user model.User
if err := db.DB.Where("username = ?", req.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return
}
if !utils.CheckPasswordHash(req.Password, user.Password) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid username or password"})
return
}
token, err := utils.GenerateToken(user.ID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": token,
})
}

View File

@@ -0,0 +1,42 @@
package api
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"hightube/internal/utils"
)
// AuthMiddleware intercepts requests, validates JWT, and injects user_id into context
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header is required"})
c.Abort()
return
}
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header format must be Bearer {token}"})
c.Abort()
return
}
tokenStr := parts[1]
userIDStr, err := utils.ParseToken(tokenStr)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
c.Abort()
return
}
userID, _ := strconv.ParseUint(userIDStr, 10, 32)
c.Set("user_id", uint(userID))
c.Next()
}
}

50
internal/api/room.go Normal file
View File

@@ -0,0 +1,50 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
"hightube/internal/db"
"hightube/internal/model"
)
// GetMyRoom returns the room details for the currently authenticated user
func GetMyRoom(c *gin.Context) {
userID, _ := c.Get("user_id")
var room model.Room
if err := db.DB.Where("user_id = ?", userID).First(&room).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Room not found"})
return
}
c.JSON(http.StatusOK, gin.H{
"room_id": room.ID,
"title": room.Title,
"stream_key": room.StreamKey,
"is_active": room.IsActive,
})
}
// GetActiveRooms returns a list of all currently active live rooms
func GetActiveRooms(c *gin.Context) {
var rooms []model.Room
// Fetch rooms where is_active is true
if err := db.DB.Where("is_active = ?", true).Find(&rooms).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch active rooms"})
return
}
// Return safe information (do not leak stream keys)
var result []map[string]interface{}
for _, r := range rooms {
result = append(result, map[string]interface{}{
"room_id": r.ID,
"title": r.Title,
"user_id": r.UserID,
})
}
c.JSON(http.StatusOK, gin.H{"active_rooms": result})
}

30
internal/api/router.go Normal file
View File

@@ -0,0 +1,30 @@
package api
import (
"github.com/gin-gonic/gin"
)
// SetupRouter configures the Gin router and defines API endpoints
func SetupRouter() *gin.Engine {
// 设置为发布模式,消除 "[WARNING] Running in debug mode" 警告
gin.SetMode(gin.ReleaseMode)
r := gin.Default()
// 清除代理信任警告 "[WARNING] You trusted all proxies"
r.SetTrustedProxies(nil)
// Public routes
r.POST("/api/register", Register)
r.POST("/api/login", Login)
r.GET("/api/rooms/active", GetActiveRooms)
// Protected routes (require JWT)
authGroup := r.Group("/api")
authGroup.Use(AuthMiddleware())
{
authGroup.GET("/room/my", GetMyRoom)
}
return r
}