all repos

onasty @ a705d9c

a one-time notes service
5 files changed, 73 insertions(+), 24 deletions(-)
refactor: don't pass note password in url (#167)

* refactor(api): send note password through post body

* refactor(web): use new api for passworded notes

* refactor(web): fix linter

* refactor(notesrv): use constant value placeholder for empty passwords
Author: Olexandr Smirnov ss2316544@gmail.com
Committed by: GitHub noreply@github.com
Committed at: 2025-07-15 14:36:15 +0300
Parent: 36f59cd
M e2e/apiv1_notes_test.go
···
        139
        139
         	e.Equal(dbNote.ReadAt.IsZero(), false)

      
        140
        140
         }

      
        141
        141
         

      
        
        142
        +type apiv1NoteGetWithPasswordRequest struct {

      
        
        143
        +	Password string `json:"password"`

      
        
        144
        +}

      
        
        145
        +

      
        142
        146
         func (e *AppTestSuite) TestNoteV1_GetWithPassword() {

      
        143
        147
         	content := e.uuid()

      
        144
        148
         	passwd := e.uuid()

      ···
        156
        160
         	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)

      
        157
        161
         

      
        158
        162
         	httpResp = e.httpRequest(

      
        159
        
        -		http.MethodGet,

      
        160
        
        -		"/api/v1/note/"+bodyCreated.Slug+"?password="+passwd,

      
        161
        
        -		nil,

      
        
        163
        +		http.MethodPost,

      
        
        164
        +		"/api/v1/note/"+bodyCreated.Slug+"/view",

      
        
        165
        +		e.jsonify(apiv1NoteGetWithPasswordRequest{

      
        
        166
        +			Password: passwd,

      
        
        167
        +		}),

      
        162
        168
         	)

      
        163
        169
         	e.Equal(httpResp.Code, http.StatusOK)

      
        164
        170
         

      ···
        208
        214
         	e.readBodyAndUnjsonify(httpResp.Body, &bodyCreated)

      
        209
        215
         

      
        210
        216
         	httpResp = e.httpRequest(

      
        211
        
        -		http.MethodGet,

      
        212
        
        -		"/api/v1/note/"+bodyCreated.Slug+"?password="+e.uuid(),

      
        213
        
        -		nil,

      
        
        217
        +		http.MethodPost,

      
        
        218
        +		"/api/v1/note/"+bodyCreated.Slug+"/view",

      
        
        219
        +		e.jsonify(apiv1NoteGetWithPasswordRequest{

      
        
        220
        +			Password: e.uuid(),

      
        
        221
        +		}),

      
        214
        222
         	)

      
        215
        223
         	e.Equal(httpResp.Code, http.StatusNotFound)

      
        216
        224
         }

      
M internal/service/notesrv/input.go
···
        2
        2
         

      
        3
        3
         import "github.com/olexsmir/onasty/internal/dtos"

      
        4
        4
         

      
        
        5
        +const EmptyPassword = ""

      
        
        6
        +

      
        5
        7
         // GetNoteBySlugInput used as input for [GetBySlugAndRemoveIfNeeded]

      
        6
        8
         type GetNoteBySlugInput struct {

      
        7
        9
         	// Slug is a note's slug :) *Required*

      ···
        13
        15
         }

      
        14
        16
         

      
        15
        17
         func (i GetNoteBySlugInput) HasPassword() bool {

      
        16
        
        -	return i.Password != ""

      
        
        18
        +	return i.Password != EmptyPassword

      
        17
        19
         }

      
M internal/transport/http/apiv1/apiv1.go
···
        63
        63
         	note := r.Group("/note")

      
        64
        64
         	{

      
        65
        65
         		note.GET("/:slug", a.getNoteBySlugHandler)

      
        
        66
        +		note.POST("/:slug/view", a.getNoteBySlugAndPasswordHandler)

      
        66
        67
         		note.GET("/:slug/meta", a.getNoteMetadataByIDHandler)

      
        67
        68
         

      
        68
        69
         		possiblyAuthorized := note.Group("", a.couldBeAuthorizedMiddleware)

      
M internal/transport/http/apiv1/note.go
···
        60
        60
         		c.Request.Context(),

      
        61
        61
         		notesrv.GetNoteBySlugInput{

      
        62
        62
         			Slug:     c.Param("slug"),

      
        63
        
        -			Password: c.Query("password"),

      
        
        63
        +			Password: notesrv.EmptyPassword,

      
        
        64
        +		},

      
        
        65
        +	)

      
        
        66
        +	if err != nil {

      
        
        67
        +		errorResponse(c, err)

      
        
        68
        +		return

      
        
        69
        +	}

      
        
        70
        +

      
        
        71
        +	status := http.StatusOK

      
        
        72
        +	if !note.ReadAt.IsZero() {

      
        
        73
        +		status = http.StatusNotFound

      
        
        74
        +	}

      
        
        75
        +

      
        
        76
        +	c.JSON(status, getNoteBySlugResponse{

      
        
        77
        +		Content:              note.Content,

      
        
        78
        +		ReadAt:               note.ReadAt,

      
        
        79
        +		CreatedAt:            note.CreatedAt,

      
        
        80
        +		ExpiresAt:            note.ExpiresAt,

      
        
        81
        +		BurnBeforeExpiration: note.BurnBeforeExpiration,

      
        
        82
        +	})

      
        
        83
        +}

      
        
        84
        +

      
        
        85
        +type getNoteBuySlugAndPasswordRequest struct {

      
        
        86
        +	Password string `json:"password"`

      
        
        87
        +}

      
        
        88
        +

      
        
        89
        +func (a *APIV1) getNoteBySlugAndPasswordHandler(c *gin.Context) {

      
        
        90
        +	var req getNoteBuySlugAndPasswordRequest

      
        
        91
        +	if err := c.ShouldBindJSON(&req); err != nil {

      
        
        92
        +		newError(c, http.StatusBadRequest, "invalid request")

      
        
        93
        +		return

      
        
        94
        +	}

      
        
        95
        +

      
        
        96
        +	note, err := a.notesrv.GetBySlugAndRemoveIfNeeded(

      
        
        97
        +		c.Request.Context(),

      
        
        98
        +		notesrv.GetNoteBySlugInput{

      
        
        99
        +			Slug:     c.Param("slug"),

      
        
        100
        +			Password: req.Password,

      
        64
        101
         		},

      
        65
        102
         	)

      
        66
        103
         	if err != nil {

      
M web/src/Api/Note.elm
···
        7
        7
         import Iso8601

      
        8
        8
         import Json.Encode as E

      
        9
        9
         import Time exposing (Posix)

      
        10
        
        -import Url

      
        11
        10
         

      
        12
        11
         

      
        13
        12
         create :

      ···
        59
        58
             }

      
        60
        59
             -> Effect msg

      
        61
        60
         get options =

      
        62
        
        -    Effect.sendApiRequest

      
        63
        
        -        { endpoint =

      
        64
        
        -            "/api/v1/note/"

      
        65
        
        -                ++ options.slug

      
        66
        
        -                ++ (case options.password of

      
        67
        
        -                        Just p ->

      
        68
        
        -                            "?password=" ++ Url.percentEncode p

      
        
        61
        +    case options.password of

      
        
        62
        +        Just passwd ->

      
        
        63
        +            Effect.sendApiRequest

      
        
        64
        +                { endpoint = "/api/v1/note/" ++ options.slug ++ "/view"

      
        
        65
        +                , method = "POST"

      
        
        66
        +                , body = E.object [ ( "password", E.string passwd ) ] |> Http.jsonBody

      
        
        67
        +                , onResponse = options.onResponse

      
        
        68
        +                , decoder = Note.decode

      
        
        69
        +                }

      
        69
        70
         

      
        70
        
        -                        Nothing ->

      
        71
        
        -                            ""

      
        72
        
        -                   )

      
        73
        
        -        , method = "GET"

      
        74
        
        -        , body = Http.emptyBody

      
        75
        
        -        , onResponse = options.onResponse

      
        76
        
        -        , decoder = Note.decode

      
        77
        
        -        }

      
        
        71
        +        Nothing ->

      
        
        72
        +            Effect.sendApiRequest

      
        
        73
        +                { endpoint = "/api/v1/note/" ++ options.slug

      
        
        74
        +                , method = "GET"

      
        
        75
        +                , body = Http.emptyBody

      
        
        76
        +                , onResponse = options.onResponse

      
        
        77
        +                , decoder = Note.decode

      
        
        78
        +                }

      
        78
        79
         

      
        79
        80
         

      
        80
        81
         getMetadata :