1
0
mirror of https://github.com/lus/pasty.git synced 2023-08-10 21:13:09 +03:00

Implement API v2 (#13)

* Add documentation notice

* Rename the deletion token to modification token

* Implement API basics

* Implement paste modification endpoint (#10)

* Implement paste metadata

* Implement report webhook support

* Document API

* Document paste entity

* Update syntax highlighting types (js -> jsonc)

* Update migrator
This commit is contained in:
Lukas Schulte Pelkum
2021-07-22 22:26:21 +02:00
committed by GitHub
parent 4c392b4b52
commit 99504e0bba
18 changed files with 723 additions and 71 deletions

View File

@@ -10,20 +10,21 @@ import (
// Config represents the general application configuration structure
type Config struct {
WebAddress string
StorageType shared.StorageType
HastebinSupport bool
IDLength int
DeletionTokens bool
DeletionTokenMaster string
DeletionTokenLength int
RateLimit string
LengthCap int
AutoDelete *AutoDeleteConfig
File *FileConfig
Postgres *PostgresConfig
MongoDB *MongoDBConfig
S3 *S3Config
WebAddress string
StorageType shared.StorageType
HastebinSupport bool
IDLength int
ModificationTokens bool
ModificationTokenMaster string
ModificationTokenLength int
RateLimit string
LengthCap int
AutoDelete *AutoDeleteConfig
Reports *ReportConfig
File *FileConfig
Postgres *PostgresConfig
MongoDB *MongoDBConfig
S3 *S3Config
}
// AutoDeleteConfig represents the configuration specific for the AutoDelete behaviour
@@ -61,6 +62,13 @@ type S3Config struct {
Bucket string
}
// ReportConfig represents the configuration specific for the report system
type ReportConfig struct {
Reports bool
ReportWebhook string
ReportWebhookToken string
}
// Current holds the currently loaded config
var Current *Config
@@ -69,20 +77,25 @@ func Load() {
env.Load()
Current = &Config{
WebAddress: env.MustString("WEB_ADDRESS", ":8080"),
StorageType: shared.StorageType(strings.ToLower(env.MustString("STORAGE_TYPE", "file"))),
HastebinSupport: env.MustBool("HASTEBIN_SUPPORT", false),
IDLength: env.MustInt("ID_LENGTH", 6),
DeletionTokens: env.MustBool("DELETION_TOKENS", true),
DeletionTokenMaster: env.MustString("DELETION_TOKEN_MASTER", ""),
DeletionTokenLength: env.MustInt("DELETION_TOKEN_LENGTH", 12),
RateLimit: env.MustString("RATE_LIMIT", "30-M"),
LengthCap: env.MustInt("LENGTH_CAP", 50_000),
WebAddress: env.MustString("WEB_ADDRESS", ":8080"),
StorageType: shared.StorageType(strings.ToLower(env.MustString("STORAGE_TYPE", "file"))),
HastebinSupport: env.MustBool("HASTEBIN_SUPPORT", false),
IDLength: env.MustInt("ID_LENGTH", 6),
ModificationTokens: env.MustBool("MODIFICATION_TOKENS", env.MustBool("DELETION_TOKENS", true)), // ---
ModificationTokenMaster: env.MustString("MODIFICATION_TOKEN_MASTER", env.MustString("DELETION_TOKEN_MASTER", "")), // - We don't want to destroy peoples old configuration
ModificationTokenLength: env.MustInt("MODIFICATION_TOKEN_LENGTH", env.MustInt("DELETION_TOKEN_LENGTH", 12)), // ---
RateLimit: env.MustString("RATE_LIMIT", "30-M"),
LengthCap: env.MustInt("LENGTH_CAP", 50_000),
AutoDelete: &AutoDeleteConfig{
Enabled: env.MustBool("AUTODELETE", false),
Lifetime: env.MustDuration("AUTODELETE_LIFETIME", 720*time.Hour),
TaskInterval: env.MustDuration("AUTODELETE_TASK_INTERVAL", 5*time.Minute),
},
Reports: &ReportConfig{
Reports: env.MustBool("REPORTS", false),
ReportWebhook: env.MustString("REPORT_WEBHOOK", ""),
ReportWebhookToken: env.MustString("REPORT_WEBHOOK_TOKEN", ""),
},
File: &FileConfig{
Path: env.MustString("STORAGE_FILE_PATH", "./data"),
},

57
internal/report/report.go Normal file
View File

@@ -0,0 +1,57 @@
package report
import (
"encoding/json"
"fmt"
"github.com/lus/pasty/internal/config"
"github.com/valyala/fasthttp"
)
// ReportRequest represents a report request sent to the report webhook
type ReportRequest struct {
Paste string `json:"paste"`
Reason string `json:"reason"`
Timestamp int64 `json:"timestamp"`
}
// ReportResponse represents a report response received from the report webhook
type ReportResponse struct {
Message string `json:"message"`
}
// SendReport sends a report request to the report webhook
func SendReport(reportRequest *ReportRequest) (*ReportResponse, error) {
request := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(request)
response := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(response)
request.Header.SetMethod(fasthttp.MethodPost)
request.SetRequestURI(config.Current.Reports.ReportWebhook)
if config.Current.Reports.ReportWebhookToken != "" {
request.Header.Set("Authorization", "Bearer "+config.Current.Reports.ReportWebhookToken)
}
data, err := json.Marshal(reportRequest)
if err != nil {
return nil, err
}
request.SetBody(data)
if err := fasthttp.Do(request, response); err != nil {
return nil, err
}
status := response.StatusCode()
if status < 200 || status > 299 {
return nil, fmt.Errorf("the report webhook responded with an unexpected error: %d (%s)", status, string(response.Body()))
}
reportResponse := new(ReportResponse)
if err := json.Unmarshal(response.Body(), reportResponse); err != nil {
return nil, err
}
return reportResponse, nil
}

View File

@@ -1,30 +1,43 @@
package shared
import (
"log"
"github.com/alexedwards/argon2id"
)
// Paste represents a saved paste
type Paste struct {
ID string `json:"id" bson:"_id"`
Content string `json:"content" bson:"content"`
DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"`
Created int64 `json:"created" bson:"created"`
AutoDelete bool `json:"autoDelete" bson:"autoDelete"`
ID string `json:"id" bson:"_id"`
Content string `json:"content" bson:"content"`
DeletionToken string `json:"deletionToken,omitempty" bson:"deletionToken"` // Required for legacy paste storage support
ModificationToken string `json:"modificationToken,omitempty" bson:"modificationToken"`
Created int64 `json:"created" bson:"created"`
AutoDelete bool `json:"autoDelete" bson:"autoDelete"`
Metadata map[string]interface{} `json:"metadata" bson:"metadata"`
}
// HashDeletionToken hashes the current deletion token of a paste
func (paste *Paste) HashDeletionToken() error {
hash, err := argon2id.CreateHash(paste.DeletionToken, argon2id.DefaultParams)
// HashModificationToken hashes the current modification token of a paste
func (paste *Paste) HashModificationToken() error {
hash, err := argon2id.CreateHash(paste.ModificationToken, argon2id.DefaultParams)
if err != nil {
return err
}
paste.DeletionToken = hash
paste.ModificationToken = hash
return nil
}
// CheckDeletionToken checks whether or not the given deletion token is correct
func (paste *Paste) CheckDeletionToken(deletionToken string) bool {
match, err := argon2id.ComparePasswordAndHash(deletionToken, paste.DeletionToken)
// CheckModificationToken checks whether or not the given modification token is correct
func (paste *Paste) CheckModificationToken(modificationToken string) bool {
// The modification token may be stored in the deletion token field in old pastes
usedToken := paste.ModificationToken
if usedToken == "" {
usedToken = paste.DeletionToken
if usedToken != "" {
log.Println("WARNING: You seem to have pastes with the old 'deletionToken' field stored in your storage driver. Though this does not cause any issues right now, it may in the future. Consider some kind of migration.")
}
}
match, err := argon2id.ComparePasswordAndHash(modificationToken, usedToken)
return err == nil && match
}

View File

@@ -117,8 +117,9 @@ func (driver *MongoDBDriver) Save(paste *shared.Paste) error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Insert the paste object
_, err := collection.InsertOne(ctx, paste)
// Upsert the paste object
filter := bson.M{"_id": paste.ID}
_, err := collection.UpdateOne(ctx, filter, paste, options.Update().SetUpsert(true))
return err
}

View File

@@ -0,0 +1,5 @@
begin;
alter table if exists "pastes" rename column "modificationToken" to "deletionToken";
commit;

View File

@@ -0,0 +1,5 @@
begin;
alter table if exists "pastes" rename column "deletionToken" to "modificationToken";
commit;

View File

@@ -0,0 +1,5 @@
begin;
alter table if exists "pastes" drop column "metadata";
commit;

View File

@@ -0,0 +1,5 @@
begin;
alter table if exists "pastes" add column "metadata" jsonb not null;
commit;

View File

@@ -82,7 +82,7 @@ func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
row := driver.pool.QueryRow(context.Background(), query, id)
paste := new(shared.Paste)
if err := row.Scan(&paste.ID, &paste.Content, &paste.DeletionToken, &paste.Created, &paste.AutoDelete); err != nil {
if err := row.Scan(&paste.ID, &paste.Content, &paste.ModificationToken, &paste.Created, &paste.AutoDelete, &paste.Metadata); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, nil
}
@@ -93,9 +93,18 @@ func (driver *PostgresDriver) Get(id string) (*shared.Paste, error) {
// Save saves a paste
func (driver *PostgresDriver) Save(paste *shared.Paste) error {
query := "INSERT INTO pastes VALUES ($1, $2, $3, $4, $5)"
query := `
INSERT INTO pastes (id, content, modificationToken, created, autoDelete)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (id) DO UPDATE
SET content = excluded.token,
modificationToken = excluded.modificationToken,
created = excluded.created,
autoDelete = excluded.autoDelete,
metadata = excluded.metadata
`
_, err := driver.pool.Exec(context.Background(), query, paste.ID, paste.Content, paste.DeletionToken, paste.Created, paste.AutoDelete)
_, err := driver.pool.Exec(context.Background(), query, paste.ID, paste.Content, paste.ModificationToken, paste.Created, paste.AutoDelete, paste.Metadata)
return err
}

View File

@@ -50,11 +50,11 @@ func HastebinSupportHandler(ctx *fasthttp.RequestCtx) {
AutoDelete: config.Current.AutoDelete.Enabled,
}
// Set a deletion token
if config.Current.DeletionTokens {
paste.DeletionToken = utils.RandomString(config.Current.DeletionTokenLength)
// Set a modification token
if config.Current.ModificationTokens {
paste.ModificationToken = utils.RandomString(config.Current.ModificationTokenLength)
err = paste.HashDeletionToken()
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())

View File

@@ -0,0 +1,26 @@
package v1
import "github.com/lus/pasty/internal/shared"
type legacyPaste struct {
ID string `json:"id"`
Content string `json:"content"`
DeletionToken string `json:"deletionToken,omitempty"`
Created int64 `json:"created"`
AutoDelete bool `json:"autoDelete"`
}
func legacyFromModern(paste *shared.Paste) *legacyPaste {
deletionToken := paste.ModificationToken
if deletionToken == "" {
deletionToken = paste.DeletionToken
}
return &legacyPaste{
ID: paste.ID,
Content: paste.Content,
DeletionToken: deletionToken,
Created: paste.Created,
AutoDelete: paste.AutoDelete,
}
}

View File

@@ -37,10 +37,11 @@ func v1GetPaste(ctx *fasthttp.RequestCtx) {
ctx.SetBodyString("paste not found")
return
}
paste.DeletionToken = ""
legacyPaste := legacyFromModern(paste)
legacyPaste.DeletionToken = ""
// Respond with the paste
jsonData, err := json.Marshal(paste)
jsonData, err := json.Marshal(legacyPaste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
@@ -91,13 +92,13 @@ func v1PostPaste(ctx *fasthttp.RequestCtx) {
AutoDelete: config.Current.AutoDelete.Enabled,
}
// Set a deletion token
deletionToken := ""
if config.Current.DeletionTokens {
deletionToken = utils.RandomString(config.Current.DeletionTokenLength)
paste.DeletionToken = deletionToken
// Set a modification token
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenLength)
paste.ModificationToken = modificationToken
err = paste.HashDeletionToken()
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
@@ -114,8 +115,8 @@ func v1PostPaste(ctx *fasthttp.RequestCtx) {
}
// Respond with the paste
pasteCopy := *paste
pasteCopy.DeletionToken = deletionToken
pasteCopy := legacyFromModern(paste)
pasteCopy.DeletionToken = modificationToken
jsonData, err := json.Marshal(pasteCopy)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
@@ -139,9 +140,9 @@ func v1DeletePaste(ctx *fasthttp.RequestCtx) {
return
}
// Validate the deletion token of the paste
deletionToken := values["deletionToken"]
if deletionToken == "" {
// Validate the modification token of the paste
modificationToken := values["deletionToken"]
if modificationToken == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing 'deletionToken' field")
return
@@ -160,8 +161,8 @@ func v1DeletePaste(ctx *fasthttp.RequestCtx) {
return
}
// Check if the deletion token is correct
if (config.Current.DeletionTokenMaster == "" || deletionToken != config.Current.DeletionTokenMaster) && !paste.CheckDeletionToken(deletionToken) {
// Check if the modification token is correct
if (config.Current.ModificationTokenMaster == "" || modificationToken != config.Current.ModificationTokenMaster) && !paste.CheckModificationToken(modificationToken) {
ctx.SetStatusCode(fasthttp.StatusForbidden)
ctx.SetBodyString("invalid deletion token")
return

View File

@@ -0,0 +1,276 @@
package v2
import (
"encoding/json"
"strings"
"time"
"github.com/fasthttp/router"
"github.com/lus/pasty/internal/config"
"github.com/lus/pasty/internal/report"
"github.com/lus/pasty/internal/shared"
"github.com/lus/pasty/internal/storage"
"github.com/lus/pasty/internal/utils"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/valyala/fasthttp"
)
// InitializePastesController initializes the '/v2/pastes/*' controller
func InitializePastesController(group *router.Group, rateLimiterMiddleware *limitFasthttp.Middleware) {
// moms spaghetti
group.GET("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(endpointGetPaste)))
group.POST("/", rateLimiterMiddleware.Handle(endpointCreatePaste))
group.PATCH("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(middlewareValidateModificationToken(endpointModifyPaste))))
group.DELETE("/{id}", rateLimiterMiddleware.Handle(middlewareInjectPaste(middlewareValidateModificationToken(endpointDeletePaste))))
if config.Current.Reports.Reports {
group.POST("/{id}/report", rateLimiterMiddleware.Handle(middlewareInjectPaste(endpointReportPaste)))
}
}
// middlewareInjectPaste retrieves and injects the paste with the specified ID
func middlewareInjectPaste(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
pasteID := ctx.UserValue("id").(string)
paste, err := storage.Current.Get(pasteID)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if paste == nil {
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBodyString("paste not found")
return
}
if paste.Metadata == nil {
paste.Metadata = map[string]interface{}{}
}
ctx.SetUserValue("_paste", paste)
next(ctx)
}
}
// middlewareValidateModificationToken extracts and validates a given modification token for an injected paste
func middlewareValidateModificationToken(next fasthttp.RequestHandler) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
authHeaderSplit := strings.SplitN(string(ctx.Request.Header.Peek("Authorization")), " ", 2)
if len(authHeaderSplit) < 2 || authHeaderSplit[0] != "Bearer" {
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
ctx.SetBodyString("unauthorized")
return
}
modificationToken := authHeaderSplit[1]
if config.Current.ModificationTokenMaster != "" && modificationToken == config.Current.ModificationTokenMaster {
next(ctx)
return
}
valid := paste.CheckModificationToken(modificationToken)
if !valid {
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
ctx.SetBodyString("unauthorized")
return
}
next(ctx)
}
}
// endpointGetPaste handles the 'GET /v2/pastes/{id}' endpoint
func endpointGetPaste(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
paste.DeletionToken = ""
paste.ModificationToken = ""
jsonData, err := json.Marshal(paste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}
type endpointCreatePastePayload struct {
Content string `json:"content"`
Metadata map[string]interface{} `json:"metadata"`
}
// endpointCreatePaste handles the 'POST /v2/pastes' endpoint
func endpointCreatePaste(ctx *fasthttp.RequestCtx) {
// Read, parse and validate the request payload
payload := new(endpointCreatePastePayload)
if err := json.Unmarshal(ctx.PostBody(), payload); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if payload.Content == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing paste content")
return
}
if config.Current.LengthCap > 0 && len(payload.Content) > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("too large paste content")
return
}
// Acquire a new paste ID
id, err := storage.AcquireID()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Prepare the paste object
if payload.Metadata == nil {
payload.Metadata = map[string]interface{}{}
}
paste := &shared.Paste{
ID: id,
Content: payload.Content,
Created: time.Now().Unix(),
AutoDelete: config.Current.AutoDelete.Enabled,
Metadata: payload.Metadata,
}
// Create a new modification token if enabled
modificationToken := ""
if config.Current.ModificationTokens {
modificationToken = utils.RandomString(config.Current.ModificationTokenLength)
paste.ModificationToken = modificationToken
err = paste.HashModificationToken()
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
// Save the paste
err = storage.Current.Save(paste)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
// Respond with the paste
pasteCopy := *paste
pasteCopy.ModificationToken = modificationToken
jsonData, err := json.Marshal(pasteCopy)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetStatusCode(fasthttp.StatusCreated)
ctx.SetBody(jsonData)
}
type endpointModifyPastePayload struct {
Content *string `json:"content"`
Metadata map[string]interface{} `json:"metadata"`
}
// endpointModifyPaste handles the 'PATCH /v2/pastes/{id}' endpoint
func endpointModifyPaste(ctx *fasthttp.RequestCtx) {
// Read, parse and validate the request payload
payload := new(endpointModifyPastePayload)
if err := json.Unmarshal(ctx.PostBody(), payload); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if payload.Content != nil && *payload.Content == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing paste content")
return
}
if payload.Content != nil && config.Current.LengthCap > 0 && len(*payload.Content) > config.Current.LengthCap {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("too large paste content")
return
}
// Modify the paste itself
paste := ctx.UserValue("_paste").(*shared.Paste)
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 := storage.Current.Save(paste); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
// endpointDeletePaste handles the 'DELETE /v2/pastes/{id}' endpoint
func endpointDeletePaste(ctx *fasthttp.RequestCtx) {
paste := ctx.UserValue("_paste").(*shared.Paste)
if err := storage.Current.Delete(paste.ID); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
}
type endpointReportPastePayload struct {
Reason string `json:"reason"`
}
func endpointReportPaste(ctx *fasthttp.RequestCtx) {
// Read, parse and validate the request payload
payload := new(endpointReportPastePayload)
if err := json.Unmarshal(ctx.PostBody(), payload); err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
if payload.Reason == "" {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
ctx.SetBodyString("missing report reason")
return
}
request := &report.ReportRequest{
Paste: ctx.UserValue("_paste").(*shared.Paste).ID,
Reason: payload.Reason,
Timestamp: time.Now().Unix(),
}
response, err := report.SendReport(request)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
jsonData, err := json.Marshal(response)
if err != nil {
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBodyString(err.Error())
return
}
ctx.SetBody(jsonData)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/lus/pasty/internal/static"
"github.com/lus/pasty/internal/storage"
v1 "github.com/lus/pasty/internal/web/controllers/v1"
v2 "github.com/lus/pasty/internal/web/controllers/v2"
"github.com/ulule/limiter/v3"
limitFasthttp "github.com/ulule/limiter/v3/drivers/middleware/fasthttp"
"github.com/ulule/limiter/v3/drivers/store/memory"
@@ -61,12 +62,25 @@ func Serve() error {
v1Route.GET("/info", func(ctx *fasthttp.RequestCtx) {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"deletionTokens": config.Current.DeletionTokens,
"deletionTokens": config.Current.ModificationTokens,
})
ctx.SetBody(jsonData)
})
v1.InitializePastesController(v1Route.Group("/pastes"), rateLimiterMiddleware)
}
v2Route := apiRoute.Group("/v2")
{
v2Route.GET("/info", func(ctx *fasthttp.RequestCtx) {
jsonData, _ := json.Marshal(map[string]interface{}{
"version": static.Version,
"modificationTokens": config.Current.ModificationTokens,
"reports": config.Current.Reports.Reports,
})
ctx.SetBody(jsonData)
})
v2.InitializePastesController(v2Route.Group("/pastes"), rateLimiterMiddleware)
}
}
// Route the hastebin documents route if hastebin support is enabled