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:
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)
|
||||
})
|
||||
}
|
Reference in New Issue
Block a user