feat: implement geometry and project management modules with associated controllers, services, and routes
This commit is contained in:
@@ -113,7 +113,7 @@ func (s *FiberServer) SetupServer(
|
||||
entityService := services.NewEntityService(entityRepo)
|
||||
geometryService := services.NewGeometryService(geometryRepo)
|
||||
wikiService := services.NewWikiService(wikiRepo)
|
||||
projectService := services.NewProjectService(projectRepo)
|
||||
projectService := services.NewProjectService(projectRepo, redis)
|
||||
commitService := services.NewCommitService(poolPg, commitRepo, projectRepo, redis)
|
||||
submissionService := services.NewSubmissionService(
|
||||
submissionRepo, projectRepo, commitRepo,
|
||||
|
||||
@@ -194,3 +194,14 @@ JOIN entity_geometries eg ON eg.entity_id = e.id AND eg.geometry_id = pairs.gid
|
||||
JOIN geometries g ON g.id = pairs.gid
|
||||
WHERE e.is_deleted = false
|
||||
AND g.is_deleted = false;
|
||||
|
||||
-- name: GetGeometriesByBoundWith :many
|
||||
SELECT
|
||||
id, geo_type, draw_geometry, bound_with, time_start, time_end, project_id,
|
||||
ST_XMin(bbox)::float8 as min_lng,
|
||||
ST_YMin(bbox)::float8 as min_lat,
|
||||
ST_XMax(bbox)::float8 as max_lng,
|
||||
ST_YMax(bbox)::float8 as max_lat,
|
||||
is_deleted, created_at, updated_at
|
||||
FROM geometries
|
||||
WHERE bound_with = $1 AND is_deleted = false;
|
||||
|
||||
@@ -118,3 +118,30 @@ func (h *GeometryController) SearchGeometriesByEntityName(c fiber.Ctx) error {
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// GetGeometriesByBoundWith handles fetching geometries by their bound_with reference.
|
||||
// @Summary Get geometries by bound_with ID
|
||||
// @Description Get a list of geometries that are bound to the specified geometry ID
|
||||
// @Tags Geometries
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param bound_with path string true "Bound-with Geometry ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /geometries/bound-with/{bound_with} [get]
|
||||
func (h *GeometryController) GetGeometriesByBoundWith(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
boundWith := c.Params("bound_with")
|
||||
res, err := h.service.GetGeometriesByBoundWith(ctx, boundWith)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -456,3 +456,105 @@ func (h *ProjectController) ChangeOwner(c fiber.Ctx) error {
|
||||
Data: res,
|
||||
})
|
||||
}
|
||||
|
||||
// LockProject godoc
|
||||
// @Summary Lock a project
|
||||
// @Description Acquire an exclusive editing lock on a project for 15 minutes
|
||||
// @Tags Projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Project ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 409 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /projects/{id}/lock [post]
|
||||
func (h *ProjectController) LockProject(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
projectID := c.Params("id")
|
||||
uid := c.Locals("uid").(string)
|
||||
|
||||
err := h.service.LockProject(ctx, uid, projectID)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "Project locked successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// UnlockProject godoc
|
||||
// @Summary Unlock a project
|
||||
// @Description Release the exclusive editing lock on a project
|
||||
// @Tags Projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Project ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 403 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /projects/{id}/unlock [post]
|
||||
func (h *ProjectController) UnlockProject(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
projectID := c.Params("id")
|
||||
uid := c.Locals("uid").(string)
|
||||
|
||||
err := h.service.UnlockProject(ctx, uid, projectID)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "Project unlocked successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// HeartbeatProject godoc
|
||||
// @Summary Heartbeat to refresh project lock
|
||||
// @Description Refresh the TTL of the exclusive editing lock on a project for another 15 minutes
|
||||
// @Tags Projects
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "Project ID"
|
||||
// @Success 200 {object} response.CommonResponse
|
||||
// @Failure 400 {object} response.CommonResponse
|
||||
// @Failure 409 {object} response.CommonResponse
|
||||
// @Failure 500 {object} response.CommonResponse
|
||||
// @Router /projects/{id}/heartbeat [post]
|
||||
func (h *ProjectController) HeartbeatProject(c fiber.Ctx) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
projectID := c.Params("id")
|
||||
uid := c.Locals("uid").(string)
|
||||
|
||||
err := h.service.HeartbeatProject(ctx, uid, projectID)
|
||||
if err != nil {
|
||||
return c.Status(err.Code).JSON(response.CommonResponse{
|
||||
Status: false,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(response.CommonResponse{
|
||||
Status: true,
|
||||
Message: "Lock refreshed successfully",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -259,6 +259,70 @@ func (q *Queries) GetEntityGeometriesByPairs(ctx context.Context, arg GetEntityG
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getGeometriesByBoundWith = `-- name: GetGeometriesByBoundWith :many
|
||||
SELECT
|
||||
id, geo_type, draw_geometry, bound_with, time_start, time_end, project_id,
|
||||
ST_XMin(bbox)::float8 as min_lng,
|
||||
ST_YMin(bbox)::float8 as min_lat,
|
||||
ST_XMax(bbox)::float8 as max_lng,
|
||||
ST_YMax(bbox)::float8 as max_lat,
|
||||
is_deleted, created_at, updated_at
|
||||
FROM geometries
|
||||
WHERE bound_with = $1 AND is_deleted = false
|
||||
`
|
||||
|
||||
type GetGeometriesByBoundWithRow struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
GeoType int16 `json:"geo_type"`
|
||||
DrawGeometry json.RawMessage `json:"draw_geometry"`
|
||||
BoundWith pgtype.UUID `json:"bound_with"`
|
||||
TimeStart pgtype.Int4 `json:"time_start"`
|
||||
TimeEnd pgtype.Int4 `json:"time_end"`
|
||||
ProjectID pgtype.UUID `json:"project_id"`
|
||||
MinLng float64 `json:"min_lng"`
|
||||
MinLat float64 `json:"min_lat"`
|
||||
MaxLng float64 `json:"max_lng"`
|
||||
MaxLat float64 `json:"max_lat"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetGeometriesByBoundWith(ctx context.Context, boundWith pgtype.UUID) ([]GetGeometriesByBoundWithRow, error) {
|
||||
rows, err := q.db.Query(ctx, getGeometriesByBoundWith, boundWith)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetGeometriesByBoundWithRow{}
|
||||
for rows.Next() {
|
||||
var i GetGeometriesByBoundWithRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.GeoType,
|
||||
&i.DrawGeometry,
|
||||
&i.BoundWith,
|
||||
&i.TimeStart,
|
||||
&i.TimeEnd,
|
||||
&i.ProjectID,
|
||||
&i.MinLng,
|
||||
&i.MinLat,
|
||||
&i.MaxLng,
|
||||
&i.MaxLat,
|
||||
&i.IsDeleted,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getGeometriesByIDs = `-- name: GetGeometriesByIDs :many
|
||||
SELECT
|
||||
id, geo_type, draw_geometry, bound_with, time_start, time_end, project_id,
|
||||
|
||||
@@ -29,6 +29,7 @@ type GeometryRepository interface {
|
||||
CreateEntityGeometries(ctx context.Context, params sqlc.CreateEntityGeometriesParams) error
|
||||
BulkDeleteEntityGeometriesByEntityId(ctx context.Context, entityId pgtype.UUID) error
|
||||
GetByProjectID(ctx context.Context, projectID pgtype.UUID) ([]*models.GeometryEntity, error)
|
||||
GetGeometriesByBoundWith(ctx context.Context, boundWith pgtype.UUID) ([]*models.GeometryEntity, error)
|
||||
DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error
|
||||
BulkDeleteEntityGeometriesByGeometryID(ctx context.Context, geometryID pgtype.UUID) error
|
||||
DeleteEntityGeometry(ctx context.Context, entityID pgtype.UUID, geometryID pgtype.UUID) error
|
||||
@@ -357,6 +358,56 @@ func (r *geometryRepository) GetByProjectID(ctx context.Context, projectID pgtyp
|
||||
return geometries, nil
|
||||
}
|
||||
|
||||
func (r *geometryRepository) GetGeometriesByBoundWith(ctx context.Context, boundWith pgtype.UUID) ([]*models.GeometryEntity, error) {
|
||||
cacheKey := fmt.Sprintf("geometry:bound_with:%s", convert.UUIDToString(boundWith))
|
||||
var cachedIDs []string
|
||||
if err := r.c.Get(ctx, cacheKey, &cachedIDs); err == nil && len(cachedIDs) > 0 {
|
||||
return r.getByIDsWithFallback(ctx, cachedIDs)
|
||||
}
|
||||
|
||||
rows, err := r.q.GetGeometriesByBoundWith(ctx, boundWith)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var geometries []*models.GeometryEntity
|
||||
var ids []string
|
||||
geometryToCache := make(map[string]any)
|
||||
|
||||
for _, row := range rows {
|
||||
geometry := &models.GeometryEntity{
|
||||
ID: convert.UUIDToString(row.ID),
|
||||
GeoType: row.GeoType,
|
||||
DrawGeometry: row.DrawGeometry,
|
||||
BoundWith: convert.UUIDToStringPtr(row.BoundWith),
|
||||
TimeStart: convert.Int4ToInt32(row.TimeStart),
|
||||
TimeEnd: convert.Int4ToInt32(row.TimeEnd),
|
||||
Bbox: &response.Bbox{
|
||||
MinLng: row.MinLng,
|
||||
MinLat: row.MinLat,
|
||||
MaxLng: row.MaxLng,
|
||||
MaxLat: row.MaxLat,
|
||||
},
|
||||
ProjectID: convert.UUIDToString(row.ProjectID),
|
||||
IsDeleted: row.IsDeleted,
|
||||
CreatedAt: convert.TimeToPtr(row.CreatedAt),
|
||||
UpdatedAt: convert.TimeToPtr(row.UpdatedAt),
|
||||
}
|
||||
ids = append(ids, geometry.ID)
|
||||
geometries = append(geometries, geometry)
|
||||
geometryToCache[fmt.Sprintf("geometry:id:%s", geometry.ID)] = geometry
|
||||
}
|
||||
|
||||
if len(geometryToCache) > 0 {
|
||||
_ = r.c.MSet(ctx, geometryToCache, constants.NormalCacheDuration)
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
_ = r.c.Set(ctx, cacheKey, ids, constants.ListCacheDuration)
|
||||
}
|
||||
|
||||
return geometries, nil
|
||||
}
|
||||
|
||||
func (r *geometryRepository) DeleteByIDs(ctx context.Context, ids []pgtype.UUID) error {
|
||||
err := r.q.DeleteGeometriesByIDs(ctx, ids)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,5 +10,6 @@ func GeometryRoutes(router fiber.Router, geometryController *controllers.Geometr
|
||||
geometry := router.Group("/geometries")
|
||||
geometry.Get("/", geometryController.SearchGeometries)
|
||||
geometry.Get("/entity", geometryController.SearchGeometriesByEntityName)
|
||||
geometry.Get("/bound-with/:bound_with", geometryController.GetGeometriesByBoundWith)
|
||||
geometry.Get("/:id", geometryController.GetGeometryById)
|
||||
}
|
||||
|
||||
@@ -66,6 +66,24 @@ func ProjectRoutes(
|
||||
controller.ChangeOwner,
|
||||
)
|
||||
|
||||
route.Post(
|
||||
"/:id/lock",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.LockProject,
|
||||
)
|
||||
|
||||
route.Post(
|
||||
"/:id/unlock",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.UnlockProject,
|
||||
)
|
||||
|
||||
route.Post(
|
||||
"/:id/heartbeat",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
controller.HeartbeatProject,
|
||||
)
|
||||
|
||||
route.Get(
|
||||
"/:id",
|
||||
middlewares.JwtAccess(userRepo),
|
||||
|
||||
@@ -52,6 +52,12 @@ func (s *commitService) checkWritePermission(ctx context.Context, userID string,
|
||||
return fiber.NewError(fiber.StatusNotFound, "Project not found")
|
||||
}
|
||||
|
||||
lockKey := fmt.Sprintf("project:lock:%s", convert.UUIDToString(projectUUID))
|
||||
var lockUser string
|
||||
if err := s.c.Get(ctx, lockKey, &lockUser); err == nil && lockUser != "" && lockUser != userID {
|
||||
return fiber.NewError(fiber.StatusConflict, "Cannot commit: Project is locked by another user who is editing")
|
||||
}
|
||||
|
||||
if project.UserID == userID {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type GeometryService interface {
|
||||
GetGeometryByID(ctx context.Context, id string) (*response.GeometryResponse, *fiber.Error)
|
||||
SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, *fiber.Error)
|
||||
SearchGeometriesByEntityName(ctx context.Context, req *request.SearchGeometriesByEntityNameDto) (*response.SearchGeometriesByEntityNameResponse, *fiber.Error)
|
||||
GetGeometriesByBoundWith(ctx context.Context, boundWith string) ([]*response.GeometryResponse, *fiber.Error)
|
||||
}
|
||||
|
||||
type geometryService struct {
|
||||
@@ -42,6 +43,18 @@ func (s *geometryService) GetGeometryByID(ctx context.Context, id string) (*resp
|
||||
return geometry.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (s *geometryService) GetGeometriesByBoundWith(ctx context.Context, boundWith string) ([]*response.GeometryResponse, *fiber.Error) {
|
||||
boundWithUUID, err := convert.StringToUUID(boundWith)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid bound_with geometry ID format")
|
||||
}
|
||||
geometries, err := s.geometryRepo.GetGeometriesByBoundWith(ctx, boundWithUUID)
|
||||
if err != nil {
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch geometries by bound_with")
|
||||
}
|
||||
return models.GeometriesEntityToResponse(geometries), nil
|
||||
}
|
||||
|
||||
func (s *geometryService) SearchGeometries(ctx context.Context, req *request.SearchGeometryDto) ([]*response.GeometryResponse, *fiber.Error) {
|
||||
params := sqlc.SearchGeometriesParams{}
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -27,15 +30,20 @@ type ProjectService interface {
|
||||
UpdateMemberRole(ctx context.Context, claims *response.JWTClaims, projectID string, memberUserID string, dto *request.UpdateProjectMemberDto) (*response.ProjectResponse, *fiber.Error)
|
||||
RemoveMember(ctx context.Context, claims *response.JWTClaims, projectID string, memberUserID string) *fiber.Error
|
||||
ChangeOwner(ctx context.Context, claims *response.JWTClaims, projectID string, newOwnerID string) (*response.ProjectResponse, *fiber.Error)
|
||||
LockProject(ctx context.Context, userID string, projectID string) *fiber.Error
|
||||
UnlockProject(ctx context.Context, userID string, projectID string) *fiber.Error
|
||||
HeartbeatProject(ctx context.Context, userID string, projectID string) *fiber.Error
|
||||
}
|
||||
|
||||
type projectService struct {
|
||||
projectRepo repositories.ProjectRepository
|
||||
c cache.Cache
|
||||
}
|
||||
|
||||
func NewProjectService(projectRepo repositories.ProjectRepository) ProjectService {
|
||||
func NewProjectService(projectRepo repositories.ProjectRepository, c cache.Cache) ProjectService {
|
||||
return &projectService{
|
||||
projectRepo: projectRepo,
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +86,16 @@ func (s *projectService) GetProjectByID(ctx context.Context, id string) (*respon
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Project not found")
|
||||
}
|
||||
|
||||
return project.ToResponse(), nil
|
||||
res := project.ToResponse()
|
||||
lockKey := fmt.Sprintf("project:lock:%s", id)
|
||||
var lockUser string
|
||||
if err := s.c.Get(ctx, lockKey, &lockUser); err == nil && lockUser != "" {
|
||||
res.LockedBy = &lockUser
|
||||
} else {
|
||||
res.LockedBy = nil
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *projectService) GetProjectByUserID(ctx context.Context, userID string, dto *request.GetProjectsByUserDto) ([]*response.ProjectResponse, *fiber.Error) {
|
||||
@@ -419,3 +436,72 @@ func (s *projectService) ChangeOwner(ctx context.Context, claims *response.JWTCl
|
||||
|
||||
return project.ToResponse(), nil
|
||||
}
|
||||
|
||||
func (s *projectService) LockProject(ctx context.Context, userID string, projectID string) *fiber.Error {
|
||||
projectUUID, err := convert.StringToUUID(projectID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid project ID format")
|
||||
}
|
||||
|
||||
_, err = s.projectRepo.GetByID(ctx, projectUUID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, "Project not found")
|
||||
}
|
||||
|
||||
lockKey := fmt.Sprintf("project:lock:%s", projectID)
|
||||
var lockUser string
|
||||
err = s.c.Get(ctx, lockKey, &lockUser)
|
||||
if err == nil && lockUser != "" {
|
||||
if lockUser != userID {
|
||||
return fiber.NewError(fiber.StatusConflict, "Project is locked by another user")
|
||||
}
|
||||
err = s.c.Set(ctx, lockKey, userID, 3*time.Minute)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh lock")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.c.Set(ctx, lockKey, userID, 3*time.Minute)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to acquire lock")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *projectService) UnlockProject(ctx context.Context, userID string, projectID string) *fiber.Error {
|
||||
lockKey := fmt.Sprintf("project:lock:%s", projectID)
|
||||
var lockUser string
|
||||
err := s.c.Get(ctx, lockKey, &lockUser)
|
||||
if err != nil || lockUser == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if lockUser != userID {
|
||||
return fiber.NewError(fiber.StatusForbidden, "You cannot unlock a project locked by another user")
|
||||
}
|
||||
|
||||
_ = s.c.Del(ctx, lockKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *projectService) HeartbeatProject(ctx context.Context, userID string, projectID string) *fiber.Error {
|
||||
lockKey := fmt.Sprintf("project:lock:%s", projectID)
|
||||
var lockUser string
|
||||
err := s.c.Get(ctx, lockKey, &lockUser)
|
||||
if err != nil || lockUser == "" {
|
||||
return fiber.NewError(fiber.StatusConflict, "Lock does not exist or has expired")
|
||||
}
|
||||
|
||||
if lockUser != userID {
|
||||
return fiber.NewError(fiber.StatusForbidden, "Lock is held by another user")
|
||||
}
|
||||
|
||||
err = s.c.Set(ctx, lockKey, userID, 3*time.Minute)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh lock")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user