initial core commit

This commit is contained in:
2025-12-28 13:24:16 +01:00
commit 0e8d108156
12 changed files with 519 additions and 0 deletions
+60
View File
@@ -0,0 +1,60 @@
package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// Health returns basic service health
func Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// Hello returns a simple greeting
func Hello(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "hello from lambda-iot core"})
}
// Login issues a JWT token for demo purposes
func Login(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.BindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid json"})
return
}
// demo credentials
if req.Username != "admin" || req.Password != "password" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid credentials"})
return
}
// create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": req.Username,
"exp": time.Now().Add(time.Hour).Unix(),
})
secret := "secret"
if s := c.GetHeader("X-JWT-SECRET"); s != "" {
secret = s
}
signed, err := token.SignedString([]byte(secret))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not sign token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": signed})
}
// Protected requires a valid JWT and returns the token claims
func Protected(c *gin.Context) {
if v, ok := c.Get("claims"); ok {
c.JSON(http.StatusOK, gin.H{"claims": v})
return
}
c.JSON(http.StatusUnauthorized, gin.H{"error": "no claims"})
}
+87
View File
@@ -0,0 +1,87 @@
package handler
import (
"encoding/json"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func TestHealth(t *testing.T) {
r := gin.New()
r.GET("/health", Health)
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 got %d", w.Code)
}
var body map[string]string
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatalf("decode: %v", err)
}
if body["status"] != "ok" {
t.Fatalf("unexpected body: %#v", body)
}
}
func TestHello(t *testing.T) {
r := gin.New()
r.GET("/hello", Hello)
req := httptest.NewRequest("GET", "/hello", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 got %d", w.Code)
}
var body map[string]string
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatalf("decode: %v", err)
}
if body["message"] == "" {
t.Fatalf("unexpected body: %#v", body)
}
}
func TestLoginAndProtected(t *testing.T) {
secret := "testsecret"
// create token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "tester",
"exp": time.Now().Add(time.Hour).Unix(),
})
signed, err := token.SignedString([]byte(secret))
if err != nil {
t.Fatalf("sign token: %v", err)
}
// test protected route
r := gin.New()
// use middleware inline for test
r.Use(func(c *gin.Context) {
c.Request.Header.Set("Authorization", "Bearer "+signed)
})
r.GET("/protected", func(c *gin.Context) {
c.Set("claims", jwt.MapClaims{"sub": "tester"})
Protected(c)
})
req := httptest.NewRequest("GET", "/protected", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("expected 200 got %d", w.Code)
}
var body map[string]interface{}
if err := json.NewDecoder(w.Body).Decode(&body); err != nil {
t.Fatalf("decode: %v", err)
}
if body["claims"] == nil {
t.Fatalf("expected claims in response")
}
}
+42
View File
@@ -0,0 +1,42 @@
package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
// AuthMiddleware validates a JWT token from the Authorization header
func AuthMiddleware(secret string) gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if auth == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing authorization header"})
return
}
parts := strings.SplitN(auth, " ", 2)
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid authorization header"})
return
}
tokenStr := parts[1]
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
// ensure signing method is HMAC
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrTokenUnverifiable
}
return []byte(secret), nil
})
if err != nil || !token.Valid {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
// expose token claims to handlers
if claims, ok := token.Claims.(jwt.MapClaims); ok {
c.Set("claims", claims)
}
c.Next()
}
}
+23
View File
@@ -0,0 +1,23 @@
package middleware
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
// GinLogger returns a middleware that logs requests using the standard logger
func GinLogger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
latency := time.Since(start)
status := c.Writer.Status()
if len(c.Errors) > 0 {
log.Printf("error: method=%s path=%s status=%d latency=%s errs=%s", c.Request.Method, c.Request.URL.Path, status, latency.String(), c.Errors.String())
return
}
log.Printf("method=%s path=%s status=%d latency=%s", c.Request.Method, c.Request.URL.Path, status, latency.String())
}
}