diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..40c483e --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,23 @@ +package auth + +import ( + link "git.doolta.com/doolta/kit/pkg/link" +) + +var validURL = "https://api.mailcape.com" + +var linkService link.Service + +// LoginResponse is the struct holding JWT token information +type LoginResponse struct { + UserID int64 `json:"user_id"` + Email string `json:"email"` + IsAdmin bool `json:"is_admin"` + Token string `json:"token"` + User string `json:"user"` +} + +type LoginData struct { + Email string `json:"email"` + Password string `json:"password"` +} diff --git a/auth/service.go b/auth/service.go new file mode 100644 index 0000000..648497b --- /dev/null +++ b/auth/service.go @@ -0,0 +1,170 @@ +package auth + +import ( + "encoding/json" + "fmt" + "intranet/internal/dependencies" + "intranet/internal/models" + "intranet/pkg/token" + + "net/http" + "strconv" + "time" + + "github.com/gin-gonic/gin" + jwt "github.com/golang-jwt/jwt/v5" + "golang.org/x/crypto/bcrypt" +) + +// Service for auth (Interface AND implementation) +// All the business logic should be here +type Service interface { + // FindByEmail(id string) ([]*models.Auth, error) + GetLoginHandler() func(http.ResponseWriter, *http.Request) + GetSignupHandler() func(http.ResponseWriter, *http.Request) + GetAdminRoleRequiredMiddleware() gin.HandlerFunc +} + +// AuthService godoc +type AuthServiceImplementation struct { + DB dependencies.DB + Log dependencies.Logger +} + +// NewAuthService returns a service to manipulate auth +func NewAuthService(db dependencies.DB, log dependencies.Logger) *AuthServiceImplementation { + return &AuthServiceImplementation{db, log} + +} + +// GetLoginHandler returns an handled for the login route +func (s *AuthServiceImplementation) GetLoginHandler() http.HandlerFunc { + log := s.Log + db := s.DB + log.Info("[LoginHandler] Installed") + //@return a http.handlerFunc that will check for loginDataand return a JWT token if login is successful + + return func(w http.ResponseWriter, r *http.Request) { + log.Info("[LoginHandler] Start") + + loginData := LoginData{} + + // Decode the incoming note json + err := json.NewDecoder(r.Body).Decode(&loginData) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + email := loginData.Email + password := loginData.Password + + var u models.Account + db.Where("email = ?", email).First(&u) + + if (u == models.Account{}) { + http.Error(w, "Invalid login/pass", http.StatusUnauthorized) + return + } + + // Check password + log.Debug("Before check") + err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) + log.Debug("After check (%s)", err) + if err != nil { + log.Info(err) + http.Error(w, "Invalid login/pass", http.StatusUnauthorized) + return + } + + // Return JWT if password valid + // TODO: add role from database (not hardcoded) + tokenString, err := token.CreateToken(strconv.FormatInt(int64(u.ID), 9), token.TokenSecret, "admin") + if err != nil { + log.Info(err) + http.Error(w, "Unable to create token", http.StatusUnauthorized) + return + } + log.Debug("Token generated " + tokenString) + expire := time.Now().AddDate(-1, 0, 1) + cookie := http.Cookie{Name: "token", Value: tokenString, Expires: expire} + http.SetCookie(w, &cookie) + + j, err := json.Marshal(LoginResponse{UserID: u.ID, Email: u.Email, IsAdmin: true, Token: tokenString, User: u.Email}) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write(j) + } +} + +// GetSignupHandler returns a handler for the signup route +func (s *AuthServiceImplementation) GetSignupHandler() http.HandlerFunc { + log := s.Log + log.Info("[SignupHandler] Installed") + return func(w http.ResponseWriter, r *http.Request) { + email := r.FormValue("email") + password := r.FormValue("password") + + log.Infof("In signup handler %s", email) + + if email == "" { + http.Error(w, "Email missing", http.StatusInternalServerError) + return + } + + bytes, err := bcrypt.GenerateFromPassword([]byte(password), 13) + if err != nil { + log.Error("Can't generate hash for password: " + err.Error()) + http.Error(w, "Can't generate hash for password", http.StatusInternalServerError) + return + } + u := models.Account{Email: email, Password: string(bytes), Status: "pending"} + u, err = s.createUser(&u) + if err != nil { + log.Infof("Could'nt create Account = (%+v)", u) + log.Error(err) + http.Error(w, "Can't create account in database", http.StatusInternalServerError) + return + } + http.Redirect(w, r, "/", http.StatusMovedPermanently) + } +} + +// createUser is a method on AuthServiceImplementation that create an user +// copying the part in GetSignupHandler that create a user in database +func (s *AuthServiceImplementation) createUser(u *models.Account) (models.Account, error) { + db := s.DB + db.Create(u) + if u.ID == 0 { + return *u, fmt.Errorf("Can't create account in database") + } + return *u, nil +} + +// GetAdminRoleRequiredMiddleware returns a middleware that checks if the user is an admin +func (s *AuthServiceImplementation) GetAdminRoleRequiredMiddleware() http.HandlerFunc { + log := s.Log + log.Info("Installing RoleMiddleware") + return func(next http.HandlerFunc) http.HandlerFunc { + log.Info("[RoleMiddleware]") + claims, OK := c.Get("claims") + if !OK { + http.Error(w, "No claims found", http.StatusForbidden) + return + } + + claimsMap := claims.(jwt.MapClaims) + + log.Infof("[RoleMiddleware] claimsMap[role] = (%s)", claimsMap["Role"]) + log.Infof("[RoleMiddleware] claims = (%+v)", claims) + if claimsMap["Role"] != "admin" { + http.Error(w, "Not an admin", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + } +} diff --git a/go.mod b/go.mod index 638067e..35801da 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,15 @@ module git.doolta.com/doolta/go-kit go 1.22.1 require ( + git.doolta.com/doolta/kit v1.2.3 github.com/epfl-si/go-toolbox v0.6.15 github.com/gin-gonic/gin v1.9.1 github.com/golang-jwt/jwt/v5 v5.2.1 go.uber.org/zap v1.27.0 + golang.org/x/crypto v0.19.0 + gorm.io/driver/mysql v1.5.4 + gorm.io/driver/postgres v1.5.7 + gorm.io/gorm v1.25.10 ) require ( @@ -18,7 +23,14 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.18.0 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jinzhu/gorm v1.9.16 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect @@ -26,11 +38,11 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect + github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.19.0 // indirect golang.org/x/net v0.21.0 // indirect golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index 2e32dd6..d27bb7c 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,7 @@ +git.doolta.com/doolta/kit v1.2.3 h1:xJ+LbWDopzVEyCwEI+gUh0RtSF4wvg6CSgadvH1SX/4= +git.doolta.com/doolta/kit v1.2.3/go.mod h1:JU4jwlc7qrNZgzlKupVTd7iYIqO1odGJA3zlPxkPUQ8= +github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.11.0 h1:FwNNv6Vu4z2Onf1++LNzxB/QhitD8wuTdpZzMTGITWo= @@ -12,8 +16,12 @@ github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLI 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/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/epfl-si/go-toolbox v0.6.15 h1:Bo9t4MAVr7LN6mV/BGitZAVytbMkLfPpIwSsVpva8T8= github.com/epfl-si/go-toolbox v0.6.15/go.mod h1:q48G3oNV2nWDTMBHfcG7qiUV8kAkLHc8AmndJZQlRIc= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -28,23 +36,50 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= +github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= +github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= +github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -54,6 +89,8 @@ github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOS github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= 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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -77,24 +114,43 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.v3 v3.0.0-20200313102051-9f266ea9e77c/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= +gorm.io/driver/mysql v1.5.4 h1:igQmHfKcbaTVyAIHNhhB888vvxh8EdQ2uSUT0LPcBso= +gorm.io/driver/mysql v1.5.4/go.mod h1:9rYxJph/u9SWkWc9yY4XJ1F/+xO0S/ChOmbk3+Z5Tvs= +gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= +gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= +gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= +gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= +gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/gormdb/mysql.go b/gormdb/mysql.go new file mode 100644 index 0000000..55fe950 --- /dev/null +++ b/gormdb/mysql.go @@ -0,0 +1,61 @@ +package gormdb + +import ( + "fmt" + "os" + "time" + + "go.uber.org/zap" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// GetGormDB returns a Gorm database connection. +// +// Parameters: +// - log *zap.Logger: a logger instance +// - host string: the database host +// - name string: the database name +// - user string: the database user +// - pass string: the database password +// - port string: the database port +// - param string: the specific database parameters +// - maxIdle int: the maximum number of idle connections +// - maxOpen int: the maximum number of open connections +// +// Return type(s): +// - *gorm.DB: the Gorm database connection +// - error: an error, if any, encountered during the connection +func GetMySQLDB(log *zap.Logger, host, name, user, pass, port, param string, maxIdle int, maxOpen int) (*gorm.DB, error) { + //log.Infof("[GetGormDB] Connecting to 'database' %s on host %s as user '%s' (%s)", name, host, user, param) + logLevel := logger.Silent + if os.Getenv("LOG_LEVEL") == "info" { + logLevel = logger.Info + } + + var dsn string + + if param != "" { + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s", user, pass, host, port, name, param) + } else { + dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, pass, host, port, name) + } + db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logLevel), + }) + if err != nil { + log.Error(fmt.Sprintf("GetGormDB:%s", err)) + return db, err + } + + log.Info(fmt.Sprintf("GetGormDB:successfully connected on host '%s' to database '%s' as user '%s' (%s)", host, name, user, param)) + + sqlDB, err := db.DB() + sqlDB.SetMaxIdleConns(maxIdle) + sqlDB.SetMaxOpenConns(maxOpen) + sqlDB.SetConnMaxLifetime(time.Hour) + sqlDB.SetConnMaxIdleTime(2 * time.Minute) + + return db, nil +} diff --git a/gormdb/postgresql.go b/gormdb/postgresql.go new file mode 100644 index 0000000..811fb42 --- /dev/null +++ b/gormdb/postgresql.go @@ -0,0 +1,27 @@ +package gormdb + +import ( + "fmt" + + "go.uber.org/zap" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +// GetGormDB initializes a connection to the database and returns a handle +func GetPostgresqlDB(log *zap.Logger, host, name, user, pass, port string) (*gorm.DB, error) { + log.Info(fmt.Sprintf("[GetPostgresqlDB] Connecting to 'database' %s on host %s as user '%s'", name, host, user)) + dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", host, port, user, pass, name) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Info), + }) + if err != nil { + log.Error(fmt.Sprintf("[GetPostgresqlDB] err=%s\n", err)) + return db, err + } + + log.Info(fmt.Sprintf("[GetPostgresqlDB] Successfully connected on host '%s' to database '%s' as user '%s'", host, name, user)) + + return db, nil +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..fe62185 --- /dev/null +++ b/log/log.go @@ -0,0 +1,57 @@ +package log + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func GetLoggerConfig(logLevel string, stdouts []string, stderrs []string, encoding string) zap.Config { + level := zap.InfoLevel + if logLevel == "debug" { + level = zap.DebugLevel + } + if logLevel == "error" { + level = zap.ErrorLevel + } + if logLevel == "warn" { + level = zap.WarnLevel + } + if logLevel == "fatal" { + level = zap.FatalLevel + } + + // Get a new logger + encoderCfg := zap.NewProductionEncoderConfig() + encoderCfg.TimeKey = "timegenerated" + encoderCfg.LevelKey = "log.level" + encoderCfg.EncodeTime = zapcore.RFC3339TimeEncoder + encoderCfg.EncodeLevel = zapcore.CapitalLevelEncoder + + config := zap.Config{ + Level: zap.NewAtomicLevelAt(level), + Development: false, + DisableCaller: true, + DisableStacktrace: false, + Sampling: nil, + Encoding: encoding, + EncoderConfig: encoderCfg, + OutputPaths: stdouts, + ErrorOutputPaths: stderrs, + InitialFields: map[string]interface{}{ + //"pid": os.Getpid(), + }, + } + + return config +} + +// GetLogger returns a new logger with the specified log level. +// +// Parameters: +// - logLevel: a string representing the log level ("debug", "error", "warn", "fatal") +// +// Return type(s): +// - *zap.Logger: a pointer to the logger +func GetLogger(logLevel string) *zap.Logger { + return zap.Must(GetLoggerConfig(logLevel, []string{"stdout"}, []string{"stderr"}, "json").Build()) +} diff --git a/token/token.go b/token/token.go new file mode 100644 index 0000000..bf8d670 --- /dev/null +++ b/token/token.go @@ -0,0 +1,149 @@ +// Package token handles JWT tokens manipulation +package token + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + jwt "github.com/golang-jwt/jwt/v5" + "go.uber.org/zap" +) + +// Authenticater is the interface that wraps the Authenticate method +type Authenticater interface { + Authenticate(login, pass string) (CustomClaims, error) +} + +// CustomClaims is the struct that represents the claims of a JWT token in EPFL context +type CustomClaims struct { + Sciper string `json:"sciper"` + jwt.RegisteredClaims +} + +// Validate validates the claims of a JWT token +func (m CustomClaims) Validate() error { + if m.Sciper == "" { + return errors.New("sciper must be set") + } + return nil +} + +// Token is the struct that represents a JWT token +type Token struct { + JWT *jwt.Token +} + +// New creates a new JWT token +func New(claims CustomClaims) *Token { + jwt := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + + return &Token{JWT: jwt} +} + +// Parse parses a JWT token +func Parse(tokenString string, secret []byte) (*Token, error) { + t, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + // Don't forget to validate the alg is what you expect: + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) + } + + return secret, nil + }) + + if err != nil { + return nil, err + } + + return &Token{t}, nil +} + +// Sign signs a JWT token +func (t *Token) Sign(secret []byte) (string, error) { + return t.JWT.SignedString([]byte(secret)) +} + +// Claims returns the claims of a JWT token +func (t *Token) Claims() jwt.MapClaims { + return t.JWT.Claims.(jwt.MapClaims) +} + +// Set sets a claim in a JWT token +func (t *Token) Set(key string, value interface{}) { + t.Claims()[key] = value +} + +// Get gets a claim from a JWT token +func (t *Token) Get(key string) interface{} { + return t.Claims()[key] +} + +// GetString gets a claim from a JWT token as a string +func (t *Token) GetString(key string) string { + return t.Claims()[key].(string) +} + +// ToJSON converts a JWT token to JSON +func (t *Token) ToJSON() (string, error) { + return t.JWT.Raw, nil +} + +// PostLoginHandler is the handler that checks the login and password and returns a JWT token +func PostLoginHandler(log *zap.Logger, auth Authenticater, secret []byte) gin.HandlerFunc { + log.Info("Creating login handler") + return func(c *gin.Context) { + login := c.PostForm("login") + pass := c.PostForm("pass") + + log.Info("Login attempt", zap.String("login", login)) + + claims, err := auth.Authenticate(login, pass) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + return + } + + t := New(claims) + encoded, err := t.Sign(secret) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, gin.H{"access_token": encoded}) + } +} + +// GinMiddleware is the middleware that checks the JWT token +func GinMiddleware(secret []byte) gin.HandlerFunc { + return func(c *gin.Context) { + authorizationHeaderString := c.GetHeader("Authorization") + if authorizationHeaderString == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "No token provided"}) + c.Abort() + return + } + + // Check that the authorization header starts with "Bearer" + if len(authorizationHeaderString) < 7 || authorizationHeaderString[:7] != "Bearer " { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) + c.Abort() + return + } + + // Extract the token from the authorization header + tokenString := authorizationHeaderString[7:] + + t, err := Parse(tokenString, secret) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()}) + c.Abort() + return + } + + c.Set("token", t) + c.Next() + } +} diff --git a/token/token_example_test.go b/token/token_example_test.go new file mode 100644 index 0000000..4581c43 --- /dev/null +++ b/token/token_example_test.go @@ -0,0 +1,37 @@ +package token_test + +import ( + "fmt" + + "github.com/epfl-si/go-toolbox/token" +) + +func Example_creating() { + t := token.New(token.CustomClaims{Sciper: "321014"}) + encoded, err := t.Sign([]byte("secret")) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(encoded) + // Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY2lwZXIiOiIzMjEwMTQifQ.7Nf7BUmLmN2RGXwf2nr-cOwkcsCkWO2i6YgLZdItrek +} + +func Example_decoding() { + tokenString := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY2lwZXIiOiIzMjEwMTQifQ.7Nf7BUmLmN2RGXwf2nr-cOwkcsCkWO2i6YgLZdItrek" + decoded, err := token.Parse(tokenString, []byte("secret")) + if err != nil { + fmt.Println(err) + return + } + + fmt.Println(decoded.JWT.Raw) + fmt.Println(decoded.JWT.Header["alg"]) + fmt.Println(decoded.JWT.Header["typ"]) + // fmt.Printf("%+v", decoded.JWT) + // Output: + // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY2lwZXIiOiIzMjEwMTQifQ.7Nf7BUmLmN2RGXwf2nr-cOwkcsCkWO2i6YgLZdItrek + // HS256 + // JWT +} diff --git a/web/http_servemux.go b/web/http_servemux.go new file mode 100644 index 0000000..45c6b33 --- /dev/null +++ b/web/http_servemux.go @@ -0,0 +1,48 @@ +// Package web provides HTTP server related functions and structures. +package web + +import ( + "context" + "net/http" + + "go.uber.org/zap" +) + +// NewHTTPServer takes a database connection and a logger as parameters +// It returns a pointer to a Server struct +func NewHTTPServer(log *zap.Logger) *Server { + router := http.NewServeMux() + services := make(map[string]interface{}) + + return &Server{ + Log: log, + Router: router, + Services: services, + } +} + +// Run starts the HTTP server on port passed as parameter (":12345"). +func (s *Server) Run(port string) { + http.ListenAndServe(port, s.Router) +} + +// AddRoute adds a new route to the server's router. +// Adding server resources in context +func (s *Server) AddRoute(pattern string, handlefunc http.HandlerFunc) { + s.Router.Handle(pattern, s.WithServerContext(handlefunc)) +} + +// WithServerContext adds the logger and services to the request context. +func (s *Server) WithServerContext(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), "log", s.Log) + ctx = context.WithValue(ctx, "services", s.Services) + next.ServeHTTP(w, r.WithContext(ctx)) + } +} + +func GetServerContext(r *http.Request) (*zap.Logger, map[string]interface{}) { + services := r.Context().Value("services").(map[string]interface{}) + log := r.Context().Value("log").(*zap.Logger) + return log, services +} diff --git a/web/routes.go b/web/routes.go new file mode 100644 index 0000000..47e4997 --- /dev/null +++ b/web/routes.go @@ -0,0 +1,17 @@ +// Package web provides the web server for the application. +package web + +import ( + "fmt" + "net/http" +) + +// Routes sets up the routes for the server. +func (s *Server) Routes() { + s.Log.Info("Health check handler installed") + s.AddRoute("GET /health", func(w http.ResponseWriter, r *http.Request) { + s.Log.Debug("Health check handler called") + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Hello, World!") + }) +} diff --git a/web/server.go b/web/server.go new file mode 100644 index 0000000..ba6c316 --- /dev/null +++ b/web/server.go @@ -0,0 +1,20 @@ +// Package web provides HTTP server related functions and structures. +package web + +import ( + "net/http" + + "go.uber.org/zap" +) + +// Server represents a HTTP server with logging and routing capabilities. +type Server struct { + Log *zap.Logger + Router *http.ServeMux + Services map[string]interface{} +} + +// AddService adds a service to the server's service map. +func (s *Server) AddService(name string, service interface{}) { + s.Services[name] = service +}