UPDATE: Media module
All checks were successful
Build and Release / release (push) Successful in 1m7s
All checks were successful
Build and Release / release (push) Successful in 1m7s
This commit is contained in:
230
internal/controllers/mediaController.go
Normal file
230
internal/controllers/mediaController.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/services"
|
||||
"history-api/pkg/validator"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
type MediaController struct {
|
||||
service services.MediaService
|
||||
}
|
||||
|
||||
func NewMediaController(svc services.MediaService) *MediaController {
|
||||
return &MediaController{service: svc}
|
||||
}
|
||||
|
||||
// GetMediaByID godoc
|
||||
// @Summary Get media by ID
|
||||
// @Description Retrieve a media file by its ID
|
||||
// @Tags Media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "Media ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /media/{id} [get]
|
||||
func (m *MediaController) GetMediaByID(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
mediaId := c.Params("id")
|
||||
res, err := m.service.GetMediaByID(ctx, mediaId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// SearchMedia godoc
|
||||
// @Summary Search media
|
||||
// @Description Search media with filters, pagination
|
||||
// @Tags Media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Items per page"
|
||||
// @Param keyword query string false "Search keyword"
|
||||
// @Success 200 {object} response.PaginatedResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /media [get]
|
||||
func (m *MediaController) SearchMedia(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.SearchMediaDto{}
|
||||
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
res, err := m.service.SearchMedia(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(res)
|
||||
}
|
||||
|
||||
// DeleteMedia godoc
|
||||
// @Summary Delete media
|
||||
// @Description Delete a media file by ID
|
||||
// @Tags Media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Media ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /media/{id} [delete]
|
||||
func (m *MediaController) DeleteMedia(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
claimsVal := c.Locals("user_claims")
|
||||
if claimsVal == nil {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Unauthorized",
|
||||
})
|
||||
}
|
||||
|
||||
claims, ok := claimsVal.(*response.JWTClaims)
|
||||
if !ok {
|
||||
return c.Status(fiber.StatusUnauthorized).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "Invalid user claims",
|
||||
})
|
||||
}
|
||||
|
||||
mediaId := c.Params("id")
|
||||
err := m.service.DeleteMedia(ctx, claims, mediaId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "Media deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UploadServerSide godoc
|
||||
// @Summary Upload media (server-side)
|
||||
// @Description Upload media file through server
|
||||
// @Tags Media
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param file formData file true "Upload file"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /media/upload [post]
|
||||
func (m *MediaController) UploadServerSide(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
fileHeader, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: "File is required",
|
||||
})
|
||||
}
|
||||
|
||||
url, err := m.service.UploadServerSide(ctx, c.Locals("uid").(string), fileHeader)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: url,
|
||||
Message: "Media uploaded successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GeneratePresignedURL godoc
|
||||
// @Summary Generate presigned URL
|
||||
// @Description Generate a presigned URL for direct upload to storage
|
||||
// @Tags Media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param filename query string true "File name"
|
||||
// @Param contentType query string true "Content type"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /media/presigned [get]
|
||||
func (m *MediaController) GeneratePresignedURL(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.PreSignedDto{}
|
||||
if err := validator.ValidateQueryDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
res, err := m.service.GeneratePresignedURL(ctx, c.Locals("uid").(string), dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(res)
|
||||
}
|
||||
|
||||
// PreSignedCompleted godoc
|
||||
// @Summary Confirm presigned upload
|
||||
// @Description Confirm that upload via presigned URL is completed
|
||||
// @Tags Media
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param key query string true "Storage key"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /media/presigned/complete [post]
|
||||
func (m *MediaController) PreSignedCompleted(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dto := &request.PreSignedCompleteDto{}
|
||||
if err := validator.ValidateBodyDto(c, dto); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
res, err := m.service.PreSignedCompleted(ctx, c.Locals("uid").(string), dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(res)
|
||||
}
|
||||
@@ -12,11 +12,15 @@ import (
|
||||
)
|
||||
|
||||
type UserController struct {
|
||||
service services.UserService
|
||||
service services.UserService
|
||||
mediaService services.MediaService
|
||||
}
|
||||
|
||||
func NewUserController(svc services.UserService) *UserController {
|
||||
return &UserController{service: svc}
|
||||
func NewUserController(svc services.UserService, mediaSvc services.MediaService) *UserController {
|
||||
return &UserController{
|
||||
service: svc,
|
||||
mediaService: mediaSvc,
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserCurrent godoc
|
||||
@@ -47,6 +51,61 @@ func (h *UserController) GetUserCurrent(c fiber.Ctx) error {
|
||||
})
|
||||
}
|
||||
|
||||
// GetUserMedia godoc
|
||||
// @Summary Get current user's media
|
||||
// @Description Retrieve media list of the currently authenticated user
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/current/media [get]
|
||||
func (h *UserController) GetUserMedia(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := h.mediaService.GetMediaByUserID(ctx, c.Locals("uid").(string))
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// GetMediaByUserID godoc
|
||||
// @Summary Get user's media by user ID
|
||||
// @Description Retrieve media list by specific user ID
|
||||
// @Tags Users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users/{id}/media [get]
|
||||
func (h *UserController) GetMediaByUserID(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
userId := c.Params("id")
|
||||
res, err := h.mediaService.GetMediaByUserID(ctx, userId)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateProfile godoc
|
||||
// @Summary Update user profile
|
||||
// @Description Update the profile details of the currently authenticated user
|
||||
@@ -250,11 +309,11 @@ func (h *UserController) GetUserById(c fiber.Ctx) error {
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param query query request.SearchUserDto false "Search Query"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Success 200 {object} response.PaginatedResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /users [get]
|
||||
func (h *UserController) Search(c fiber.Ctx) error {
|
||||
func (h *UserController) SearchUser(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -265,7 +324,7 @@ func (h *UserController) Search(c fiber.Ctx) error {
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
res, err := h.service.Search(ctx, dto)
|
||||
res, err := h.service.SearchUser(ctx, dto)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
|
||||
@@ -3,19 +3,14 @@ package request
|
||||
type PreSignedDto struct {
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
ContentType string `json:"contentType" validate:"required"`
|
||||
Size int64 `json:"size" validate:"required"`
|
||||
}
|
||||
|
||||
type PreSignedCompleteDto struct {
|
||||
FileName string `json:"fileName" validate:"required"`
|
||||
MediaId string `json:"mediaId" validate:"required"`
|
||||
PublicUrl string `json:"publicUrl" validate:"required"`
|
||||
TokenID string `json:"token_id" validate:"required"`
|
||||
}
|
||||
|
||||
type SearchMediaDto struct {
|
||||
MediaId string `query:"media_id" validate:"omitempty"`
|
||||
FileName string `query:"file_name" validate:"omitempty"`
|
||||
SortBy string `query:"sort_by" default:"created_at" validate:"oneof=created_at updated_at"`
|
||||
Order string `query:"order" default:"desc" validate:"oneof=asc desc"`
|
||||
Page int `query:"page" default:"1" validate:"min=1"`
|
||||
Limit int `query:"limit" default:"10" validate:"min=1,max=100"`
|
||||
CursorPaginationDto
|
||||
Search string `json:"search" query:"search" validate:"omitempty,min=2,max=200"`
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ package response
|
||||
import "time"
|
||||
|
||||
type PreSignedResponse struct {
|
||||
UploadUrl string `json:"uploadUrl"`
|
||||
PublicUrl string `json:"publicUrl"`
|
||||
FileName string `json:"fileName"`
|
||||
MediaId string `json:"mediaId"`
|
||||
SignedHeaders map[string]string `json:"signedHeaders"`
|
||||
TokenID string `json:"token_id"`
|
||||
UploadUrl string `json:"upload_url"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
SignedHeaders map[string]string `json:"signed_headers"`
|
||||
}
|
||||
|
||||
type MediaResponse struct {
|
||||
|
||||
@@ -17,6 +17,18 @@ type MediaEntity struct {
|
||||
UpdatedAt *time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type MediaStorageEntity struct {
|
||||
ID string `json:"id"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
}
|
||||
|
||||
func (e * MediaEntity) ToStorageEntity() *MediaStorageEntity {
|
||||
return &MediaStorageEntity{
|
||||
ID: e.ID,
|
||||
StorageKey: e.StorageKey,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *MediaEntity) ToResponse() *response.MediaResponse {
|
||||
return &response.MediaResponse{
|
||||
ID: e.ID,
|
||||
@@ -30,3 +42,11 @@ func (e *MediaEntity) ToResponse() *response.MediaResponse {
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func MediaEntitiesToResponse(entities []*MediaEntity) []*response.MediaResponse {
|
||||
responses := make([]*response.MediaResponse, len(entities))
|
||||
for i, entity := range entities {
|
||||
responses[i] = entity.ToResponse()
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
@@ -8,6 +8,16 @@ type TokenEntity struct {
|
||||
TokenType constants.TokenType `json:"token_type"`
|
||||
}
|
||||
|
||||
type TokenUploadEntity struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
StorageKey string `json:"storage_key"`
|
||||
OriginalName string `json:"original_name"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int64 `json:"size"`
|
||||
FileMetadata []byte `json:"file_metadata"`
|
||||
}
|
||||
|
||||
type OAuthState struct {
|
||||
State string `json:"state"`
|
||||
RedirectURL string `json:"redirect"`
|
||||
|
||||
@@ -13,9 +13,14 @@ type TokenRepository interface {
|
||||
Get(ctx context.Context, email string, tokenType constants.TokenType) (*models.TokenEntity, error)
|
||||
Create(ctx context.Context, token *models.TokenEntity) error
|
||||
Delete(ctx context.Context, email string, tokenType constants.TokenType) error
|
||||
|
||||
CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error)
|
||||
CreateVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||
DeleteVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) error
|
||||
|
||||
CreateUploadToken(ctx context.Context, userId string, token *models.TokenUploadEntity) error
|
||||
GetUploadToken(ctx context.Context, userId string, id string) (*models.TokenUploadEntity, error)
|
||||
DeleteUploadToken(ctx context.Context, userId string, id string) error
|
||||
}
|
||||
|
||||
type tokenRepository struct {
|
||||
@@ -37,16 +42,38 @@ func (t *tokenRepository) DeleteVerified(ctx context.Context, email string, toke
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
return t.c.Del(ctx, cacheKey)
|
||||
}
|
||||
|
||||
|
||||
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
exists, err := t.c.Exists(ctx, cacheKey)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (t *tokenRepository) CheckVerified(ctx context.Context, email string, tokenType constants.TokenType, id string) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:verified:%d:%s:%s", tokenType.Value(), email, id)
|
||||
func (t *tokenRepository) CreateUploadToken(ctx context.Context, userId string, token *models.TokenUploadEntity) error {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, token.ID)
|
||||
err := t.c.Set(ctx, cacheKey, token, constants.TokenUploadDuration)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tokenRepository) GetUploadToken(ctx context.Context, userId string, id string) (*models.TokenUploadEntity, error) {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, id)
|
||||
var token models.TokenUploadEntity
|
||||
err := t.c.Get(ctx, cacheKey, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &token, err
|
||||
}
|
||||
|
||||
func (t *tokenRepository) DeleteUploadToken(ctx context.Context, userId string, id string) error {
|
||||
cacheKey := fmt.Sprintf("token:%d:%s:%s", constants.TokenUpload.Value(), userId, id)
|
||||
return t.c.Del(ctx, cacheKey)
|
||||
}
|
||||
|
||||
func (t *tokenRepository) CheckCooldown(ctx context.Context, email string, tokenType constants.TokenType) (bool, error) {
|
||||
cacheKey := fmt.Sprintf("token:cooldown:%d:%s", tokenType.Value(), email)
|
||||
exists, err := t.c.Exists(ctx, cacheKey)
|
||||
return exists, err
|
||||
}
|
||||
|
||||
53
internal/routes/mediaRoute.go
Normal file
53
internal/routes/mediaRoute.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"history-api/internal/controllers"
|
||||
"history-api/internal/middlewares"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/constants"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
func MediaRoutes(app *fiber.App, controller *controllers.MediaController, userRepo repositories.UserRepository) {
|
||||
route := app.Group("/media")
|
||||
route.Get(
|
||||
"/",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.SearchMedia,
|
||||
)
|
||||
|
||||
route.Post(
|
||||
"/upload",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.UploadServerSide,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/presigned",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GeneratePresignedURL,
|
||||
)
|
||||
|
||||
route.Post(
|
||||
"/presigned/complete",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GeneratePresignedURL,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.GetMediaByID,
|
||||
)
|
||||
|
||||
route.Delete(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.DeleteMedia,
|
||||
)
|
||||
|
||||
}
|
||||
@@ -16,19 +16,26 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
||||
"/",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.Search,
|
||||
controller.SearchUser,
|
||||
)
|
||||
|
||||
|
||||
route.Get(
|
||||
"/current",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GetUserCurrent,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/current/media",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.GetUserMedia,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.Search,
|
||||
controller.SearchUser,
|
||||
)
|
||||
|
||||
route.Put(
|
||||
@@ -43,6 +50,14 @@ func UserRoutes(app *fiber.App, controller *controllers.UserController, userRepo
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.DeleteUser,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/:id/media",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
middlewares.RequireAnyRole(constants.ADMIN, constants.MOD),
|
||||
controller.GetMediaByUserID,
|
||||
)
|
||||
|
||||
route.Patch(
|
||||
"/:id/restore",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
|
||||
@@ -2,19 +2,35 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"history-api/internal/dtos/request"
|
||||
"history-api/internal/dtos/response"
|
||||
"history-api/internal/gen/sqlc"
|
||||
"history-api/internal/models"
|
||||
"history-api/internal/repositories"
|
||||
"history-api/pkg/cache"
|
||||
"history-api/pkg/constants"
|
||||
"history-api/pkg/convert"
|
||||
"history-api/pkg/storage"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type MediaService interface {
|
||||
GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error)
|
||||
GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error)
|
||||
SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error)
|
||||
DeleteMedia(ctx context.Context, mediaId string) error
|
||||
GetMediaByTarget(ctx context.Context, targetType string, targetId string) ([]*response.MediaResponse, error)
|
||||
DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) error
|
||||
UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error)
|
||||
GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error)
|
||||
PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error)
|
||||
@@ -22,56 +38,292 @@ type MediaService interface {
|
||||
|
||||
type mediaService struct {
|
||||
mediaRepo repositories.MediaRepository
|
||||
tokenRepo repositories.TokenRepository
|
||||
s storage.Storage
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewMediaService(
|
||||
mediaRepo repositories.MediaRepository,
|
||||
tokenRepo repositories.TokenRepository,
|
||||
s storage.Storage,
|
||||
c cache.Cache,
|
||||
) MediaService {
|
||||
return &mediaService{
|
||||
mediaRepo: mediaRepo,
|
||||
tokenRepo: tokenRepo,
|
||||
s: s,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteMedia implements [MediaService].
|
||||
func (m *mediaService) DeleteMedia(ctx context.Context, mediaId string) error {
|
||||
panic("unimplemented")
|
||||
func (m *mediaService) DeleteMedia(ctx context.Context, claims *response.JWTClaims, mediaId string) error {
|
||||
mediaIdUUID, err := convert.StringToUUID(mediaId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
media, err := m.mediaRepo.GetByID(ctx, mediaIdUUID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
shoudDelete := false
|
||||
if slices.Contains(claims.Roles, constants.ADMIN) || slices.Contains(claims.Roles, constants.MOD) || media.UserID == claims.UId {
|
||||
shoudDelete = true
|
||||
}
|
||||
|
||||
if !shoudDelete {
|
||||
return fiber.NewError(fiber.StatusForbidden, "You don't have permission to delete this media")
|
||||
}
|
||||
|
||||
err = m.mediaRepo.Delete(ctx, mediaIdUUID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
m.c.PublishTask(ctx, constants.StreamStorageName, constants.TaskTypeDeleteMedia, media.ToStorageEntity())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GeneratePresignedURL implements [MediaService].
|
||||
func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (m *mediaService) GetMediaByID(ctx context.Context, id string) (*response.MediaResponse, error) {
|
||||
mediaId, err := convert.StringToUUID(id)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
media, err := m.mediaRepo.GetByID(ctx, mediaId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return media.ToResponse(), nil
|
||||
}
|
||||
|
||||
// GetMediaByID implements [MediaService].
|
||||
func (m *mediaService) GetMediaByID(ctx context.Context, mediaId string) (*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
func (m *mediaService) GetMediaByUserID(ctx context.Context, id string) ([]*response.MediaResponse, error) {
|
||||
userId, err := convert.StringToUUID(id)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
medias, err := m.mediaRepo.GetByUserID(ctx, userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return models.MediaEntitiesToResponse(medias), nil
|
||||
}
|
||||
|
||||
// GetMediaByTarget implements [MediaService].
|
||||
func (m *mediaService) GetMediaByTarget(ctx context.Context, targetType string, targetId string) ([]*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// GetMediaByUserID implements [MediaService].
|
||||
func (m *mediaService) GetMediaByUserID(ctx context.Context, userId string) ([]*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// PreSignedCompleted implements [MediaService].
|
||||
func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// SearchMedia implements [MediaService].
|
||||
func (m *mediaService) SearchMedia(ctx context.Context, dto *request.SearchMediaDto) (*response.PaginatedResponse, error) {
|
||||
panic("unimplemented")
|
||||
arg := sqlc.SearchMediasParams{
|
||||
Limit: int32(dto.Limit + 1),
|
||||
}
|
||||
|
||||
if dto.Cursor != "" {
|
||||
pgID, err := convert.StringToUUID(dto.Cursor)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid cursor format")
|
||||
}
|
||||
arg.Cursor = pgID
|
||||
}
|
||||
|
||||
if dto.Search != "" {
|
||||
arg.SearchText = pgtype.Text{String: dto.Search, Valid: true}
|
||||
}
|
||||
|
||||
rows, err := m.mediaRepo.Search(ctx, arg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hasMore := false
|
||||
var nextCursor string
|
||||
|
||||
if len(rows) > dto.Limit {
|
||||
hasMore = true
|
||||
nextCursor = rows[dto.Limit-1].ID
|
||||
rows = rows[:dto.Limit]
|
||||
}
|
||||
|
||||
res := &response.PaginatedResponse{
|
||||
Data: rows,
|
||||
Status: true,
|
||||
Message: "",
|
||||
}
|
||||
res.Pagination.HasMore = hasMore
|
||||
res.Pagination.NextCursor = nextCursor
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// UploadServerSide implements [MediaService].
|
||||
func (m *mediaService) UploadServerSide(ctx context.Context, userId string, fileHeader *multipart.FileHeader) (*response.MediaResponse, error) {
|
||||
panic("unimplemented")
|
||||
userIdUUID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Cannot open file")
|
||||
}
|
||||
defer file.Close()
|
||||
var reader io.Reader = file
|
||||
fileExt := filepath.Ext(fileHeader.Filename)
|
||||
contentType := fileHeader.Header.Get("Content-Type")
|
||||
mid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
|
||||
}
|
||||
newFileName := mid.String() + fileExt
|
||||
originalName := fileHeader.Filename
|
||||
encodedName := url.QueryEscape(originalName)
|
||||
|
||||
dispositionType := "attachment"
|
||||
if strings.HasPrefix(contentType, "image/") || contentType == "application/pdf" {
|
||||
dispositionType = "inline"
|
||||
}
|
||||
|
||||
contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
|
||||
dispositionType,
|
||||
"file"+fileExt,
|
||||
encodedName,
|
||||
)
|
||||
|
||||
metadata := map[string]string{
|
||||
"original-name": encodedName,
|
||||
}
|
||||
|
||||
mdByte, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
|
||||
}
|
||||
|
||||
err = m.s.Upload(ctx, newFileName, reader, fileHeader.Size, storage.UploadOptions{
|
||||
ContentType: contentType,
|
||||
ContentDisposition: contentDisposition,
|
||||
Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to upload file to storage")
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to upload file")
|
||||
}
|
||||
|
||||
media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
|
||||
UserID: userIdUUID,
|
||||
StorageKey: newFileName,
|
||||
OriginalName: originalName,
|
||||
MimeType: contentType,
|
||||
Size: fileHeader.Size,
|
||||
FileMetadata: mdByte,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return media.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (m *mediaService) GeneratePresignedURL(ctx context.Context, userId string, dto *request.PreSignedDto) (*response.PreSignedResponse, error) {
|
||||
fileExt := filepath.Ext(dto.FileName)
|
||||
mid, err := uuid.NewV7()
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate media ID")
|
||||
}
|
||||
newFileName := mid.String() + fileExt
|
||||
encodedName := url.QueryEscape(dto.FileName)
|
||||
|
||||
dispositionType := "attachment"
|
||||
if dto.ContentType == "application/pdf" || (len(dto.ContentType) > 6 && dto.ContentType[:6] == "image/") {
|
||||
dispositionType = "inline"
|
||||
}
|
||||
|
||||
contentDisposition := fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s",
|
||||
dispositionType, "file"+fileExt, encodedName)
|
||||
|
||||
metadata := map[string]string{
|
||||
"original-name": encodedName,
|
||||
}
|
||||
mdByte, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to encode metadata")
|
||||
}
|
||||
|
||||
presignedURL, err := m.s.PresignUpload(ctx, newFileName, constants.PreSignedURLDuration, storage.UploadOptions{
|
||||
ContentType: dto.ContentType,
|
||||
ContentDisposition: contentDisposition,
|
||||
Metadata: metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate presigned URL")
|
||||
}
|
||||
|
||||
tokenId := uuid.New().String()
|
||||
err = m.tokenRepo.CreateUploadToken(
|
||||
ctx,
|
||||
userId,
|
||||
&models.TokenUploadEntity{
|
||||
ID: tokenId,
|
||||
UserID: userId,
|
||||
StorageKey: newFileName,
|
||||
OriginalName: dto.FileName,
|
||||
MimeType: dto.ContentType,
|
||||
Size: dto.Size,
|
||||
FileMetadata: mdByte,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Internal Server Error")
|
||||
}
|
||||
|
||||
return &response.PreSignedResponse{
|
||||
TokenID: tokenId,
|
||||
UploadUrl: presignedURL,
|
||||
StorageKey: newFileName,
|
||||
SignedHeaders: map[string]string{
|
||||
"x-amz-meta-original-name": encodedName,
|
||||
"Content-Disposition": contentDisposition,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mediaService) PreSignedCompleted(ctx context.Context, userId string, dto *request.PreSignedCompleteDto) (*response.MediaResponse, error) {
|
||||
token, err := m.tokenRepo.GetUploadToken(ctx, userId, dto.TokenID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get upload token")
|
||||
}
|
||||
if token == nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid or expired token")
|
||||
}
|
||||
userIdUUID, err := convert.StringToUUID(userId)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
err = m.s.Move(
|
||||
ctx,
|
||||
&storage.MoveOptions{
|
||||
Bucket: m.s.GetTempBucket(),
|
||||
Key: token.StorageKey,
|
||||
},
|
||||
&storage.MoveOptions{
|
||||
Bucket: m.s.GetMainBucket(),
|
||||
Key: token.StorageKey,
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to move file to final destination")
|
||||
}
|
||||
|
||||
media, err := m.mediaRepo.Create(ctx, sqlc.CreateMediaParams{
|
||||
UserID: userIdUUID,
|
||||
StorageKey: token.StorageKey,
|
||||
OriginalName: token.OriginalName,
|
||||
MimeType: token.MimeType,
|
||||
Size: token.Size,
|
||||
FileMetadata: token.FileMetadata,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create media record")
|
||||
}
|
||||
|
||||
_ = m.tokenRepo.DeleteUploadToken(ctx, userId, dto.TokenID)
|
||||
|
||||
return media.ToResponse(), nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type UserService interface {
|
||||
ChangeRoleUser(ctx context.Context, dto *request.ChangeRoleDto) (*response.UserResponse, error)
|
||||
RestoreUser(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
GetUserByID(ctx context.Context, userId string) (*response.UserResponse, error)
|
||||
Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||
SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error)
|
||||
}
|
||||
|
||||
type userService struct {
|
||||
@@ -208,7 +208,7 @@ func (u *userService) RestoreUser(ctx context.Context, userId string) (*response
|
||||
return user.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (u *userService) Search(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||
func (u *userService) SearchUser(ctx context.Context, dto *request.SearchUserDto) (*response.PaginatedResponse, error) {
|
||||
arg := sqlc.SearchUsersParams{
|
||||
Limit: int32(dto.Limit + 1),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user