feat: share code.
This commit is contained in:
@@ -2,6 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -24,6 +25,8 @@ const (
|
||||
lessonPlanStorageDir = "storage/lesson_plans"
|
||||
lessonPlanFieldName = "file"
|
||||
lessonPlanCleanupDays = 30
|
||||
shareCodeExpiry = 5 * time.Minute
|
||||
shareCodeDigits = 6
|
||||
)
|
||||
|
||||
type LessonPlanController struct {
|
||||
@@ -35,6 +38,12 @@ type lessonPlanPaginationParams struct {
|
||||
PageSize int `form:"pageSize,default=10"`
|
||||
}
|
||||
|
||||
type lessonPlanShareCodeResponse struct {
|
||||
Code string `json:"code"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
FileID uint `json:"fileId"`
|
||||
}
|
||||
|
||||
func NewLessonPlanController() *LessonPlanController {
|
||||
return &LessonPlanController{DB: config.DB}
|
||||
}
|
||||
@@ -216,6 +225,101 @@ func (lc *LessonPlanController) Download(c *gin.Context) {
|
||||
c.FileAttachment(record.FilePath, record.OriginalFilename)
|
||||
}
|
||||
|
||||
func (lc *LessonPlanController) GenerateShareCode(c *gin.Context) {
|
||||
record, err := lc.findLessonPlan(c.Param("id"))
|
||||
if err != nil {
|
||||
respondLessonPlanLookupError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
userID, username, ok := currentUser(c)
|
||||
if !ok {
|
||||
writeError(c, http.StatusUnauthorized, "invalid user context")
|
||||
return
|
||||
}
|
||||
role := currentUserRole(c)
|
||||
if !canAccessLessonPlan(record, userID, role) {
|
||||
writeError(c, http.StatusForbidden, "permission denied")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
expiresAt := now.Add(shareCodeExpiry).UnixMilli()
|
||||
var created models.AppFileShareCode
|
||||
|
||||
if err := lc.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&models.AppFileShareCode{}).
|
||||
Where("app_file_id = ? AND is_active = ?", record.ID, true).
|
||||
Update("is_active", false).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shareCode, err := generateUniqueShareCode(tx, now.UnixMilli())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
created = models.AppFileShareCode{
|
||||
AppFileID: record.ID,
|
||||
ShareCode: shareCode,
|
||||
GeneratedByID: userID,
|
||||
GeneratedByName: username,
|
||||
ExpiresAt: expiresAt,
|
||||
IsActive: true,
|
||||
}
|
||||
return tx.Create(&created).Error
|
||||
}); err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to generate share code")
|
||||
return
|
||||
}
|
||||
|
||||
writeSuccess(c, http.StatusOK, "share code generated", lessonPlanShareCodeResponse{
|
||||
Code: created.ShareCode,
|
||||
ExpiresAt: created.ExpiresAt,
|
||||
FileID: record.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (lc *LessonPlanController) DownloadByShareCode(c *gin.Context) {
|
||||
shareCode := strings.TrimSpace(c.Param("code"))
|
||||
if len(shareCode) != shareCodeDigits {
|
||||
writeError(c, http.StatusBadRequest, "invalid share code")
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
var share models.AppFileShareCode
|
||||
if err := lc.DB.
|
||||
Where("share_code = ? AND is_active = ? AND expires_at > ?", shareCode, true, now).
|
||||
Order("id DESC").
|
||||
First(&share).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
writeError(c, http.StatusNotFound, "share code expired or not found")
|
||||
return
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to query share code")
|
||||
return
|
||||
}
|
||||
|
||||
var record models.AppFile
|
||||
if err := lc.DB.Where("id = ? AND file_type = ?", share.AppFileID, models.AppFileTypeLessonPlan).First(&record).Error; err != nil {
|
||||
respondLessonPlanLookupError(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := lc.DB.Model(&models.AppFile{}).
|
||||
Where("id = ?", record.ID).
|
||||
Updates(map[string]interface{}{
|
||||
"download_count": gorm.Expr("download_count + 1"),
|
||||
"last_download_at": now,
|
||||
}).Error; err != nil {
|
||||
writeError(c, http.StatusInternalServerError, "failed to update download stats")
|
||||
return
|
||||
}
|
||||
|
||||
c.FileAttachment(record.FilePath, record.OriginalFilename)
|
||||
}
|
||||
|
||||
func (lc *LessonPlanController) Delete(c *gin.Context) {
|
||||
record, err := lc.findLessonPlan(c.Param("id"))
|
||||
if err != nil {
|
||||
@@ -336,6 +440,9 @@ func canAccessLessonPlan(record models.AppFile, userID uint, role models.UserRol
|
||||
}
|
||||
|
||||
func deleteLessonPlanRecord(db *gorm.DB, record *models.AppFile) error {
|
||||
if err := db.Where("app_file_id = ?", record.ID).Delete(&models.AppFileShareCode{}).Error; err != nil {
|
||||
return fmt.Errorf("failed to delete share codes: %w", err)
|
||||
}
|
||||
if err := os.Remove(record.FilePath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to remove file: %w", err)
|
||||
}
|
||||
@@ -360,3 +467,37 @@ func respondLessonPlanLookupError(c *gin.Context, err error) {
|
||||
}
|
||||
writeError(c, http.StatusInternalServerError, "failed to query lesson plan")
|
||||
}
|
||||
|
||||
func generateUniqueShareCode(db *gorm.DB, nowMillis int64) (string, error) {
|
||||
for i := 0; i < 10; i++ {
|
||||
shareCode, err := randomNumericCode(shareCodeDigits)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var count int64
|
||||
if err := db.Model(&models.AppFileShareCode{}).
|
||||
Where("share_code = ? AND is_active = ? AND expires_at > ?", shareCode, true, nowMillis).
|
||||
Count(&count).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
if count == 0 {
|
||||
return shareCode, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("failed to allocate unique share code")
|
||||
}
|
||||
|
||||
func randomNumericCode(length int) (string, error) {
|
||||
if length <= 0 {
|
||||
return "", fmt.Errorf("invalid code length")
|
||||
}
|
||||
buffer := make([]byte, length)
|
||||
if _, err := rand.Read(buffer); err != nil {
|
||||
return "", err
|
||||
}
|
||||
digits := make([]byte, length)
|
||||
for i, value := range buffer {
|
||||
digits[i] = byte('0' + (value % 10))
|
||||
}
|
||||
return string(digits), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user