From a91aa9dac387503460c9ecb53c4952d98f015856 Mon Sep 17 00:00:00 2001 From: JesusPerez Date: Mon, 10 Jan 2022 09:53:01 +0000 Subject: [PATCH] chore: code tested for cvgen genadmin --- .gitignore | 157 +++++++++++++++++++ Dockerfile | 22 +++ LICENSE | 21 +++ crypt.go | 62 ++++++++ email.go | 134 ++++++++++++++++ handlers.go | 133 ++++++++++++++++ handlers_auth.go | 108 +++++++++++++ handlers_config.go | 82 ++++++++++ handlers_mng_users.go | 347 ++++++++++++++++++++++++++++++++++++++++++ handlers_tracking.go | 153 +++++++++++++++++++ handlers_user.go | 238 +++++++++++++++++++++++++++++ jwt.go | 183 ++++++++++++++++++++++ loads.go | 193 +++++++++++++++++++++++ logo/srvcvgen_b.svg | 1 - logo/srvcvgen_w.svg | 1 - logs.go | 124 +++++++++++++++ main.go | 138 +++++++++++++++++ models.go | 155 +++++++++++++++++++ redis.go | 41 +++++ routes.go | 189 +++++++++++++++++++++++ types.go | 139 +++++++++++++++++ utils.go | 246 ++++++++++++++++++++++++++++++ websrvr.go | 175 +++++++++++++++++++++ 23 files changed, 3040 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 crypt.go create mode 100644 email.go create mode 100644 handlers.go create mode 100644 handlers_auth.go create mode 100644 handlers_config.go create mode 100644 handlers_mng_users.go create mode 100644 handlers_tracking.go create mode 100644 handlers_user.go create mode 100644 jwt.go create mode 100644 loads.go delete mode 100644 logo/srvcvgen_b.svg delete mode 100644 logo/srvcvgen_w.svg create mode 100644 logs.go create mode 100644 main.go create mode 100644 models.go create mode 100644 redis.go create mode 100644 routes.go create mode 100644 types.go create mode 100644 utils.go create mode 100644 websrvr.go 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