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

@ -14,6 +14,8 @@ type Config struct {
EncryptionKeyLength int `json:"encryption_key_length"`
AllowClientEncryptionKey bool `json:"allow_client_encryption_key"`
AllowNoEncryption bool `json:"allow_no_encryption"`
EnablePasswordProtection bool `json:"enable_password_protection"`
EnableUploadFileButton bool `json:"enable_upload_file_button"`
ExpirationInterval int `json:"expiration_interval"`
ListenAddress string `json:"listen_address"`
ListenPort int `json:"listen_port"`
@ -27,7 +29,6 @@ type Config struct {
ObservationInterval int `json:"observation_internal"`
Languages []string `json:"languages"`
Language string `json:"language"`
EnableUploadFileButton bool `json:"enable_upload_file_button"`
TLSCertFile string `json:"tls_cert_file"`
TLSKeyFile string `json:"tls_key_file"`
BootstrapDirectory string `json:"bootstrap_directory"`
@ -75,8 +76,9 @@ func NewConfig() *Config {
"SQL",
"YAML",
},
Language: "text",
EnableUploadFileButton: true,
Language: "text",
EnableUploadFileButton: true,
EnablePasswordProtection: true,
}
}

View file

@ -8,6 +8,7 @@ import (
"time"
"github.com/bwmarrin/snowflake"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@ -122,7 +123,7 @@ func (d *Database) Get(id string) (*Note, error) {
return nil, nil
}
func (d *Database) Create(content []byte, encryptionKey string, encrypted bool, expiration int, deleteAfterRead bool, language string) (note *Note, err error) {
func (d *Database) Create(content []byte, password string, encryptionKey string, encrypted bool, expiration int, deleteAfterRead bool, language string) (note *Note, err error) {
if expiration == 0 {
expiration = d.expiration
}
@ -148,6 +149,7 @@ func (d *Database) Create(content []byte, encryptionKey string, encrypted bool,
DeleteAfterRead: deleteAfterRead,
Language: language,
}
if encryptionKey != "" {
if err = internal.ValidateEncryptionKey(encryptionKey); err != nil {
return nil, err
@ -158,12 +160,22 @@ func (d *Database) Create(content []byte, encryptionKey string, encrypted bool,
}
note.Encrypted = true
}
if password != "" {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
note.PasswordHash = hash
}
trx := d.db.Create(note)
defer trx.Commit()
if trx.Error != nil {
d.logger.Warn("could not create note", slog.Any("error", trx.Error))
return nil, trx.Error
}
return note, nil
}

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))
}

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) {

View file

@ -10,6 +10,7 @@ type Note struct {
ID string `json:"id" gorm:"primaryKey"`
Content []byte `json:"content" gorm:"not null"`
Encrypted bool `json:"encrypted"`
PasswordHash []byte `json:"password_hash"`
ExpiresAt time.Time `json:"expires_at" gorm:"index"`
DeleteAfterRead bool `json:"delete_after_read"`
Language string `json:"language"`

View file

@ -71,20 +71,7 @@ func WriteError(w http.ResponseWriter, message string, err error) {
}.ToJSON())
}
type GetEncryptedWebNoteHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
db *Database
}
type ClientsHandler struct {
Templates *template.Template
PageData PageData
logger *slog.Logger
}
//go:embed templates/*
//go:embed templates/*.html
var templatesFS embed.FS
func (s *Server) Start() error {
@ -108,17 +95,18 @@ func (s *Server) Start() error {
}
r.Path("/api/note").Handler(createNoteHandler).Methods("POST")
getEncryptedNoteHandler := &GetEncryptedNoteHandler{
logger: s.logger,
db: s.db,
getProtectedNoteHandler := &GetProtectedNoteHandler{
logger: s.logger,
db: s.db,
maxUploadSize: s.config.MaxUploadSize,
}
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(getEncryptedNoteHandler).Methods("GET")
r.Path("/api/note/{id:[a-zA-Z0-9]+}").Handler(getProtectedNoteHandler).Methods("POST")
getNoteHandler := &GetNoteHandler{
logger: s.logger,
db: s.db,
}
r.Path("/{id:[a-zA-Z0-9]+}").Handler(getNoteHandler).Methods("GET")
r.Path("/api/note/{id:[a-zA-Z0-9]+}").Handler(getNoteHandler).Methods("GET")
// Web pages
funcs := template.FuncMap{
@ -134,6 +122,7 @@ func (s *Server) Start() error {
Languages: s.config.Languages,
BootstrapDirectory: s.config.BootstrapDirectory,
EnableUploadFileButton: s.config.EnableUploadFileButton,
EnablePasswordProtection: s.config.EnablePasswordProtection,
AllowClientEncryptionKey: s.config.AllowClientEncryptionKey,
AllowNoEncryption: s.config.AllowNoEncryption,
}
@ -177,6 +166,48 @@ func (s *Server) Start() error {
}
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(encryptedWebNoteHandler).Methods("GET")
protectedEncryptedWebNoteHandler := &GetProtectedEncryptedWebNoteHandler{
Templates: templates,
PageData: p,
logger: s.logger,
db: s.db,
maxUploadSize: s.config.MaxUploadSize,
}
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(protectedEncryptedWebNoteHandler).Methods("POST")
encryptedRawWebNoteHandler := &GetEncryptedRawWebNoteHandler{
Templates: templates,
PageData: p,
logger: s.logger,
db: s.db,
}
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}/raw").Handler(encryptedRawWebNoteHandler).Methods("GET")
protectedEncryptedRawWebNoteHandler := &GetProtectedEncryptedRawWebNoteHandler{
Templates: templates,
PageData: p,
logger: s.logger,
db: s.db,
}
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}/raw").Handler(protectedEncryptedRawWebNoteHandler).Methods("POST")
rawWebNoteHandler := &GetRawWebNoteHandler{
Templates: templates,
PageData: p,
logger: s.logger,
db: s.db,
}
r.Path("/{id:[a-zA-Z0-9]+}/raw").Handler(rawWebNoteHandler).Methods("GET")
protectedRawWebNoteHandler := &GetProtectedRawWebNoteHandler{
Templates: templates,
PageData: p,
logger: s.logger,
db: s.db,
maxUploadSize: s.config.MaxUploadSize,
}
r.Path("/{id:[a-zA-Z0-9]+}/raw").Handler(protectedRawWebNoteHandler).Methods("POST")
webNoteHandler := &GetWebNoteHandler{
Templates: templates,
PageData: p,
@ -185,6 +216,15 @@ func (s *Server) Start() error {
}
r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(webNoteHandler).Methods("GET")
protectedWebNoteHandler := &GetProtectedWebNoteHandler{
Templates: templates,
PageData: p,
logger: s.logger,
db: s.db,
maxUploadSize: s.config.MaxUploadSize,
}
r.Path("/{id:[a-zA-Z0-9]+}.html").Handler(protectedWebNoteHandler).Methods("POST")
if s.config.BootstrapDirectory != "" {
r.PathPrefix("/static/bootstrap/").Handler(http.StripPrefix("/static/bootstrap/", http.FileServer(http.Dir(s.config.BootstrapDirectory))))
}

View file

@ -0,0 +1,8 @@
{{define "error"}}
<div class="container mb-4 text-center">
<div class="alert alert-danger" role="alert">
<p>Could not show note</p>
<p><strong>{{.Err}}</strong></p>
</div>
</div>
{{end}}

View file

@ -13,6 +13,14 @@
</div>
<div class="container text-center justify-content-center w-75 mb-4">
<div class="row align-items-center">
{{if .EnablePasswordProtection}}
<div class="col-1">
<label class="col-form-label col-form-label-sm" for="password">Password</label>
</div>
<div class="col">
<input type="password" class="form-control" id="password" name="password">
</div>
{{end}}
{{if .AllowClientEncryptionKey}}
<div class="col-1">
<label class="col-form-label col-form-label-sm" for="encryption-key">Encryption key</label>
@ -25,9 +33,9 @@
{{end}}
{{if .AllowNoEncryption}}
<div class="col-1">
<input type="checkbox" class="form-check-input" for="no-encryption-key" id="no-encryption-key"
value="no-encryption-key" name="no-encryption-key">
<label class="col-form-label col-form-label-sm" for="no-encryption-key">No encryption</label>
<input type="checkbox" class="form-check-input" for="no-encryption" id="no-encryption"
value="no-encryption" name="no-encryption">
<label class="col-form-label col-form-label-sm" for="no-encryption">No encryption</label>
</div>
{{end}}
<div class="col-1">

View file

@ -1,69 +1,51 @@
{{define "note"}}
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
{{block "head" .}}{{end}}
<body>
{{block "header" .}}{{end}}
{{if ne .Err nil}}
<div class="container mb-4 text-center">
<div class="alert alert-danger" role="alert">
<p>Could not show note</p>
<p><strong>{{.Err}}</strong></p>
{{if ne .Err nil}}
{{block "error" .}}{{end}}
{{else}}
<div class="container mb-4">
<div class="d-flex flex-wrap py-2">
<span class="fs-4 d-flex mb-3 mb-md-0 me-md-auto text-decoration-none">Note {{.Note.ID}}</span>
<ul class="nav nav-pills align-items-center">
<li class="nav-item px-2">
<a href="" id="rawURL">raw</a>
<script>document.getElementById("rawURL").href = window.location.href.replace(".html", "/raw");</script>
</li>
<li class="nav-item px-2">
{{.Note.Language}}
</li>
{{if eq .Note.DeleteAfterRead false}}
<li class="nav-item px-2">
expires in {{HumanDuration (TimeDiff .Note.ExpiresAt)}}
</li>
{{end}}
</ul>
</div>
<div class="row">
<div id="editor" name="editor" class="form-control" style="height: 300px; resize: vertical; overflow: auto;">
</div>
</div>
{{else}}
<div class="container mb-4">
<div class="d-flex flex-wrap py-2">
<span class="fs-4 d-flex mb-3 mb-md-0 me-md-auto text-decoration-none">Note {{.Note.ID}}</span>
<ul class="nav nav-pills align-items-center">
<li class="nav-item px-2">
<a href="" id="rawURL">raw</a>
<script>document.getElementById("rawURL").href = window.location.href.replace(".html", "");</script>
</li>
<li class="nav-item px-2">
{{.Note.Language}}
</li>
{{if eq .Note.DeleteAfterRead false}}
<li class="nav-item px-2">
expires in {{HumanDuration (TimeDiff .Note.ExpiresAt)}}
</li>
{{end}}
</ul>
</div>
<div class="row">
<div id="editor" name="editor" class="form-control"
style="height: 300px; resize: vertical; overflow: auto;"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script>
<script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' } });
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs/loader.min.js"></script>
<script>
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs' } });
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('editor'), {
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark",
language: "{{.Note.Language}}",
readOnly: true,
value: "{{string .Note.Content}}"
});
// Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setTheme("vs")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark")
}
});
require(['vs/editor/editor.main'], function () {
var editor = monaco.editor.create(document.getElementById('editor'), {
theme: document.documentElement.getAttribute('data-bs-theme') == 'light' ? "vs" : "vs-dark",
language: "{{.Note.Language}}",
readOnly: true,
value: "{{string .Note.Content}}"
});
</script>
</div>
{{end}}
{{block "footer" .}}{{end}}
</body>
</html>
// Dark mode
document.getElementById("lightSwitch").addEventListener("click", () => {
if (document.documentElement.getAttribute('data-bs-theme') == 'light') {
monaco.editor.setTheme("vs")
} else if (document.documentElement.getAttribute('data-bs-theme') == 'dark') {
monaco.editor.setTheme("vs-dark")
}
});
});
</script>
</div>
{{end}}
{{end}}

View file

@ -0,0 +1,21 @@
{{define "protectedNote"}}
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
{{block "head" .}}{{end}}
<body>
{{block "header" .}}{{end}}
{{if ne .Err nil}}
{{block "error" .}}{{end}}
{{else}}
{{block "note" .}}{{end}}
{{end}}
{{block "footer" .}}{{end}}
</body>
</html>
{{end}}

View file

@ -0,0 +1,42 @@
{{define "unprotectedNote"}}
<!DOCTYPE html>
<html lang="en" data-bs-theme="light">
{{block "head" .}}{{end}}
<body>
{{block "header" .}}{{end}}
{{if ne .Err nil}}
{{block "error" .}}{{end}}
{{else}}
<div class="container mb-4">
{{if gt (len .Note.PasswordHash) 0}}
<form id="form" action="#" method="post" enctype="multipart/form-data">
<div class="container mb-4 w-25">
<div class="row text-center justify-content-center">
<label class="col-form-label" for="password">Password</label>
</div>
</div>
<div class="container mb-4 w-25">
<div class="row text-center justify-content-center">
<input type="password" class="form-control" id="password" name="password">
</div>
</div>
<div class="container mb-4 w-25">
<div class="row text-center justify-content-center">
<button type="submit" id="submit" class="btn btn-success">Submit</button>
</div>
</div>
</form>
</div>
{{else}}
{{block "note" .}}{{end}}
{{end}}
{{end}}
{{block "footer" .}}{{end}}
</body>
</html>
{{end}}