feat: Add password protection
All checks were successful
/ pre-commit (push) Successful in 1m20s

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

@ -12,6 +12,7 @@ import (
"strings"
"github.com/gorilla/mux"
"golang.org/x/crypto/bcrypt"
"git.riou.xyz/jriou/coller/internal"
)
@ -25,6 +26,7 @@ type PageData struct {
Err error
URL string
Note *Note
EnablePasswordProtection bool
EnableUploadFileButton bool
AllowClientEncryptionKey bool
AllowNoEncryption bool
@ -111,6 +113,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
}
h.logger.Debug("checking inputs")
password := r.FormValue("password")
noEncryption := r.FormValue("no-encryption")
encryptionKey := r.FormValue("encryption-key")
expiration := r.FormValue("expiration")
@ -141,7 +144,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
}
h.logger.Debug("saving note to the database")
note, err := h.db.Create(content, encryptionKey, encryptionKey != "", expirationInt, deleteAfterRead != "", language)
note, err := h.db.Create(content, password, encryptionKey, encryptionKey != "", expirationInt, deleteAfterRead != "", language)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
@ -160,19 +163,19 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
}
h.logger.Debug("rendering page")
h.Templates.ExecuteTemplate(w, "create", h.PageData)
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
type GetWebNoteHandler struct {
type GetRawWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
}
func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *GetRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "note"
templateName := "unprotectedNote"
vars := mux.Vars(r)
id := vars["id"]
@ -180,7 +183,7 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = fmt.Errorf("could not find note: %v", err)
h.PageData.Err = fmt.Errorf("could not get raw note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
@ -199,13 +202,88 @@ func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Note = note
h.logger.Debug("rendering note web page")
h.Templates.ExecuteTemplate(w, "note", h.PageData)
h.logger.Debug("rendering page")
if len(note.PasswordHash) > 0 {
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(note.Content))
}
func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type GetProtectedRawWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
maxUploadSize int64
}
func (h *GetProtectedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "note"
templateName := "protectedNote"
vars := mux.Vars(r)
id := vars["id"]
h.logger.Debug("parsing multipart form")
err := r.ParseMultipartForm(h.maxUploadSize)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
password := r.FormValue("password")
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = fmt.Errorf("could not get raw note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note == nil {
h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note.Encrypted {
h.PageData.Err = fmt.Errorf("note is encrypted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil {
h.PageData.Err = fmt.Errorf("invalid password")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
h.PageData.Note = note
h.logger.Debug("rendering page")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(note.Content))
}
type GetEncryptedRawWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
}
func (h *GetEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "unprotectedNote"
vars := mux.Vars(r)
id := vars["id"]
@ -214,7 +292,7 @@ func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = fmt.Errorf("could not find note: %v", err)
h.PageData.Err = fmt.Errorf("could not get raw note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
@ -228,7 +306,220 @@ func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if err != nil {
h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err)
h.PageData.Err = fmt.Errorf("could not decrypt note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
}
h.PageData.Note = note
h.logger.Debug("rendering page")
if len(note.PasswordHash) > 0 {
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(note.Content))
}
type GetProtectedEncryptedRawWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
maxUploadSize int64
}
func (h *GetProtectedEncryptedRawWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "protectedNote"
vars := mux.Vars(r)
id := vars["id"]
encryptionKey := vars["encryptionKey"]
h.logger.Debug("parsing multipart form")
err := r.ParseMultipartForm(h.maxUploadSize)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
password := r.FormValue("password")
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = fmt.Errorf("could not get raw note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note == nil {
h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil {
h.PageData.Err = fmt.Errorf("invalid password")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if err != nil {
h.PageData.Err = fmt.Errorf("could not decrypt note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
}
h.PageData.Note = note
h.logger.Debug("rendering page")
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(note.Content))
}
type GetWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
}
func (h *GetWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "unprotectedNote"
vars := mux.Vars(r)
id := vars["id"]
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note == nil {
h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note.Encrypted {
h.PageData.Err = fmt.Errorf("note is encrypted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
h.PageData.Note = note
h.logger.Debug("rendering page")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
type GetProtectedWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
maxUploadSize int64
}
func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "protectedNote"
vars := mux.Vars(r)
id := vars["id"]
h.logger.Debug("parsing multipart form")
err := r.ParseMultipartForm(h.maxUploadSize)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
password := r.FormValue("password")
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note == nil {
h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note.Encrypted {
h.PageData.Err = fmt.Errorf("note is encrypted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil {
h.PageData.Err = fmt.Errorf("invalid password")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
h.PageData.Note = note
h.logger.Debug("rendering page")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
type GetEncryptedWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
}
func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "unprotectedNote"
vars := mux.Vars(r)
id := vars["id"]
encryptionKey := vars["encryptionKey"]
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note == nil {
h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if err != nil {
h.PageData.Err = fmt.Errorf("could not decrypt note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
@ -237,7 +528,74 @@ func (h *GetEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
h.PageData.Note = note
h.logger.Debug("rendering encrypted note web page")
h.Templates.ExecuteTemplate(w, "note", h.PageData)
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
type GetProtectedEncryptedWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
maxUploadSize int64
}
func (h *GetProtectedEncryptedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.PageData.Err = nil
templateName := "protectedNote"
vars := mux.Vars(r)
id := vars["id"]
encryptionKey := vars["encryptionKey"]
h.logger.Debug("parsing multipart form")
err := r.ParseMultipartForm(h.maxUploadSize)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
password := r.FormValue("password")
note, err := h.db.Get(id)
if err != nil {
h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if note == nil {
h.PageData.Err = fmt.Errorf("note doesn't exist or has been deleted")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if err := bcrypt.CompareHashAndPassword(note.PasswordHash, []byte(password)); err != nil {
h.PageData.Err = fmt.Errorf("invalid password")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if err != nil {
h.PageData.Err = fmt.Errorf("could not decrypt note")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
return
}
}
h.PageData.Note = note
h.logger.Debug("rendering encrypted note web page")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
type ClientsHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
}
func (h *ClientsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {