This commit is contained in:
2025-12-04 10:55:25 +08:00
commit 60992c1e44
19 changed files with 1058 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
package main
const (
metricsPath = "/metrics"
)
+48
View File
@@ -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
}
+46
View File
@@ -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)
}
+21
View File
@@ -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"
}
}
}
+17
View File
@@ -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"
+44
View File
@@ -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
)
+129
View File
@@ -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=
+26
View File
@@ -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
}
}
+11
View File
@@ -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:"
)
+89
View File
@@ -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)
}
}
}
+28
View File
@@ -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)
}
}
+246
View File
@@ -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())
}
+6
View File
@@ -0,0 +1,6 @@
package middleware
const (
Authorization = "Authorization"
Unauthorized = "Unauthorized"
)
+218
View File
@@ -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
}
+21
View File
@@ -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
}
+26
View File
@@ -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
}
+54
View File
@@ -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!")
}
}
+17
View File
@@ -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)
}
+6
View File
@@ -0,0 +1,6 @@
package services
func Authorize() bool {
// Authorization logic here
return true
}