From 27ca713cd4d413ff368fe2f1db1af638e4b68db7 Mon Sep 17 00:00:00 2001 From: "Alessandro (Ale) Segala" <43508+ItalyPaleAle@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:53:36 -0800 Subject: [PATCH] fix: one-time-access-token route should get user ID from URL only (#1358) --- backend/internal/common/errors.go | 14 +++++++++ .../internal/controller/user_controller.go | 21 ++++++++++--- backend/internal/dto/one_time_access_dto.go | 3 +- .../service/one_time_access_service.go | 30 +++++++++++++++++-- frontend/src/lib/services/user-service.ts | 2 +- 5 files changed, 60 insertions(+), 10 deletions(-) diff --git a/backend/internal/common/errors.go b/backend/internal/common/errors.go index 9a0b41b0..eb5e57ed 100644 --- a/backend/internal/common/errors.go +++ b/backend/internal/common/errors.go @@ -139,6 +139,20 @@ func (e *TooManyRequestsError) Error() string { } func (e *TooManyRequestsError) HttpStatusCode() int { return http.StatusTooManyRequests } +type UserIdNotProvidedError struct{} + +func (e *UserIdNotProvidedError) Error() string { + return "User id not provided" +} +func (e *UserIdNotProvidedError) HttpStatusCode() int { return http.StatusBadRequest } + +type UserNotFoundError struct{} + +func (e *UserNotFoundError) Error() string { + return "User not found" +} +func (e *UserNotFoundError) HttpStatusCode() int { return http.StatusNotFound } + type ClientIdOrSecretNotProvidedError struct{} func (e *ClientIdOrSecretNotProvidedError) Error() string { diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index 78a5563c..b38eeabc 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -4,6 +4,7 @@ import ( "net/http" "time" + "github.com/pocket-id/pocket-id/backend/internal/common" "github.com/pocket-id/pocket-id/backend/internal/utils/cookie" "github.com/gin-gonic/gin" @@ -322,22 +323,34 @@ func (uc *UserController) updateCurrentUserProfilePictureHandler(c *gin.Context) func (uc *UserController) createOneTimeAccessTokenHandler(c *gin.Context, own bool) { var input dto.OneTimeAccessTokenCreateDto - if err := c.ShouldBindJSON(&input); err != nil { + err := c.ShouldBindJSON(&input) + if err != nil { _ = c.Error(err) return } - var ttl time.Duration + var ( + userID string + ttl time.Duration + ) if own { - input.UserID = c.GetString("userID") + // Get user ID from context and force the default TTL + userID = c.GetString("userID") ttl = defaultOneTimeAccessTokenDuration } else { + // Get user ID from URL parameter, and optional TTL from body + userID = c.Param("id") ttl = input.TTL.Duration if ttl <= 0 { ttl = defaultOneTimeAccessTokenDuration } } - token, err := uc.oneTimeAccessService.CreateOneTimeAccessToken(c.Request.Context(), input.UserID, ttl) + if userID == "" { + _ = c.Error(&common.UserIdNotProvidedError{}) + return + } + + token, err := uc.oneTimeAccessService.CreateOneTimeAccessToken(c.Request.Context(), userID, ttl) if err != nil { _ = c.Error(err) return diff --git a/backend/internal/dto/one_time_access_dto.go b/backend/internal/dto/one_time_access_dto.go index 336def70..a99dc5ac 100644 --- a/backend/internal/dto/one_time_access_dto.go +++ b/backend/internal/dto/one_time_access_dto.go @@ -3,8 +3,7 @@ package dto import "github.com/pocket-id/pocket-id/backend/internal/utils" type OneTimeAccessTokenCreateDto struct { - UserID string `json:"userId"` - TTL utils.JSONDuration `json:"ttl" binding:"ttl"` + TTL utils.JSONDuration `json:"ttl" binding:"ttl"` } type OneTimeAccessEmailAsUnauthenticatedUserDto struct { diff --git a/backend/internal/service/one_time_access_service.go b/backend/internal/service/one_time_access_service.go index a9d80d80..1b84f498 100644 --- a/backend/internal/service/one_time_access_service.go +++ b/backend/internal/service/one_time_access_service.go @@ -79,7 +79,7 @@ func (s *OneTimeAccessService) requestOneTimeAccessEmailInternal(ctx context.Con tx.Rollback() }() - user, err := s.userService.GetUser(ctx, userID) + user, err := s.userService.getUserInternal(ctx, userID, tx) if err != nil { return nil, err } @@ -131,8 +131,32 @@ func (s *OneTimeAccessService) requestOneTimeAccessEmailInternal(ctx context.Con } func (s *OneTimeAccessService) CreateOneTimeAccessToken(ctx context.Context, userID string, ttl time.Duration) (token string, err error) { - token, _, err = s.createOneTimeAccessTokenInternal(ctx, userID, ttl, false, s.db) - return token, err + tx := s.db.Begin() + defer func() { + tx.Rollback() + }() + + // Load the user to ensure it exists + _, err = s.userService.getUserInternal(ctx, userID, tx) + if errors.Is(err, gorm.ErrRecordNotFound) { + return "", &common.UserNotFoundError{} + } else if err != nil { + return "", err + } + + // Create the one-time access token + token, _, err = s.createOneTimeAccessTokenInternal(ctx, userID, ttl, false, tx) + if err != nil { + return "", err + } + + // Commit + err = tx.Commit().Error + if err != nil { + return "", err + } + + return token, nil } func (s *OneTimeAccessService) createOneTimeAccessTokenInternal(ctx context.Context, userID string, ttl time.Duration, withDeviceToken bool, tx *gorm.DB) (token string, deviceToken *string, err error) { diff --git a/frontend/src/lib/services/user-service.ts b/frontend/src/lib/services/user-service.ts index e577d513..cbf68241 100644 --- a/frontend/src/lib/services/user-service.ts +++ b/frontend/src/lib/services/user-service.ts @@ -72,7 +72,7 @@ export default class UserService extends APIService { }; createOneTimeAccessToken = async (userId: string = 'me', ttl?: string | number) => { - const res = await this.api.post(`/users/${userId}/one-time-access-token`, { userId, ttl }); + const res = await this.api.post(`/users/${userId}/one-time-access-token`, { ttl }); return res.data.token; };