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:
parent
61ca30690b
commit
9e0254c0b5
16 changed files with 713 additions and 135 deletions
|
@ -32,6 +32,7 @@ type NotePayload struct {
|
|||
Expiration int `json:"expiration,omitempty"`
|
||||
DeleteAfterRead bool `json:"delete_after_read,omitempty"`
|
||||
Language string `json:"language"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type NoteResponse struct {
|
||||
|
@ -76,6 +77,7 @@ func handleMain() int {
|
|||
bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token")
|
||||
askBearer := flag.Bool("ask-bearer", false, "Read bearer token from input")
|
||||
language := flag.String("language", "", "Language of the note")
|
||||
password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to access the note")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
|
@ -172,6 +174,9 @@ func handleMain() int {
|
|||
if *language != "" {
|
||||
p.Language = *language
|
||||
}
|
||||
if *password != "" {
|
||||
p.Password = *password
|
||||
}
|
||||
|
||||
if *encryptionKey != "" {
|
||||
logger.Debug("validating encryption key")
|
||||
|
@ -242,21 +247,24 @@ func handleMain() int {
|
|||
logger.Debug("finding note location")
|
||||
var location string
|
||||
noteURL := *url + "/" + jsonBody.ID
|
||||
if *encryptionKey != "" {
|
||||
if *copier {
|
||||
location = fmt.Sprintf("copier -encryption-key %s %s", *encryptionKey, noteURL)
|
||||
} else {
|
||||
if *html {
|
||||
location = fmt.Sprintf("%s/%s.html", noteURL, *encryptionKey)
|
||||
} else {
|
||||
location = fmt.Sprintf("%s/%s", noteURL, *encryptionKey)
|
||||
}
|
||||
if *copier {
|
||||
location = "copier"
|
||||
if *encryptionKey != "" {
|
||||
location += " -encryption-key " + *encryptionKey
|
||||
}
|
||||
if *password != "" {
|
||||
location += " -password '" + *password + "'"
|
||||
}
|
||||
location += " " + noteURL
|
||||
} else {
|
||||
location = noteURL
|
||||
if *encryptionKey != "" {
|
||||
location += "/" + *encryptionKey
|
||||
}
|
||||
if *html {
|
||||
location = fmt.Sprintf("%s.html", noteURL)
|
||||
location += ".html"
|
||||
} else {
|
||||
location = noteURL
|
||||
location += "/raw"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,8 @@ The file format is **JSON**:
|
|||
* **encryption_key_length** (int): Number of characters for generated encryption key (default 16)
|
||||
* **allow_client_encryption_key** (bool): Allow encryption key provided by the client on the web UI
|
||||
* **allow_no_encryption** (bool): Allow notes without encryption
|
||||
* **enable_password_encryption** (bool): Enable password to protect notes (default true)
|
||||
* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true)
|
||||
* **expiration_interval** (int): Number of seconds to wait between two expiration runs
|
||||
* **listen_address** (string): Address to listen for the web server (default "0.0.0.0")
|
||||
* **listen_port** (int): Port to listen for the web server (default 8080)
|
||||
|
@ -35,7 +37,6 @@ The file format is **JSON**:
|
|||
* **observation_internal** (int): Number of seconds to wait between two observations (default 60)
|
||||
* **languages** ([]string): List of supported [languages](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages)
|
||||
* **language** (string): Default language (default "text")
|
||||
* **enable_upload_file_button** (bool): Display the upload file button in the UI (default true)
|
||||
* **tls_cert_file** (string): Path to TLS certificate file to enable HTTPS
|
||||
* **tls_key_file** (string): Path to TLS key file to enable HTTPS
|
||||
* **bootstrap_directory** (string): Serve [Bootstrap](https://getbootstrap.com/) assets from this local directory (ex: "./node_modules/bootstrap/dist"). See **Dependencies** for details.
|
||||
|
@ -64,19 +65,38 @@ Response (JSON):
|
|||
* **id** (string): ID of the note
|
||||
|
||||
|
||||
### GET /\<id\>/\<encryptionKey\>
|
||||
### GET /api/note/\<id\>/\<encryptionKey\>
|
||||
|
||||
> [!WARNING]
|
||||
> Potential encryption key leak
|
||||
|
||||
Return content of a note encrypted by the given encryption key.
|
||||
|
||||
### GET /\<id\>
|
||||
### POST /api/note/\<id\>/\<encryptionKey\>
|
||||
|
||||
> [!WARNING]
|
||||
> Potential encryption key leak
|
||||
|
||||
Return content of a protected note encrypted by the given encryption key.
|
||||
|
||||
Body (JSON):
|
||||
* **password** (string): password used to protect the note (required)
|
||||
|
||||
### GET /api/note/\<id\>
|
||||
|
||||
Return content of a note.
|
||||
|
||||
If the note is encrypted, the encrypted value is returned (application/octet-stream). Otherwise, the text is returned (text/plain).
|
||||
|
||||
### POST /api/note/\<id\>
|
||||
|
||||
Return content of a protected note.
|
||||
|
||||
If the note is encrypted, the encrypted value is returned (application/octet-stream). Otherwise, the text is returned (text/plain).
|
||||
|
||||
Body (JSON):
|
||||
* **password** (string): password used to protect the note (required)
|
||||
|
||||
### Errors
|
||||
|
||||
Errors return **500 Server Internal Error** with the **JSON** payload:
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
|
@ -21,6 +24,10 @@ var (
|
|||
GitCommit string
|
||||
)
|
||||
|
||||
type NotePayload struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
func handleMain() int {
|
||||
|
||||
flag.Usage = usage
|
||||
|
@ -34,6 +41,7 @@ func handleMain() int {
|
|||
fileName := flag.String("file", "", "Write content of the note to a file")
|
||||
bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token")
|
||||
askBearer := flag.Bool("ask-bearer", false, "Read bearer token from input")
|
||||
password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to access the note")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
|
@ -47,7 +55,7 @@ func handleMain() int {
|
|||
return internal.RC_ERROR
|
||||
}
|
||||
|
||||
url := flag.Args()[0]
|
||||
rawURL := flag.Args()[0]
|
||||
|
||||
var level slog.Level
|
||||
if *debug {
|
||||
|
@ -81,21 +89,50 @@ func handleMain() int {
|
|||
fmt.Print("\n")
|
||||
}
|
||||
|
||||
logger.Debug("creating http request")
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
logger.Debug("parsing url", slog.Any("url", rawURL))
|
||||
u, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return internal.ReturnError(logger, "could not create request", err)
|
||||
return internal.ReturnError(logger, "could not parse url", err)
|
||||
}
|
||||
u.Path = "api/note" + u.Path
|
||||
|
||||
rawURL = u.String()
|
||||
|
||||
logger.Debug("creating http request")
|
||||
var req *http.Request
|
||||
if *password != "" {
|
||||
body := &NotePayload{
|
||||
Password: *password,
|
||||
}
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return internal.ReturnError(logger, "could not create note payload", err)
|
||||
}
|
||||
req, err = http.NewRequest("POST", rawURL, bytes.NewBuffer(payload))
|
||||
if err != nil {
|
||||
return internal.ReturnError(logger, "could not create request", err)
|
||||
}
|
||||
} else {
|
||||
req, err = http.NewRequest("GET", rawURL, nil)
|
||||
if err != nil {
|
||||
return internal.ReturnError(logger, "could not create request", err)
|
||||
}
|
||||
}
|
||||
|
||||
if *bearer != "" {
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", *bearer))
|
||||
}
|
||||
|
||||
logger.Debug("parsing url", slog.Any("url", url))
|
||||
logger.Debug("executing http request", slog.Any("method", req.Method), slog.Any("url", rawURL))
|
||||
r, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return internal.ReturnError(logger, "could not retreive note", err)
|
||||
}
|
||||
|
||||
if r.StatusCode >= 300 {
|
||||
return internal.ReturnError(logger, "could not retreive note", fmt.Errorf("status code %d", r.StatusCode))
|
||||
}
|
||||
|
||||
logger.Debug("decoding body")
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
|
|
14
src/go.mod
14
src/go.mod
|
@ -1,15 +1,15 @@
|
|||
module git.riou.xyz/jriou/coller
|
||||
|
||||
go 1.24
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.6
|
||||
toolchain go1.24.7
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/prometheus/client_golang v1.23.0
|
||||
golang.design/x/clipboard v0.7.1
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/term v0.34.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/term v0.35.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.30.0
|
||||
|
@ -33,8 +33,8 @@ require (
|
|||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 // indirect
|
||||
golang.org/x/image v0.28.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.0 // indirect
|
||||
google.golang.org/protobuf v1.36.6 // indirect
|
||||
)
|
||||
|
|
10
src/go.sum
10
src/go.sum
|
@ -52,6 +52,8 @@ golang.design/x/clipboard v0.7.1 h1:OEG3CmcYRBNnRwpDp7+uWLiZi3hrMRJpE9JkkkYtz2c=
|
|||
golang.design/x/clipboard v0.7.1/go.mod h1:i5SiIqj0wLFw9P/1D7vfILFK0KHMk7ydE72HRrUIgkg=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476 h1:Wdx0vgH5Wgsw+lF//LJKmWOJBLWX6nprsMqnf99rYDE=
|
||||
golang.org/x/exp/shiny v0.0.0-20250606033433-dcc06ee1d476/go.mod h1:ygj7T6vSGhhm/9yTpOQQNvuAUFziTH7RUiH74EoE2C8=
|
||||
golang.org/x/image v0.28.0 h1:gdem5JW1OLS4FbkWgLO+7ZeFzYtL3xClb97GaUzYMFE=
|
||||
|
@ -60,12 +62,20 @@ golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f h1:/n+PL2HlfqeSiDCuhdBbRN
|
|||
golang.org/x/mobile v0.0.0-20250606033058-a2a15c67f36f/go.mod h1:ESkJ836Z6LpG6mTVAhA48LpfW/8fNR0ifStlH2axyfg=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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))))
|
||||
}
|
||||
|
|
8
src/server/templates/error.html
Normal file
8
src/server/templates/error.html
Normal 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}}
|
|
@ -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">
|
||||
|
|
|
@ -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}}
|
21
src/server/templates/protectedNote.html
Normal file
21
src/server/templates/protectedNote.html
Normal 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}}
|
42
src/server/templates/unprotectedNote.html
Normal file
42
src/server/templates/unprotectedNote.html
Normal 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}}
|
Loading…
Add table
Add a link
Reference in a new issue