fix(P2): rand.Read错误处理 + JWT随机密钥 + 密码环境变量化 + 前端路由守卫
This commit is contained in:
17
.env.example
Normal file
17
.env.example
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# feedsystem_video_go 环境变量模板
|
||||||
|
# 复制此文件为 .env 后修改实际值
|
||||||
|
# .env 已被 .gitignore 忽略,不会提交到仓库
|
||||||
|
|
||||||
|
# MySQL
|
||||||
|
MYSQL_ROOT_PASSWORD=123456
|
||||||
|
MYSQL_DATABASE=feedsystem
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_PASSWORD=123456
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
RABBITMQ_USER=admin
|
||||||
|
RABBITMQ_PASS=password123
|
||||||
|
|
||||||
|
# JWT (生产环境务必修改为随机强密钥)
|
||||||
|
JWT_SECRET=change-me-in-production
|
||||||
@@ -1,65 +1,74 @@
|
|||||||
// internal/auth/jwt.go
|
// internal/auth/jwt.go
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"crypto/rand"
|
||||||
"os"
|
"encoding/hex"
|
||||||
"time"
|
"errors"
|
||||||
|
"log"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"os"
|
||||||
)
|
"time"
|
||||||
|
|
||||||
func jwtSecret() []byte {
|
"github.com/golang-jwt/jwt/v5"
|
||||||
secret := os.Getenv("JWT_SECRET")
|
)
|
||||||
if secret == "" {
|
|
||||||
secret = "change-me-in-env"
|
func jwtSecret() []byte {
|
||||||
}
|
secret := os.Getenv("JWT_SECRET")
|
||||||
return []byte(secret)
|
if secret == "" {
|
||||||
}
|
b := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(b); err != nil {
|
||||||
type Claims struct {
|
log.Printf("FATAL: cannot generate JWT secret: %v", err)
|
||||||
AccountID uint `json:"account_id"`
|
return []byte("fallback-unsafe-key-change-me")
|
||||||
Username string `json:"username"`
|
}
|
||||||
jwt.RegisteredClaims
|
secret = hex.EncodeToString(b)
|
||||||
}
|
log.Printf("WARNING: JWT_SECRET not set, generated random key. All tokens invalid on restart.")
|
||||||
|
}
|
||||||
func GenerateToken(accountID uint, username string) (string, error) {
|
return []byte(secret)
|
||||||
now := time.Now()
|
}
|
||||||
|
|
||||||
claims := Claims{
|
type Claims struct {
|
||||||
AccountID: accountID,
|
AccountID uint `json:"account_id"`
|
||||||
Username: username,
|
Username string `json:"username"`
|
||||||
RegisteredClaims: jwt.RegisteredClaims{
|
jwt.RegisteredClaims
|
||||||
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
}
|
||||||
IssuedAt: jwt.NewNumericDate(now),
|
|
||||||
NotBefore: jwt.NewNumericDate(now),
|
func GenerateToken(accountID uint, username string) (string, error) {
|
||||||
},
|
now := time.Now()
|
||||||
}
|
|
||||||
|
claims := Claims{
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
AccountID: accountID,
|
||||||
|
Username: username,
|
||||||
return token.SignedString(jwtSecret())
|
RegisteredClaims: jwt.RegisteredClaims{
|
||||||
}
|
ExpiresAt: jwt.NewNumericDate(now.Add(24 * time.Hour)),
|
||||||
|
IssuedAt: jwt.NewNumericDate(now),
|
||||||
func ParseToken(tokenString string) (*Claims, error) {
|
NotBefore: jwt.NewNumericDate(now),
|
||||||
token, err := jwt.ParseWithClaims(
|
},
|
||||||
tokenString,
|
}
|
||||||
&Claims{},
|
|
||||||
func(token *jwt.Token) (interface{}, error) {
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
if token.Method == nil || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
|
||||||
return nil, errors.New("unexpected signing method")
|
return token.SignedString(jwtSecret())
|
||||||
}
|
}
|
||||||
return jwtSecret(), nil
|
|
||||||
},
|
func ParseToken(tokenString string) (*Claims, error) {
|
||||||
)
|
token, err := jwt.ParseWithClaims(
|
||||||
if err != nil {
|
tokenString,
|
||||||
return nil, err
|
&Claims{},
|
||||||
}
|
func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if token.Method == nil || token.Method.Alg() != jwt.SigningMethodHS256.Alg() {
|
||||||
claims, ok := token.Claims.(*Claims)
|
return nil, errors.New("unexpected signing method")
|
||||||
if !ok || !token.Valid {
|
}
|
||||||
return nil, jwt.ErrTokenInvalidClaims
|
return jwtSecret(), nil
|
||||||
}
|
},
|
||||||
|
)
|
||||||
return claims, nil
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
claims, ok := token.Claims.(*Claims)
|
||||||
|
if !ok || !token.Valid {
|
||||||
|
return nil, jwt.ErrTokenInvalidClaims
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,241 +1,253 @@
|
|||||||
package video
|
package video
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"feedsystem_video_go/internal/account"
|
"feedsystem_video_go/internal/account"
|
||||||
"feedsystem_video_go/internal/middleware/jwt"
|
"feedsystem_video_go/internal/middleware/jwt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type VideoHandler struct {
|
type VideoHandler struct {
|
||||||
service *VideoService
|
service *VideoService
|
||||||
accountService *account.AccountService
|
accountService *account.AccountService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVideoHandler(service *VideoService, accountService *account.AccountService) *VideoHandler {
|
func NewVideoHandler(service *VideoService, accountService *account.AccountService) *VideoHandler {
|
||||||
return &VideoHandler{service: service, accountService: accountService}
|
return &VideoHandler{service: service, accountService: accountService}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vh *VideoHandler) PublishVideo(c *gin.Context) {
|
func (vh *VideoHandler) PublishVideo(c *gin.Context) {
|
||||||
var req PublishVideoRequest
|
var req PublishVideoRequest
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
authorId, err := jwt.GetAccountID(c)
|
authorId, err := jwt.GetAccountID(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
username, err := jwt.GetUsername(c)
|
username, err := jwt.GetUsername(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
video := &Video{
|
video := &Video{
|
||||||
AuthorID: authorId,
|
AuthorID: authorId,
|
||||||
Username: username,
|
Username: username,
|
||||||
Title: req.Title,
|
Title: req.Title,
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
PlayURL: req.PlayURL,
|
PlayURL: req.PlayURL,
|
||||||
CoverURL: req.CoverURL,
|
CoverURL: req.CoverURL,
|
||||||
CreateTime: time.Now(),
|
CreateTime: time.Now(),
|
||||||
}
|
}
|
||||||
if err := vh.service.Publish(c.Request.Context(), video); err != nil {
|
if err := vh.service.Publish(c.Request.Context(), video); err != nil {
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(200, video)
|
c.JSON(200, video)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (vh *VideoHandler) UploadVideo(c *gin.Context) {
|
func (vh *VideoHandler) UploadVideo(c *gin.Context) {
|
||||||
authorId, err := jwt.GetAccountID(c)
|
authorId, err := jwt.GetAccountID(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := c.FormFile("file")
|
f, err := c.FormFile("file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxSize = 200 << 20
|
const maxSize = 200 << 20
|
||||||
if f.Size <= 0 || f.Size > maxSize {
|
if f.Size <= 0 || f.Size > maxSize {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||||
if ext != ".mp4" {
|
if ext != ".mp4" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .mp4 is allowed"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "only .mp4 is allowed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
date := time.Now().Format("20060102")
|
date := time.Now().Format("20060102")
|
||||||
relDir := filepath.Join("videos", fmt.Sprintf("%d", authorId), date)
|
relDir := filepath.Join("videos", fmt.Sprintf("%d", authorId), date)
|
||||||
root := filepath.Join(".run", "uploads")
|
root := filepath.Join(".run", "uploads")
|
||||||
absDir := filepath.Join(root, relDir)
|
absDir := filepath.Join(root, relDir)
|
||||||
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := randHex(16) + ext
|
filename, err := randHex(16)
|
||||||
absPath := filepath.Join(absDir, filename)
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate filename"})
|
||||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
return
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
filename = filename + ext
|
||||||
}
|
absPath := filepath.Join(absDir, filename)
|
||||||
|
|
||||||
urlPath := path.Join("/static", "videos", fmt.Sprintf("%d", authorId), date, filename)
|
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
c.JSON(http.StatusOK, gin.H{
|
return
|
||||||
"url": buildAbsoluteURL(c, urlPath),
|
}
|
||||||
"play_url": buildAbsoluteURL(c, urlPath),
|
|
||||||
})
|
urlPath := path.Join("/static", "videos", fmt.Sprintf("%d", authorId), date, filename)
|
||||||
}
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
func (vh *VideoHandler) UploadCover(c *gin.Context) {
|
"url": buildAbsoluteURL(c, urlPath),
|
||||||
authorId, err := jwt.GetAccountID(c)
|
"play_url": buildAbsoluteURL(c, urlPath),
|
||||||
if err != nil {
|
})
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
|
||||||
}
|
func (vh *VideoHandler) UploadCover(c *gin.Context) {
|
||||||
|
authorId, err := jwt.GetAccountID(c)
|
||||||
f, err := c.FormFile("file")
|
if err != nil {
|
||||||
if err != nil {
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
f, err := c.FormFile("file")
|
||||||
const maxSize = 10 << 20
|
if err != nil {
|
||||||
if f.Size <= 0 || f.Size > maxSize {
|
c.JSON(http.StatusBadRequest, gin.H{"error": "missing file"})
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
return
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
const maxSize = 10 << 20
|
||||||
ext := strings.ToLower(filepath.Ext(f.Filename))
|
if f.Size <= 0 || f.Size > maxSize {
|
||||||
switch ext {
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid file size"})
|
||||||
case ".jpg", ".jpeg", ".png", ".webp":
|
return
|
||||||
default:
|
}
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp is allowed"})
|
|
||||||
return
|
ext := strings.ToLower(filepath.Ext(f.Filename))
|
||||||
}
|
switch ext {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".webp":
|
||||||
date := time.Now().Format("20060102")
|
default:
|
||||||
relDir := filepath.Join("covers", fmt.Sprintf("%d", authorId), date)
|
c.JSON(http.StatusBadRequest, gin.H{"error": "only .jpg/.jpeg/.png/.webp is allowed"})
|
||||||
root := filepath.Join(".run", "uploads")
|
return
|
||||||
absDir := filepath.Join(root, relDir)
|
}
|
||||||
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
date := time.Now().Format("20060102")
|
||||||
return
|
relDir := filepath.Join("covers", fmt.Sprintf("%d", authorId), date)
|
||||||
}
|
root := filepath.Join(".run", "uploads")
|
||||||
|
absDir := filepath.Join(root, relDir)
|
||||||
filename := randHex(16) + ext
|
if err := os.MkdirAll(absDir, 0o755); err != nil {
|
||||||
absPath := filepath.Join(absDir, filename)
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
filename, err := randHex(16)
|
||||||
}
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate filename"})
|
||||||
urlPath := path.Join("/static", "covers", fmt.Sprintf("%d", authorId), date, filename)
|
return
|
||||||
|
}
|
||||||
c.JSON(http.StatusOK, gin.H{
|
filename = filename + ext
|
||||||
"url": buildAbsoluteURL(c, urlPath),
|
absPath := filepath.Join(absDir, filename)
|
||||||
"cover_url": buildAbsoluteURL(c, urlPath),
|
|
||||||
})
|
if err := c.SaveUploadedFile(f, absPath); err != nil {
|
||||||
}
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
func randHex(n int) string {
|
}
|
||||||
b := make([]byte, n)
|
|
||||||
_, _ = rand.Read(b)
|
urlPath := path.Join("/static", "covers", fmt.Sprintf("%d", authorId), date, filename)
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"url": buildAbsoluteURL(c, urlPath),
|
||||||
func buildAbsoluteURL(c *gin.Context, p string) string {
|
"cover_url": buildAbsoluteURL(c, urlPath),
|
||||||
scheme := "http"
|
})
|
||||||
if c.Request.TLS != nil {
|
}
|
||||||
scheme = "https"
|
|
||||||
}
|
func randHex(n int) (string, error) {
|
||||||
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
|
b := make([]byte, n)
|
||||||
scheme = xf
|
if _, err := rand.Read(b); err != nil {
|
||||||
}
|
return "", fmt.Errorf("rand.Read: %w", err)
|
||||||
return fmt.Sprintf("%s://%s%s", scheme, c.Request.Host, p)
|
}
|
||||||
}
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
func (vh *VideoHandler) DeleteVideo(c *gin.Context) {
|
|
||||||
var req DeleteVideoRequest
|
func buildAbsoluteURL(c *gin.Context, p string) string {
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
scheme := "http"
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
if c.Request.TLS != nil {
|
||||||
return
|
scheme = "https"
|
||||||
}
|
}
|
||||||
authorId, err := jwt.GetAccountID(c)
|
if xf := c.GetHeader("X-Forwarded-Proto"); xf != "" {
|
||||||
if err != nil {
|
scheme = xf
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
return fmt.Sprintf("%s://%s%s", scheme, c.Request.Host, p)
|
||||||
}
|
}
|
||||||
if err := vh.service.Delete(c.Request.Context(), req.ID, authorId); err != nil {
|
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
func (vh *VideoHandler) DeleteVideo(c *gin.Context) {
|
||||||
return
|
var req DeleteVideoRequest
|
||||||
}
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
c.JSON(200, gin.H{"message": "video deleted"})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
}
|
return
|
||||||
|
}
|
||||||
func (vh *VideoHandler) ListByAuthorID(c *gin.Context) {
|
authorId, err := jwt.GetAccountID(c)
|
||||||
var req ListByAuthorIDRequest
|
if err != nil {
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
return
|
||||||
return
|
}
|
||||||
}
|
if err := vh.service.Delete(c.Request.Context(), req.ID, authorId); err != nil {
|
||||||
videos, err := vh.service.ListByAuthorID(c.Request.Context(), req.AuthorID)
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
if err != nil {
|
return
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
c.JSON(200, gin.H{"message": "video deleted"})
|
||||||
}
|
}
|
||||||
if videos == nil {
|
|
||||||
videos = []Video{}
|
func (vh *VideoHandler) ListByAuthorID(c *gin.Context) {
|
||||||
}
|
var req ListByAuthorIDRequest
|
||||||
c.JSON(200, videos)
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
}
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
func (vh *VideoHandler) GetDetail(c *gin.Context) {
|
}
|
||||||
var req GetDetailRequest
|
videos, err := vh.service.ListByAuthorID(c.Request.Context(), req.AuthorID)
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err != nil {
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
video, err := vh.service.GetDetail(c.Request.Context(), req.ID)
|
if videos == nil {
|
||||||
if err != nil {
|
videos = []Video{}
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
c.JSON(200, videos)
|
||||||
}
|
}
|
||||||
c.JSON(200, video)
|
|
||||||
}
|
func (vh *VideoHandler) GetDetail(c *gin.Context) {
|
||||||
|
var req GetDetailRequest
|
||||||
func (vh *VideoHandler) UpdateLikesCount(c *gin.Context) {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
var req UpdateLikesCountRequest
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
return
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
}
|
||||||
return
|
video, err := vh.service.GetDetail(c.Request.Context(), req.ID)
|
||||||
}
|
if err != nil {
|
||||||
if err := vh.service.UpdateLikesCount(c.Request.Context(), req.ID, req.LikesCount); err != nil {
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
c.JSON(400, gin.H{"error": err.Error()})
|
return
|
||||||
return
|
}
|
||||||
}
|
c.JSON(200, video)
|
||||||
c.JSON(200, gin.H{"message": "likes count updated"})
|
}
|
||||||
}
|
|
||||||
|
func (vh *VideoHandler) UpdateLikesCount(c *gin.Context) {
|
||||||
|
var req UpdateLikesCountRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := vh.service.UpdateLikesCount(c.Request.Context(), req.ID, req.LikesCount); err != nil {
|
||||||
|
c.JSON(400, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(200, gin.H{"message": "likes count updated"})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,106 +1,106 @@
|
|||||||
version: '3.8'
|
version: '3.8'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mysql:
|
mysql:
|
||||||
image: mysql:8.0
|
image: mysql:8.0
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: "123456"
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-123456}
|
||||||
MYSQL_DATABASE: "feedsystem"
|
MYSQL_DATABASE: ${MYSQL_DATABASE:-feedsystem}
|
||||||
TZ: "Asia/Shanghai"
|
TZ: "Asia/Shanghai"
|
||||||
ports:
|
ports:
|
||||||
- "3307:3306"
|
- "3307:3306"
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- mysql_data:/var/lib/mysql
|
||||||
command:
|
command:
|
||||||
- --default-authentication-plugin=mysql_native_password
|
- --default-authentication-plugin=mysql_native_password
|
||||||
- --character-set-server=utf8mb4
|
- --character-set-server=utf8mb4
|
||||||
- --collation-server=utf8mb4_0900_ai_ci
|
- --collation-server=utf8mb4_0900_ai_ci
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p123456 --silent"]
|
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -p123456 --silent"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
restart: always
|
restart: always
|
||||||
command: ["redis-server", "--appendonly", "yes", "--requirepass", "123456"]
|
command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:-123456}"]
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
test: ["CMD", "redis-cli", "-a", "123456", "ping"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
image: rabbitmq:3-management
|
image: rabbitmq:3-management
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "5672:5672"
|
- "5672:5672"
|
||||||
- "15672:15672"
|
- "15672:15672"
|
||||||
environment:
|
environment:
|
||||||
RABBITMQ_DEFAULT_USER: admin
|
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-admin}
|
||||||
RABBITMQ_DEFAULT_PASS: password123
|
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASS:-password123}
|
||||||
volumes:
|
volumes:
|
||||||
- rabbitmq_data:/var/lib/rabbitmq
|
- rabbitmq_data:/var/lib/rabbitmq
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "rabbitmq-diagnostics -q ping"]
|
test: ["CMD-SHELL", "rabbitmq-diagnostics -q ping"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 20
|
retries: 20
|
||||||
|
|
||||||
backend:
|
backend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
target: api
|
target: api
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||||
- backend_uploads:/app/.run/uploads
|
- backend_uploads:/app/.run/uploads
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
worker:
|
worker:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
target: worker
|
target: worker
|
||||||
restart: always
|
restart: always
|
||||||
volumes:
|
volumes:
|
||||||
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
- ./backend/configs/config.docker.yaml:/app/configs/config.yaml:ro
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
rabbitmq:
|
rabbitmq:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: frontend/Dockerfile
|
dockerfile: frontend/Dockerfile
|
||||||
restart: always
|
restart: always
|
||||||
ports:
|
ports:
|
||||||
- "5173:80"
|
- "5173:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
redis_data:
|
redis_data:
|
||||||
rabbitmq_data:
|
rabbitmq_data:
|
||||||
backend_uploads:
|
backend_uploads:
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,39 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
|
||||||
import HomeView from '../views/HomeView.vue'
|
import HomeView from '../views/HomeView.vue'
|
||||||
import HotView from '../views/HotView.vue'
|
import HotView from '../views/HotView.vue'
|
||||||
import VideoView from '../views/VideoView.vue'
|
import VideoView from '../views/VideoView.vue'
|
||||||
import VideoDetailView from '../views/VideoDetailView.vue'
|
import VideoDetailView from '../views/VideoDetailView.vue'
|
||||||
import AccountView from '../views/AccountView.vue'
|
import AccountView from '../views/AccountView.vue'
|
||||||
import ChangePasswordView from '../views/ChangePasswordView.vue'
|
import ChangePasswordView from '../views/ChangePasswordView.vue'
|
||||||
import RegisterView from '../views/RegisterView.vue'
|
import RegisterView from '../views/RegisterView.vue'
|
||||||
import SettingsView from '../views/SettingsView.vue'
|
import SettingsView from '../views/SettingsView.vue'
|
||||||
import UserProfileView from '../views/UserProfileView.vue'
|
import UserProfileView from '../views/UserProfileView.vue'
|
||||||
|
import { useAuthStore } from '../stores/auth'
|
||||||
const router = createRouter({
|
|
||||||
history: createWebHistory(),
|
const router = createRouter({
|
||||||
routes: [
|
history: createWebHistory(),
|
||||||
{ path: '/', name: 'home', component: HomeView },
|
routes: [
|
||||||
{ path: '/feed', redirect: '/' },
|
{ path: '/', name: 'home', component: HomeView },
|
||||||
{ path: '/hot', name: 'hot', component: HotView },
|
{ path: '/feed', redirect: '/' },
|
||||||
{ path: '/video', name: 'video', component: VideoView },
|
{ path: '/hot', name: 'hot', component: HotView },
|
||||||
{ path: '/video/:id', name: 'video-detail', component: VideoDetailView, props: true },
|
{ path: '/video', name: 'video', component: VideoView, meta: { requiresAuth: true } },
|
||||||
{ path: '/account', name: 'account', component: AccountView },
|
{ path: '/video/:id', name: 'video-detail', component: VideoDetailView, props: true },
|
||||||
{ path: '/account/register', name: 'account-register', component: RegisterView },
|
{ path: '/account', name: 'account', component: AccountView },
|
||||||
{ path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView },
|
{ path: '/account/register', name: 'account-register', component: RegisterView },
|
||||||
{ path: '/settings', name: 'settings', component: SettingsView },
|
{ path: '/account/change-password', name: 'account-change-password', component: ChangePasswordView },
|
||||||
{ path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true },
|
{ path: '/settings', name: 'settings', component: SettingsView, meta: { requiresAuth: true } },
|
||||||
],
|
{ path: '/u/:id', name: 'user-profile', component: UserProfileView, props: true },
|
||||||
})
|
],
|
||||||
|
})
|
||||||
export default router
|
|
||||||
|
router.beforeEach((to, _from, next) => {
|
||||||
|
const auth = useAuthStore()
|
||||||
|
if (to.meta.requiresAuth && !auth.isLoggedIn) {
|
||||||
|
next({ path: '/account', query: { redirect: to.fullPath } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
|
|||||||
Reference in New Issue
Block a user