1
0
Fork 0
forked from jriou/coller

feat: Add password protection

Fixes #37.

BREAKING CHANGE: API routes are prefixed by /api/note.

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-09-27 08:35:26 +02:00
commit 9e0254c0b5
Signed by: jriou
GPG key ID: 9A099EDA51316854
16 changed files with 713 additions and 135 deletions

View file

@ -7,6 +7,7 @@ import (
"net/http"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"git.riou.xyz/jriou/coller/internal"
)
@ -25,6 +26,7 @@ type CreateNoteHandler struct {
type CreateNotePayload struct {
Content string `json:"content"`
Password string `json:"password"`
EncryptionKey string `json:"encryption_key"`
Encrypted bool `json:"encrypted"`
Expiration int `json:"expiration"`
@ -66,7 +68,7 @@ func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
note, err := h.db.Create(content, body.EncryptionKey, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language)
note, err := h.db.Create(content, body.Password, body.EncryptionKey, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language)
if err != nil {
WriteError(w, "could not create note", err)
return
@ -92,6 +94,10 @@ func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
WriteError(w, "could not get note", err)
} else if note == nil {
w.WriteHeader(http.StatusNotFound)
h.logger.Error("note does not exists", slog.Any("note_id", id))
} else if note.PasswordHash != nil {
w.WriteHeader(http.StatusBadRequest)
h.logger.Error("note is password protected", slog.Any("note_id", note.ID))
} else {
if note.Encrypted {
w.Header().Set("Content-Type", "application/octet-stream")
@ -101,17 +107,32 @@ func (h *GetNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
type GetEncryptedNoteHandler struct {
logger *slog.Logger
db *Database
type GetProtectedNoteHandler struct {
logger *slog.Logger
db *Database
maxUploadSize int64
}
func (h *GetEncryptedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type GetProtectedNotePayload struct {
EncryptionKey string `json:"encryption_key"`
Password string `json:"password"`
}
func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
vars := mux.Vars(r)
id := vars["id"]
encryptionKey := vars["encryptionKey"]
bodyReader := http.MaxBytesReader(w, r.Body, h.maxUploadSize)
defer r.Body.Close()
var body GetProtectedNotePayload
err := json.NewDecoder(bodyReader).Decode(&body)
if err != nil {
WriteError(w, "could not decode payload to read protected note", err)
return
}
note, err := h.db.Get(id)
@ -123,14 +144,22 @@ func (h *GetEncryptedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
return
}
if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if body.EncryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, body.EncryptionKey)
if err != nil {
WriteError(w, "could not decrypt note", err)
return
}
}
if body.Password != "" && (note.PasswordHash != nil || len(note.PasswordHash) > 0) {
err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(body.Password))
if err != nil {
WriteError(w, "could not validate password", err)
return
}
}
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(note.Content))
}