mirror of
https://github.com/lus/pasty.git
synced 2023-08-10 21:13:09 +03:00
reimplement v2 API controller
This commit is contained in:
parent
a8077a54f9
commit
941da057ae
@ -45,7 +45,7 @@ func main() {
|
||||
var driver storage.Driver
|
||||
switch strings.TrimSpace(strings.ToLower(cfg.StorageDriver)) {
|
||||
case "postgres":
|
||||
driver = new(postgres.Driver)
|
||||
driver = postgres.New(cfg.Postgres.DSN)
|
||||
break
|
||||
default:
|
||||
log.Fatal().Str("driver_name", cfg.StorageDriver).Msg("An invalid storage driver name was given.")
|
||||
@ -54,7 +54,7 @@ func main() {
|
||||
|
||||
// Initialize the configured storage driver
|
||||
log.Info().Str("driver_name", cfg.StorageDriver).Msg("Initializing the storage driver...")
|
||||
if err := driver.Initialize(context.Background(), cfg); err != nil {
|
||||
if err := driver.Initialize(context.Background()); err != nil {
|
||||
log.Fatal().Err(err).Str("driver_name", cfg.StorageDriver).Msg("The storage driver could not be initialized.")
|
||||
return
|
||||
}
|
||||
|
2
go.mod
2
go.mod
@ -3,6 +3,8 @@ module github.com/lus/pasty
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736
|
||||
github.com/go-chi/chi/v5 v5.0.8
|
||||
github.com/golang-migrate/migrate/v4 v4.16.1
|
||||
github.com/jackc/pgx/v5 v5.3.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
|
38
go.sum
38
go.sum
@ -1,5 +1,7 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736 h1:qZaEtLxnqY5mJ0fVKbk31NVhlgi0yrKm51Pq/I5wcz4=
|
||||
github.com/alexedwards/argon2id v0.0.0-20230305115115-4b3c3280a736/go.mod h1:mTeFRcTdnpzOlRjMoFYC/80HwVUreupyAiqPkCZQOXc=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@ -8,6 +10,8 @@ github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m3
|
||||
github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/go-chi/chi/v5 v5.0.8 h1:lD+NLqFcAi1ovnVZpsnObHGW4xb4J8lNmoYVfECH1Y0=
|
||||
github.com/go-chi/chi/v5 v5.0.8/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.16.1 h1:O+0C55RbMN66pWm5MjO6mw0px6usGpY0+bkSGW9zCo0=
|
||||
@ -54,23 +58,57 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI=
|
||||
golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
19
internal/pastes/id_generation.go
Normal file
19
internal/pastes/id_generation.go
Normal file
@ -0,0 +1,19 @@
|
||||
package pastes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lus/pasty/internal/randx"
|
||||
)
|
||||
|
||||
func GenerateID(ctx context.Context, repo Repository, charset string, length int) (string, error) {
|
||||
for {
|
||||
id := randx.String(charset, length)
|
||||
existing, err := repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if existing == nil {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,31 @@
|
||||
package pastes
|
||||
|
||||
import "github.com/alexedwards/argon2id"
|
||||
|
||||
type Paste struct {
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
ModificationToken string `json:"modificationToken,omitempty"`
|
||||
Created int64 `json:"created"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
ID string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
ModificationToken string `json:"modificationToken,omitempty"`
|
||||
Created int64 `json:"created"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
func (paste *Paste) HashModificationToken() error {
|
||||
if paste.ModificationToken == "" {
|
||||
return nil
|
||||
}
|
||||
hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paste.ModificationToken = hash
|
||||
return nil
|
||||
}
|
||||
|
||||
func (paste *Paste) CheckModificationToken(modificationToken string) bool {
|
||||
if paste.ModificationToken == "" {
|
||||
return false
|
||||
}
|
||||
match, err := argon2id.ComparePasswordAndHash(modificationToken, paste.ModificationToken)
|
||||
return err == nil && match
|
||||
}
|
||||
|
10
internal/slices/slice_utils.go
Normal file
10
internal/slices/slice_utils.go
Normal file
@ -0,0 +1,10 @@
|
||||
package slices
|
||||
|
||||
func Contains[T comparable](src []T, val T) bool {
|
||||
for _, elem := range src {
|
||||
if elem == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -2,12 +2,11 @@ package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lus/pasty/internal/config"
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
)
|
||||
|
||||
type Driver interface {
|
||||
Initialize(ctx context.Context, cfg *config.Config) error
|
||||
Initialize(ctx context.Context) error
|
||||
Close() error
|
||||
Pastes() pastes.Repository
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ import (
|
||||
_ "github.com/golang-migrate/migrate/v4/database/postgres"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/lus/pasty/internal/config"
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"github.com/lus/pasty/internal/storage"
|
||||
"github.com/rs/zerolog/log"
|
||||
@ -18,14 +17,21 @@ import (
|
||||
var migrations embed.FS
|
||||
|
||||
type Driver struct {
|
||||
dsn string
|
||||
connPool *pgxpool.Pool
|
||||
pastes *pasteRepository
|
||||
}
|
||||
|
||||
var _ storage.Driver = (*Driver)(nil)
|
||||
|
||||
func (driver *Driver) Initialize(ctx context.Context, cfg *config.Config) error {
|
||||
pool, err := pgxpool.New(ctx, cfg.Postgres.DSN)
|
||||
func New(dsn string) *Driver {
|
||||
return &Driver{
|
||||
dsn: dsn,
|
||||
}
|
||||
}
|
||||
|
||||
func (driver *Driver) Initialize(ctx context.Context) error {
|
||||
pool, err := pgxpool.New(ctx, driver.dsn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -36,7 +42,7 @@ func (driver *Driver) Initialize(ctx context.Context, cfg *config.Config) error
|
||||
pool.Close()
|
||||
return err
|
||||
}
|
||||
migrator, err := migrate.NewWithSourceInstance("iofs", source, cfg.Postgres.DSN)
|
||||
migrator, err := migrate.NewWithSourceInstance("iofs", source, driver.dsn)
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
return err
|
||||
|
33
internal/web/response_writer.go
Normal file
33
internal/web/response_writer.go
Normal file
@ -0,0 +1,33 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func writeErr(writer http.ResponseWriter, err error) {
|
||||
writeString(writer, http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
func writeString(writer http.ResponseWriter, status int, value string) {
|
||||
writer.WriteHeader(status)
|
||||
writer.Write([]byte(value))
|
||||
}
|
||||
|
||||
func writeJSON(writer http.ResponseWriter, status int, value any) error {
|
||||
jsonData, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
writer.WriteHeader(status)
|
||||
writer.Write(jsonData)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeJSONOrErr(writer http.ResponseWriter, status int, value any) {
|
||||
if err := writeJSON(writer, status, value); err != nil {
|
||||
writeErr(writer, err)
|
||||
}
|
||||
}
|
49
internal/web/server.go
Normal file
49
internal/web/server.go
Normal file
@ -0,0 +1,49 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/lus/pasty/internal/storage"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
// The address the web server should listen to.
|
||||
Address string
|
||||
|
||||
// The storage driver to use.
|
||||
Storage storage.Driver
|
||||
|
||||
// Whether the Hastebin support should be enabled.
|
||||
// If this is set to 'false', the Hastebin specific endpoints will not be registered.
|
||||
HastebinSupport bool
|
||||
|
||||
// The length of newly generated paste IDs.
|
||||
PasteIDLength int
|
||||
// The charset to use when generating new paste IDs.
|
||||
PasteIDCharset string
|
||||
|
||||
// The maximum length of newly generated pastes.
|
||||
PasteLengthCap int
|
||||
|
||||
// Whether modification tokens are enabled.
|
||||
ModificationTokensEnabled bool
|
||||
// The length of newly generated modification tokens.
|
||||
ModificationTokenLength int
|
||||
// The charset to use when generating new modification tokens.
|
||||
ModificationTokenCharset string
|
||||
|
||||
// The administration tokens.
|
||||
AdminTokens []string
|
||||
}
|
||||
|
||||
func (server *Server) Start() error {
|
||||
router := chi.NewRouter()
|
||||
|
||||
// Register the paste API endpoints
|
||||
router.With(server.v2MiddlewareInjectPaste).Get("/api/v2/pastes/{paste_id}", server.v2EndpointGetPaste)
|
||||
router.Post("/api/v2/pastes", server.v2EndpointCreatePaste)
|
||||
router.With(server.v2MiddlewareInjectPaste, server.v2MiddlewareAuthorize).Patch("/api/v2/pastes/{paste_id}", server.v2EndpointModifyPaste)
|
||||
router.Delete("/api/v2/pastes/{paste_id}", server.v2EndpointDeletePaste)
|
||||
|
||||
return http.ListenAndServe(server.Address, router)
|
||||
}
|
70
internal/web/v2_end_create_paste.go
Normal file
70
internal/web/v2_end_create_paste.go
Normal file
@ -0,0 +1,70 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"github.com/lus/pasty/internal/randx"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type v2EndpointCreatePastePayload struct {
|
||||
Content string `json:"content"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
func (server *Server) v2EndpointCreatePaste(writer http.ResponseWriter, request *http.Request) {
|
||||
// Read, parse and validate the request payload
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
payload := new(v2EndpointCreatePastePayload)
|
||||
if err := json.Unmarshal(body, payload); err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
if payload.Content == "" {
|
||||
writeString(writer, http.StatusBadRequest, "missing paste content")
|
||||
return
|
||||
}
|
||||
if server.PasteLengthCap > 0 && len(payload.Content) > server.PasteLengthCap {
|
||||
writeString(writer, http.StatusBadRequest, "too large paste content")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := pastes.GenerateID(request.Context(), server.Storage.Pastes(), server.PasteIDCharset, server.PasteIDLength)
|
||||
if err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
paste := &pastes.Paste{
|
||||
ID: id,
|
||||
Content: payload.Content,
|
||||
Created: time.Now().Unix(),
|
||||
Metadata: payload.Metadata,
|
||||
}
|
||||
|
||||
modificationToken := ""
|
||||
if server.ModificationTokensEnabled {
|
||||
modificationToken = randx.String(server.ModificationTokenCharset, server.ModificationTokenLength)
|
||||
paste.ModificationToken = modificationToken
|
||||
|
||||
if err := paste.HashModificationToken(); err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := server.Storage.Pastes().Upsert(request.Context(), paste); err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
|
||||
cpy := *paste
|
||||
cpy.ModificationToken = modificationToken
|
||||
writeJSONOrErr(writer, http.StatusCreated, cpy)
|
||||
}
|
18
internal/web/v2_end_delete_paste.go
Normal file
18
internal/web/v2_end_delete_paste.go
Normal file
@ -0,0 +1,18 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (server *Server) v2EndpointDeletePaste(writer http.ResponseWriter, request *http.Request) {
|
||||
paste, ok := request.Context().Value("paste").(*pastes.Paste)
|
||||
if !ok {
|
||||
writeString(writer, http.StatusInternalServerError, "missing paste object")
|
||||
return
|
||||
}
|
||||
|
||||
if err := server.Storage.Pastes().DeleteByID(request.Context(), paste.ID); err != nil {
|
||||
writeErr(writer, err)
|
||||
}
|
||||
}
|
18
internal/web/v2_end_get_paste.go
Normal file
18
internal/web/v2_end_get_paste.go
Normal file
@ -0,0 +1,18 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func (server *Server) v2EndpointGetPaste(writer http.ResponseWriter, request *http.Request) {
|
||||
paste, ok := request.Context().Value("paste").(*pastes.Paste)
|
||||
if !ok {
|
||||
writeString(writer, http.StatusInternalServerError, "missing paste object")
|
||||
return
|
||||
}
|
||||
|
||||
cpy := *paste
|
||||
cpy.ModificationToken = ""
|
||||
writeJSONOrErr(writer, http.StatusOK, cpy)
|
||||
}
|
60
internal/web/v2_end_modify_paste.go
Normal file
60
internal/web/v2_end_modify_paste.go
Normal file
@ -0,0 +1,60 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type v2EndpointModifyPastePayload struct {
|
||||
Content *string `json:"content"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
func (server *Server) v2EndpointModifyPaste(writer http.ResponseWriter, request *http.Request) {
|
||||
paste, ok := request.Context().Value("paste").(*pastes.Paste)
|
||||
if !ok {
|
||||
writeString(writer, http.StatusInternalServerError, "missing paste object")
|
||||
return
|
||||
}
|
||||
|
||||
// Read, parse and validate the request payload
|
||||
body, err := io.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
payload := new(v2EndpointModifyPastePayload)
|
||||
if err := json.Unmarshal(body, payload); err != nil {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
if payload.Content != nil && *payload.Content == "" {
|
||||
writeString(writer, http.StatusBadRequest, "missing paste content")
|
||||
return
|
||||
}
|
||||
if payload.Content != nil && server.PasteLengthCap > 0 && len(*payload.Content) > server.PasteLengthCap {
|
||||
writeString(writer, http.StatusBadRequest, "too large paste content")
|
||||
return
|
||||
}
|
||||
|
||||
// Modify the paste itself
|
||||
if payload.Content != nil {
|
||||
paste.Content = *payload.Content
|
||||
}
|
||||
if payload.Metadata != nil {
|
||||
for key, value := range payload.Metadata {
|
||||
if value == nil {
|
||||
delete(paste.Metadata, key)
|
||||
continue
|
||||
}
|
||||
paste.Metadata[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// Save the modified paste
|
||||
if err := server.Storage.Pastes().Upsert(request.Context(), paste); err != nil {
|
||||
writeErr(writer, err)
|
||||
}
|
||||
}
|
37
internal/web/v2_mid_authorize.go
Normal file
37
internal/web/v2_mid_authorize.go
Normal file
@ -0,0 +1,37 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"github.com/lus/pasty/internal/pastes"
|
||||
"github.com/lus/pasty/internal/slices"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (server *Server) v2MiddlewareAuthorize(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
paste, ok := request.Context().Value("paste").(*pastes.Paste)
|
||||
if !ok {
|
||||
writeString(writer, http.StatusInternalServerError, "missing paste object")
|
||||
return
|
||||
}
|
||||
|
||||
authHeader := strings.SplitN(request.Header.Get("Authorization"), " ", 2)
|
||||
if len(authHeader) != 2 || authHeader[0] != "Bearer" {
|
||||
writeString(writer, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := slices.Contains(server.AdminTokens, authHeader[1])
|
||||
if isAdmin {
|
||||
next.ServeHTTP(writer, request)
|
||||
return
|
||||
}
|
||||
|
||||
if !server.ModificationTokensEnabled || !paste.CheckModificationToken(authHeader[1]) {
|
||||
writeString(writer, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(writer, request)
|
||||
})
|
||||
}
|
38
internal/web/v2_mid_inject_paste.go
Normal file
38
internal/web/v2_mid_inject_paste.go
Normal file
@ -0,0 +1,38 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (server *Server) v2MiddlewareInjectPaste(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
pasteID := strings.TrimSpace(chi.URLParam(request, "paste_id"))
|
||||
if pasteID == "" {
|
||||
writeString(writer, http.StatusNotFound, "paste not found")
|
||||
return
|
||||
}
|
||||
|
||||
paste, err := server.Storage.Pastes().FindByID(request.Context(), pasteID)
|
||||
if err != nil {
|
||||
if pasteID == "" {
|
||||
writeErr(writer, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if paste == nil {
|
||||
writeString(writer, http.StatusNotFound, "paste not found")
|
||||
return
|
||||
}
|
||||
|
||||
if paste.Metadata == nil {
|
||||
paste.Metadata = make(map[string]any)
|
||||
}
|
||||
|
||||
request = request.WithContext(context.WithValue(request.Context(), "paste", paste))
|
||||
|
||||
next.ServeHTTP(writer, request)
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
const API_BASE_URL = location.protocol + "//" + location.host + "/api/v2";
|
||||
const API_BASE_URL = location.protocol + "//" + location.host + "/web/v2";
|
||||
|
||||
export async function getAPIInformation() {
|
||||
return fetch(API_BASE_URL + "/info");
|
||||
|
Loading…
Reference in New Issue
Block a user