diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c32f48c --- /dev/null +++ b/.gitignore @@ -0,0 +1,157 @@ +build +.k +OLD +tries +tmp +test +# enviroment to load on bin/build +.env + +# where souce code is clone with git +clone + +# where tools command are found +tools + +# where pipeline templates are found +templates + +# OSX leaves these everywhere on SMB shares +._* + +# OSX trash +.DS_Store + +# Eclipse files +.classpath +.project +.settings/** + +# Files generated by JetBrains IDEs, e.g. IntelliJ IDEA +.idea/ +*.iml + +# Vscode files +.vscode + +# This is where the result of the go build goes +/output*/ +/_output*/ +/_output + +# Emacs save files +*~ +\#*\# +.\#* + +# Vim-related files +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist + +# cscope-related files +cscope.* + +# Go test binaries +*.test +/hack/.test-cmd-auth + +# JUnit test output from ginkgo e2e tests +/junit*.xml + +# Mercurial files +**/.hg +**/.hg* + +# Vagrant +.vagrant +network_closure.sh + +# Local cluster env variables +/cluster/env.sh + +# Compiled binaries in third_party +/third_party/pkg + +# Also ignore etcd installed by hack/install-etcd.sh +/third_party/etcd* +/default.etcd + +# User cluster configs +.kubeconfig + +.tags* + +# Version file for dockerized build +.dockerized-kube-version-defs + +# Web UI +/www/master/node_modules/ +/www/master/npm-debug.log +/www/master/shared/config/development.json + +# Karma output +/www/test_out + +# precommit temporary directories created by ./hack/verify-generated-docs.sh and ./hack/lib/util.sh +/_tmp/ +/doc_tmp/ + +# Test artifacts produced by Jenkins jobs +/_artifacts/ + +# Go dependencies installed on Jenkins +/_gopath/ + +# Config directories created by gcloud and gsutil on Jenkins +/.config/gcloud*/ +/.gsutil/ + +# CoreOS stuff +/cluster/libvirt-coreos/coreos_*.img + +# Juju Stuff +/cluster/juju/charms/* +/cluster/juju/bundles/local.yaml + +# Downloaded Kubernetes binary release +/kubernetes/ + +# direnv .envrc files +.envrc + +# Downloaded kubernetes binary release tar ball +kubernetes.tar.gz + +# generated files in any directory +# TODO(thockin): uncomment this when we stop committing the generated files. +#zz_generated.* +zz_generated.openapi.go +zz_generated_*_test.go + +# TODO(roycaihw): remove this when we stop committing the generated definition +!staging/src/k8s.io/apiextensions-apiserver/pkg/generated/openapi/zz_generated.openapi.go +# low-change blueprint in code-generator to notice changes +!staging/src/k8s.io/code-generator/_examples/apiserver/openapi/zz_generated.openapi.go +# low-change sample-apiserver spec to be compilable when published +!staging/src/k8s.io/sample-apiserver/pkg/generated/openapi/zz_generated.openapi.go + +# make-related metadata +/.make/ + +# Just in time generated data in the source, should never be committed +/test/e2e/generated/bindata.go + +# This file used by some vendor repos (e.g. github.com/go-openapi/...) to store secret variables and should not be ignored +!\.drone\.sec + +# Godeps workspace +/Godeps/_workspace + +/bazel-* +*.pyc + +# generated by verify-vendor.sh +vendordiff.patch diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..786813f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM golang:1.17.6 as builder +ENV GOOS linux +ENV CGO_ENABLED 0 +# Add a work directory +WORKDIR /app +# Cache and install dependencies +COPY go.mod go.sum ./ +RUN go mod download +# Copy app files +COPY . . +# Build app +RUN go build -o servgen + +FROM alpine:3.15 as production +# Add certificates +RUN apk add --no-cache ca-certificates +# Copy built binary from builder +COPY --from=builder servgen . +# Expose port +#EXPOSE 8080 +# Exec built binary +CMD ./servgen diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bc3d6cc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2022 Jesús Pérez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crypt.go b/crypt.go new file mode 100644 index 0000000..3f2d05f --- /dev/null +++ b/crypt.go @@ -0,0 +1,62 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "fmt" + "io" +) + +func getKey(key []byte) (string,error) { + bytes := make([]byte, 32) //generate a random 32 byte key for AES-256 + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes),nil +} +func encrypt(text string, keytext string) (string, error) { + key, _ := hex.DecodeString(keytext) + plaintext := []byte(text) + block, err := aes.NewCipher(key) + if err != nil { + return "",err + } + //GCM - https://en.wikipedia.org/wiki/Galois/Counter_Mode + //https://golang.org/pkg/crypto/cipher/#NewGCM + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "",err + } + nonce := make([]byte, aesGCM.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "",err + } + //Encrypt the data using aesGCM.Seal + //Since we don't want to save the nonce somewhere else in this case, we add it as a prefix to the encrypted data. The first nonce argument in Seal is the prefix. + ciphertext := aesGCM.Seal(nonce, nonce, plaintext, nil) + return fmt.Sprintf("%x", ciphertext),nil +} + +func decrypt(encodedtext string, keytext string) (string,error) { + key, _ := hex.DecodeString(keytext) + enc, _ := hex.DecodeString(encodedtext) + block, err := aes.NewCipher(key) + if err != nil { + return "",err + } + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "",err + } + nonceSize := aesGCM.NonceSize() + //Extract the nonce from the encrypted data + nonce, ciphertext := enc[:nonceSize], enc[nonceSize:] + //Decrypt the data + text, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "",err + } + return fmt.Sprintf("%s", text), nil +} \ No newline at end of file diff --git a/email.go b/email.go new file mode 100644 index 0000000..0edf80f --- /dev/null +++ b/email.go @@ -0,0 +1,134 @@ +package main + +import ( + "bytes" + "crypto/tls" + "fmt" + "html/template" + "log" + "strings" + "time" + + cfg "github.com/jesusperez/cfgsrv" + // "text/template" + "github.com/toorop/go-dkim" + mail "github.com/xhit/go-simple-mail/v2" +) + +func email(cfg *cfg.Config, tplType string, tplPath string, attachPath string, attachName string, request Mail, bodyData interface{}) error { + + server := mail.NewSMTPClient() + + // SMTP Server + server.Host = cfg.MailHost + server.Port = cfg.MailPort + server.Username = cfg.MailUser + server.Password = cfg.MailPswd + server.Encryption = mail.EncryptionSTARTTLS + + // Since v2.3.0 you can specified authentication type: + // - PLAIN (default) + // - LOGIN + // - CRAM-MD5 + // - None + server.Authentication = mail.AuthPlain + + // Variable to keep alive connection + server.KeepAlive = true + + // Timeout for connect to SMTP Server + server.ConnectTimeout = 10 * time.Second + + // Timeout for send the data and wait respond + server.SendTimeout = 10 * time.Second + + // Set TLSConfig to provide custom TLS configuration. For example, + // to skip TLS verification (useful for testing): + server.TLSConfig = &tls.Config{InsecureSkipVerify: true} + + // SMTP client + smtpClient,err := server.Connect() + if err != nil{ + return err + } + + // New email simple html with inline and CC + email := mail.NewMSG() + email.SetFrom(request.Sender).AddTo(strings.Join(request.To,",")) + if len(request.Cc) > 0 { + email.AddCc(strings.Join(request.Cc,",")) + } + if len(request.Bcc) > 0 { + email.AddBcc(strings.Join(request.Bcc,",")) + } + email.SetSubject(request.Subject) + // email.SetListUnsubscribe(""). + + // email.AddHeader("Date",time.Now().Format(time.RFC1123Z)) + + var body bytes.Buffer + if len(tplPath) > 0 { + t, _ := template.ParseFiles(tplPath) + // body.Write([]byte(msg)) + //body.Write([]byte(fmt.Sprintf("%s \n%s\n\n", subject,mimeHeaders))) + t.Execute(&body, bodyData) + request.Body = body + } + contentType := mail.TextPlain + + switch(tplType) { + case "text": + contentType = mail.TextPlain + case "html": + contentType = mail.TextHTML + } + var privateKey []byte + // if len(cfg.MailCertPath) > 0 { + // var err error + // privateKey, err = loadPath(cfg.MailCertPath) + // if err != nil { + // return err + // } + // } + // you can add dkim signature to the email. + // to add dkim, you need a private key already created one. + if privateKey != nil && len(privateKey) > 0 { + options := dkim.NewSigOptions() + options.PrivateKey = privateKey + options.Domain = cfg.MailCertDom + options.Selector = "default" + options.SignatureExpireIn = 3600 + options.Headers = []string{"from", "date", "mime-version", "received", "received"} + options.AddSignatureTimestamp = true + options.Canonicalization = "relaxed/relaxed" + msg := []byte(request.Body.String()) + errdkim := dkim.Sign(&msg, options) + if errdkim != nil { + return errdkim + } + email.SetBody(contentType, string(msg)) + } else { + email.SetBody(contentType, request.Body.String()) + } + if len(attachPath) > 0 { + email.Attach(&mail.File{FilePath: attachPath, Name: attachName, Inline: true}) + } + // also you can add body from []byte with SetBodyData, example: + // email.SetBodyData(mail.TextHTML, []byte(htmlBody)) + // or alternative part + // email.AddAlternativeData(mail.TextHTML, []byte(htmlBody)) + + // always check error after send + if email.Error != nil{ + log.Fatal(email.Error) + } + // Sending email. + // Call Send and pass the client + err = email.Send(smtpClient) + if err != nil { + fmt.Printf("Error send mail: %v",err) + return err + } + fmt.Println("Email Sent!") + return nil +} \ No newline at end of file diff --git a/handlers.go b/handlers.go new file mode 100644 index 0000000..d1a2ff9 --- /dev/null +++ b/handlers.go @@ -0,0 +1,133 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + + "github.com/gin-gonic/gin" + cvdata "github.com/jesusperez/cvdata" +) +func user_has_role(usr *User, c *gin.Context, rtenv *RouteEnv, role string) (*User,bool) { + var user *User + var okusr bool + if usr == nil { + idusr := rtenv.AuthMiddleware.IdentityHandler(c) + user, okusr = idusr.(*User) + if !okusr || len(user.UserName) == 0 { + return nil,false + } + } else { + user = usr + } + hasRole := false + _, okmdl := rtenv.Users.Accounts[user.UserName] + if ! okmdl { + return nil,false + } + if rtenv.Cfg.UseAuthz { + hasRole,_ = rtenv.Enforcer.HasRoleForUser(user.UserName, role) + } else { + hasRole = true + // TODO fix this if no Cfg.UseAuthz + } + return user,hasRole +} +func get_page_handle(c *gin.Context, rtenv *RouteEnv) { + tkn := "" + id := c.Params.ByName(rtenv.Cfg.Routes["page"].Param) + role := rtenv.Cfg.AdminRole + hasRole := false + mdlUsr, okmdl := rtenv.MdlsUsrs[id] + if okmdl { + if rtenv.Cfg.UseAuthz { + hasRole,_ = rtenv.Enforcer.HasRoleForUser(mdlUsr.User, role) + } + logRoute(c,rtenv,"page",fmt.Sprintf("get /page/%s", id),fmt.Sprintf("get %s (%s %v) %s",mdlUsr.User,role,hasRole,tkn)) + if rtenv.Cfg.UseJWT { + c.HTML(http.StatusOK, "welcome", gin.H{ + "title": fmt.Sprintf("Main website %s for %s (%v)",id,mdlUsr.User,hasRole), + "token": tkn, + }) + } else { + c.HTML(http.StatusOK, "welcome", gin.H{ + "title": fmt.Sprintf("Main website %s for %s (%v)",id,mdlUsr.User,hasRole), + }) + } + } else { + logRoute(c,rtenv,"page",fmt.Sprintf("get /page/%s", id),fmt.Sprintf("get %s (%s %v) %s",mdlUsr.User,role,hasRole,tkn)) + c.HTML(http.StatusOK, "welcome", gin.H{ + "title": fmt.Sprintf("Main website public"), + }) + } +} +func get_data_handle(c *gin.Context, rtenv *RouteEnv) { + // fmt.Printf("context: %+v\n", c) + target := c.Params.ByName(rtenv.Cfg.Routes["data"].Param) + if target == "-" { + target = "main" + } + logRoute(c,rtenv,"data",fmt.Sprintf("get %s",target), fmt.Sprintf("get %s",target)) + path := fmt.Sprintf("%s/%s.json",rtenv.Cfg.DataDistPath,target) + _, err := os.Open(path) + if rtenv.Cfg.UseDist && errors.Is(err, os.ErrNotExist) { + path = fmt.Sprintf("%s/%s",rtenv.Cfg.DataPath,target) + fmt.Printf("YAML path: %+v\n", path) + data,error := cvdata.LoadCVData(path, rtenv.Cfg,rtenv.Cfg.UseRepoOnReq) + if error != nil { + logRoute(c,rtenv,"data",fmt.Sprintf("Error yaml %s",target), fmt.Sprintf("Err %v",error)) + c.JSON(http.StatusNotAcceptable, gin.H{"error": "Error reading file"}) + } else { + logRoute(c,rtenv,"data",fmt.Sprintf("OK yaml %s",target), fmt.Sprintf("OK %s",target)) + c.JSON(http.StatusOK, data) + } + } else { + data, error := ioutil.ReadFile(path) + if error != nil { + logRoute(c,rtenv,"data",fmt.Sprintf("Error json %s",target), fmt.Sprintf("Err %v",error)) + c.JSON(http.StatusNotAcceptable, gin.H{"error": "Error reading file"}) + } else { + logRoute(c,rtenv,"data",fmt.Sprintf("OK json %s",target), fmt.Sprintf("OK %s",target)) + c.Data(http.StatusOK, "application/json", data) + //c.Data(http.StatusOK, "application/json", []byte(fmt.Sprintf("{\"models\": %s, \"data\": %s}",datamodels,data))) + } + } +} +func post_data_handle(c *gin.Context, rtenv *RouteEnv) { + var cvpost cvdata.CVPostData + role := rtenv.Cfg.AdminRole + err := c.BindJSON(&cvpost) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + if cvpost.U == "" || len(cvpost.Data) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"info": "error info"}) + return + } + // roles,_ := enforcer.GetRolesForUser(user) + hasRole,_ := rtenv.Enforcer.HasRoleForUser(cvpost.U, role) + fmt.Printf("%s (%s) %+v\n",cvpost.U, role, hasRole) + if !hasRole { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + keys,res := cvpost.Data.Write(rtenv.Cfg) + if res != nil { + logRoute(c,rtenv,"post_data",fmt.Sprintf("Error post %s: %s",cvpost.U,keys), fmt.Sprintf("error: %+v",res)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + if rtenv.Cfg.GenDist { + errModel := createRootModels(rtenv.Cfg) + if errModel != nil { + fmt.Printf("Error createRootModels: %v\n",errModel) + } + } + logRoute(c,rtenv,"post_data",fmt.Sprintf("post %s: %s",cvpost.U,keys), fmt.Sprintf("post %s: %s",cvpost.U,keys)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + // c.IndentedJSON(http.StatusCreated, cvdata) +} \ No newline at end of file diff --git a/handlers_auth.go b/handlers_auth.go new file mode 100644 index 0000000..be9a15e --- /dev/null +++ b/handlers_auth.go @@ -0,0 +1,108 @@ +package main + +import ( + b64 "encoding/base64" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func get_auth_handle(c *gin.Context, rtenv *RouteEnv) { + id := c.Params.ByName(rtenv.Cfg.Routes["auth"].Param) + pass := "" + role := rtenv.Cfg.AdminRole + tkn := "-" + hasRole := false + var err error + if strings.Contains(id,rtenv.Cfg.AuthSep) { + s := strings.Split(id,rtenv.Cfg.AuthSep) + id = s[0] + pasw,_ := b64.StdEncoding.DecodeString(s[1]) + pass = string(pasw) + } + mdlUsr, okmdl := rtenv.MdlsUsrs[id] + data := &User{ + UserName: id, + UUID: id, + Data: "", + FirstName: "", + LastName: "", + } + if okmdl { + if val, ok := rtenv.Users.Accounts[mdlUsr.User]; ok { + if len(pass) == 0 { + c.JSON(http.StatusOK, gin.H{"auth": "?"}) + return + } + txtdata := "" + txtdata,err = decrypt(val.Passwd, string(CRYPTKEY)) + if txtdata != pass { + c.JSON(http.StatusOK, gin.H{"auth": "?"}) + return + } + } + hasRole,_ = rtenv.Enforcer.HasRoleForUser(mdlUsr.User, role) + data.UserName = mdlUsr.User + data.Data = mdlUsr.Data + data.FirstName = id + logRoute(c,rtenv,"auth",fmt.Sprintf("get %s %s", rtenv.Cfg.Routes["auth"].Path,id),fmt.Sprintf("get %s (%s %v) %s",mdlUsr.User,role,hasRole,tkn)) + } else { + logRoute(c,rtenv,"auth",fmt.Sprintf("get %s %s", rtenv.Cfg.Routes["auth"].Path,id),fmt.Sprintf("get %s (%s %v) %s","-",role,hasRole,tkn)) + } + if rtenv.Cfg.UseJWT { + if rtenv.AuthMiddleware == nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to auth"}) + return + } + tkn,err = makeTokenString(rtenv,data) + if err != nil { + fmt.Printf("tkn err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to auth"}) + return + } + fmt.Printf("tkn: %+v\n", tkn) + } + if rtenv.Cfg.UseJWT { + c.JSON(http.StatusOK, gin.H{"auth": tkn, "user": mdlUsr.User, "model": mdlUsr.Model, "data": mdlUsr.Data, "hasrole": hasRole}) + } else { + c.JSON(http.StatusOK, gin.H{"pass": pass, "user": mdlUsr.User, "model": mdlUsr.Model, "data": mdlUsr.Data, "hasrole": hasRole}) + } +} + +func get_auth_refresh_handle(c *gin.Context, rtenv *RouteEnv) { + // token,expire,err := refreshToken(c,rtenv,data interface{}) (string, time.Time, error) { + claims, err := rtenv.AuthMiddleware.CheckIfTokenExpire(c) + if err != nil { + fmt.Printf("Error Refresh token: %v\n",err) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Token is expire"}) + return + } + var tkn string + data := &User{ + UserName: "", + UUID: "", + Data: "", + FirstName: "", + LastName: "", + } + fmt.Printf("Refresh token: %v\n",claims) + if val, ok := claims["id"]; ok { + data.UserName = fmt.Sprintf("%s",val) + } + if val, ok := claims["uuid"]; ok { + data.UUID = fmt.Sprintf("%s",val) + } + if val, ok := claims["data"]; ok { + data.Data = fmt.Sprintf("%s",val) + } + tkn,err = makeTokenString(rtenv,data) + if err != nil { + fmt.Printf("tkn err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to auth"}) + return + } + fmt.Printf("tkn: %+v\n", tkn) + c.JSON(http.StatusOK, gin.H{"auth": tkn, "user": data.UserName, "uuid": data.UUID, "data": data.Data }) +} \ No newline at end of file diff --git a/handlers_config.go b/handlers_config.go new file mode 100644 index 0000000..266e03e --- /dev/null +++ b/handlers_config.go @@ -0,0 +1,82 @@ +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + utils "github.com/jesusperez/datautils" + "gopkg.in/yaml.v2" +) + +func get_config_handle(c *gin.Context, rtenv *RouteEnv) { + idusr := rtenv.AuthMiddleware.IdentityHandler(c) + user, okusr := idusr.(*User) + if !okusr || len(user.UserName) == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + role := rtenv.Cfg.AdminRole + if _,res := user_has_role(user,c,rtenv,role); !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + path := fmt.Sprintf("%s_new.yaml",strings.Replace(rtenv.RunFlags.cfgPath,".yaml","",-1)) + if utils.ExistsPath(path) { + config,err := loadConfig(path) + if err != nil { + logRoute(c,rtenv,"config",fmt.Sprintf("get /config error %v",err),fmt.Sprintf("get config_new %s (%s)",user.UserName,role)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load config_new"}) + return + } + logRoute(c,rtenv,"config","get /config",fmt.Sprintf("get config_new %s (%s)",user.UserName,role)) + c.JSON(http.StatusOK, config) + } else { + logRoute(c,rtenv,"config","get /config",fmt.Sprintf("get config %s (%s)",user.UserName,role)) + c.JSON(http.StatusOK, rtenv.Cfg) + } +} + +func post_config_handle(c *gin.Context, rtenv *RouteEnv) { + var dataconfig ConfigPostData + role := rtenv.Cfg.AdminRole + err := c.BindJSON(&dataconfig) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save config"}) + return + } + if len(dataconfig.Id) == 0 || len(dataconfig.Config.Routes) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"config": "error config"}) + return + } + // roles,_ := enforcer.GetRolesForUser(user) + hasRole,_ := rtenv.Enforcer.HasRoleForUser(dataconfig.Id, role) + fmt.Printf("%s (%s) %+v\n",dataconfig.Id, role, hasRole) + if !hasRole { + logRoute(c,rtenv,"post_config",fmt.Sprintf("post %s: has no role",dataconfig.Id), fmt.Sprintf("post %s: %s",dataconfig.Id,dataconfig.Val)) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + data, err := yaml.Marshal(dataconfig.Config) + if err != nil { + logRoute(c,rtenv,"post_config",fmt.Sprintf("post %s: error %v ",dataconfig.Id,err), fmt.Sprintf("post %s: %s",dataconfig.Id,dataconfig.Val)) + fmt.Printf("Error Parse dataconfig %s (%s): %v\n", dataconfig.Id, dataconfig.Val, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save config"}) + return + } + path := fmt.Sprintf("%s.yaml",dataconfig.Val) + if path == rtenv.RunFlags.cfgPath { + path = fmt.Sprintf("%s_new.yaml",dataconfig.Val) + } + if res := utils.WriteData(string(data),path) ; res != nil { + logRoute(c,rtenv,"post_config",fmt.Sprintf("post %s: write error %v ",dataconfig.Id,err), fmt.Sprintf("post %s: %s",dataconfig.Id,dataconfig.Val)) + fmt.Printf("Error write config from %s to %s: %v\n", dataconfig.Id, path, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save config"}) + } else { + fmt.Printf("Config from %s writed to: %s\n", dataconfig.Id, path) + logRoute(c,rtenv,"post_config",fmt.Sprintf("post %s: %s",dataconfig.Id,dataconfig.Val), fmt.Sprintf("post %s: %s",dataconfig.Id,dataconfig.Val)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + } +} \ No newline at end of file diff --git a/handlers_mng_users.go b/handlers_mng_users.go new file mode 100644 index 0000000..7dcded5 --- /dev/null +++ b/handlers_mng_users.go @@ -0,0 +1,347 @@ +package main + +import ( + "bytes" + b64 "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + utils "github.com/jesusperez/datautils" + "gopkg.in/yaml.v2" +) +func send_invitation_handle(c *gin.Context, rtenv *RouteEnv) { + role := rtenv.Cfg.AdminRole + user,res := user_has_role(nil,c,rtenv,role) + if !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + var sendData PostInvitation + err := c.BindJSON(&sendData) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + if len(sendData.Data.Email) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"error": "no data found"}) + return + } + accept_langs := strings.Split(c.Request.Header.Get("Accept-Language"),";") + langs := strings.Split(accept_langs[0], ",") + tplPath := fmt.Sprintf("%s/%s_%s",rtenv.Cfg.TplsMailPath,langs[0],rtenv.Cfg.TplsMail["newuser"].Path) + if ! utils.ExistsPath(tplPath) { + tplPath = fmt.Sprintf("%s/%s",rtenv.Cfg.TplsMailPath,rtenv.Cfg.TplsMail["newuser"].Path) + if ! utils.ExistsPath(tplPath) { + c.JSON(http.StatusNotAcceptable, gin.H{"error": "no template found"}) + return + } + } + target := rtenv.Cfg.MailFrom + bodyData := sendData + if len(sendData.Data.Expire) > 0 { + bodyData.Data.Expire = fmt.Sprintf("Expire: %s",sendData.Data.Expire) + } + request := Mail{ + Sender: target, + To: []string{sendData.Data.Email}, + Cc: []string{}, + Bcc: []string{}, + Subject: fmt.Sprintf("Invitation "), + Body: bytes.Buffer{}, + } + err = email(rtenv.Cfg, "text", tplPath, "", "", request,bodyData) + if err == nil { + fmt.Printf("Invitation send from (%s): %s\n",user.UserName, target) + logRoute(c,rtenv,"send_inivitation",fmt.Sprintf("get %s: %s",user.UserName,target), fmt.Sprintf("get %s: %s",user.UserName,target)) + c.JSON(http.StatusOK, sendData) + } else { + fmt.Printf("Error send invitation from (%s) %s: %v\n", user.UserName, target, err) + logRoute(c,rtenv,"send_invitation",fmt.Sprintf("get %s: read error %v ",user.UserName,err), fmt.Sprintf("get %s: %s",user.UserName,target)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send invitation"}) + } +} +func get_invitation_handle(c *gin.Context, rtenv *RouteEnv) { + id := c.Params.ByName(rtenv.Cfg.Routes["invitation"].Param) + usrsInvitations,err := getUsersInvitations(rtenv.Cfg.InvitationsPath,true) + if err != nil { + logRoute(c,rtenv,"users",fmt.Sprintf("get /invitation error %v",err),fmt.Sprintf("get invitation %s",id)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to get invitation %s",id)}) + return + } + if val, ok := usrsInvitations[id]; ok { + invitation, valid, needSync := checkUserInvitation(id, usrsInvitations[id], 1) + if (needSync) { + allInvitations, err := loadUsersInvitations(rtenv.Cfg.InvitationsPath) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to validate invitation %s",id)}) + return + } + allInvitations[id]=invitation + strdata, yamlerr := yaml.Marshal(allInvitations) + if yamlerr != nil { + fmt.Printf("Error parse invitations yaml %s: %v",rtenv.Cfg.InvitationsPath,yamlerr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to sync invitations %s",id)}) + return + } + if res := utils.WriteData(string(strdata),rtenv.Cfg.InvitationsPath) ; res != nil { + logRoute(c,rtenv,"post_users",fmt.Sprintf("Error write invitations: %s",id), fmt.Sprintf("error: %+v",res)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to sync invitation %s",id)}) + return + } + } + if !valid { + logRoute(c,rtenv,"users",fmt.Sprintf("get /invitation error %v",err),fmt.Sprintf("get invitation not valid %s",id)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to validate invitation %s",id)}) + return + } + tkn := "" + if rtenv.Cfg.UseAuthz { + data := &User{ + UserName: val.Email, + UUID: id, + Data: "Invitation", + FirstName: "", + LastName: "", + } + tkn,err = makeTokenString(rtenv,data) + if err != nil { + fmt.Printf("tkn err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to auth"}) + return + } + } + logRoute(c,rtenv,"users",fmt.Sprintf("get /invitation %s",id),fmt.Sprintf("get invitation %s (%s) %s",val.Email,val.Role,val.Data)) + data := NewUserData{ Email: val.Email, Role: val.Role, Data: val.Data } + strdata,err := json.Marshal(data) + if err == nil { + c.JSON(http.StatusOK, gin.H{"data": b64.StdEncoding.EncodeToString(strdata),"auth": tkn}) + return + } else { + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to confirm invitation %s",id)}) + return + } + } else { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invitation not found"}) + } +} +func get_users_handle(c *gin.Context, rtenv *RouteEnv) { + idusr := rtenv.AuthMiddleware.IdentityHandler(c) + user, okusr := idusr.(*User) + if !okusr || len(user.UserName) == 0 { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + role := rtenv.Cfg.AdminRole + if _,res := user_has_role(user,c,rtenv,role); !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + usersData := &UsersData { + UsersData: "", + ModelsData: "", + Invitations: "", + AuthzModel: "", + AuthzPolicy: "", + } + if utils.ExistsPath(rtenv.Cfg.UsersPath) { + data,err := loadPath(rtenv.Cfg.UsersPath) + if err != nil { + logRoute(c,rtenv,"config",fmt.Sprintf("get /users error %v",err),fmt.Sprintf("get userspath %s (%s)",user.UserName,role)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load users data"}) + return + } + usersData.UsersData = b64.StdEncoding.EncodeToString(data) + } + if utils.ExistsPath(rtenv.Cfg.UsersModelsPath) { + data,err := loadPath(rtenv.Cfg.UsersModelsPath) + if err != nil { + logRoute(c,rtenv,"config",fmt.Sprintf("get /users error %v",err),fmt.Sprintf("get usersmodels %s (%s)",user.UserName,role)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load users models"}) + return + } + usersData.ModelsData = b64.StdEncoding.EncodeToString(data) + } + if utils.ExistsPath(rtenv.Cfg.InvitationsPath) { + data,err := loadPath(rtenv.Cfg.InvitationsPath) + if err != nil { + logRoute(c,rtenv,"config",fmt.Sprintf("get /users error %v",err),fmt.Sprintf("get invitations %s (%s)",user.UserName,role)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load users invitations"}) + return + } + usersData.Invitations = b64.StdEncoding.EncodeToString(data) + } + if rtenv.Cfg.UseAuthz { + if utils.ExistsPath(rtenv.Cfg.AuthzModel) { + data,err := loadPath(rtenv.Cfg.AuthzModel) + if err != nil { + logRoute(c,rtenv,"config",fmt.Sprintf("get /users error %v",err),fmt.Sprintf("get authzmodels %s (%s)",user.UserName,role)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load authz models"}) + return + } + usersData.AuthzModel = b64.StdEncoding.EncodeToString(data) + } + if utils.ExistsPath(rtenv.Cfg.AuthzPolicy) { + data,err := loadPath(rtenv.Cfg.AuthzPolicy) + if err != nil { + logRoute(c,rtenv,"config",fmt.Sprintf("get /users error %v",err),fmt.Sprintf("get authzpolizy %s (%s)",user.UserName,role)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load authz policy"}) + return + } + usersData.AuthzPolicy = b64.StdEncoding.EncodeToString(data) + } + } + logRoute(c,rtenv,"config","get /users",fmt.Sprintf("get users %s (%s)",user.UserName,role)) + c.JSON(http.StatusOK, gin.H{"data": usersData}) +} +func save_users_data(saveInfo SaveDataInfo,c *gin.Context,rtenv *RouteEnv) bool { + path := fmt.Sprintf("%s%s",saveInfo.Prfx, saveInfo.Path) + data,err := b64.StdEncoding.DecodeString(saveInfo.Data) + if err != nil { + fmt.Printf("Error save %s: %v",path,err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save %s",saveInfo.Title)}) + return false + } + var accounts map[string]Account + err = yaml.Unmarshal([]byte(data), &accounts) + if err != nil { + fmt.Printf("Error parse yaml %s: %v",path,err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save %s",saveInfo.Title)}) + return false + } + prfx := rtenv.Cfg.PasswdEnc + var allUsers = map[string]Account{} + for key, item := range accounts { + if strings.HasPrefix(item.Passwd,"enc:") { + txtPasswd := strings.Replace(item.Passwd,prfx,"",-1) + passwd,err := encrypt(txtPasswd, string(CRYPTKEY)) + if err == nil { + item.Passwd = passwd + allUsers[key] = item + } + } else { + allUsers[key] = item + } + } + strdata, yamlerr := yaml.Marshal(allUsers) + if yamlerr != nil { + fmt.Printf("Error parse yaml %s: %v",path,yamlerr) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save %s",saveInfo.Title)}) + return false + } + if res := utils.WriteData(string(strdata),path) ; res != nil { + logRoute(c,rtenv,"post_users",fmt.Sprintf("Error post %s: %s",saveInfo.Title,saveInfo.Id), fmt.Sprintf("error: %+v",res)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save %s",saveInfo.Title)}) + return false + } + return true +} +func save_file_data(saveInfo SaveDataInfo,c *gin.Context,rtenv *RouteEnv) bool { + path := fmt.Sprintf("%s%s",saveInfo.Prfx, saveInfo.Path) + data,err := b64.StdEncoding.DecodeString(saveInfo.Data) + if err != nil { + fmt.Printf("Error save %s: %v",path,err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save %s",saveInfo.Title)}) + return false + } + if res := utils.WriteData(string(data),path) ; res != nil { + logRoute(c,rtenv,"post_users",fmt.Sprintf("Error post %s: %s",saveInfo.Title,saveInfo.Id), fmt.Sprintf("error: %+v",res)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save %s",saveInfo.Title)}) + return false + } + return true +} +func post_users_handle(c *gin.Context, rtenv *RouteEnv) { + var postdata UsersDataPost + role := rtenv.Cfg.AdminRole + err := c.BindJSON(&postdata) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save users"}) + return + } + if len(postdata.Id) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"info": "error users"}) + return + } + hasRole,_ := rtenv.Enforcer.HasRoleForUser(postdata.Id,role) + // fmt.Printf("post users: %s (%s) %+v\n",postdata.Id, role, hasRole) + if !hasRole { + logRoute(c,rtenv,"post_users",fmt.Sprintf("post %s: has no role",postdata.Id), fmt.Sprintf("post %s",postdata.Id)) + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + savedList := "" + if len(postdata.Data.UsersData) > 0 { + saveInfo := SaveDataInfo{ + Title: "users", + Data: postdata.Data.UsersData, + Id: postdata.Id, + Path: rtenv.Cfg.UsersPath, + Prfx: "new_", + } + if !save_users_data(saveInfo,c,rtenv) { + return + } + savedList = "usersData" + } + if len(postdata.Data.ModelsData) > 0 { + saveInfo := SaveDataInfo{ + Title: "users models", + Data: postdata.Data.ModelsData, + Id: postdata.Id, + Path: rtenv.Cfg.UsersModelsPath, + Prfx: "new_", + } + if !save_file_data(saveInfo,c,rtenv) { + return + } + savedList = fmt.Sprintf("%s,modelsData",savedList) + } + if len(postdata.Data.Invitations) > 0 { + saveInfo := SaveDataInfo{ + Title: "users invitations", + Data: postdata.Data.Invitations, + Id: postdata.Id, + Path: rtenv.Cfg.InvitationsPath, + Prfx: "new_", + } + if !save_file_data(saveInfo,c,rtenv) { + return + } + savedList = fmt.Sprintf("%s,invitations",savedList) + } + if rtenv.Cfg.UseAuthz { + if len(postdata.Data.AuthzModel) > 0 { + saveInfo := SaveDataInfo{ + Title: "users authz models", + Data: postdata.Data.AuthzModel, + Id: postdata.Id, + Path: rtenv.Cfg.AuthzModel, + Prfx: "new_", + } + if !save_file_data(saveInfo,c,rtenv) { + return + } + savedList = fmt.Sprintf("%s,authzModels",savedList) + } + if len(postdata.Data.AuthzPolicy) > 0 { + saveInfo := SaveDataInfo{ + Title: "users authz policy", + Data: postdata.Data.AuthzPolicy, + Id: postdata.Id, + Path: rtenv.Cfg.AuthzPolicy, + Prfx: "new_", + } + if !save_file_data(saveInfo,c,rtenv) { + return + } + savedList = fmt.Sprintf("%s,authzPolicy",savedList) + } + } + logRoute(c,rtenv,"post_users",fmt.Sprintf("post %s: users",postdata.Id), fmt.Sprintf("post %s: %s",postdata.Id,savedList)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) +} diff --git a/handlers_tracking.go b/handlers_tracking.go new file mode 100644 index 0000000..68fd04c --- /dev/null +++ b/handlers_tracking.go @@ -0,0 +1,153 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + cfg "github.com/jesusperez/cfgsrv" + utils "github.com/jesusperez/datautils" +) +func get_tracking_list_handle(c *gin.Context, rtenv *RouteEnv) { + role := rtenv.Cfg.AdminRole + user,res := user_has_role(nil,c,rtenv,role) + if !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + var trackingData []string + result := false + path := "" + var err error + switch (rtenv.Cfg.TrackingStore) { + case cfg.DsRedis: + path = "redis" + // TODO send to redis client + case cfg.DsFile: + path = rtenv.Cfg.TrackingOut + trackingData = getFileMatch(path,".json",false,false) + result = true + } + if result { + fmt.Printf("Tracking actions from (%s): %s\n",user.UserName, path) + logRoute(c,rtenv,"get_tracking",fmt.Sprintf("get %s: %s",user.UserName,path), fmt.Sprintf("get %s: %s",user.UserName,path)) + // c.JSON(http.StatusOK, gin.H{"data": trackingData}) + c.JSON(http.StatusOK, trackingData) + } else { + logRoute(c,rtenv,"get_tracking",fmt.Sprintf("get %s: read error %v ",user.UserName,err), fmt.Sprintf("get %s: %s",user.UserName,path)) + fmt.Printf("Error get tracking from (%s) %s: %v\n", user.UserName, path, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read tracking list"}) + } +} +func get_tracking_handle(c *gin.Context, rtenv *RouteEnv) { + role := rtenv.Cfg.AdminRole + user,res := user_has_role(nil,c,rtenv,role) + if !res { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + target := c.Params.ByName(rtenv.Cfg.Routes["tracking"].Param) + var trackingData []TrackAction + result := false + path := "" + var err error + switch (rtenv.Cfg.TrackingStore) { + case cfg.DsRedis: + path = "redis" + // TODO send to redis client + case cfg.DsFile: + path = fmt.Sprintf("%s/%s.json",rtenv.Cfg.TrackingOut,target) + var data []byte + data, err = loadPath(path) + if err == nil { + err = json.Unmarshal([]byte(fmt.Sprintf("[%s{}]",string(data))), &trackingData) + if err == nil { + result = true + } + } + } + if result { + fmt.Printf("Tracking actions from (%s): %s\n",user.UserName, path) + logRoute(c,rtenv,"get_tracking",fmt.Sprintf("get %s: %s",user.UserName,path), fmt.Sprintf("get %s: %s",user.UserName,path)) + // c.JSON(http.StatusOK, gin.H{"data": trackingData}) + c.JSON(http.StatusOK, trackingData) + } else { + logRoute(c,rtenv,"get_tracking",fmt.Sprintf("get %s: read error %v ",user.UserName,err), fmt.Sprintf("get %s: %s",user.UserName,path)) + fmt.Printf("Error get tracking from (%s) %s: %v\n", user.UserName, path, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to read tracking"}) + } +} + +func post_tracking_handle(c *gin.Context, rtenv *RouteEnv) { + var trackpost PostTrackAction + err := c.BindJSON(&trackpost) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + if len(trackpost.Data) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"track": "error data"}) + return + } + userid := trackpost.Data + _, okmdl := rtenv.MdlsUsrs[userid] + if !okmdl { + _, okusr := rtenv.Users.Accounts[userid] + if !okusr { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + } + auth := "" + if rtenv.Cfg.UseJWT { + tk, errtk := rtenv.AuthMiddleware.ParseToken(c) + if errtk == nil { + auth = fmt.Sprintf("%v",tk.Raw) + } + } + trackaction := &TrackAction{ + When: trackpost.When, + Where: trackpost.Where, + What: trackpost.What, + Context: trackpost.Context, + Data: trackpost.Data, + Auth: auth, + } + data,err := json.Marshal(trackaction) + if err != nil { + logRoute(c,rtenv,"post_trackng",fmt.Sprintf("post %s: error %v ",trackpost.Data,err), fmt.Sprintf("post %s",userid)) + fmt.Printf("Error Parse tracking %s: %v\n", trackpost.Data, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save tracking"}) + return + } + result := false + path := "" + switch (rtenv.Cfg.TrackingStore) { + case cfg.DsRedis: + path = "redis" + // TODO send to redis client + case cfg.DsFile: + path = fmt.Sprintf("%s/%s.json",rtenv.Cfg.TrackingOut,time.Now().Format("2006_01_02")) + err := utils.CheckDirPath(fmt.Sprintf("%s",rtenv.Cfg.TrackingOut)) + if err != nil { + log.Fatalf("setReqLog path %s: %v",rtenv.Cfg.TrackingOut,err) + } + err = utils.WriteAppendData(fmt.Sprintf("%s,",string(data)),path) + if err == nil { + result = true + } + } + if result { + fmt.Printf("Track action from %s writed to: %s\n", userid, path) + logRoute(c,rtenv,"post_tracking",fmt.Sprintf("post %s: %s",userid,path), fmt.Sprintf("post %s: %s",userid,path)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + } else { + logRoute(c,rtenv,"post_tracking",fmt.Sprintf("post %s: write error %v ",userid,err), fmt.Sprintf("post %s: %s",userid,path)) + fmt.Printf("Error write tracking from %s to %s: %v\n", userid, path, err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save tracking"}) + } +} diff --git a/handlers_user.go b/handlers_user.go new file mode 100644 index 0000000..2543450 --- /dev/null +++ b/handlers_user.go @@ -0,0 +1,238 @@ +package main + +import ( + "bytes" + b64 "encoding/base64" + "fmt" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/google/uuid" + + cvdata "github.com/jesusperez/cvdata" + utils "github.com/jesusperez/datautils" + "gopkg.in/yaml.v2" +) +func create_password(val string) (string,error) { + txtVal,err := b64.StdEncoding.DecodeString(val) + if err != nil { + return "",err + } + pswd,err := encrypt(string(txtVal), string(CRYPTKEY)) + if err != nil { + return "",err + } + return pswd,nil +} +func write_accounts(newAccounts map[string]Account,rtenv *RouteEnv) error { + strdata, err := yaml.Marshal(newAccounts) + if err != nil { + return err + } + if res := utils.WriteData(string(strdata),rtenv.Cfg.UsersPath) ; res != nil { + return res + } + acc,err := loadUsersAccounts(rtenv.Cfg.UsersPath) + if err != nil { + return err + } else { + rtenv.Users = acc + } + return nil +} +func get_recoveryaccess_handle(c *gin.Context, rtenv *RouteEnv) { + // fmt.Printf("context: %+v\n", c) + target := c.Params.ByName(rtenv.Cfg.Routes["recoveryaccess"].Param) + if target == "-" { + target = "main" + } + var user Account + if val, ok := rtenv.Users.Accounts[target]; ok { + user = val + } else { + fmt.Printf("Error Recovery access for (%s) %s\n", user.Id, target) + logRoute(c,rtenv,"Recovery access send_invitation",fmt.Sprintf("get %s: read error",user.Id), fmt.Sprintf("get %s: %s",user.Id,target)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery request"}) + return + } + uuid := uuid.NewString() + uuid = strings.Replace(uuid,"-","",-1) + appDB[uuid]=AppData{ + Data: user, + Expire: time.Now().Add(5*time.Minute).Format(time.RFC3339), + } + reqData := UsersRecover{ + Id: uuid, + Data: user, + } + accept_langs := strings.Split(c.Request.Header.Get("Accept-Language"),";") + langs := strings.Split(accept_langs[0], ",") + tplPath := fmt.Sprintf("%s/%s_%s",rtenv.Cfg.TplsMailPath,langs[0],rtenv.Cfg.TplsMail["recovery"].Path) + if ! utils.ExistsPath(tplPath) { + tplPath = fmt.Sprintf("%s/%s",rtenv.Cfg.TplsMailPath,rtenv.Cfg.TplsMail["recovery"].Path) + if ! utils.ExistsPath(tplPath) { + c.JSON(http.StatusNotAcceptable, gin.H{"error": "no template found"}) + return + } + } + request := Mail{ + Sender: rtenv.Cfg.MailFrom, + To: []string{user.Email}, + Cc: []string{}, + Bcc: []string{}, + Subject: fmt.Sprintf("Recovery access"), + Body: bytes.Buffer{}, + } + err := email(rtenv.Cfg, "text", tplPath, "", "", request,reqData) + if err == nil { + fmt.Printf("Recovery access for (%s): %s\n",user.Id, target) + logRoute(c,rtenv,"Recovery access",fmt.Sprintf("get %s: %s",user.Id,target), fmt.Sprintf("get %s: %s",user.Id,target)) + c.JSON(http.StatusOK, gin.H{"received": "ok"}) + } else { + fmt.Printf("Error Recovery access for (%s) %s: %v\n", user.Id, target, err) + logRoute(c,rtenv,"Recovery access send_invitation",fmt.Sprintf("get %s: read error %v ",user.Id,err), fmt.Sprintf("get %s: %s",user.Id,target)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery request"}) + } +} +func post_recoveryaccess_handle(c *gin.Context, rtenv *RouteEnv) { + var postdata UsersRecoveryPost + err := c.BindJSON(&postdata) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery user"}) + return + } + if len(postdata.Id) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"info": "error recovery user"}) + return + } + valid := false + var user Account + if data, ok := appDB[postdata.Id]; ok { + user = data.Data.(Account) + res,err := time.Parse(time.RFC3339,data.Expire) + if err == nil { + if res.After(time.Now()) { + valid = true + } + } + } + if !valid { + fmt.Printf("Error Recovery access for %s\n", user.Id) + logRoute(c,rtenv,"Recovery access send_invitation",fmt.Sprintf("get %s: read error %v ",user.Id,err), fmt.Sprintf("get %s",user.Id)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery request"}) + return + } + accounts,err := loadUsersData(rtenv.Cfg.UsersPath) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery user"}) + return + } + if account, ok := accounts[user.Id]; ok { + newAccounts := accounts + pswd,err := create_password(postdata.Val) + if err != nil { + fmt.Printf("Error data: %v",err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed user data %s",user.Id)}) + } + account.Passwd = pswd + newAccounts[user.Id] = account + errwrite := write_accounts(newAccounts,rtenv) + if err != nil { + logRoute(c,rtenv,"post_recoveryaccess",fmt.Sprintf("Error post save recoveryaccess %s",user.Id), fmt.Sprintf("error: %+v",errwrite)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save user data")}) + return + } + delete(appDB,postdata.Id) + logRoute(c,rtenv,"post_recoveryaccess",fmt.Sprintf("post %s: users",postdata.Id), fmt.Sprintf("post recovery access: %s",user.Id)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + } else { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery user"}) + return + } +} +func post_newuser_handle(c *gin.Context, rtenv *RouteEnv) { + var postdata NewUserData + err := c.BindJSON(&postdata) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + fmt.Printf("postdata: %+v\n", postdata) + if len(postdata.Username) == 0 || len(postdata.Passwd) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"info": "error info"}) + return + } + pswd, err := create_password(postdata.Passwd) + if err != nil { + fmt.Printf("Error data: %v",err) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed user data %s",postdata.Username)}) + } + newUser := Account{ + Username: postdata.Username, + Email: postdata.Email, + Passwd: pswd, + Data: postdata.Username, + Web: true, + Id: postdata.Username, + } + newAccounts,err := loadUsersData(rtenv.Cfg.UsersPath) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed recovery user"}) + return + } + newAccounts[postdata.Username] = newUser + errwrite := write_accounts(newAccounts,rtenv) + if err != nil { + logRoute(c,rtenv,"post_newuser",fmt.Sprintf("Error post save newuser %s",postdata.Username), fmt.Sprintf("error: %+v",errwrite)) + c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to save user data")}) + return + } + logRoute(c,rtenv,"post_newuser",fmt.Sprintf("post newuser: %s",postdata.Username), fmt.Sprintf("post newuser: %s",postdata.Username)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + // c.IndentedJSON(http.StatusCreated, cvdata) +} + +func post_usermodel_handle(c *gin.Context, rtenv *RouteEnv) { + var cvpost cvdata.CVPostData + role := rtenv.Cfg.AdminRole + err := c.BindJSON(&cvpost) + if err != nil { + fmt.Printf("err: %+v\n", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + if cvpost.U == "" || len(cvpost.Data) == 0 { + c.JSON(http.StatusNotAcceptable, gin.H{"info": "error info"}) + return + } + // roles,_ := enforcer.GetRolesForUser(user) + hasRole,_ := rtenv.Enforcer.HasRoleForUser(cvpost.U, role) + fmt.Printf("%s (%s) %+v\n",cvpost.U, role, hasRole) + if !hasRole { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication failed"}) + return + } + keys,res := cvpost.Data.Write(rtenv.Cfg) + if res != nil { + logRoute(c,rtenv,"post_newuser",fmt.Sprintf("Error post %s: %s",cvpost.U,keys), fmt.Sprintf("error: %+v",res)) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save info"}) + return + } + if rtenv.Cfg.GenDist { + errModel := createRootModels(rtenv.Cfg) + if errModel != nil { + fmt.Printf("Error createRootModels: %v\n",errModel) + } + } + logRoute(c,rtenv,"post_newuser",fmt.Sprintf("post %s: %s",cvpost.U,keys), fmt.Sprintf("post %s: %s",cvpost.U,keys)) + c.JSON(http.StatusOK, gin.H{"status": "ok"}) + // c.IndentedJSON(http.StatusCreated, cvdata) +} + diff --git a/jwt.go b/jwt.go new file mode 100644 index 0000000..9b11480 --- /dev/null +++ b/jwt.go @@ -0,0 +1,183 @@ +package main + +import ( + b64 "encoding/base64" + "io/ioutil" + "time" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/gin-gonic/gin" + gojwt "github.com/golang-jwt/jwt/v4" + log "github.com/sirupsen/logrus" +) + +// func makeTokenString(SigningAlgorithm string, username string, cfg *Config) string { +// if SigningAlgorithm == "" { +// SigningAlgorithm = "HS256" +// } +// token := jwt.New(jwt.GetSigningMethod(SigningAlgorithm)) +// claims := token.Claims.(jwt.MapClaims) +// claims["identity"] = username +// claims["exp"] = time.Now().Add(time.Hour).Unix() +// claims["orig_iat"] = time.Now().Unix() +// var tokenString string +// if SigningAlgorithm == "RS256" { +// keyData, _ := ioutil.ReadFile(cfg.KeyPem) +// signKey, _ := jwt.ParseRSAPrivateKeyFromPEM(keyData) +// tokenString, _ = token.SignedString(signKey) +// } else { +// tokenString, _ = token.SignedString(key) +// } +// return tokenString +// } +// func makeTokenString(mw *jwt.GinJWTMiddleware,data interface{}) (string,error) { +func makeTokenString(rtenv *RouteEnv,data interface{}) (string,error) { + if rtenv.AuthMiddleware == nil { + return "", nil + } + token := gojwt.New(gojwt.GetSigningMethod(rtenv.AuthMiddleware.SigningAlgorithm)) + claims := token.Claims.(gojwt.MapClaims) + if rtenv.AuthMiddleware.PayloadFunc != nil { + for key, value := range rtenv.AuthMiddleware.PayloadFunc(data) { + claims[key] = value + } + } + expire := rtenv.AuthMiddleware.TimeFunc().Add(rtenv.AuthMiddleware.Timeout) + claims["exp"] = expire.Unix() + claims["orig_iat"] = rtenv.AuthMiddleware.TimeFunc().Unix() + var tokenString string + var err error + if rtenv.Cfg.SigningAlgorithm == "RS256" { + // if mw.usingPublicKeyAlgo() { + keyData, _ := ioutil.ReadFile(rtenv.Cfg.JwtKeyPem) + signKey, _ := gojwt.ParseRSAPrivateKeyFromPEM(keyData) + tokenString, err = token.SignedString(signKey) + } else { + tokenString, err = token.SignedString(rtenv.AuthMiddleware.Key) + } + return tokenString, err +} +func getJwt(rtenv *RouteEnv)*jwt.GinJWTMiddleware { + // the jwt middleware + authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ + Realm: rtenv.Cfg.JwtRealm, + Key: []byte(rtenv.Cfg.JwtKey), + Timeout: time.Duration(rtenv.Cfg.JwtTimeout) * time.Minute, + MaxRefresh: time.Duration(rtenv.Cfg.JwtMaxRefresh) * time.Minute, + IdentityKey: rtenv.Cfg.IdentityKey, + PrivKeyFile: rtenv.Cfg.KeyPem, + PubKeyFile: rtenv.Cfg.CertPem, + PayloadFunc: func(data interface{}) jwt.MapClaims { + if v, ok := data.(*User); ok { + return jwt.MapClaims{ + rtenv.Cfg.IdentityKey: v.UserName, + "uuid": v.UUID, + "data": v.Data, + } + } + return jwt.MapClaims{} + }, + IdentityHandler: func(c *gin.Context) interface{} { + claims := jwt.ExtractClaims(c) + username := "" + name, okname := claims[rtenv.Cfg.IdentityKey].(string) + if (okname) { + username = name + } + return &User{ UserName: username } + }, + Authenticator: func(c *gin.Context) (interface{}, error) { + var loginVals Login + if err := c.ShouldBind(&loginVals); err != nil { + return "", jwt.ErrMissingLoginValues + } + userID := loginVals.Username + password := loginVals.Password + if val, ok := rtenv.Users.Accounts[userID]; ok { + pasw,_ := b64.StdEncoding.DecodeString(password) + pass := string(pasw) + txtdata,err := decrypt(val.Passwd, string(CRYPTKEY)) + if err == nil && txtdata == pass { + return &User{ + UserName: val.Id, + LastName: "", + FirstName: "", + }, nil + } + } + return nil, jwt.ErrFailedAuthentication + }, + Authorizator: func(data interface{}, c *gin.Context) bool { + if v, ok := data.(*User); ok { + if v.UserName == rtenv.Cfg.PubUser { + return true + } + if _, ok := rtenv.Users.Accounts[v.UserName]; ok { + return true + } else { + return false + } + } + return false + }, + Unauthorized: func(c *gin.Context, code int, message string) { + c.JSON(code, gin.H{ + "code": code, + "message": message, + }) + }, + // TokenLookup is a string in the form of ":" that is used + // to extract token from the request. + // Optional. Default value "header:Authorization". + // Possible values: + // - "header:" + // - "query:" + // - "cookie:" + // - "param:" + TokenLookup: "header: Authorization, query: token, cookie: jwt", + // TokenLookup: "query:token", + // TokenLookup: "cookie:token", + + // TokenHeadName is a string in the header. Default value is "Bearer" + TokenHeadName: "Bearer", + // TimeFunc provides the current time. You can override it to use another time value. This is useful for testing or if your server uses a different time zone than your tokens. + TimeFunc: time.Now, + }) + if err != nil { + log.Fatal("JWT Error:" + err.Error()) + } + return authMiddleware +} + +// func refreshToken(c *gin.Context, rtenv *RouteEnv,data interface{}) (string, time.Time, error) { +// var expire time.Time +// claims, err := rtenv.AuthMiddleware.CheckIfTokenExpire(c) +// if err != nil { +// return "", expire, err +// } +// fmt.Printf("Refresh token: %v\n",claims) +// var token string +// token,err = makeTokenString(rtenv.AuthMiddleware,data) +// expire = time.Now() + +// // // set cookie +// // if rtenv.AuthMiddleware.SendCookie { +// // expireCookie := rtenv.AuthMiddleware.TimeFunc().Add(rtenv.AuthMiddleware.CookieMaxAge) +// // maxage := int(expireCookie.Unix() - time.Now().Unix()) + +// // if rtenv.AuthMiddleware.CookieSameSite != 0 { +// // c.SetSameSite(rtenv.AuthMiddleware.CookieSameSite) +// // } + +// // c.SetCookie( +// // rtenv.AuthMiddleware.CookieName, +// // tokenString, +// // maxage, +// // "/", +// // rtenv.AuthMiddleware.CookieDomain, +// // rtenv.AuthMiddleware.SecureCookie, +// // rtenv.AuthMiddleware.CookieHTTPOnly, +// // ) +// // } +// return token, expire, nil +// } \ No newline at end of file diff --git a/loads.go b/loads.go new file mode 100644 index 0000000..106e105 --- /dev/null +++ b/loads.go @@ -0,0 +1,193 @@ +package main + +import ( + "fmt" + "time" + + cfg "github.com/jesusperez/cfgsrv" + utils "github.com/jesusperez/datautils" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) +func loadUsersData(usersPath string) (map[string]Account, error) { + var usersData map[string]Account + d,file,errpath := decoderFromFile(usersPath) + if errpath != nil { + return nil, errpath + } + defer file.Close() + if err := d.Decode(&usersData); err != nil { + log.Fatalf("Decode: %v", err) + return nil, err + } + return usersData, nil +} +func loadUsersAccounts(usersPath string) (*UsersAccounts, error) { + accounts,err := loadUsersData(usersPath) + if err != nil { + return nil, err + } + keyAccounts := accounts + for key, user := range accounts { + user.Id = key + keyAccounts[key] = user + if len(user.Email) > 0 { + accounts[user.Email] = user + } + } + users := &UsersAccounts{ + Accounts: accounts, + } + return users, nil +} +func loadWebAccounts(users *UsersAccounts) (*WebAccounts) { + webusers := &WebAccounts{} + for key, user := range users.Accounts { + if user.Web { + webusers.Accounts[key] = user.Passwd + } + } + return webusers +} +func loadModelsUsers(modelsPath string) (map[string]ModelUser, error) { + var mdlsUsers map[string]ModelUser + d,file,errpath := decoderFromFile(modelsPath) + if errpath != nil { + return nil, errpath + } + defer file.Close() + if err := d.Decode(&mdlsUsers); err != nil { + log.Fatalf("Decode: %v", err) + return nil, err + } + // fmt.Printf("users: %+v\n", users) + return mdlsUsers, nil +} +func checkUserInvitation(key string,invitation Invitation, num int) (Invitation,bool,bool) { + userInvitation := invitation + needSync := false + valid := true + if invitation.Howmany > -1 && num > 0 { + needSync = true + userInvitation.Howmany = invitation.Howmany - num + if userInvitation.Howmany < 0 { + userInvitation.Howmany = 0 + userInvitation.Active = false + valid = false + } + } + if len(invitation.Expire) > 0 { + res,err := time.Parse(time.RFC3339,invitation.Expire) + if err == nil { + if res.Before(time.Now()) { + needSync = true + userInvitation.Active = false + valid = false + } + } else { + fmt.Printf("Error parse invitation %s expire %s: %v",key,invitation.Expire,err) + } + } + return userInvitation,valid,needSync +} +func checkUsersInvitations(invitations map[string]Invitation,writeSync bool,invitationsPath string) map[string]Invitation { + usersInvitations := map[string]Invitation{} + savedInvitations := invitations + needSync := false + for key, item := range invitations { + if item.Active { + invitation,valid,itemSync := checkUserInvitation(key,item,0) + if (itemSync && !needSync) { + needSync = true + savedInvitations[key] = invitation + } + if (valid) { + usersInvitations[key] = invitation + } + } + } + if (writeSync && needSync) { + strdata, yamlerr := yaml.Marshal(savedInvitations) + if yamlerr != nil { + fmt.Printf("Error parse invitations: %v",yamlerr) + } else { + if res := utils.WriteData(string(strdata),invitationsPath) ; res != nil { + fmt.Printf("Error write invitations: %v",res) + } + } + } + return usersInvitations +} +func loadUsersInvitations(invitationsPath string) (map[string]Invitation, error) { + var invitations map[string]Invitation + d,file,errpath := decoderFromFile(invitationsPath) + if errpath != nil { + return nil, errpath + } + defer file.Close() + if err := d.Decode(&invitations); err != nil { + log.Fatalf("Decode: %v", err) + return nil, err + } + return invitations, nil +} +func getUsersInvitations(invitationsPath string,writeSync bool) (map[string]Invitation, error) { + invitations,err := loadUsersInvitations(invitationsPath) + if err != nil { + fmt.Printf("Error load invitations: %v",err) + return nil,err + } + return checkUsersInvitations(invitations,writeSync,invitationsPath), nil +} +func loadConfig(path string) (*cfg.Config, error) { + config := &cfg.Config{} + // fmt.Printf("Loaded config: %#v\n", path) + // if yamlFile, err := ioutil.ReadFile(path); err == nil { + // err = yaml.Unmarshal(yamlFile, config) + // if err != nil { + // log.Fatalf("Unmarshal: %v", err) + // } + // } + // var data interface{} = config + d,file,errpath := decoderFromFile(path) + if errpath != nil { + return nil, errpath + } + defer file.Close() + if err := d.Decode(&config); err != nil { + return nil, err + } + if len(config.MailPswd) > 0 { + txtdata,err := decrypt(config.MailPswd, string(CRYPTKEY)) + if err == nil { + config.MailPswd = txtdata + } + } + return config, nil +} +func newDataConfig(runFlags RunFlags) (*cfg.Config, error) { + config,err := loadConfig(runFlags.cfgPath) + if err != nil { + log.Fatalf("Decode: %v", err) + return nil, err + } + DEBUGLEVEL = config.DebugLevel + return config, nil +} + +func loadDataConfig(runFlags RunFlags) (*cfg.Config, error) { + cfg, err := newDataConfig(runFlags) + if err != nil { + log.Fatalf("Load data config: %v", err) + return cfg, err + } + return cfg, nil +} + +func loadcertkey(runFlags RunFlags) { + var err error + CRYPTKEY, err = loadPath(runFlags.keyPath) + if err != nil { + log.Fatalf("Error load key: %v", err) + } +} diff --git a/logo/srvcvgen_b.svg b/logo/srvcvgen_b.svg deleted file mode 100644 index 9534835..0000000 --- a/logo/srvcvgen_b.svg +++ /dev/null @@ -1 +0,0 @@ -Recurso 3genC V Srv \ No newline at end of file diff --git a/logo/srvcvgen_w.svg b/logo/srvcvgen_w.svg deleted file mode 100644 index 11c2fc6..0000000 --- a/logo/srvcvgen_w.svg +++ /dev/null @@ -1 +0,0 @@ -Recurso 4genC V Srv \ No newline at end of file diff --git a/logs.go b/logs.go new file mode 100644 index 0000000..debe824 --- /dev/null +++ b/logs.go @@ -0,0 +1,124 @@ +package main + +import ( + "fmt" + "net" + "net/http" + "os" + "strings" + "time" + + "github.com/gin-gonic/gin" + cfg "github.com/jesusperez/cfgsrv" + utils "github.com/jesusperez/datautils" + log "github.com/sirupsen/logrus" +) + +func setReqLog(cfg *cfg.Config) { + todayLog := fmt.Sprintf("%s/%s.json",cfg.RequestOut,time.Now().Format("2006_01_02")) + if todayLog == PathReqLog { + return + } else { + PathReqLog = todayLog + } + err := utils.CheckDirPath(fmt.Sprintf("%s",cfg.RequestOut)) + if err != nil { + log.Fatalf("setReqLog path %s: %v",cfg.RequestOut,err) + } + reqOut, err := os.OpenFile(PathReqLog, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + log.Fatalf("setReqLog: %v", err) + } + ReqLog = log.New() + ReqLog.SetOutput(reqOut) + ReqLog.SetFormatter(&log.JSONFormatter{ + PrettyPrint: false, + }) +} +func setLog(cfg *cfg.Config) { + environment := os.Getenv("ENVIRONMENT") + if environment == "production" { + log.SetFormatter(&log.JSONFormatter{}) + file, err := os.OpenFile(cfg.LogOut, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + log.Fatalf("Open LogOut: %v", err) + } + log.SetOutput(file) + // Only log the warning severity or above. + log.SetLevel(log.WarnLevel) + } else { + // The TextFormatter is default, you don't actually have to do this. + log.SetFormatter(&log.TextFormatter{}) + log.SetOutput(os.Stdout) + } + log.SetFormatter(&log.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + }) +} + +func getIP(r *http.Request) (string, error) { + //Get IP from the X-REAL-IP header + ip := r.Header.Get("X-REAL-IP") + netIP := net.ParseIP(ip) + if netIP != nil { + return ip, nil + } + //Get IP from X-FORWARDED-FOR header + ips := r.Header.Get("X-FORWARDED-FOR") + splitIps := strings.Split(ips, ",") + for _, ip := range splitIps { + netIP := net.ParseIP(ip) + if netIP != nil { + return ip, nil + } + } + //Get IP from RemoteAddr + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return "", err + } + netIP = net.ParseIP(ip) + if netIP != nil { + return ip, nil + } + return "", fmt.Errorf("No valid ip found") +} + +func logRequest(c *gin.Context, rtenv *RouteEnv, run string, route string, info string, infoReq string, tkn string) { + accept_langs := strings.Split(c.Request.Header.Get("Accept-Language"),";") + langs := strings.Split(accept_langs[0], ",") + ip, _ := getIP(c.Request) + // header, _ := ioutil.ReadAll(c.Request.Header) + //println(string(c.header)) + // println(c.Header) + // ipp := c.Request.RemoteAddr + // xforward := c.Request.Header.Get("X-Forwarded-For") + // fm.Println("IP : ", ipp) + // fmt.Println("X-Forwarded-For : ", xforward) + // fmt.Println("IP : ", ip) + // // if err != nil { + // dt := time.Now() + // fmt.Println("Current date and time is: ", dt.String()) + // fmt.Println(dt.Format("01-02-2006 15:04:05")) + // fmt.Println(dt.Format(time.RFC3339Nano)) + if len(info) > 0 { + setReqLog(rtenv.Cfg) + ReqLog.WithFields(log.Fields{ + "run": run, + "route": route, + "lang": fmt.Sprintf("%+v",langs), + "agent": fmt.Sprintf("%+v",c.Request.UserAgent()), + "ip": ip, + "tkn": tkn, + }).Info(info) + } + if len(infoReq) > 0 { + log.WithFields(log.Fields{ + "run": "route", + "route": "/", + "ip": ip, + "tkn": tkn, + }).Info(infoReq) + } +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..37bb760 --- /dev/null +++ b/main.go @@ -0,0 +1,138 @@ +package main + +import ( + "embed" + "flag" + "fmt" + "os" + "strings" + + "github.com/google/uuid" + log "github.com/sirupsen/logrus" +) + +const VERSION = "0.0.1" +const SEPARATOR = "::" +const CFGPATH = "config.yaml" +const GENPATH = "dist" +const DFLTCOMMAND = "webservice" +const KEYPATH = ".k" + +var CRYPTKEY []byte +var DEBUGLEVEL int +var ReqLog *log.Logger +var PathReqLog string + +var f embed.FS +var appDB = make(map[string]AppData) + +func ParseFlags() (RunFlags,error) { + runFlags := RunFlags { + command: os.Getenv("CVGEN_COMMAND"), + cfgPath: os.Getenv("CVGEN_CFGPATH"), + genPath: os.Getenv("CVGEN_GENPATH"), + keyPath: os.Getenv("CVGEN_KEYPATH"), + } + if runFlags.command == "" { + runFlags.command = DFLTCOMMAND + } + if runFlags.cfgPath == "" { + runFlags.cfgPath = CFGPATH + } + if runFlags.keyPath == "" { + runFlags.keyPath = KEYPATH + } + if runFlags.genPath == "" { + runFlags.genPath = GENPATH + } + commandInfo := fmt.Sprintf("command to run [\n%s]", + "uuid, huuid, genkey, encrypt, decrypt, gendata, webservice") + flag.StringVar(&runFlags.command, "c", runFlags.command, commandInfo) + flag.StringVar(&runFlags.cfgPath, "f", runFlags.cfgPath, "path to config file") + flag.StringVar(&runFlags.modelName, "m", runFlags.modelName, "[gendata] model-name to generate json file") + flag.StringVar(&runFlags.genPath, "g", runFlags.genPath, "[gendata] path to generate model-name.json file") + flag.StringVar(&runFlags.keyPath, "k", runFlags.keyPath, "path to key file") + flag.StringVar(&runFlags.text, "t", runFlags.text, "[crypt] text to encrypt/decrypt") + flag.StringVar(&runFlags.url, "u", runFlags.url, "[webservice] url to open in browser") + flag.StringVar(&runFlags.textPath, "p", runFlags.textPath, "[crypt] text path to encrypt/decrypt") + flag.BoolVar(&runFlags.version,"v",false,"version") + flag.Parse() + return runFlags,nil +} + +func run() int { + runFlags, err := ParseFlags() + if err != nil { + log.Fatalf("Run flags parse: %v", err) + } + switch runFlags.command { + case "genkey": + ky, err := loadPath(runFlags.keyPath) + if err != nil { + log.Fatalf("Error load key: %v", err) + } + randKey, err := getKey(ky) + if err != nil { + log.Fatalf("Error gen key: %v", err) + } + fmt.Printf("%s\n", randKey) + return 0 + case "encrypt": + var data string + loadcertkey(runFlags) + if len(runFlags.text) > 0 { + data,err = encrypt(runFlags.text, string(CRYPTKEY)) + } else if len(runFlags.textPath) > 0 { + fileData, err := loadPath(runFlags.textPath) + if err != nil { + log.Fatalf("Error encrypt: %v", err) + } + data,err = encrypt(string(fileData), string(CRYPTKEY)) + } + fmt.Printf("%s\n", data) + return 0 + case "decrypt": + var data string + loadcertkey(runFlags) + if len(runFlags.text) > 0 { + data,err = decrypt(runFlags.text, string(CRYPTKEY)) + } else if len(runFlags.textPath) > 0 { + fileData, err := loadPath(runFlags.textPath) + if err != nil { + log.Fatalf("Error decrypt: %v", err) + } + data,err = decrypt(string(fileData), string(CRYPTKEY)) + } + fmt.Printf("%s\n", data) + return 0 + case "gendata": + return gendata(runFlags) + case "webservice": + loadcertkey(runFlags) + return webservice(runFlags) + case "huuid": + uuid := uuid.NewString() + fmt.Printf("%s\n", uuid) + return 0 + case "uuid": + uuid := uuid.NewString() + fmt.Printf("%s\n", strings.Replace(uuid,"-","",-1)) + return 0 + default: + fmt.Fprintln(os.Stderr, "Unknown command: ", runFlags.command) + return 99 + } +} + +func main() { + if len(os.Args) > 1 && (os.Args[1] == "-v" || os.Args[1] == "version") { + fmt.Fprintf(os.Stdout, "ServCVgen version:%s\n",VERSION) + os.Exit(0) + } + // if len(os.Args) > 1 || len(os.Getenv("CVGEN_COMMAND")) > 0 { + // os.Exit(run()) + // } else { + // fmt.Fprintln(os.Stderr, "Use --help to see options") + // } + os.Exit(run()) +} diff --git a/models.go b/models.go new file mode 100644 index 0000000..1015143 --- /dev/null +++ b/models.go @@ -0,0 +1,155 @@ +package main + +import ( + "encoding/json" + "fmt" + "os/exec" + "path/filepath" + "strings" + + "os" + + "gopkg.in/yaml.v2" + + cfg "github.com/jesusperez/cfgsrv" + cvdata "github.com/jesusperez/cvdata" + utils "github.com/jesusperez/datautils" +) + +func createRootModels(config *cfg.Config) error { + modelsFullPath := fmt.Sprintf("%s/%s",config.DataPath,config.DataModelsRoot) + var models []cvdata.ModelType + err := filepath.Walk(config.DataPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + if info.IsDir() { + if _, err := os.Stat(fmt.Sprintf("%s/core.yaml",path)); err == nil { + if !strings.Contains(path,"lang") { + name := filepath.Base(path) + modelPath := strings.Replace(path,config.DataPath,"",-1) + models = append(models, cvdata.ModelType{ + Id: name, + Title: name, + Path: fmt.Sprintf("~/%s",modelPath), + }) + } + } + } + return nil + }) + if err != nil { + fmt.Printf("Error create %s: %v\n",modelsFullPath, err) + return err + } + var rootModels cvdata.Models + rootModels.Models = models + data, err := yaml.Marshal(rootModels) + if err != nil { + fmt.Printf("Error create yaml models data: %v\n", err) + return err + } + dataModelsPath := fmt.Sprintf("%s/%s",config.DataPath,config.DataModelsRoot) + if res := utils.WriteData(string(data),dataModelsPath) ; res != nil { + fmt.Printf("Error write %s: %v\n", dataModelsPath, err) + return res + } + data, err = json.Marshal(rootModels) + if err != nil { + fmt.Printf("Error create json models data: %v\n", err) + return err + } + dataModelsPath = fmt.Sprintf("%s/%s",config.DataDistPath,strings.Replace(config.DataModelsRoot,".yaml",".json",-1)) + if res := utils.WriteData(string(data),dataModelsPath) ; res != nil { + fmt.Printf("Error write %s: %v\n", dataModelsPath, err) + return res + } + fmt.Printf("Root models write from %s\n", modelsFullPath) + return nil +} +func genJsonModels(config *cfg.Config) error { + if config.UseRepo && config.RepoPath != "" { + var errgit error + var cmd *exec.Cmd + if cmd,errgit = utils.GitPull(config.RepoPath,config.RepoName,config.BackgGit,config.QuietGit); errgit != nil { + fmt.Printf("Error pull %s (%s): %v\n", config.RepoPath, config.RepoName,errgit) + if config.BackgGit { + if errgit = cmd.Wait(); errgit != nil { + fmt.Printf("Error pull %s (%s): %v\n", config.RepoPath, config.RepoName,errgit) + } + } + } + } + err := filepath.Walk(config.DataPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return err + } + if info.IsDir() { + if _, err := os.Stat(fmt.Sprintf("%s/core.yaml",path)); err == nil { + if !strings.Contains(path,"lang") { + dir := filepath.Base(path) + exclude := false + for _,itm := range config.GenExcludeList { + if itm == dir { + exclude = true + break + } + } + if !exclude { + if errjson := makeModelJson(config, dir, config.DataDistPath, true, false); errjson != nil { + fmt.Printf("Error write cv %s: %v\n", dir, errjson) + } + } + } + } + return err + } + return nil + }) + return err +} +func makeModelJson(cfg *cfg.Config, modelName string, genPath string, quiet bool, useRepo bool) error { + cvdata, error := cvdata.LoadCVData(fmt.Sprintf("%s/%s",cfg.DataPath,modelName), cfg, useRepo) + if error != nil { + fmt.Printf("Error generating data %v\n", error) + } + data, err := json.Marshal(cvdata) + if err != nil { + fmt.Printf("Error Parse cv %s: %v\n", modelName, err) + return err + } else { + err := utils.CheckDirPath(fmt.Sprintf("%s",genPath)) + if err == nil { + path := fmt.Sprintf("%s/%s.json",genPath,modelName) + if res := utils.WriteData(string(data),path) ; res != nil { + fmt.Printf("Error write cv %s: %v\n", modelName, err) + return err + } else { + if quiet { + fmt.Printf("CV %s writed to: %s\n", modelName,path) + } + } + } else { + return err + } + } + return nil +} +func gendata(runFlags RunFlags) int { + cfg,err := loadDataConfig(runFlags) + //fmt.Printf("Loaded config: %#v\n", cfg) + if err != nil { + return 1 + } + errModel := createRootModels(cfg) + if errModel != nil { + return 1 + } + errModel = makeModelJson(cfg, runFlags.modelName, runFlags.genPath,true,true) + if errModel != nil { + return 1 + } + return 0 +} diff --git a/redis.go b/redis.go new file mode 100644 index 0000000..375ab4d --- /dev/null +++ b/redis.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + + "github.com/go-redis/redis/v8" + cfg "github.com/jesusperez/cfgsrv" +) + +func load_redis(cfg *cfg.Config) *redis.Client { + + rdb := redis.NewClient(&redis.Options{ + Addr: fmt.Sprintf("%s:%d",cfg.RedisHost,cfg.RedisPort), + Password: cfg.RedisPswd, // no password set + DB: 0, // TODO cfg.RedisDB name to number, // 0, use default DB + }) + return rdb + } +//} +//func load_redis(rtenv *RouteEnv) { + // err := rdb.Set(ctx, "key", "value", 0).Err() + // if err != nil { + // panic(err) + // } + + // val, err := rdb.Get(ctx, "key").Result() + // if err != nil { + // panic(err) + // } + // fmt.Println("key", val) + + // val2, err := rdb.Get(ctx, "key2").Result() + // if err == redis.Nil { + // fmt.Println("key2 does not exist") + // } else if err != nil { + // panic(err) + // } else { + // fmt.Println("key2", val2) + // } +//} + diff --git a/routes.go b/routes.go new file mode 100644 index 0000000..c099066 --- /dev/null +++ b/routes.go @@ -0,0 +1,189 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + jwt "github.com/appleboy/gin-jwt/v2" + "github.com/casbin/casbin/v2" + "github.com/gin-gonic/gin" + cfg "github.com/jesusperez/cfgsrv" +) + +type RouteEnv struct { + Cfg *cfg.Config + Users *UsersAccounts + WebUsrs *WebAccounts + MdlsUsrs map[string]ModelUser + Enforcer *casbin.Enforcer + AuthMiddleware *jwt.GinJWTMiddleware + Store *interface{} + RunFlags *RunFlags +} +func logRoute(c *gin.Context, rtenv *RouteEnv, ky string, info string, infoReq string) { + tkn := "" + if rtenv.Cfg.UseJWT { + tk, errtk := rtenv.AuthMiddleware.ParseToken(c) + if errtk == nil { + tkn = fmt.Sprintf("%v",tk.Raw) + } + } + logRequest(c, rtenv, "route", rtenv.Cfg.Routes[ky].Path, info, infoReq, tkn) +} +func getRoutes(router *gin.Engine, cfg *cfg.Config, users *UsersAccounts, mdlsUsrs map[string]ModelUser, enforcer *casbin.Enforcer, store *interface{}, runFlags *RunFlags) { + var authorized *gin.RouterGroup + routenv := &RouteEnv{ + Cfg: cfg, + Users: users, + MdlsUsrs: mdlsUsrs, + Enforcer: enforcer, + AuthMiddleware: nil, + Store: store, + RunFlags: runFlags, + } + if cfg.UseJWT { + routenv.AuthMiddleware = getJwt(routenv) + router.POST(cfg.Routes["post_login"].Path, routenv.AuthMiddleware.LoginHandler) + authorized = router.Group(cfg.RootAuthGroup) + // authorized.GET(cfg.Routes["refreshauth"].Path,routenv.AuthMiddleware.RefreshHandler) + authorized.GET(cfg.Routes["refreshauth"].Path, func(c *gin.Context) { + get_auth_refresh_handle(c, routenv) + }) + authorized.Use(routenv.AuthMiddleware.MiddlewareFunc()) + } else { + webusrs := loadWebAccounts(users) + // router.POST(cfg.Routes["post_login"].Path, routenv.AuthMiddleware.LoginHandler) + authorized = router.Group(cfg.Routes["root"].Path, gin.BasicAuth(webusrs.Accounts)) + } + authorized.GET("/hello", func(c *gin.Context) { + if cfg.UseJWT { + claims := jwt.ExtractClaims(c) + user, _ := c.Get(cfg.IdentityKey) + c.JSON(200, gin.H{ + "userID": claims[cfg.IdentityKey], + "userName": user.(*User).UserName, + "text": "Hello World.", + }) + } else { + c.JSON(200, gin.H{ + "userID": "", + "userName": "", + "text": "Hello World.", + }) + } + }) + // authorized.GET("/page/:id", func(c *gin.Context) { + // logRoute := func(info string, infoReq string) { + // logRequest(c, cfg, "route", "/", info, infoReq) + // } + // claims := jwt.ExtractClaims(c) + // user, _ := c.Get(identityKey) + // id := c.Params.ByName("id") + // logRoute(fmt.Sprintf("get /"), fmt.Sprintf("get /")) + // c.HTML(http.StatusOK, "welcome", gin.H{ + // "title": fmt.Sprintf("Main website %s for %s (%s)",id,user.(*User).UserName,claims[identityKey]), + // }) + // }) + router.GET(cfg.Routes["root"].Path, func(c *gin.Context) { + logRoute(c,routenv,"root",fmt.Sprintf("get %s",cfg.Routes["root"].Path), fmt.Sprintf("get %s",cfg.Routes["root"].Path)) + c.HTML(http.StatusOK, "welcome", gin.H{ + "title": "Main website public", + }) + }) + router.GET(cfg.Routes["page"].Path, func(c *gin.Context) { + get_page_handle(c, routenv ) + }) + router.GET(cfg.Routes["auth"].Path, func(c *gin.Context) { + get_auth_handle(c, routenv ) + }) + router.GET(cfg.Routes["invitation"].Path, func(c *gin.Context) { + get_invitation_handle(c, routenv ) + }) + authorized.POST(cfg.Routes["sendinvitation"].Path, func(c *gin.Context) { + send_invitation_handle(c, routenv ) + }) + authorized.GET(cfg.Routes["users"].Path, func(c *gin.Context) { + get_users_handle(c, routenv ) + }) + authorized.POST(cfg.Routes["post_users"].Path, func(c *gin.Context) { + post_users_handle(c, routenv ) + }) + authorized.POST(cfg.Routes["post_newuser"].Path, func(c *gin.Context) { + post_newuser_handle(c, routenv ) + }) + router.GET(cfg.Routes["recoveryaccess"].Path, func(c *gin.Context) { + get_recoveryaccess_handle(c, routenv ) + }) + router.POST(cfg.Routes["post_recoveryaccess"].Path, func(c *gin.Context) { + post_recoveryaccess_handle(c, routenv ) + + }) + router.GET("/user/:name", func(c *gin.Context) { + user := c.Params.ByName("name") + value, ok := users.Accounts[user] + role := cfg.AdminRole + if ok { + roles,_ := enforcer.GetUsersForRole(role) + hasRole := false + c.JSON(http.StatusOK, gin.H{"user": user, "value": value, "roles": roles, "hasrole": hasRole}) + } else { + c.HTML(http.StatusOK, "index", gin.H{ + "title": "Main website", + }) + } + }) + // authorized.GET(cfg.Routes["data"].Path, func(c *gin.Context) { + router.GET(cfg.Routes["data"].Path, func(c *gin.Context) { + get_data_handle(c, routenv ) + }) + for ky, tpl := range cfg.TemplatesFiles { + if ky != "index" { + fmt.Printf("%s -> %v\n",ky,tpl) + kyval := fmt.Sprintf("%s",ky) + router.GET(tpl.Route, func(c *gin.Context) { + c.HTML(http.StatusOK, kyval, gin.H{ + "title": fmt.Sprintf("%s website",kyval), + }) + }) + } + } + authorized.POST(cfg.Routes["post_data"].Path, func(c *gin.Context) { + post_data_handle(c, routenv ) + }) + authorized.GET(cfg.Routes["trackinglist"].Path, func(c *gin.Context) { + get_tracking_list_handle(c, routenv ) + }) + authorized.GET(cfg.Routes["tracking"].Path, func(c *gin.Context) { + get_tracking_handle(c, routenv ) + }) + authorized.POST(cfg.Routes["post_tracking"].Path, func(c *gin.Context) { + post_tracking_handle(c, routenv ) + }) + authorized.GET(cfg.Routes["config"].Path, func(c *gin.Context) { + get_config_handle(c, routenv ) + }) + authorized.POST(cfg.Routes["post_config"].Path, func(c *gin.Context) { + post_config_handle(c, routenv ) + }) + router.GET("favicon.ico", func(c *gin.Context) { + file, _ := f.ReadFile(fmt.Sprintf("%s/favicon.svg",cfg.AssetsPath)) + c.Data( + http.StatusOK, + "image/x-icon", + file, + ) + }) + if cfg.UseJWT { + router.NoRoute(routenv.AuthMiddleware.MiddlewareFunc(), func(c *gin.Context) { + claims := jwt.ExtractClaims(c) + log.Printf("NoRoute claims: %#v\n", claims) + c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) + }) + } else { + router.NoRoute(func(c *gin.Context) { + // log.Printf("NoRoute claims: %#v\n", claims) + c.JSON(404, gin.H{"code": "PAGE_NOT_FOUND", "message": "Page not found"}) + }) + } +} \ No newline at end of file diff --git a/types.go b/types.go new file mode 100644 index 0000000..6488834 --- /dev/null +++ b/types.go @@ -0,0 +1,139 @@ +package main + +import ( + "bytes" + + "github.com/gin-gonic/gin" + cfg "github.com/jesusperez/cfgsrv" +) + +type RunFlags struct { + command string + cfgPath string + usersPath string + modelName string + keyPath string + genPath string + text string + textPath string + url string + version bool +} + +type ModelUser struct { + Model string `yaml:"model"` + User string `yaml:"user"` + Active bool `yaml:"active"` + Data string `yaml:"data"` +} +type Invitation struct { + Email string `yaml:"email"` + Createdby string `yaml:"createdby"` + Expire string `yaml:"expire"` + Howmany int `yaml:"howmany"` + Role string `yaml:"role"` + Description string `yaml:"description"` + Data string `yaml:"data"` + Active bool `yaml:"active"` +} +type PostInvitation struct { + Id string + Data Invitation +} +type Account struct { + Username string `yaml:"username"` + Passwd string `yaml:"passwd"` + Email string `yaml:"email"` + Description string `yaml:"description"` + Data string `yaml:"data"` + Web bool `yaml:"web"` + Id string `yaml:"id"` +} +type UsersAccounts struct { + Accounts map[string]Account `yaml:"accounts"` +} +type NewUserData struct { + Username string `yaml:"username" json:"username"` + Passwd string `yaml:"password" json:"password"` + Email string `yaml:"email" json:"email"` + Role string `yaml:"role" json:"role"` + Data string `yaml:"data" json:"data"` + } +type WebAccounts struct { + Accounts gin.Accounts `yaml:"accounts"` +} +type Login struct { + Username string `form:"username" json:"username" binding:"required"` + Password string `form:"password" json:"password" binding:"required"` +} +type User struct { + UserName string + UUID string + Data string + FirstName string + LastName string +} +// type Data interface { +// type Config, int8, int16, int32, int64 +// } + +type ConfigPostData struct { + Id string `yaml:"id" json:"id"` + Val string `yaml:"val" json:"val"` + Config cfg.Config `yaml:"config" json:"config"` +} +type TrackAction struct { + When string `yaml:"when" json:"when"` + Where string `yaml:"where" json:"where"` + What string `yaml:"what" json:"what"` + Context string `yaml:"context" json:"context"` + Data string `yaml:"data" json:"data"` + Auth string `yaml:"auth" json:"auth"` +} +type PostTrackAction struct { + When string `yaml:"when" json:"when"` + Where string `yaml:"where" json:"where"` + What string `yaml:"what" json:"what"` + Context string `yaml:"context" json:"context"` + Data string `yaml:"data" json:"data"` +} +type UsersData struct { + UsersData string `yaml:"usersData" json:"usersData"` + ModelsData string `yaml:"modelsData" json:"modelsData"` + Invitations string `yaml:"invitations" json:"invitations"` + AuthzModel string `yaml:"authzModel" json:"authzModel"` + AuthzPolicy string `yaml:"authzPolicy" json:"authzPolicy"` +} +type UsersDataPost struct { + Id string `yaml:"id" json:"id"` + Val string `yaml:"val" json:"val"` + Data UsersData `yaml:"data" json:"data"` +} +type UsersRecoveryPost struct { + Id string `yaml:"id" json:"id"` + Val string `yaml:"val" json:"val"` +} +type UsersRecover struct { + Id string `yaml:"id" json:"id"` + Data Account `yaml:"data" json:"data"` +} +type SaveDataInfo struct { + Title string + Data string + Id string + Path string + Prfx string +} +type Mail struct { + Sender string + To []string + Cc []string + Bcc []string + Subject string + Body bytes.Buffer + // Headers textproto.MIMEHeader +} +type AppData struct { + Data interface{} + Expire string +} \ No newline at end of file diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..98d297f --- /dev/null +++ b/utils.go @@ -0,0 +1,246 @@ +package main + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + // "io/ioutil" + // "path/filepath" + // "time" + + //"github.com/go-git/go-git/v5" + "gopkg.in/yaml.v3" +) + +func loadPath(path string) ([]byte,error) { + content, err := ioutil.ReadFile(path) + if err != nil { + return nil,err + } + return content, nil +} +func decoderFromFile(path string) (*yaml.Decoder, *os.File, error) { + file, err := os.Open(path) + if err != nil { + // log.Fatalf("open file error: %v", err) + fmt.Printf("open file error: %v\n", err) + return nil,nil, err + } + return yaml.NewDecoder(file), file, nil +} +func existsPathErr(path string) error { + _, err := os.Stat(path) + if errors.Is(err, os.ErrNotExist) { + return err // errors.New("File not exists") + } + return nil +} +func existsPath(path string) bool { + _, err := os.Stat(path) + return !errors.Is(err, os.ErrNotExist) +} +func checkDirPath(path string) error { + if ! existsPath(path) { + os.Mkdir(path, os.FileMode(0755)) + if err := existsPathErr(path); err != nil { + fmt.Printf("Error creating path %s: %v\n", path,err) + return err + } + } + return nil +} +// https://github.com/go-git/go-git/blob/master/_examples/commit/main.go +func writeData(data string, path string) error { + //fmt.Printf("Write %s\n", path) + f, err := os.Create(path) + if err != nil { + fmt.Printf("Error create %s: %v\n", path, err) + return err + } + w := bufio.NewWriter(f) + n4, err := w.WriteString(data) + if err != nil { + fmt.Printf("Error writing %s: %v\n", path, err) + return err + } + if DEBUGLEVEL > 0 { + fmt.Printf("%s wrote %d bytes\n", path, n4) + } + w.Flush() + return nil +} + +func execCommand(command string, args []string,quiet bool) (string,error) { + cmd := exec.Command(command, args...) + cmd.Stdin = strings.NewReader("") + var out bytes.Buffer + cmd.Stdout = &out + err := cmd.Run() + if err != nil { + if !quiet { + fmt.Printf("Error exec command %s: %#v\n", cmd,err) + } + return out.String(),err + } + if !quiet { + fmt.Printf("%q\n", out.String()) + } + return out.String(),nil +} +func gitPull(path string, name string,quiet bool) error { + fmt.Printf("Git Pull: %#v\n", path) + _, err := execCommand("git",[]string{"-C",path,"pull",name},quiet) + return err +} +func gitCommit(path string, file string, msg string, target string, quiet bool) error { + fmt.Printf("Git Commit: %#v\n", path) + if out,err := execCommand("git",[]string{"-C",path,"add",file},quiet); err != nil { + fmt.Printf("Error git add %s: %s\n %#v\n", path, out,err) + //return err + } + if _,err := execCommand("git",[]string{"-C",path,"commit","-m",msg},quiet); err != nil { + //if len(out) != 0 { + //fmt.Printf("Error commit %s: %s\n %#v\n", path, out,err) + // return err + // } + return nil + } + // if out,err := execCommand("git",[]string{"-C",path,"diff","--stat","--cached",target},quiet); err != nil { + // if len(out) == 0 { + // return nil + // } else { + // fmt.Printf("Error commit %s: %s\n %#v\n", path, out,err) + // return err + // } + // } + if _,err := execCommand("git",[]string{"-C",path,"push",target},quiet); err != nil { + return err + } + return nil +} +func getFileMatch(root string, extension string, fullpath bool, withExtension bool) []string { + var listFiles[]string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Println(err) + return nil + } + if !info.IsDir() && filepath.Ext(path) == extension { + filepath := path + if !fullpath { + filepath = strings.Replace(path,fmt.Sprintf("%s/",root),"",-1) + } + if !withExtension { + filepath = strings.Replace(filepath,fmt.Sprintf("%s",extension),"",-1) + } + listFiles = append(listFiles, filepath) + } + return nil + }) + if err != nil { + fmt.Printf("No files found in %s: %v\n",root, err) + return nil + } + return listFiles +} +/* +func gitGetWorktree(path string) (*git.Repository, *git.Worktree, error) { + repo, err := git.PlainOpen(path) + if err != nil { + fmt.Printf("Error path %s: %v\n", path, err) + return nil,nil,err + } + worktree, err := repo.Worktree() + if err != nil { + fmt.Printf("Error worktree %s: %v\n", path, err) + return nil,nil,err + } + return repo,worktree,nil +} +func gitPull(path string, name string) error { + fmt.Printf("Git Pull: %#v\n", path) + repo,worktree,err := gitGetWorktree(path) + if err != nil { + fmt.Printf("Error worktree %s: %v\n", path, err) + return err + } + err = worktree.Pull(&git.PullOptions{RemoteName: name}) + if err != nil { + fmt.Printf("Error pull %s: %v\n", path, err) + return err + } + if DEBUGLEVEL > 0 { + // Print the latest commit that was just pulled + ref, err := repo.Head() + if err != nil { + fmt.Printf("Error repo Head %s: %v\n", path, err) + return err + } + commit, err := repo.CommitObject(ref.Hash()) + if err != nil { + fmt.Printf("Error commit object %s: %v\n", path, err) + return err + } + fmt.Println(commit) + } + return nil +} +func gitCommit(path string, file string, msg string, name string, email string, when time.Time ) error { + // Opens an already existing repository. + repo,worktree,err := gitGetWorktree(path) + if err != nil { + fmt.Printf("Error worktree %s: %v\n", path, err) + return err + } + // Info("echo \"hello world!\" > example-git-file") + //filename := filepath.Join(path, file) + //err = ioutil.WriteFile(filename, []byte("hello world!"), 0644) + // Adds the new file to the staging area. + _, err = worktree.Add(file) + if err != nil { + fmt.Printf("Error git add %s: %v\n", file, err) + return err + } + fmt.Printf("git add %s\n",file) + if DEBUGLEVEL > 0 { + status, err := worktree.Status() + if err != nil { + fmt.Printf("Error git status %s: %v\n", file, err) + return err + } + fmt.Println(status) + } + // Commits the current staging area to the repository, with the new file + // just created. We should provide the object.Signature of Author of the + // commit Since version 5.0.1, we can omit the Author signature, being read + // from the git config files. + // Info("git commit -m \"example go-git commit\"") + commit, err := worktree.Commit(msg, &git.CommitOptions{ + Author: &object.Signature{ + Name: name, + Email: email, + When: when, + }, + }) + if err != nil { + fmt.Printf("Error git create commit %s: %v\n", file, err) + return err + } + // Prints the current HEAD to verify that all worked well. + // Info("git show -s") + obj, err := repo.CommitObject(commit) + if err != nil { + fmt.Printf("Error git commit %s: %v\n", file, err) + return err + } + fmt.Println(obj) + return nil +} +*/ diff --git a/websrvr.go b/websrvr.go new file mode 100644 index 0000000..e37b96c --- /dev/null +++ b/websrvr.go @@ -0,0 +1,175 @@ +package main + +import ( + "fmt" + "path/filepath" + "strings" + "time" + + cfg "github.com/jesusperez/cfgsrv" + log "github.com/sirupsen/logrus" + + "github.com/casbin/casbin/v2" + + "github.com/gin-contrib/cors" + "github.com/gin-contrib/multitemplate" + "github.com/gin-gonic/gin" + "github.com/pkg/browser" +) + +func loadTemplates(templatesDir string,templatesFiles map[string]cfg.TemplateItem,includesPath string, layoutsPath string) multitemplate.Renderer { + r := multitemplate.NewRenderer() + layouts, err := filepath.Glob(strings.Replace(layoutsPath,"@",(templatesDir+"/"),-1) + "/*.html") + if err != nil { + log.Fatalf("Error auth model loaded: %v\n", err) + } + includes, err := filepath.Glob(strings.Replace(includesPath,"@",(templatesDir+"/"),-1) + "/*.html") + if err != nil { + log.Fatalf("Error auth model loaded: %v\n", err) + } + for ky, tpl := range templatesFiles { + if strings.Contains(tpl.Path,"@") { + r.AddFromFiles(ky,templatesDir + strings.Replace(tpl.Path,"@","/",-1)) + } else { + r.AddFromFiles(ky,tpl.Path) + } + } + // Generate our templates map from our layouts/ and includes/ directories + for _, include := range includes { + layoutCopy := make([]string, len(layouts)) + copy(layoutCopy, layouts) + files := append(layoutCopy, include) + r.AddFromFiles(filepath.Base(include), files...) + } + return r +} + +func run_web(cfg *cfg.Config,users *UsersAccounts,mdlsUsrs map[string]ModelUser, runFlags *RunFlags) { + // logger := log.New(os.Stderr, "", 0) + var err error + var enforcer *casbin.Enforcer + if cfg.UseAuthz { + enforcer,err = casbin.NewEnforcer(cfg.AuthzModel, cfg.AuthzPolicy) + // load the casbin model and policy from files, database is also supported. + if err != nil { + log.Fatalf("Error auth model loaded: %v\n",err) + } + } + // logger.Println("[WARNING] DON'T USE THE EMBED CERTS FROM THIS EXAMPLE IN PRODUCTION ENVIRONMENT, GENERATE YOUR OWN!") + gin.SetMode(gin.ReleaseMode) + router := gin.Default() + + corsConfig := cors.Config{ + AllowOrigins: cfg.AllowOrigins, // []string{"*"}, // , // []string{"http://localhost:3333"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"}, + AllowHeaders: []string{"Origin", "Authorization","Content-Length", "Content-Type","X-Request-With","Accept","Accept-Encoding"}, + ExposeHeaders: []string{"Content-Length"}, + AllowCredentials: true, + MaxAge: 12 * time.Hour, + // // AllowOriginFunc: func(origin string) bool { + // // return origin == "https://github.com" + // // }, + // MaxAge: 12 * time.Hour, + } + // corsConfig := cors.DefaultConfig() + // corsConfig.AllowAllOrigins = []string{"http://localhost:3333'"} + // corsConfig.AllowCredentials = true + // c.Header("Access-Control-Allow-Origin", "*") + // c.Header("Access-Control-Allow-Credentials", "true") + // c.Header("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With") + // c.Header("Access-Control-Allow-Methods", "POST,HEAD,PATCH, OPTIONS, GET, PUT") + + corsConfig.AllowOrigins = cfg.AllowOrigins // []string{"http://localhost:3333"}, + router.Use(cors.New(corsConfig)) + + //fmt.Printf("cfg AllowOrigin: %v\n", cfg.AllowOrigins) + //db["jesus"] = "DONE" + // listTemplates := getFileMatch(cfg.TemplatesRoot,cfg.TemplatesExt) + //fmt.Printf("templates list: %v\n", listTemplates) + log.WithFields(log.Fields{ + "run": "run_web", + }).Info(fmt.Sprintf("templates path: %v\n", cfg.TemplatesFiles)) + //var templ + //templ := template.Must(template.New("").ParseFS(f,cfg.TemplatesPaths[:]...)) + //templ := template.Must(template.ParseFS(f,cfg.TemplatesPaths[:]...)) + //templ := template.Must(template.ParseFS(f,"templates")) + //templ := template.Must(template.ParseFS(f,cfg.TemplatesPaths[:]...)) + //router.SetHTMLTemplate(templ) + //router.LoadHTMLGlob("templates/*.tmpl") + router.HTMLRender = loadTemplates(cfg.TemplatesRoot,cfg.TemplatesFiles,cfg.TemplatesIncludes,cfg.TemplatesLayouts) + // router.Use(ginI18n.Localize( + // ginI18n.WithGetLngHandle( + // func(context *gin.Context, defaultLng string) string { + // lng := context.Query("lng") + // if lng == "" { + // return defaultLng + // } + // return lng + // }, + // ), + // )) + // apply i18n middleware + + // router.Use(func(c *gin.Context) { + // c.Set("cfg",cfg) + // // c.String(http.StatusNotFound, "Requested url does not exist") + // //c.Abort() + // c.Next() + // }) + + // router.StaticFS("/public", http.FS(f)) + router.Static(cfg.AssetsURL,cfg.AssetsPath) + var store interface{} + if len(cfg.RedisHost) > 0 { + store = load_redis(cfg) + } + + getRoutes(router,cfg,users,mdlsUsrs,enforcer,&store,runFlags) + + hostport := fmt.Sprintf("%s:%d",cfg.Host,cfg.Port) + log.WithFields(log.Fields{ + "run": "run_web", + }).Info(fmt.Sprintf("Running on %s://%s",cfg.Protocol,hostport)) + if cfg.OpenBrowser { + if len(runFlags.url) == 0 { + browser.OpenURL(fmt.Sprintf("%s://%s",cfg.Protocol,hostport)) + } else { + browser.OpenURL(fmt.Sprintf(runFlags.url)) + } + } + err = router.Run(hostport) + if err != nil { + log.Fatalf("Error run webserver: %v", err) + } +} + +func webservice(runFlags RunFlags) int { + cfg,err := loadDataConfig(runFlags) + //fmt.Printf("Loaded config: %#v\n", cfg) + if err != nil { + return 1 + } + if cfg.GenDist { + errModel := createRootModels(cfg) + if errModel != nil { + fmt.Printf("Error createRootModels: %v\n",errModel) + return 1 + } + generr := genJsonModels(cfg) + if generr != nil { + fmt.Printf("Error genJsonModels: %v\n",generr) + return 1 + } + } + acc,err := loadUsersAccounts(cfg.UsersPath) + if err != nil { + return 1 + } + mdlsUsrs,err := loadModelsUsers(cfg.UsersModelsPath) + if err != nil { + return 1 + } + setLog(cfg) + run_web(cfg,acc,mdlsUsrs,&runFlags) + return 0 +} \ No newline at end of file