mirror of
https://github.com/DevVoxel/vectordns-server.git
synced 2026-02-27 01:40:12 +00:00
Initial commit: Go DNS microservice boilerplate
This commit is contained in:
4
.env.example
Normal file
4
.env.example
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
PORT=8080
|
||||||
|
API_KEY=
|
||||||
|
CORS_ORIGINS=http://localhost:3000
|
||||||
|
LOG_FORMAT=text
|
||||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Binary
|
||||||
|
/server
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
12
Dockerfile
Normal file
12
Dockerfile
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
FROM golang:1.23-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY . .
|
||||||
|
RUN CGO_ENABLED=0 go build -o server ./cmd/server
|
||||||
|
|
||||||
|
FROM alpine:3.20
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
COPY --from=build /app/server /usr/local/bin/server
|
||||||
|
EXPOSE 8080
|
||||||
|
CMD ["server"]
|
||||||
61
README.md
Normal file
61
README.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# VectorDNS Go Server
|
||||||
|
|
||||||
|
A lightweight Go DNS lookup microservice powered by Chi and miekg/dns.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy env config
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Run the server
|
||||||
|
go run ./cmd/server
|
||||||
|
```
|
||||||
|
|
||||||
|
The server starts on `http://localhost:8080`.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/healthz` | Health check |
|
||||||
|
| POST | `/api/v1/dns/lookup` | DNS record lookup |
|
||||||
|
|
||||||
|
### DNS Lookup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8080/api/v1/dns/lookup \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"domain": "example.com", "types": ["A", "MX"]}'
|
||||||
|
```
|
||||||
|
|
||||||
|
If `types` is omitted, all 9 record types are queried (A, AAAA, MX, TXT, NS, CNAME, SOA, CAA, SRV).
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
All config is via environment variables (or `.env` file):
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|----------|---------|-------------|
|
||||||
|
| `PORT` | `8080` | Server port |
|
||||||
|
| `API_KEY` | (empty) | API key for auth (disabled if empty) |
|
||||||
|
| `CORS_ORIGINS` | `http://localhost:3000` | Comma-separated allowed origins |
|
||||||
|
| `LOG_FORMAT` | `text` | `text` or `json` |
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t vectordns-server .
|
||||||
|
docker run -p 8080:8080 --env-file .env vectordns-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Router**: [Chi v5](https://github.com/go-chi/chi)
|
||||||
|
- **DNS**: [miekg/dns](https://github.com/miekg/dns)
|
||||||
|
- **Logging**: stdlib `log/slog`
|
||||||
|
- **Rate Limiting**: [go-chi/httprate](https://github.com/go-chi/httprate)
|
||||||
|
- **CORS**: [go-chi/cors](https://github.com/go-chi/cors)
|
||||||
72
cmd/server/main.go
Normal file
72
cmd/server/main.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi/v5"
|
||||||
|
chimw "github.com/go-chi/chi/v5/middleware"
|
||||||
|
"github.com/go-chi/cors"
|
||||||
|
"github.com/go-chi/httprate"
|
||||||
|
|
||||||
|
"github.com/vectordns/server/internal/config"
|
||||||
|
"github.com/vectordns/server/internal/handler"
|
||||||
|
"github.com/vectordns/server/internal/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
|
||||||
|
if cfg.LogFormat == "json" {
|
||||||
|
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, nil)))
|
||||||
|
}
|
||||||
|
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
// Middleware stack
|
||||||
|
r.Use(chimw.Recoverer)
|
||||||
|
r.Use(chimw.RequestID)
|
||||||
|
r.Use(chimw.RealIP)
|
||||||
|
r.Use(slogMiddleware)
|
||||||
|
r.Use(cors.Handler(cors.Options{
|
||||||
|
AllowedOrigins: cfg.CORSOrigins,
|
||||||
|
AllowedMethods: []string{"GET", "POST", "OPTIONS"},
|
||||||
|
AllowedHeaders: []string{"Content-Type", "Authorization", "X-API-Key"},
|
||||||
|
MaxAge: 86400,
|
||||||
|
}))
|
||||||
|
r.Use(httprate.LimitByIP(60, time.Minute))
|
||||||
|
|
||||||
|
// Public routes
|
||||||
|
r.Get("/healthz", handler.Health)
|
||||||
|
|
||||||
|
// API routes (with API key auth)
|
||||||
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
|
r.Use(middleware.APIKey(cfg.APIKey))
|
||||||
|
r.Post("/dns/lookup", handler.DNSLookup)
|
||||||
|
})
|
||||||
|
|
||||||
|
addr := fmt.Sprintf(":%s", cfg.Port)
|
||||||
|
slog.Info("starting server", "addr", addr)
|
||||||
|
if err := http.ListenAndServe(addr, r); err != nil {
|
||||||
|
slog.Error("server failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func slogMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
start := time.Now()
|
||||||
|
ww := chimw.NewWrapResponseWriter(w, r.ProtoMajor)
|
||||||
|
next.ServeHTTP(ww, r)
|
||||||
|
slog.Info("request",
|
||||||
|
"method", r.Method,
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"status", ww.Status(),
|
||||||
|
"duration_ms", time.Since(start).Milliseconds(),
|
||||||
|
"request_id", chimw.GetReqID(r.Context()),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
19
go.mod
Normal file
19
go.mod
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module github.com/vectordns/server
|
||||||
|
|
||||||
|
go 1.23.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1
|
||||||
|
github.com/go-chi/cors v1.2.1
|
||||||
|
github.com/go-chi/httprate v0.14.1
|
||||||
|
github.com/miekg/dns v1.1.62
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
golang.org/x/mod v0.18.0 // indirect
|
||||||
|
golang.org/x/net v0.27.0 // indirect
|
||||||
|
golang.org/x/sync v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
|
golang.org/x/tools v0.22.0 // indirect
|
||||||
|
)
|
||||||
20
go.sum
Normal file
20
go.sum
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
|
||||||
|
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||||
|
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||||
|
github.com/go-chi/httprate v0.14.1 h1:EKZHYEZ58Cg6hWcYzoZILsv7ppb46Wt4uQ738IRtpZs=
|
||||||
|
github.com/go-chi/httprate v0.14.1/go.mod h1:TUepLXaz/pCjmCtf/obgOQJ2Sz6rC8fSf5cAt5cnTt0=
|
||||||
|
github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ=
|
||||||
|
github.com/miekg/dns v1.1.62/go.mod h1:mvDlcItzm+br7MToIKqkglaGhlFMHJ9DTNNWONWXbNQ=
|
||||||
|
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
|
||||||
|
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
|
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||||
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
|
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
|
||||||
|
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
|
||||||
29
internal/config/config.go
Normal file
29
internal/config/config.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Port string
|
||||||
|
APIKey string
|
||||||
|
CORSOrigins []string
|
||||||
|
LogFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() *Config {
|
||||||
|
return &Config{
|
||||||
|
Port: getEnv("PORT", "8080"),
|
||||||
|
APIKey: getEnv("API_KEY", ""),
|
||||||
|
CORSOrigins: strings.Split(getEnv("CORS_ORIGINS", "http://localhost:3000"), ","),
|
||||||
|
LogFormat: getEnv("LOG_FORMAT", "text"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
112
internal/dns/resolver.go
Normal file
112
internal/dns/resolver.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
mdns "github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultTypes = []string{"A", "AAAA", "MX", "TXT", "NS", "CNAME", "SOA", "CAA", "SRV"}
|
||||||
|
|
||||||
|
const defaultResolver = "8.8.8.8:53"
|
||||||
|
|
||||||
|
var typeMap = map[string]uint16{
|
||||||
|
"A": mdns.TypeA,
|
||||||
|
"AAAA": mdns.TypeAAAA,
|
||||||
|
"MX": mdns.TypeMX,
|
||||||
|
"TXT": mdns.TypeTXT,
|
||||||
|
"NS": mdns.TypeNS,
|
||||||
|
"CNAME": mdns.TypeCNAME,
|
||||||
|
"SOA": mdns.TypeSOA,
|
||||||
|
"CAA": mdns.TypeCAA,
|
||||||
|
"SRV": mdns.TypeSRV,
|
||||||
|
}
|
||||||
|
|
||||||
|
func Lookup(req LookupRequest) (*LookupResponse, error) {
|
||||||
|
domain := req.Domain
|
||||||
|
if !strings.HasSuffix(domain, ".") {
|
||||||
|
domain += "."
|
||||||
|
}
|
||||||
|
|
||||||
|
types := req.Types
|
||||||
|
if len(types) == 0 {
|
||||||
|
types = defaultTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
records := make(map[string][]Record)
|
||||||
|
hasDNSSEC := false
|
||||||
|
|
||||||
|
c := new(mdns.Client)
|
||||||
|
c.Net = "udp"
|
||||||
|
|
||||||
|
for _, t := range types {
|
||||||
|
qtype, ok := typeMap[strings.ToUpper(t)]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
m := new(mdns.Msg)
|
||||||
|
m.SetQuestion(domain, qtype)
|
||||||
|
m.SetEdns0(4096, true)
|
||||||
|
|
||||||
|
r, _, err := c.Exchange(m, defaultResolver)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.AuthenticatedData {
|
||||||
|
hasDNSSEC = true
|
||||||
|
}
|
||||||
|
|
||||||
|
recs := parseRecords(r.Answer, strings.ToUpper(t))
|
||||||
|
if len(recs) > 0 {
|
||||||
|
records[strings.ToUpper(t)] = recs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &LookupResponse{
|
||||||
|
Domain: req.Domain,
|
||||||
|
Records: records,
|
||||||
|
Resolver: "8.8.8.8",
|
||||||
|
QueryTimeMs: time.Since(start).Milliseconds(),
|
||||||
|
DNSSEC: hasDNSSEC,
|
||||||
|
Timestamp: time.Now().UTC(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseRecords(answers []mdns.RR, qtype string) []Record {
|
||||||
|
var records []Record
|
||||||
|
for _, ans := range answers {
|
||||||
|
rec := Record{TTL: ans.Header().Ttl}
|
||||||
|
switch rr := ans.(type) {
|
||||||
|
case *mdns.A:
|
||||||
|
rec.Value = rr.A.String()
|
||||||
|
case *mdns.AAAA:
|
||||||
|
rec.Value = rr.AAAA.String()
|
||||||
|
case *mdns.MX:
|
||||||
|
rec.Value = strings.TrimSuffix(rr.Mx, ".")
|
||||||
|
rec.Priority = rr.Preference
|
||||||
|
case *mdns.TXT:
|
||||||
|
rec.Value = strings.Join(rr.Txt, " ")
|
||||||
|
case *mdns.NS:
|
||||||
|
rec.Value = strings.TrimSuffix(rr.Ns, ".")
|
||||||
|
case *mdns.CNAME:
|
||||||
|
rec.Value = strings.TrimSuffix(rr.Target, ".")
|
||||||
|
case *mdns.SOA:
|
||||||
|
rec.Value = fmt.Sprintf("%s %s %d %d %d %d %d",
|
||||||
|
rr.Ns, rr.Mbox, rr.Serial, rr.Refresh, rr.Retry, rr.Expire, rr.Minttl)
|
||||||
|
case *mdns.CAA:
|
||||||
|
rec.Value = fmt.Sprintf("%d %s \"%s\"", rr.Flag, rr.Tag, rr.Value)
|
||||||
|
case *mdns.SRV:
|
||||||
|
rec.Value = fmt.Sprintf("%d %d %d %s", rr.Priority, rr.Weight, rr.Port, strings.TrimSuffix(rr.Target, "."))
|
||||||
|
rec.Priority = rr.Priority
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
records = append(records, rec)
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
23
internal/dns/types.go
Normal file
23
internal/dns/types.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package dns
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type LookupRequest struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Types []string `json:"types,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LookupResponse struct {
|
||||||
|
Domain string `json:"domain"`
|
||||||
|
Records map[string][]Record `json:"records"`
|
||||||
|
Resolver string `json:"resolver"`
|
||||||
|
QueryTimeMs int64 `json:"query_time_ms"`
|
||||||
|
DNSSEC bool `json:"dnssec"`
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
TTL uint32 `json:"ttl"`
|
||||||
|
Priority uint16 `json:"priority,omitempty"`
|
||||||
|
}
|
||||||
41
internal/handler/dns.go
Normal file
41
internal/handler/dns.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/vectordns/server/internal/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DNSLookup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req dns.LookupRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "invalid request body", "INVALID_BODY")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.Domain == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "domain is required", "MISSING_DOMAIN")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := dns.Lookup(req)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("dns lookup failed", "domain", req.Domain, "error", err)
|
||||||
|
writeError(w, http.StatusInternalServerError, "dns lookup failed", "LOOKUP_FAILED")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, status int, msg, code string) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": msg,
|
||||||
|
"code": code,
|
||||||
|
})
|
||||||
|
}
|
||||||
14
internal/handler/health.go
Normal file
14
internal/handler/health.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Health(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"status": "ok",
|
||||||
|
"version": "0.1.0",
|
||||||
|
})
|
||||||
|
}
|
||||||
30
internal/middleware/apikey.go
Normal file
30
internal/middleware/apikey.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func APIKey(key string) func(http.Handler) http.Handler {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if key == "" {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
provided := r.Header.Get("X-API-Key")
|
||||||
|
if provided != key {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
json.NewEncoder(w).Encode(map[string]string{
|
||||||
|
"error": "invalid or missing API key",
|
||||||
|
"code": "UNAUTHORIZED",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user