From 60992c1e44c024f26090e2a426dc1802b220b94d Mon Sep 17 00:00:00 2001 From: F04C Date: Thu, 4 Dec 2025 10:55:25 +0800 Subject: [PATCH] init --- constants.go | 5 + db/db.go | 48 ++++++++ docs/docs.go | 46 ++++++++ docs/swagger.json | 21 ++++ docs/swagger.yaml | 17 +++ go.mod | 44 +++++++ go.sum | 129 +++++++++++++++++++++ handlers/authorize.go | 26 +++++ helper/constants.go | 11 ++ helper/error_logging.go | 89 +++++++++++++++ helper/response.go | 28 +++++ main.go | 246 ++++++++++++++++++++++++++++++++++++++++ middleware/constants.go | 6 + middleware/jwt.go | 218 +++++++++++++++++++++++++++++++++++ models/authorize.go | 21 ++++ models/http.go | 26 +++++ redisclient/redis.go | 54 +++++++++ routes/routes.go | 17 +++ services/authorize.go | 6 + 19 files changed, 1058 insertions(+) create mode 100644 constants.go create mode 100644 db/db.go create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 handlers/authorize.go create mode 100644 helper/constants.go create mode 100644 helper/error_logging.go create mode 100644 helper/response.go create mode 100644 main.go create mode 100644 middleware/constants.go create mode 100644 middleware/jwt.go create mode 100644 models/authorize.go create mode 100644 models/http.go create mode 100644 redisclient/redis.go create mode 100644 routes/routes.go create mode 100644 services/authorize.go diff --git a/constants.go b/constants.go new file mode 100644 index 0000000..5f0e383 --- /dev/null +++ b/constants.go @@ -0,0 +1,5 @@ +package main + +const ( + metricsPath = "/metrics" +) diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..39435e1 --- /dev/null +++ b/db/db.go @@ -0,0 +1,48 @@ +package db + +import ( + "database/sql" + "fmt" + "log" + "os" + "time" + + _ "github.com/go-sql-driver/mysql" +) + +// DB is the global database connection pool +var DB *sql.DB + +func InitDB() (*sql.DB, error) { + // Get connection details from environment variables (loaded in main) + connStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true", + os.Getenv("DB_USER"), + os.Getenv("DB_PASSWORD"), + os.Getenv("DB_HOST"), + os.Getenv("DB_PORT"), + os.Getenv("DB_NAME"), + ) + + // Open the database connection + var err error + DB, err = sql.Open("mysql", connStr) + if err != nil { + return nil, fmt.Errorf("error opening database: %v", err) + } + // Set connection pool parameters + DB.SetMaxOpenConns(100) // Maximum number of open connections to the database + DB.SetMaxIdleConns(100) // Maximum number of connections in the idle connection pool + DB.SetConnMaxLifetime(5 * time.Minute) // Maximum amount of time a connection may be reused + + // Check if the database connection is working + if err := DB.Ping(); err != nil { + log.Printf("Database connection lost: %v. Reconnecting...", err) + DB, err = InitDB() + if err != nil { + log.Printf("Failed to reconnect to database: %v", err) + } + } + + log.Print("Database connected successfully!") + return DB, nil +} diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..a14c9e6 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,46 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": { + "name": "Darrel Israel", + "email": "d.israel.psa@gmail.com" + }, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": {}, + "securityDefinitions": { + "BearerToken": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "/", + Schemes: []string{}, + Title: "UESS Authentication Microservice", + Description: "This is the API for Authentication Microservice for UESS. It doesn't support OAS 3.0 and is only for documentation purposes. The library used doesn't support @server annotation.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..c830c93 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,21 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is the API for Authentication Microservice for UESS. It doesn't support OAS 3.0 and is only for documentation purposes. The library used doesn't support @server annotation.", + "title": "UESS Authentication Microservice", + "contact": { + "name": "Darrel Israel", + "email": "d.israel.psa@gmail.com" + }, + "version": "1.0" + }, + "basePath": "/", + "paths": {}, + "securityDefinitions": { + "BearerToken": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..31db000 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,17 @@ +basePath: / +info: + contact: + email: d.israel.psa@gmail.com + name: Darrel Israel + description: This is the API for Authentication Microservice for UESS. It doesn't + support OAS 3.0 and is only for documentation purposes. The library used doesn't + support @server annotation. + title: UESS Authentication Microservice + version: "1.0" +paths: {} +securityDefinitions: + BearerToken: + in: header + name: Authorization + type: apiKey +swagger: "2.0" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3efcc55 --- /dev/null +++ b/go.mod @@ -0,0 +1,44 @@ +module authorization + +go 1.25.1 + +require ( + github.com/getsentry/sentry-go v0.39.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/golang-jwt/jwt/v5 v5.3.0 + github.com/gorilla/mux v1.8.1 + github.com/joho/godotenv v1.5.1 + github.com/prometheus/client_golang v1.23.2 + github.com/redis/go-redis/v9 v9.17.0 + github.com/rs/cors v1.11.1 + github.com/swaggo/http-swagger v1.3.4 + github.com/swaggo/swag v1.16.6 +) + +require ( + filippo.io/edwards25519 v1.1.0 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/go-openapi/jsonpointer v0.19.5 // indirect + github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/spec v0.20.6 // indirect + github.com/go-openapi/swag v0.19.15 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.6 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.28.0 // indirect + golang.org/x/tools v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..aec8593 --- /dev/null +++ b/go.sum @@ -0,0 +1,129 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/getsentry/sentry-go v0.39.0 h1:uhnexj8PNCyCve37GSqxXOeXHh4cJNLNNB4w70Jtgo0= +github.com/getsentry/sentry-go v0.39.0/go.mod h1:eRXCoh3uvmjQLY6qu63BjUZnaBu5L5WhMV1RwYO8W5s= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= +github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/spec v0.20.6 h1:ich1RQ3WDbfoeTqTAb+5EIxNmpKVJZWBNah9RAT0jIQ= +github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= +github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/v9 v9.17.0 h1:K6E+ZlYN95KSMmZeEQPbU/c++wfmEvfFB17yEAq/VhM= +github.com/redis/go-redis/v9 v9.17.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= +github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= +github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww= +github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/handlers/authorize.go b/handlers/authorize.go new file mode 100644 index 0000000..0b9c53c --- /dev/null +++ b/handlers/authorize.go @@ -0,0 +1,26 @@ +package handlers + +import ( + "authorization/helper" + "authorization/models" + "authorization/services" + "encoding/json" + "net/http" +) + +func AuthorizeHandler(w http.ResponseWriter, r *http.Request) { + + var request models.AuthorizationRequest + + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + helper.RespondWithError(w, http.StatusBadRequest, "Invalid request payload") + return + } + + allowed := services.Authorize() + if !allowed { + helper.RespondWithError(w, http.StatusForbidden, "Access denied") + return + } +} diff --git a/helper/constants.go b/helper/constants.go new file mode 100644 index 0000000..4b7748d --- /dev/null +++ b/helper/constants.go @@ -0,0 +1,11 @@ +package helper + +const ( + ContentTypeHeader = "Content-Type" + ApplicationJSON = "application/json" + ErrorLabel = "error" + MessageLabel = "message" + ErrorEncodingResponse = "Error encoding response" + ErrorFailedtoLogLoginEvent = "Failed to log login event" + WarningLabel = "WARNING:" +) diff --git a/helper/error_logging.go b/helper/error_logging.go new file mode 100644 index 0000000..c6bc698 --- /dev/null +++ b/helper/error_logging.go @@ -0,0 +1,89 @@ +package helper + +import ( + "log" + "os" + + "github.com/getsentry/sentry-go" +) + +// LogInfo logs an info message to both the local log and Sentry based on the environment. +func LogInfo(message string) { + goEnv := os.Getenv("GO_ENV") + + if goEnv == "" { + log.Fatal("GO_ENV is not set in error_logging LogInfo. Please set the GO_ENV environment variable.") + } + + if goEnv == "development" || goEnv == "debug" { + log.Println("INFO:", message) + } + if goEnv == "production" || goEnv == "canary" { + log.Println("INFO:", message) + } +} + +// LogWarn logs a warning message to both the local log and Sentry based on the environment. +func LogWarn(message string) { + goEnv := os.Getenv("GO_ENV") + + if goEnv == "" { + log.Fatal("GO_ENV is not set in error_logging LogWarn. Please set the GO_ENV environment variable.") + } + switch goEnv { + case "production", "canary": + sentry.CaptureMessage("WARNING: " + message) + case "development", "debug": + log.Println("WARNING:", message) + } +} + +// LogError logs an error message to both the local log and Sentry based on the environment. +func LogError(err error, message string) { + goEnv := os.Getenv("GO_ENV") + + if goEnv == "" { + log.Fatal("GO_ENV is not set in error_logging LogError. Please set the GO_ENV environment variable.") + } + + switch goEnv { + case "production", "canary": + if err != nil { + sentry.CaptureException(err) + } else { + sentry.CaptureMessage("ERROR: " + message) + } + log.Printf("ERROR: %s: %v", message, err) + case "development", "debug": + if err != nil { + log.Printf("ERROR: %s: %v", message, err) + } else { + log.Println("ERROR:", message) + } + } +} + +// LogFatal logs a fatal error message to both the local log and Sentry based on the environment and then exits the application. +func LogFatal(err error, message string) { + goEnv := os.Getenv("GO_ENV") + + if goEnv == "" { + log.Fatal("GO_ENV is not set in error_logging LogFatal. Please set the GO_ENV environment variable.") + } + + switch goEnv { + case "production", "canary": + if err != nil { + sentry.CaptureException(err) + } else { + sentry.CaptureMessage("FATAL: " + message) + } + log.Fatalf("FATAL: %s: %v", message, err) + case "development", "debug": + if err != nil { + log.Fatalf("FATAL: %s: %v", message, err) + } else { + log.Fatalf("FATAL: %s", message) + } + } +} diff --git a/helper/response.go b/helper/response.go new file mode 100644 index 0000000..d8bd6a2 --- /dev/null +++ b/helper/response.go @@ -0,0 +1,28 @@ +package helper + +import ( + "encoding/json" + "net/http" +) + +func RespondWithError(w http.ResponseWriter, statusCode int, message string) { + w.Header().Set(ContentTypeHeader, ApplicationJSON) + w.WriteHeader(statusCode) + if encodeErr := json.NewEncoder(w).Encode(map[string]string{ErrorLabel: message}); encodeErr != nil { + LogError(encodeErr, ErrorEncodingResponse) + } +} + +func RespondWithMessage(w http.ResponseWriter, message string) { + if encodeErr := json.NewEncoder(w).Encode(map[string]string{MessageLabel: message}); encodeErr != nil { + LogError(encodeErr, ErrorEncodingResponse) + } +} + +func RespondWithJSON(w http.ResponseWriter, statusCode int, data interface{}) { + w.Header().Set(ContentTypeHeader, ApplicationJSON) + w.WriteHeader(statusCode) + if encodeErr := json.NewEncoder(w).Encode(data); encodeErr != nil { + LogError(encodeErr, ErrorEncodingResponse) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a20f0bd --- /dev/null +++ b/main.go @@ -0,0 +1,246 @@ +package main + +import ( + "authorization/db" + "authorization/docs" + "authorization/helper" + "authorization/models" + "authorization/redisclient" + "database/sql" + "fmt" + "log" + "net" + + "authorization/routes" + "net/http" + "os" + "time" + + "github.com/getsentry/sentry-go" + "github.com/gorilla/mux" + "github.com/joho/godotenv" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/cors" +) + +// @swagger: "2.0" +// @title UESS Authentication Microservice +// @version 1.0 + +// @description This is the API for Authentication Microservice for UESS. It doesn't support OAS 3.0 and is only for documentation purposes. The library used doesn't support @server annotation. +// @contact.name Darrel Israel +// @contact.email d.israel.psa@gmail.com + +// @BasePath / + +// @securityDefinitions.apikey BearerToken +// @in header +// @name Authorization + +var ( + dbOpenConnections = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_open_connections", + Help: "Number of open database connections", + }) + dbInUseConnections = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_in_use_connections", + Help: "Number of in-use database connections", + }) + dbIdleConnections = prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "db_idle_connections", + Help: "Number of idle database connections", + }) +) + +var ( + httpRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"path", "method"}, + ) + httpRequestDuration = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "Duration of HTTP requests in seconds", + Buckets: prometheus.DefBuckets, + }, + []string{"path", "method"}, + ) + httpRequestSize = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_size_bytes", + Help: "Size of HTTP requests in bytes", + Buckets: prometheus.ExponentialBuckets(100, 10, 8), + }, + []string{"path", "method"}, + ) + httpResponseSize = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_response_size_bytes", + Help: "Size of HTTP responses in bytes", + Buckets: prometheus.ExponentialBuckets(100, 10, 8), + }, + []string{"path", "method"}, + ) +) + +func init() { + prometheus.MustRegister(httpRequestsTotal) + prometheus.MustRegister(httpRequestDuration) + prometheus.MustRegister(httpRequestSize) + prometheus.MustRegister(httpResponseSize) + prometheus.MustRegister(dbOpenConnections) + prometheus.MustRegister(dbInUseConnections) + prometheus.MustRegister(dbIdleConnections) +} + +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + if r.URL.Path != metricsPath { + helper.LogInfo(fmt.Sprintf("INFO: Started %s %s", r.Method, r.URL.Path)) + } + + httpRequestsTotal.WithLabelValues(r.URL.Path, r.Method).Inc() + + requestSize := float64(r.ContentLength) + if requestSize < 0 { + requestSize = 0 + } + httpRequestSize.WithLabelValues(r.URL.Path, r.Method).Observe(requestSize) + + rw := &models.ResponseWriter{ResponseWriter: w} + next.ServeHTTP(rw, r) + + duration := time.Since(start).Seconds() + httpRequestDuration.WithLabelValues(r.URL.Path, r.Method).Observe(duration) + httpResponseSize.WithLabelValues(r.URL.Path, r.Method).Observe(float64(rw.Size)) + + // Log completion for non-metrics endpoints + if r.URL.Path != metricsPath { + helper.LogInfo(fmt.Sprintf("INFO: Completed %s %s in %.3f seconds", r.Method, r.URL.Path, duration)) + } + }) +} + +func collectDBMetrics(database *sql.DB) { + for { + stats := database.Stats() + dbOpenConnections.Set(float64(stats.OpenConnections)) + dbInUseConnections.Set(float64(stats.InUse)) + dbIdleConnections.Set(float64(stats.Idle)) + time.Sleep(10 * time.Second) // Adjust the interval as needed + } +} + +func allowOnlyGrafana(next http.Handler, allowedIP string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + remoteIP, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + if remoteIP == allowedIP { + next.ServeHTTP(w, r) + return + } + http.Error(w, "Forbidden", http.StatusForbidden) + }) +} + +func main() { + // Load environment variables from .env file first + // Get current working directory for debugging + cwd, _ := os.Getwd() + log.Printf("Current working directory: %s", cwd) + + err := godotenv.Load() + if err != nil { + log.Printf("ERROR: Failed to load .env file from default location: %v", err) + // Try with explicit path + err = godotenv.Load(".env") + if err != nil { + log.Fatalf("FATAL: Could not load .env file: %v. Tried paths: default and ./.env", err) + } + log.Println(".env file loaded successfully from ./.env") + } else { + log.Println(".env file loaded successfully") + } + + // Verify GO_ENV is loaded + goEnv := os.Getenv("GO_ENV") + log.Printf("GO_ENV value after loading .env: '%s'", goEnv) + + if goEnv == "" { + log.Fatal("GO_ENV is not set in main. Please set the GO_ENV environment variable.") + } + + DSN := os.Getenv("DSN") + if DSN == "" { + log.Fatal("Sentry DSN is not set. Please set the DSN environment variable.") + } + + err = sentry.Init(sentry.ClientOptions{ + Dsn: os.Getenv("DSN"), + TracesSampleRate: 1.0, + Environment: goEnv, + }) + if err != nil { + log.Fatalf("sentry.Init: %s", err) + } + defer sentry.Flush(2 * time.Second) + + docs.SwaggerInfo.Host = "localhost:8080" + docs.SwaggerInfo.Schemes = []string{"http"} + + helper.LogInfo("INFO: Initializing database connection...") + var database *sql.DB + for { + database, err = db.InitDB() + if err == nil { + break + } + helper.LogError(fmt.Errorf("ERROR: error initializing database: %v", err), "database initialization error") + time.Sleep(2 * time.Second) + } + + go collectDBMetrics(database) + router := mux.NewRouter() + routes.SetupRoutes(router, database) + helper.LogInfo("INFO: Database initialized successfully.") + + allowedIP := os.Getenv("ALLOWED_IP") + helper.LogInfo("INFO: Setting up routes...") + router.Handle(metricsPath, allowOnlyGrafana(promhttp.Handler(), allowedIP)) + router.Use(loggingMiddleware) + + c := cors.New(cors.Options{ + AllowedOrigins: []string{"http://localhost:4173", "http://localhost:5173"}, // Your frontend URL + AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"}, + AllowedHeaders: []string{"*"}, // Allow all headers temporarily + AllowCredentials: true, // Critical for withCredentials requests + MaxAge: 86400, // Cache preflight results + }) + + handler := c.Handler(router) + + redisclient.Init() + + helper.LogInfo("INFO: Connected to Redis successfully!") + + helper.LogInfo("WARNING: Ensure Redis is secured to prevent unauthorized access. Use a strong password and bind Redis to localhost or a secure network.") + + helper.LogInfo("INFO: Authentication Microservice is running on http://localhost:8080") + server := &http.Server{ + Addr: ":8080", + Handler: handler, + ReadTimeout: 15 * time.Second, + WriteTimeout: 300 * time.Second, + IdleTimeout: 60 * time.Second, + } + log.Fatal(server.ListenAndServe()) +} diff --git a/middleware/constants.go b/middleware/constants.go new file mode 100644 index 0000000..67a9d68 --- /dev/null +++ b/middleware/constants.go @@ -0,0 +1,6 @@ +package middleware + +const ( + Authorization = "Authorization" + Unauthorized = "Unauthorized" +) diff --git a/middleware/jwt.go b/middleware/jwt.go new file mode 100644 index 0000000..c4b82be --- /dev/null +++ b/middleware/jwt.go @@ -0,0 +1,218 @@ +package middleware + +import ( + "authorization/helper" + "authorization/models" + "context" + "fmt" + "net/http" + "os" + "sync" + "time" + + "github.com/golang-jwt/jwt/v5" +) + +// contextKey is a custom type for context keys to avoid collisions +type contextKey string + +const ( + claimsKey contextKey = "claims" + userIDKey contextKey = "user_id" + usernameKey contextKey = "username" + roleKey contextKey = "role" +) + +// Token cache entry +type cacheEntry struct { + claims *models.Claims + expiresAt time.Time +} + +var ( + // Token cache for high-frequency requests + tokenCache = make(map[string]*cacheEntry) + tokenCacheMutex sync.RWMutex + + // Cache JWT secret to avoid repeated os.Getenv calls + jwtSecretOnce sync.Once + jwtSecretCached []byte + jwtSecretError error + + // Pre-allocate error messages to avoid repeated allocations + errMissingAuth = "missing authorization header" + errInvalidAuthFormat = "invalid authorization header format" + errInvalidToken = "Invalid token" + errExpiredToken = "Invalid or expired token" + errInvalidClaims = "Invalid token claims" +) + +// Initialize JWT secret once +func getJWTSecret() ([]byte, error) { + jwtSecretOnce.Do(func() { + secret := os.Getenv("JWT_KEY") + if secret == "" { + jwtSecretError = fmt.Errorf("JWT_KEY not set in environment") + return + } + jwtSecretCached = []byte(secret) + }) + return jwtSecretCached, jwtSecretError +} + +// Clean expired cache entries periodically +func init() { + go func() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() + for range ticker.C { + cleanExpiredTokens() + } + }() +} + +func cleanExpiredTokens() { + tokenCacheMutex.Lock() + defer tokenCacheMutex.Unlock() + + now := time.Now() + for token, entry := range tokenCache { + if now.After(entry.expiresAt) { + delete(tokenCache, token) + } + } +} + +// JWTAuth is a middleware that validates JWT tokens with caching for high-frequency requests +func JWTAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Get the Authorization header + authHeader := r.Header.Get("Authorization") + if authHeader == "" { + helper.RespondWithError(w, http.StatusUnauthorized, "Unauthorized") + return + } + + // Fast path: check if header has Bearer prefix without allocation + if len(authHeader) < 8 || authHeader[:7] != "Bearer " { + helper.RespondWithError(w, http.StatusUnauthorized, errInvalidAuthFormat) + return + } + + tokenString := authHeader[7:] // Skip "Bearer " without strings.Split allocation + + // Check cache first (read lock) + tokenCacheMutex.RLock() + if cached, exists := tokenCache[tokenString]; exists { + if time.Now().Before(cached.expiresAt) { + claims := cached.claims + tokenCacheMutex.RUnlock() + + // Add claims to context and proceed + ctx := buildContext(r.Context(), claims) + next.ServeHTTP(w, r.WithContext(ctx)) + return + } + // Token expired in cache, remove it + tokenCacheMutex.RUnlock() + tokenCacheMutex.Lock() + delete(tokenCache, tokenString) + tokenCacheMutex.Unlock() + } else { + tokenCacheMutex.RUnlock() + } + + // Parse and validate the token + token, err := jwt.ParseWithClaims(tokenString, &models.Claims{}, func(token *jwt.Token) (interface{}, error) { + // Validate the signing method + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + // Get cached JWT secret + return getJWTSecret() + }) + + if err != nil { + helper.RespondWithError(w, http.StatusUnauthorized, errExpiredToken) + return + } + + // Check if token is valid + if !token.Valid { + helper.RespondWithError(w, http.StatusUnauthorized, errInvalidToken) + return + } + + // Extract claims + claims, ok := token.Claims.(*models.Claims) + if !ok { + helper.RespondWithError(w, http.StatusUnauthorized, errInvalidClaims) + return + } + + // Cache the validated token + expiresAt := time.Now().Add(5 * time.Minute) // Cache for 5 minutes + if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(expiresAt) { + expiresAt = claims.ExpiresAt.Time + } + + tokenCacheMutex.Lock() + // Limit cache size to prevent memory issues + if len(tokenCache) > 10000000 { + // Remove oldest 10% when cache is full + count := 0 + for k := range tokenCache { + delete(tokenCache, k) + count++ + if count >= 1000000 { + break + } + } + } + tokenCache[tokenString] = &cacheEntry{ + claims: claims, + expiresAt: expiresAt, + } + tokenCacheMutex.Unlock() + + // Add claims to request context + ctx := buildContext(r.Context(), claims) + + // Call the next handler with updated context + next.ServeHTTP(w, r.WithContext(ctx)) + } +} + +// buildContext efficiently builds context with claims (reduces allocations) +func buildContext(parent context.Context, claims *models.Claims) context.Context { + ctx := context.WithValue(parent, claimsKey, claims) + ctx = context.WithValue(ctx, userIDKey, claims.UserID) + ctx = context.WithValue(ctx, usernameKey, claims.Username) + ctx = context.WithValue(ctx, roleKey, claims.Role) + return ctx +} + +// GetClaims retrieves the JWT claims from the request context +func GetClaims(r *http.Request) (*models.Claims, bool) { + claims, ok := r.Context().Value(claimsKey).(*models.Claims) + return claims, ok +} + +// GetUserID retrieves the user ID from the request context +func GetUserID(r *http.Request) (string, bool) { + userID, ok := r.Context().Value(userIDKey).(string) + return userID, ok +} + +// GetUsername retrieves the username from the request context +func GetUsername(r *http.Request) (string, bool) { + username, ok := r.Context().Value(usernameKey).(string) + return username, ok +} + +// GetRole retrieves the role from the request context +func GetRole(r *http.Request) (string, bool) { + role, ok := r.Context().Value(roleKey).(string) + return role, ok +} diff --git a/models/authorize.go b/models/authorize.go new file mode 100644 index 0000000..5ad3ee7 --- /dev/null +++ b/models/authorize.go @@ -0,0 +1,21 @@ +package models + +import "github.com/golang-jwt/jwt/v5" + +type AuthorizationRequest struct { + UserID string `json:"user_id"` + Resource string `json:"resource"` + Action string `json:"action"` +} + +type AuthorizationResponse struct { + Allowed bool `json:"allowed"` + Reason string `json:"reason,omitempty"` +} + +type Claims struct { + UserID string `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} diff --git a/models/http.go b/models/http.go new file mode 100644 index 0000000..3ce251c --- /dev/null +++ b/models/http.go @@ -0,0 +1,26 @@ +package models + +import "net/http" + +// FlusherPreservingResponseWriter wraps http.ResponseWriter and preserves http.Flusher for SSE endpoints. +type FlusherPreservingResponseWriter struct { + http.ResponseWriter +} + +func (w *FlusherPreservingResponseWriter) Flush() { + if f, ok := w.ResponseWriter.(http.Flusher); ok { + f.Flush() + } +} + +// ResponseWriter wraps http.ResponseWriter to track response size for metrics +type ResponseWriter struct { + http.ResponseWriter + Size int +} + +func (rw *ResponseWriter) Write(b []byte) (int, error) { + size, err := rw.ResponseWriter.Write(b) + rw.Size += size + return size, err +} diff --git a/redisclient/redis.go b/redisclient/redis.go new file mode 100644 index 0000000..c4fd78d --- /dev/null +++ b/redisclient/redis.go @@ -0,0 +1,54 @@ +// pkg/redisclient/redis.go + +package redisclient + +import ( + "context" + "fmt" + "os" + + "github.com/redis/go-redis/v9" +) + +var RDB *redis.Client + +func Init() { + redisHost := os.Getenv("REDIS_HOST") + if redisHost == "" { + redisHost = "localhost" + } + + redisPort := os.Getenv("REDIS_PORT") + if redisPort == "" { + redisPort = "6379" + } + + redisPassword := os.Getenv("REDIS_PASSWORD") + if redisPassword == "" { + redisPassword = "" + } + + // Configure Redis client with security settings + opts := &redis.Options{ + Addr: fmt.Sprintf("%s:%s", redisHost, redisPort), + Password: redisPassword, + DB: 0, + DisableIndentity: true, // Disable client-side caching to prevent protocol confusion + IdentitySuffix: "", // Disable identity suffix + } + + RDB = redis.NewClient(opts) + + // Test connection with authentication + ctx := context.Background() + if _, err := RDB.Ping(ctx).Result(); err != nil { + panic(fmt.Sprintf("Could not connect to Redis: %v", err)) + } + + // Log connection security status + if redisPassword != "" { + fmt.Println("✓ Redis connection secured with password authentication") + } else { + fmt.Println("⚠ WARNING: Redis connection without password - security risk!") + } +} diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..3fd63f6 --- /dev/null +++ b/routes/routes.go @@ -0,0 +1,17 @@ +package routes + +import ( + "authorization/handlers" + "authorization/middleware" + "database/sql" + + "github.com/gorilla/mux" + httpSwagger "github.com/swaggo/http-swagger" +) + +func SetupRoutes(router *mux.Router, db *sql.DB) { + authzRoutes := router.PathPrefix("/v1/auth").Subrouter() + authzRoutes.HandleFunc("/check", middleware.JWTAuth(handlers.AuthorizeHandler)).Methods("POST") + + router.PathPrefix("/swagger/").Handler(httpSwagger.WrapHandler) +} diff --git a/services/authorize.go b/services/authorize.go new file mode 100644 index 0000000..09e0e20 --- /dev/null +++ b/services/authorize.go @@ -0,0 +1,6 @@ +package services + +func Authorize() bool { + // Authorization logic here + return true +}