all repos

onasty @ c24198aff6c81e08e99ba6c8cda4d671c09338bc

a one-time notes service
5 files changed, 50 insertions(+), 7 deletions(-)
fix(api): prevent note creation when BurnBeforeExpiration is set without ExpiresAt; handle empty slug correctly (#197)

* fix(api): `BurnBeforeExpiration` cannot be used if `ExpiresAt` is not provided

* fix(api): validate slug only if it was provided

* feat(web): don't allow use to set `BurnBeforeExpiration` if `ExpiresAt` isn't set

* fix: grammar
Author: Olexandr Smirnov olexsmir@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-08-27 16:12:11 +0300
Parent: 3b5e67f
M e2e/apiv1_notes_test.go

@@ -135,6 +135,19 @@ e.Equal(inp.Content, dbNote.Content)

}, }, { + name: "burn before expiration, but without expiration time", + inp: apiv1NoteCreateRequest{ //nolint:exhaustruct + Content: e.uuid(), + BurnBeforeExpiration: true, + }, + assert: func(r *httptest.ResponseRecorder, _ apiv1NoteCreateRequest) { + var body errorResponse + e.readBodyAndUnjsonify(r.Body, &body) + + e.Equal(models.ErrNoteCannotBeBurnt.Error(), body.Message) + }, + }, + { name: "set password", inp: apiv1NoteCreateRequest{ //nolint:exhaustruct Content: e.uuid(),
M internal/models/note.go

@@ -18,8 +18,11 @@ var (

ErrNoteContentIsEmpty = errors.New("note: content is empty") ErrNoteSlugIsAlreadyInUse = errors.New("note: slug is already in use") ErrNoteSlugIsInvalid = errors.New("note: slug is invalid") - ErrNoteExpired = errors.New("note: expired") - ErrNoteNotFound = errors.New("note: not found") + ErrNoteCannotBeBurnt = errors.New( + "note: cannot be burnt before expiration if expiration time is not provided", + ) + ErrNoteExpired = errors.New("note: expired") + ErrNoteNotFound = errors.New("note: not found") ) type Note struct {

@@ -40,12 +43,16 @@ if n.Content == "" {

return ErrNoteContentIsEmpty } - if !slugPattern.MatchString(n.Slug) { + if n.Slug != "" && !slugPattern.MatchString(n.Slug) { return ErrNoteSlugIsInvalid } if n.IsExpired() { return ErrNoteExpired + } + + if n.BurnBeforeExpiration && n.ExpiresAt.IsZero() { + return ErrNoteCannotBeBurnt } if _, exists := notAllowedSlugs[n.Slug]; exists {
M internal/models/note_test.go

@@ -43,6 +43,20 @@ ExpiresAt: time.Now().Add(-time.Hour),

} assert.EqualError(t, n.Validate(), ErrNoteExpired.Error()) }) + t.Run("should fail if burn before expiration is set, and expiration time is not", + func(t *testing.T) { + n := Note{ + Content: "content", + BurnBeforeExpiration: true, + } + + assert.EqualError(t, n.Validate(), ErrNoteCannotBeBurnt.Error()) + }, + ) + t.Run("should not fail if slug is not provided", func(t *testing.T) { + n := Note{Content: "the content"} + assert.NoError(t, n.Validate()) + }) t.Run("should fail if slug is empty", func(t *testing.T) { n := Note{Content: "the content", Slug: " "} assert.EqualError(t, n.Validate(), ErrNoteSlugIsInvalid.Error())
M internal/transport/http/apiv1/response.go

@@ -32,6 +32,7 @@ errors.Is(err, models.ErrUserWrongCredentials) ||

// notes errors.Is(err, notesrv.ErrNotePasswordNotProvided) || errors.Is(err, models.ErrNoteContentIsEmpty) || + errors.Is(err, models.ErrNoteCannotBeBurnt) || errors.Is(err, models.ErrNoteSlugIsAlreadyInUse) || errors.Is(err, models.ErrNoteSlugIsInvalid) { newError(c, http.StatusBadRequest, err.Error())
M web/src/Pages/Home_.elm

@@ -287,7 +287,7 @@ }

] , H.div [ A.class "space-y-6" ] [ viewExpirationTimeSelector - , viewBurnBeforeExpirationCheckbox + , viewBurnBeforeExpirationCheckbox (isCheckBoxDisabled model.expirationTime) ] ] , H.div [ A.class "flex justify-end" ]

@@ -341,8 +341,8 @@ )

] -viewBurnBeforeExpirationCheckbox : Html Msg -viewBurnBeforeExpirationCheckbox = +viewBurnBeforeExpirationCheckbox : Bool -> Html Msg +viewBurnBeforeExpirationCheckbox isDisabled = H.div [ A.class "space-y-2" ] [ H.div [ A.class "flex items-start space-x-3" ] [ H.input

@@ -350,14 +350,22 @@ [ E.onCheck UserClickedCheckbox

, A.id "burn" , A.type_ "checkbox" , A.class "mt-1 h-4 w-4 text-black border-gray-300 rounded focus:ring-black focus:ring-2" + , A.disabled isDisabled ] [] , H.div [ A.class "flex-1" ] [ H.label [ A.for "burn", A.class "block text-sm font-medium text-gray-700 cursor-pointer" ] - [ H.text "Don't delete note until expiration time, even if it has been read it" ] + [ H.text "Keep the note until its expiration time, even if it has already been read." ] + , H.span [ A.class "block text-sm font-medium text-gray-500 cursor-pointer" ] + [ H.text "Can only be used if expiration time is set" ] ] ] ] + + +isCheckBoxDisabled : Maybe Int -> Bool +isCheckBoxDisabled expirationTime = + expirationTime == Nothing isFormDisabled : Model -> Bool