|
|
|
@@ -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
|
|
|
|
|
}
|
|
|
|
|