mirror of
https://github.com/nprasad2077/NBA_Go.git
synced 2026-09-22 05:55:13 +00:00
api key auth added
This commit is contained in:
@@ -1 +1,17 @@
|
|||||||
# NBA_Go
|
# NBA_Go
|
||||||
|
|
||||||
|
### First‑time bootstrap
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. build + run
|
||||||
|
docker-compose up --build -d
|
||||||
|
|
||||||
|
# 2. create API key (ADMIN_SECRET is loaded from .env)
|
||||||
|
curl -XPOST http://localhost:8080/admin/keys \
|
||||||
|
-H "X-Admin-Secret: $ADMIN_SECRET" \
|
||||||
|
-d '{"label":"local-test"}'
|
||||||
|
# → { "id":1, "apiKey":"ab12cd…" }
|
||||||
|
|
||||||
|
# 3. call a protected endpoint
|
||||||
|
curl http://localhost:8080/api/playeradvancedstats \
|
||||||
|
-H "X-API-Key: ab12cd…"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/nprasad2077/NBA_Go/utils/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
raw, _ := security.GenerateRawKey()
|
||||||
|
fmt.Println(raw)
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ func InitDB() *gorm.DB {
|
|||||||
// Auto migrate models
|
// Auto migrate models
|
||||||
db.AutoMigrate(&models.PlayerAdvancedStat{})
|
db.AutoMigrate(&models.PlayerAdvancedStat{})
|
||||||
db.AutoMigrate(&models.PlayerTotalStat{})
|
db.AutoMigrate(&models.PlayerTotalStat{})
|
||||||
|
|
||||||
|
db.AutoMigrate(&models.APIKey{})
|
||||||
|
|
||||||
metrics.DBOperationsTotal.WithLabelValues("migrate", "database").Inc()
|
metrics.DBOperationsTotal.WithLabelValues("migrate", "database").Inc()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package controllers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/nprasad2077/NBA_Go/models"
|
||||||
|
"github.com/nprasad2077/NBA_Go/utils/security"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// very small middleware: require header X‑Admin‑Secret == $ADMIN_SECRET
|
||||||
|
func adminGuard() fiber.Handler {
|
||||||
|
secret := os.Getenv("ADMIN_SECRET")
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if c.Get("X-Admin-Secret") != secret {
|
||||||
|
return c.SendStatus(http.StatusUnauthorized)
|
||||||
|
}
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterKeyAdminRoutes(app *fiber.App, db *gorm.DB) {
|
||||||
|
admin := app.Group("/admin/keys", adminGuard())
|
||||||
|
|
||||||
|
admin.Get("/", func(c *fiber.Ctx) error {
|
||||||
|
var keys []models.APIKey
|
||||||
|
db.Find(&keys)
|
||||||
|
return c.JSON(keys)
|
||||||
|
})
|
||||||
|
|
||||||
|
admin.Post("/", func(c *fiber.Ctx) error {
|
||||||
|
var body struct{ Label string }
|
||||||
|
if err := c.BodyParser(&body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := security.GenerateRawKey()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := models.APIKey{
|
||||||
|
Hash: security.HashKey(raw),
|
||||||
|
Label: body.Label,
|
||||||
|
}
|
||||||
|
db.Create(&key)
|
||||||
|
|
||||||
|
// return ONLY the raw key once
|
||||||
|
return c.JSON(fiber.Map{
|
||||||
|
"apiKey": raw,
|
||||||
|
"id": key.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
admin.Post("/:id/revoke", func(c *fiber.Ctx) error {
|
||||||
|
id := c.Params("id")
|
||||||
|
db.Model(&models.APIKey{}).Where("id = ?", id).Update("revoked", true)
|
||||||
|
return c.JSON(fiber.Map{"revoked": id})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ func FetchPlayerAdvancedStats(db *gorm.DB) fiber.Handler {
|
|||||||
|
|
||||||
|
|
||||||
// GetAllPlayerStats godoc
|
// GetAllPlayerStats godoc
|
||||||
|
// @Security ApiKeyAuth
|
||||||
// @Summary Get player stats
|
// @Summary Get player stats
|
||||||
// @Description Returns filtered and paginated player stats
|
// @Description Returns filtered and paginated player stats
|
||||||
// @Tags PlayerStats
|
// @Tags PlayerStats
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ func FetchPlayerTotalStats(db *gorm.DB) fiber.Handler {
|
|||||||
|
|
||||||
|
|
||||||
// GetPlayerTotalStats godoc
|
// GetPlayerTotalStats godoc
|
||||||
|
// @Security ApiKeyAuth
|
||||||
// @Summary Get player total stats
|
// @Summary Get player total stats
|
||||||
// @Description Filter and paginate player totals
|
// @Description Filter and paginate player totals
|
||||||
// @Tags PlayerTotals
|
// @Tags PlayerTotals
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- api.network
|
- api.network
|
||||||
api1:
|
api1:
|
||||||
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- './data:/app/data'
|
- './data:/app/data'
|
||||||
build: .
|
build: .
|
||||||
@@ -22,6 +23,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- api.network
|
- api.network
|
||||||
api2:
|
api2:
|
||||||
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- './data:/app/data'
|
- './data:/app/data'
|
||||||
build: .
|
build: .
|
||||||
@@ -34,6 +36,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- api.network
|
- api.network
|
||||||
api3:
|
api3:
|
||||||
|
env_file: .env
|
||||||
volumes:
|
volumes:
|
||||||
- './data:/app/data'
|
- './data:/app/data'
|
||||||
build: .
|
build: .
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
//go:build docs
|
||||||
|
// +build docs
|
||||||
|
|
||||||
|
// Package docs Only contains Swagger annotations that are **global**.
|
||||||
|
// Run `swag init --parseDependency --parseInternal` after editing.
|
||||||
|
package docs
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// General API meta (kept here so CI can inject version/build).
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
|
||||||
|
// @title NBA_Go API
|
||||||
|
// @version 1.0
|
||||||
|
// @description Stats service with API‑key auth
|
||||||
|
// @BasePath /
|
||||||
|
// @schemes http https
|
||||||
|
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
// 🔐 SECURITY – this block is what you asked for
|
||||||
|
// ------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// @securityDefinitions.apikey ApiKeyAuth
|
||||||
|
// @in header
|
||||||
|
// @name X-API-Key
|
||||||
|
//
|
||||||
|
// (every protected endpoint still needs `@Security ApiKeyAuth`)
|
||||||
|
// ------------------------------------------------------------
|
||||||
@@ -17,6 +17,11 @@ const docTemplate = `{
|
|||||||
"paths": {
|
"paths": {
|
||||||
"/api/playeradvancedstats": {
|
"/api/playeradvancedstats": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Returns filtered and paginated player stats",
|
"description": "Returns filtered and paginated player stats",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -87,6 +92,11 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
"/api/playertotals": {
|
"/api/playertotals": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Filter and paginate player totals",
|
"description": "Filter and paginate player totals",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"/api/playeradvancedstats": {
|
"/api/playeradvancedstats": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Returns filtered and paginated player stats",
|
"description": "Returns filtered and paginated player stats",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -76,6 +81,11 @@
|
|||||||
},
|
},
|
||||||
"/api/playertotals": {
|
"/api/playertotals": {
|
||||||
"get": {
|
"get": {
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKeyAuth": []
|
||||||
|
}
|
||||||
|
],
|
||||||
"description": "Filter and paginate player totals",
|
"description": "Filter and paginate player totals",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
type: object
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
summary: Get player stats
|
summary: Get player stats
|
||||||
tags:
|
tags:
|
||||||
- PlayerStats
|
- PlayerStats
|
||||||
@@ -98,6 +100,8 @@ paths:
|
|||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
security:
|
||||||
|
- ApiKeyAuth: []
|
||||||
summary: Get player total stats
|
summary: Get player total stats
|
||||||
tags:
|
tags:
|
||||||
- PlayerTotals
|
- PlayerTotals
|
||||||
|
|||||||
@@ -1,36 +1,65 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"context"
|
||||||
"time"
|
"errors"
|
||||||
|
"log"
|
||||||
"github.com/gofiber/fiber/v2"
|
"os"
|
||||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
"os/signal"
|
||||||
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
"syscall"
|
||||||
"github.com/nprasad2077/NBA_Go/config"
|
"time"
|
||||||
"github.com/nprasad2077/NBA_Go/routes"
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/adaptor"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
|
"github.com/nprasad2077/NBA_Go/config"
|
||||||
"github.com/nprasad2077/NBA_Go/services"
|
"github.com/nprasad2077/NBA_Go/services"
|
||||||
"github.com/nprasad2077/NBA_Go/utils/middleware"
|
"github.com/nprasad2077/NBA_Go/controllers"
|
||||||
_ "github.com/nprasad2077/NBA_Go/docs"
|
"github.com/nprasad2077/NBA_Go/routes"
|
||||||
fiberswagger "github.com/swaggo/fiber-swagger"
|
"github.com/nprasad2077/NBA_Go/utils/middleware"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
_ "github.com/nprasad2077/NBA_Go/docs"
|
||||||
|
fiberswagger "github.com/swaggo/fiber-swagger"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := fiber.New()
|
// graceful shutdown context
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
// Add standard logger middleware
|
app := fiber.New(fiber.Config{
|
||||||
app.Use(logger.New())
|
ReadTimeout: 15 * time.Second,
|
||||||
|
WriteTimeout: 15 * time.Second,
|
||||||
// Add metrics middleware
|
ErrorHandler: func(c *fiber.Ctx, err error) error {
|
||||||
app.Use(middleware.MetricsMiddleware())
|
code := fiber.StatusInternalServerError
|
||||||
|
if e, ok := err.(*fiber.Error); ok {
|
||||||
|
code = e.Code
|
||||||
|
}
|
||||||
|
return c.Status(code).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Initialize database
|
// middlewares
|
||||||
db := config.InitDB()
|
app.Use(logger.New())
|
||||||
|
app.Use(middleware.MetricsMiddleware())
|
||||||
|
|
||||||
// Original data fetching code, no environment variable check
|
// DB
|
||||||
go func() {
|
db := config.InitDB()
|
||||||
for season := 1993; season <= 2025; season++ {
|
|
||||||
|
/* ---------- PUBLIC ROUTES (no API key) ---------- */
|
||||||
|
app.Get("/metrics", adaptor.HTTPHandler(promhttp.Handler()))
|
||||||
|
app.Get("/swagger/*", fiberswagger.WrapHandler)
|
||||||
|
controllers.RegisterKeyAdminRoutes(app, db) // guarded only by X‑Admin‑Secret
|
||||||
|
|
||||||
|
/* ---------- PROTECTED ROUTES ---------- */
|
||||||
|
app.Use(middleware.APIKeyAuth(db))
|
||||||
|
routes.RegisterPlayerAdvancedRoutes(app, db)
|
||||||
|
routes.RegisterPlayerTotalRoutes(app, db)
|
||||||
|
|
||||||
|
/* ---------- Optional import job ---------- */
|
||||||
|
go func() {
|
||||||
|
for season := 2023; season <= 2025; season++ {
|
||||||
if err := services.FetchAndStorePlayerAdvancedStats(db, season); err != nil {
|
if err := services.FetchAndStorePlayerAdvancedStats(db, season); err != nil {
|
||||||
log.Printf("Fetch failed for player advanced season %d: %v\n", season, err)
|
log.Printf("Fetch failed for player advanced season %d: %v\n", season, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -42,7 +71,7 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
for season := 1993; season <= 2025; season++ {
|
for season := 2023; season <= 2025; season++ {
|
||||||
if err := services.FetchAndStorePlayerTotalStats(db, season); err != nil {
|
if err := services.FetchAndStorePlayerTotalStats(db, season); err != nil {
|
||||||
log.Printf("Fetch failed for player totals season %d: %v\n", season, err)
|
log.Printf("Fetch failed for player totals season %d: %v\n", season, err)
|
||||||
} else {
|
} else {
|
||||||
@@ -53,15 +82,16 @@ func main() {
|
|||||||
log.Printf("player totals Import Success")
|
log.Printf("player totals Import Success")
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Add Prometheus metrics endpoint
|
/* ---------- START & SHUTDOWN ---------- */
|
||||||
app.Get("/metrics", adaptor.HTTPHandler(promhttp.Handler()))
|
go func() {
|
||||||
|
if err := app.Listen(":5000"); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Fatalf("listen: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Register routes
|
<-ctx.Done() // wait for SIGTERM/CTRL‑C
|
||||||
routes.RegisterPlayerAdvancedRoutes(app, db)
|
stop() // stop receiving more signals
|
||||||
routes.RegisterPlayerTotalRoutes(app, db)
|
log.Println("shutting down…")
|
||||||
|
_ = app.Shutdown() // stop accepting new conns
|
||||||
// Swagger endpoint
|
log.Println("bye")
|
||||||
app.Get("/swagger/*", fiberswagger.WrapHandler)
|
}
|
||||||
|
|
||||||
app.Listen(":5000")
|
|
||||||
}
|
|
||||||
|
|||||||
+48
-21
@@ -1,52 +1,79 @@
|
|||||||
|
// main_test.go
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/nprasad2077/NBA_Go/models"
|
||||||
"github.com/nprasad2077/NBA_Go/routes"
|
"github.com/nprasad2077/NBA_Go/routes"
|
||||||
"github.com/nprasad2077/NBA_Go/config"
|
"github.com/nprasad2077/NBA_Go/utils/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func setupTestApp() *fiber.App {
|
// -----------------------------------------------------------------------------
|
||||||
|
// test bootstrap
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
func setupTestApp() (*fiber.App, string) {
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
db := config.InitDB() // uses SQLite file; can be mocked or in-memory for advanced testing
|
|
||||||
|
// in‑memory SQLite so tests don’t touch real file
|
||||||
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_ = db.AutoMigrate(&models.PlayerAdvancedStat{}, &models.APIKey{})
|
||||||
|
|
||||||
|
// seed one API key we can use in the requests
|
||||||
|
rawKey := "testkey123"
|
||||||
|
db.Create(&models.APIKey{Hash: security.HashKey(rawKey)})
|
||||||
|
|
||||||
|
// register only the routes we need
|
||||||
routes.RegisterPlayerAdvancedRoutes(app, db)
|
routes.RegisterPlayerAdvancedRoutes(app, db)
|
||||||
return app
|
|
||||||
|
return app, rawKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// actual test
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
func TestGetPlayerAdvancedStats(t *testing.T) {
|
func TestGetPlayerAdvancedStats(t *testing.T) {
|
||||||
app := setupTestApp()
|
app, key := setupTestApp()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
description string
|
name string
|
||||||
route string
|
route string
|
||||||
expectedCode int
|
wantCode int
|
||||||
expectInBody string
|
wantSubstring string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
description: "valid route",
|
name: "valid route",
|
||||||
route: "/api/playeradvancedstats",
|
route: "/api/playeradvancedstats/",
|
||||||
expectedCode: 200,
|
wantCode: 200,
|
||||||
expectInBody: `"data"`, // assuming a JSON object with "data"
|
wantSubstring: `"data"`, // expect JSON payload has "data"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "invalid route",
|
name: "invalid route",
|
||||||
route: "/api/invalid",
|
route: "/api/invalid",
|
||||||
expectedCode: 404,
|
wantCode: 404,
|
||||||
expectInBody: "Cannot GET /api/invalid",
|
wantSubstring: "Cannot GET /api/invalid",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
req, _ := http.NewRequest("GET", tc.route, nil)
|
req, _ := http.NewRequest(http.MethodGet, tc.route, nil)
|
||||||
|
req.Header.Set("X-API-Key", key)
|
||||||
|
|
||||||
resp, err := app.Test(req, -1)
|
resp, err := app.Test(req, -1)
|
||||||
assert.Nil(t, err, tc.description)
|
assert.NoError(t, err, tc.name)
|
||||||
assert.Equal(t, tc.expectedCode, resp.StatusCode, tc.description)
|
assert.Equal(t, tc.wantCode, resp.StatusCode, tc.name)
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
assert.Contains(t, string(body), tc.expectInBody, tc.description)
|
assert.Contains(t, string(body), tc.wantSubstring, tc.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIKey is stored **hashed** (SHA‑256) and may be revoked at any time.
|
||||||
|
type APIKey struct {
|
||||||
|
ID uint `gorm:"primaryKey"`
|
||||||
|
Hash []byte `gorm:"uniqueIndex"`
|
||||||
|
Label string // e.g. “mobile‑app”, “data‑partner‑X”
|
||||||
|
Revoked bool
|
||||||
|
CreatedAt time.Time
|
||||||
|
RevokedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
}
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetJSON(url string) ([]byte, error) {
|
func GetJSON(url string) ([]byte, error) {
|
||||||
client := &http.Client{Timeout: 10 * time.Second}
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
resp, err := client.Get(url)
|
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
|
client := &http.Client{}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -20,7 +25,7 @@ func GetJSON(url string) ([]byte, error) {
|
|||||||
return nil, errors.New("received non-200 response: " + resp.Status)
|
return nil, errors.New("received non-200 response: " + resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/subtle"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/keyauth"
|
||||||
|
"github.com/nprasad2077/NBA_Go/models"
|
||||||
|
"github.com/nprasad2077/NBA_Go/utils/security"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// APIKeyAuth returns a Fiber Handler that validates X‑API‑Key
|
||||||
|
func APIKeyAuth(db *gorm.DB) fiber.Handler {
|
||||||
|
return keyauth.New(keyauth.Config{
|
||||||
|
KeyLookup: "header:X-API-Key",
|
||||||
|
Validator: func(c *fiber.Ctx, rawKey string) (bool, error) {
|
||||||
|
var rec models.APIKey
|
||||||
|
hash := security.HashKey(rawKey)
|
||||||
|
|
||||||
|
err := db.
|
||||||
|
Where("hash = ? AND revoked = FALSE", hash).
|
||||||
|
First(&rec).Error
|
||||||
|
if err != nil {
|
||||||
|
return false, keyauth.ErrMissingOrMalformedAPIKey
|
||||||
|
}
|
||||||
|
if subtle.ConstantTimeCompare(rec.Hash, hash) != 1 {
|
||||||
|
return false, keyauth.ErrMissingOrMalformedAPIKey
|
||||||
|
}
|
||||||
|
// put the key ID on the context for rate‑limiting or logging later
|
||||||
|
c.Locals("apiKeyID", rec.ID)
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package security
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateRawKey returns a 32‑byte cryptographically‑random string
|
||||||
|
func GenerateRawKey() (string, error) {
|
||||||
|
b := make([]byte, 32)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashKey returns the SHA‑256 hash suitable for storage.
|
||||||
|
func HashKey(raw string) []byte {
|
||||||
|
h := sha256.Sum256([]byte(raw))
|
||||||
|
return h[:]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user