diff --git a/go.mod b/go.mod index 9ac3353..35027da 100644 --- a/go.mod +++ b/go.mod @@ -2,22 +2,17 @@ module git.coopgo.io/coopgo-apps/parcoursmob go 1.18 -replace git.coopgo.io/coopgo-platform/mobility-accounts => ../../coopgo-platform/mobility-accounts/ +// replace git.coopgo.io/coopgo-platform/mobility-accounts => ../../coopgo-platform/mobility-accounts/ -replace git.coopgo.io/coopgo-platform/groups-management => ../../coopgo-platform/groups-management/ +// replace git.coopgo.io/coopgo-platform/groups-management => ../../coopgo-platform/groups-management/ -replace git.coopgo.io/coopgo-platform/fleets => ../../coopgo-platform/fleets/ +// replace git.coopgo.io/coopgo-platform/fleets => ../../coopgo-platform/fleets/ -replace git.coopgo.io/coopgo-platform/agenda => ../../coopgo-platform/agenda/ +// replace git.coopgo.io/coopgo-platform/agenda => ../../coopgo-platform/agenda/ -replace git.coopgo.io/coopgo-platform/emailing => ../../coopgo-platform/emailing/ +// replace git.coopgo.io/coopgo-platform/emailing => ../../coopgo-platform/emailing/ require ( - git.coopgo.io/coopgo-platform/agenda v0.0.0-00010101000000-000000000000 - git.coopgo.io/coopgo-platform/emailing v0.0.0-00010101000000-000000000000 - git.coopgo.io/coopgo-platform/fleets v0.0.0-00010101000000-000000000000 - git.coopgo.io/coopgo-platform/groups-management v0.0.0-00010101000000-000000000000 - git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-00010101000000-000000000000 github.com/coreos/go-oidc v2.2.1+incompatible github.com/fogleman/gg v1.3.0 github.com/go-playground/validator/v10 v10.11.0 @@ -34,6 +29,14 @@ require ( google.golang.org/protobuf v1.28.1 ) +require ( + git.coopgo.io/coopgo-platform/agenda v0.0.0-20221017030035-4a26fc791c5b + git.coopgo.io/coopgo-platform/emailing v0.0.0-20221017030337-c71888d90c15 + git.coopgo.io/coopgo-platform/fleets v0.0.0-20220905052643-be9ee8372fdd + git.coopgo.io/coopgo-platform/groups-management v0.0.0-20221017025751-671dc9a2c544 + git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20220906130339-b9a32e41bffe +) + require ( github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect diff --git a/go.sum b/go.sum index f27538e..68c1ff4 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,16 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +git.coopgo.io/coopgo-platform/agenda v0.0.0-20221017030035-4a26fc791c5b h1:7kLW1khfGguZ2aL+QpWFwZmAdEcY1MsUjLdiRufjr2s= +git.coopgo.io/coopgo-platform/agenda v0.0.0-20221017030035-4a26fc791c5b/go.mod h1:wqPvfYmzGF2cfXbs8XE1P2j5UYqZwp/La0llkl7dUkc= +git.coopgo.io/coopgo-platform/emailing v0.0.0-20221017030337-c71888d90c15 h1:+ZI4nGE6mqZ6pc7N/BizheEPRXn6Z84Sj7ikwfP2ZcU= +git.coopgo.io/coopgo-platform/emailing v0.0.0-20221017030337-c71888d90c15/go.mod h1:rmbqiHVkONcECOoPlsXlxZnD315Tiz2oRnn1M7646Kg= +git.coopgo.io/coopgo-platform/fleets v0.0.0-20220905052643-be9ee8372fdd h1:7k5QMwMm6JQ0S2bNqXEe7Ouh8N9N3yAvcWB2GRcIZLk= +git.coopgo.io/coopgo-platform/fleets v0.0.0-20220905052643-be9ee8372fdd/go.mod h1:s9OIFCNcjBAbBzRNHwoCTYV6kAntPG9CpT3GVweGdTY= +git.coopgo.io/coopgo-platform/groups-management v0.0.0-20221017025751-671dc9a2c544 h1:rMLP77uIEequVXXZ0X9G1iK2k+xvW/+58ggwxxI6gqY= +git.coopgo.io/coopgo-platform/groups-management v0.0.0-20221017025751-671dc9a2c544/go.mod h1:lozSy6qlIIYhvKKXscZzz28HAtS0qBDUTv5nofLRmYA= +git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20220906130339-b9a32e41bffe h1:4OKwfKybR0VsIw2dSM9RtqGWveWPt+JjtiiMIBrg/w0= +git.coopgo.io/coopgo-platform/mobility-accounts v0.0.0-20220906130339-b9a32e41bffe/go.mod h1:1typNYtO+PQT6KG77vs/PUv0fO60/nbeSGZL2tt1LLg= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= diff --git a/handlers/api/api.go b/handlers/api/api.go index f21d85c..785db34 100644 --- a/handlers/api/api.go +++ b/handlers/api/api.go @@ -4,8 +4,8 @@ import ( "net/http" "git.coopgo.io/coopgo-apps/parcoursmob/services" - "git.coopgo.io/coopgo-apps/parcoursmob/utils/cache" "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification" + cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage" "github.com/spf13/viper" ) @@ -13,10 +13,10 @@ type APIHandler struct { idp *identification.IdentificationProvider config *viper.Viper services *services.ServicesHandler - cache *cache.CacheHandler + cache cache.CacheHandler } -func NewAPIHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache *cache.CacheHandler) (*APIHandler, error) { +func NewAPIHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache cache.CacheHandler) (*APIHandler, error) { return &APIHandler{ idp: idp, config: cfg, diff --git a/handlers/api/export.go b/handlers/api/export.go new file mode 100644 index 0000000..2b343ff --- /dev/null +++ b/handlers/api/export.go @@ -0,0 +1,101 @@ +package api + +import ( + "encoding/csv" + "fmt" + "net/http" + "sort" + "strconv" + + "github.com/gorilla/mux" +) + +type FlatMaps []map[string]any + +func (maps FlatMaps) GetHeaders() (res []string) { + keys := map[string]bool{} + for _, m := range maps { + for k, _ := range m { + if _, ok := keys[k]; !ok { + keys[k] = true + res = append(res, k) + } + } + } + sort.Strings(res) + return +} + +func (maps FlatMaps) GetValues() (res [][]string) { + headers := maps.GetHeaders() + for _, m := range maps { + line := []string{} + for _, k := range headers { + if v, ok := m[k]; ok && v != nil { + line = append(line, fmt.Sprint(v)) + } else { + line = append(line, "") + } + } + res = append(res, line) + } + return +} + +func (h APIHandler) CacheExport(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + cacheid := vars["cacheid"] + + d, err := h.cache.Get(cacheid) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusNotFound) + return + } + + if data, ok := d.([]any); ok { + + flatmaps := FlatMaps{} + //fmt.Println(data) + + for _, v := range data { + fmt.Println(v) + fm := map[string]any{} + flatten("", v.(map[string]any), fm) + fmt.Println(fm) + flatmaps = append(flatmaps, fm) + } + + fmt.Println(flatmaps) + + w.Header().Set("Content-Type", "text/csv") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=export-%s.csv", cacheid)) + c := csv.NewWriter(w) + c.Write(flatmaps.GetHeaders()) + c.WriteAll(flatmaps.GetValues()) + return + + } + + w.WriteHeader(http.StatusNotFound) + +} + +func flatten(prefix string, src map[string]any, dest map[string]any) { + if len(prefix) > 0 { + prefix += "." + } + for k, v := range src { + switch child := v.(type) { + case map[string]any: + flatten(prefix+k, child, dest) + case []any: + for i := 0; i < len(child); i++ { + dest[prefix+k+"."+strconv.Itoa(i)] = child[i] + } + default: + fmt.Println(prefix+k, " : ", v) + dest[prefix+k] = v + } + } +} diff --git a/handlers/api/oidc.go b/handlers/api/oidc.go index f0e43cc..b517129 100644 --- a/handlers/api/oidc.go +++ b/handlers/api/oidc.go @@ -18,6 +18,7 @@ func (h APIHandler) OAuth2Callback(w http.ResponseWriter, r *http.Request) { // Extract the ID Token from OAuth2 token. rawIDToken, ok := oauth2Token.Extra("id_token").(string) if !ok { + fmt.Println("issue retrieving token") w.WriteHeader(http.StatusInternalServerError) return } @@ -36,11 +37,20 @@ func (h APIHandler) OAuth2Callback(w http.ResponseWriter, r *http.Request) { redirect := "/app/" if session.Values["redirect"] != nil && session.Values["redirect"] != "" { + fmt.Println("no redirect stuff") redirect = session.Values["redirect"].(string) delete(session.Values, "redirect") } - session.Save(r, w) + if err = session.Save(r, w); err != nil { + fmt.Println(err) + panic(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + fmt.Println(session.Values) + fmt.Println("redirect") http.Redirect(w, r, redirect, http.StatusFound) } diff --git a/handlers/application/administration.go b/handlers/application/administration.go index d8cb20a..9abb126 100644 --- a/handlers/application/administration.go +++ b/handlers/application/administration.go @@ -229,7 +229,7 @@ func (h *ApplicationHandler) AdministrationGroupInviteAdmin(w http.ResponseWrite } key := base64.RawURLEncoding.EncodeToString(b) - h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour) + h.cache.PutWithTTL("onboarding/"+key, onboarding, 168*time.Hour) // 1 week TTL data := map[string]any{ "group": groupresp.Group.ToStorageType().Data["name"], @@ -247,6 +247,89 @@ func (h *ApplicationHandler) AdministrationGroupInviteAdmin(w http.ResponseWrite return } +func (h *ApplicationHandler) AdministrationGroupInviteMember(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + groupid := vars["groupid"] + + groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(context.TODO(), &groupsmanagement.GetGroupRequest{ + Id: groupid, + Namespace: "parcoursmob_organizations", + }) + + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + group := groupresp.Group.ToStorageType() + + r.ParseForm() + + accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &accounts.GetAccountUsernameRequest{ + Username: r.FormValue("username"), + Namespace: "parcoursmob", + }) + + if err == nil { + account := accountresp.Account.ToStorageType() + account.Data["groups"] = append(account.Data["groups"].([]any), group.ID) + + as, _ := accounts.AccountFromStorageType(&account) + + _, err = h.services.GRPC.MobilityAccounts.UpdateData( + context.TODO(), + &accounts.UpdateDataRequest{ + Account: as, + }, + ) + + fmt.Println(err) + + data := map[string]any{ + "group": group.Data["name"], + } + + if err := h.emailing.Send("onboarding.existing_member", r.FormValue("username"), data); err != nil { + fmt.Println(err) + } + + http.Redirect(w, r, "/app/group/settings", http.StatusFound) + return + } else { + // Onboard now administrator + onboarding := map[string]any{ + "username": r.FormValue("username"), + "group": group.ID, + "admin": false, + } + + b := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + key := base64.RawURLEncoding.EncodeToString(b) + + h.cache.PutWithTTL("onboarding/"+key, onboarding, 168*time.Hour) // 1 week TTL + + data := map[string]any{ + "group": group.Data["name"], + "key": key, + } + + if err := h.emailing.Send("onboarding.new_member", r.FormValue("username"), data); err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + } + + http.Redirect(w, r, "/app/administration/groups/"+group.ID, http.StatusFound) + return +} + func (h *ApplicationHandler) members() ([]*accounts.Account, error) { resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), &accounts.GetAccountsRequest{ Namespaces: []string{"parcoursmob"}, diff --git a/handlers/application/application.go b/handlers/application/application.go index ea7e75a..64c402c 100644 --- a/handlers/application/application.go +++ b/handlers/application/application.go @@ -5,7 +5,7 @@ import ( "git.coopgo.io/coopgo-apps/parcoursmob/renderer" "git.coopgo.io/coopgo-apps/parcoursmob/services" - "git.coopgo.io/coopgo-apps/parcoursmob/utils/cache" + cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage" "git.coopgo.io/coopgo-platform/emailing" "github.com/spf13/viper" ) @@ -14,11 +14,11 @@ type ApplicationHandler struct { config *viper.Viper Renderer *renderer.Renderer services *services.ServicesHandler - cache *cache.CacheHandler + cache cache.CacheHandler emailing *emailing.Mailer } -func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache *cache.CacheHandler, emailing *emailing.Mailer) (*ApplicationHandler, error) { +func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache cache.CacheHandler, emailing *emailing.Mailer) (*ApplicationHandler, error) { templates_root := cfg.GetString("templates.root") renderer := renderer.NewRenderer(cfg, templates_root) return &ApplicationHandler{ diff --git a/handlers/application/beneficiaries.go b/handlers/application/beneficiaries.go index a0f9231..eb77f14 100644 --- a/handlers/application/beneficiaries.go +++ b/handlers/application/beneficiaries.go @@ -9,6 +9,7 @@ import ( "image/png" "log" "net/http" + "sort" "strconv" "strings" "time" @@ -20,6 +21,7 @@ import ( groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi" "git.coopgo.io/coopgo-platform/groups-management/storage" mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi" + mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage" "github.com/google/uuid" "github.com/gorilla/mux" "google.golang.org/protobuf/types/known/structpb" @@ -29,12 +31,21 @@ type BeneficiariesForm struct { FirstName string `json:"first_name" validate:"required"` LastName string `json:"last_name" validate:"required"` Email string `json:"email" validate:"required,email"` - Birthdate *time.Time `json:"birthdate"` + Birthdate *time.Time `json:"birthdate" validate:"required"` PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"` + FileNumber string `json:"file_number" validate:"required"` Address any `json:"address,omitempty"` Gender string `json:"gender"` } +type BeneficiariesByName []mobilityaccountsstorage.Account + +func (e BeneficiariesByName) Len() int { return len(e) } +func (e BeneficiariesByName) Less(i, j int) bool { + return e[i].Data["first_name"].(string) < e[j].Data["first_name"].(string) +} +func (e BeneficiariesByName) Swap(i, j int) { e[i], e[j] = e[j], e[i] } + func (h *ApplicationHandler) BeneficiariesList(w http.ResponseWriter, r *http.Request) { accounts, err := h.beneficiaries(r) @@ -44,6 +55,8 @@ func (h *ApplicationHandler) BeneficiariesList(w http.ResponseWriter, r *http.Re return } + sort.Sort(BeneficiariesByName(accounts)) + cacheid := uuid.NewString() h.cache.PutWithTTL(cacheid, accounts, 1*time.Hour) @@ -252,8 +265,8 @@ func filterAccount(r *http.Request, a *mobilityaccounts.Account) bool { return true } -func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]any, error) { - var accounts = []any{} +func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]mobilityaccountsstorage.Account, error) { + var accounts = []mobilityaccountsstorage.Account{} g := r.Context().Value(identification.GroupKey) if g == nil { return accounts, errors.New("no group provided") @@ -301,6 +314,7 @@ func parseBeneficiariesForm(r *http.Request) (map[string]any, error) { Email: r.PostFormValue("email"), Birthdate: date, PhoneNumber: r.PostFormValue("phone_number"), + FileNumber: r.PostFormValue("file_number"), Gender: r.PostFormValue("gender"), } diff --git a/handlers/application/journeys.go b/handlers/application/journeys.go index 7fe7d92..04d3ee4 100644 --- a/handlers/application/journeys.go +++ b/handlers/application/journeys.go @@ -81,8 +81,8 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque journeys, err = session.Journeys(context.Background(), request) if err != nil { fmt.Println(err) - w.WriteHeader(http.StatusBadRequest) - return + // w.WriteHeader(http.StatusBadRequest) + // return } //CARPOOL @@ -93,8 +93,6 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque // departuredatetime.Format("2006-01-02"), departuredatetime.Add(24*time.Hour).Format("2006-01-02")) carpoolrequest := "https://api.rdex.ridygo.fr/journeys.json" - fmt.Println(carpoolrequest) - client := &http.Client{} req, err := http.NewRequest("GET", carpoolrequest, nil) if err != nil { @@ -113,11 +111,21 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque fmt.Println(err) } - err = json.NewDecoder(resp.Body).Decode(&carpoolresults) - if err != nil { - fmt.Println(err) + if err == nil && resp.StatusCode == http.StatusOK { + err = json.NewDecoder(resp.Body).Decode(&carpoolresults) + if err != nil { + fmt.Println(err) + } + + if carpoolresults == nil { + carpoolresults = []any{} + } + } else { + carpoolresults = []any{} } + fmt.Println(carpoolresults) + // Vehicles vehiclerequest := &fleets.GetVehiclesRequest{ diff --git a/handlers/auth/auth.go b/handlers/auth/auth.go index 9267be9..43d42bd 100644 --- a/handlers/auth/auth.go +++ b/handlers/auth/auth.go @@ -3,8 +3,9 @@ package auth import ( "git.coopgo.io/coopgo-apps/parcoursmob/renderer" "git.coopgo.io/coopgo-apps/parcoursmob/services" - "git.coopgo.io/coopgo-apps/parcoursmob/utils/cache" "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification" + cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage" + "git.coopgo.io/coopgo-platform/emailing" "github.com/spf13/viper" ) @@ -13,10 +14,11 @@ type AuthHandler struct { config *viper.Viper services *services.ServicesHandler Renderer *renderer.Renderer - cache *cache.CacheHandler + cache cache.CacheHandler + emailing *emailing.Mailer } -func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache *cache.CacheHandler) (*AuthHandler, error) { +func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider, svc *services.ServicesHandler, cache cache.CacheHandler, emailing *emailing.Mailer) (*AuthHandler, error) { templates_root := cfg.GetString("templates.root") renderer := renderer.NewRenderer(cfg, templates_root) return &AuthHandler{ @@ -25,5 +27,6 @@ func NewAuthHandler(cfg *viper.Viper, idp *identification.IdentificationProvider services: svc, Renderer: renderer, cache: cache, + emailing: emailing, }, nil } diff --git a/handlers/auth/lost_password.go b/handlers/auth/lost_password.go new file mode 100644 index 0000000..9647513 --- /dev/null +++ b/handlers/auth/lost_password.go @@ -0,0 +1,97 @@ +package auth + +import ( + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "net/http" + "time" + + "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi" +) + +func (h *AuthHandler) LostPasswordInit(w http.ResponseWriter, r *http.Request) { + if r.Method == "POST" { + r.ParseForm() + email := r.FormValue("email") + if email != "" { + account, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &grpcapi.GetAccountUsernameRequest{ + Username: email, + Namespace: "parcoursmob", + }) + if err != nil { + fmt.Println(err) + http.Redirect(w, r, "/app/", http.StatusFound) + return + } + + b := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, b); err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + key := base64.RawURLEncoding.EncodeToString(b) + + passwordretrieval := map[string]any{ + "username": email, + "account_id": account.Account.Id, + "key": key, + } + + h.cache.PutWithTTL("retrieve-password/"+key, passwordretrieval, 72*time.Hour) + + if err := h.emailing.Send("auth.retrieve_password", email, passwordretrieval); err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } + + http.Redirect(w, r, "/app/", http.StatusFound) + } + } + h.Renderer.LostPasswordInit(w, r) + +} + +func (h *AuthHandler) LostPasswordRecover(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + + key := r.FormValue("key") + recover, err := h.cache.Get("retrieve-password/" + key) + if err != nil { + fmt.Println(err) + h.Renderer.LostPasswordRecoverKO(w, r, key) + return + } + + if r.Method == "POST" { + newpassword := r.FormValue("password") + if newpassword == "" { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte("Password is empty")) + return + } + + _, err := h.services.GRPC.MobilityAccounts.ChangePassword(context.TODO(), &grpcapi.ChangePasswordRequest{ + Id: recover.(map[string]any)["account_id"].(string), + Password: newpassword, + }) + + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + } + + err = h.cache.Delete("retrieve-password/" + key) + if err != nil { + fmt.Println(err) + } + + http.Redirect(w, r, "/app/", http.StatusFound) + + } + h.Renderer.LostPasswordRecover(w, r, recover) +} diff --git a/handlers/auth/onboarding.go b/handlers/auth/onboarding.go index 15f7287..f1c87a5 100644 --- a/handlers/auth/onboarding.go +++ b/handlers/auth/onboarding.go @@ -16,7 +16,7 @@ func (h *AuthHandler) Onboarding(w http.ResponseWriter, r *http.Request) { onboarding, err := h.cache.Get("onboarding/" + key) if err != nil { fmt.Println(err) - w.WriteHeader(http.StatusInternalServerError) + h.Renderer.AuthOnboardingKO(w, r, key) return } @@ -72,6 +72,12 @@ func (h *AuthHandler) Onboarding(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) return } + + err = h.cache.Delete("onboarding/" + key) + if err != nil { + fmt.Println(err) + } + http.Redirect(w, r, "/app/", http.StatusFound) } diff --git a/main.go b/main.go index 5f4d0fc..56cffec 100644 --- a/main.go +++ b/main.go @@ -11,8 +11,8 @@ import ( "git.coopgo.io/coopgo-apps/parcoursmob/handlers/auth" "git.coopgo.io/coopgo-apps/parcoursmob/renderer" "git.coopgo.io/coopgo-apps/parcoursmob/services" - "git.coopgo.io/coopgo-apps/parcoursmob/utils/cache" "git.coopgo.io/coopgo-apps/parcoursmob/utils/identification" + cache "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage" "github.com/gorilla/mux" ) @@ -33,12 +33,12 @@ func main() { panic(err) } - idp, err := identification.NewIdentificationProvider(cfg, svc) + kv, err := cache.NewKVHandler(cfg) if err != nil { panic(err) } - cache, err := cache.NewCacheHandler(cfg) + idp, err := identification.NewIdentificationProvider(cfg, svc, kv) if err != nil { panic(err) } @@ -48,9 +48,9 @@ func main() { panic(err) } - apiHandler, _ := api.NewAPIHandler(cfg, idp, svc, cache) - applicationHandler, _ := application.NewApplicationHandler(cfg, svc, cache, emailing) - authHandler, _ := auth.NewAuthHandler(cfg, idp, svc, cache) + apiHandler, _ := api.NewAPIHandler(cfg, idp, svc, kv) + applicationHandler, _ := application.NewApplicationHandler(cfg, svc, kv, emailing) + authHandler, _ := auth.NewAuthHandler(cfg, idp, svc, kv, emailing) fmt.Println("Running", service_name, ":") @@ -59,6 +59,8 @@ func main() { r.PathPrefix("/public/").Handler(http.StripPrefix("/public/", http.FileServer(http.Dir(templates_public_dir)))) r.HandleFunc("/auth/onboarding", authHandler.Onboarding) + r.HandleFunc("/auth/lost-password", authHandler.LostPasswordInit) + r.HandleFunc("/auth/lost-password/recover", authHandler.LostPasswordRecover) r.HandleFunc("/auth/groups/", authHandler.Groups) r.HandleFunc("/auth/groups/switch", authHandler.GroupSwitch) r.HandleFunc("/", redirectApp) @@ -67,6 +69,7 @@ func main() { api_router.HandleFunc("/", apiHandler.NotFound) api_router.HandleFunc("/geo/autocomplete", apiHandler.GeoAutocomplete) api_router.HandleFunc("/cache/{cacheid}", apiHandler.GetCache) + api_router.HandleFunc("/cache/{cacheid}/export", apiHandler.CacheExport) api_router.HandleFunc("/oauth2/callback", apiHandler.OAuth2Callback) application := r.PathPrefix("/app").Subrouter() @@ -104,6 +107,7 @@ func main() { appAdmin.HandleFunc("/groups/", applicationHandler.AdministrationCreateGroup) appAdmin.HandleFunc("/groups/{groupid}", applicationHandler.AdministrationGroupDisplay) appAdmin.HandleFunc("/groups/{groupid}/invite-admin", applicationHandler.AdministrationGroupInviteAdmin) + appAdmin.HandleFunc("/groups/{groupid}/invite-member", applicationHandler.AdministrationGroupInviteMember) //TODO Secure with Middleware checking for modules fmt.Println("-> HTTP server listening on", address) diff --git a/renderer/auth.go b/renderer/auth.go index ca4bd01..957359e 100644 --- a/renderer/auth.go +++ b/renderer/auth.go @@ -13,7 +13,7 @@ func (renderer *Renderer) AuthGroups(w http.ResponseWriter, r *http.Request, gro } func (renderer *Renderer) AuthOnboarding(w http.ResponseWriter, r *http.Request, key string, onboarding any) { - files := renderer.ThemeConfig.GetStringSlice("views.auth.onboarding.files") + files := renderer.ThemeConfig.GetStringSlice("views.auth.onboarding.form.files") state := NewState(r, renderer.ThemeConfig, "") state.ViewState = map[string]any{ "key": key, @@ -22,3 +22,41 @@ func (renderer *Renderer) AuthOnboarding(w http.ResponseWriter, r *http.Request, renderer.RenderNoLayout("onboarding", w, r, files, state) } + +func (renderer *Renderer) AuthOnboardingKO(w http.ResponseWriter, r *http.Request, key string) { + files := renderer.ThemeConfig.GetStringSlice("views.auth.onboarding.ko.files") + state := NewState(r, renderer.ThemeConfig, "") + state.ViewState = map[string]any{ + "key": key, + } + + renderer.RenderNoLayout("onboarding", w, r, files, state) +} + +func (renderer *Renderer) LostPasswordInit(w http.ResponseWriter, r *http.Request) { + files := renderer.ThemeConfig.GetStringSlice("views.auth.lost_password.init.files") + state := NewState(r, renderer.ThemeConfig, "") + state.ViewState = map[string]any{} + + renderer.RenderNoLayout("lost_password_init", w, r, files, state) +} + +func (renderer *Renderer) LostPasswordRecover(w http.ResponseWriter, r *http.Request, recover any) { + files := renderer.ThemeConfig.GetStringSlice("views.auth.lost_password.recover.form.files") + state := NewState(r, renderer.ThemeConfig, "") + state.ViewState = map[string]any{ + "recover": recover, + } + + renderer.RenderNoLayout("lost_password_recover", w, r, files, state) +} + +func (renderer *Renderer) LostPasswordRecoverKO(w http.ResponseWriter, r *http.Request, key string) { + files := renderer.ThemeConfig.GetStringSlice("views.auth.lost_password.recover.ko.files") + state := NewState(r, renderer.ThemeConfig, "") + state.ViewState = map[string]any{ + "key": key, + } + + renderer.RenderNoLayout("lost_password_recover_ko", w, r, files, state) +} diff --git a/renderer/beneficiaries.go b/renderer/beneficiaries.go index 614cee8..a28ec8e 100644 --- a/renderer/beneficiaries.go +++ b/renderer/beneficiaries.go @@ -4,14 +4,16 @@ import ( "encoding/json" "html/template" "net/http" + + mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage" ) const beneficiariesMenu = "beneficiaries" type BeneficiariesListState struct { - Count int `json:"count"` - CacheId string `json:"cache_id"` - Beneficiaries []any `json:"beneficiaries"` + Count int `json:"count"` + CacheId string `json:"cache_id"` + Beneficiaries []mobilityaccountsstorage.Account `json:"beneficiaries"` } func (s BeneficiariesListState) JSON() template.JS { @@ -26,7 +28,7 @@ func (s BeneficiariesListState) JSONWithLimits(a int, b int) template.JS { return s.JSON() } -func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []any, cacheid string) { +func (renderer *Renderer) BeneficiariesList(w http.ResponseWriter, r *http.Request, accounts []mobilityaccountsstorage.Account, cacheid string) { files := renderer.ThemeConfig.GetStringSlice("views.beneficiaries.list.files") state := NewState(r, renderer.ThemeConfig, beneficiariesMenu) diff --git a/renderer/vehicles.go b/renderer/vehicles.go index 0dd88d8..4ec0184 100644 --- a/renderer/vehicles.go +++ b/renderer/vehicles.go @@ -1,10 +1,14 @@ package renderer -import "net/http" +import ( + "net/http" + + mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage" +) const vehiclesMenu = "vehicles" -func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []any, searched bool, vehicles []any, beneficiary any, startdate any, enddate any) { +func (renderer *Renderer) VehiclesSearch(w http.ResponseWriter, r *http.Request, beneficiaries []mobilityaccountsstorage.Account, searched bool, vehicles []any, beneficiary any, startdate any, enddate any) { files := renderer.ThemeConfig.GetStringSlice("views.vehicles.search.files") state := NewState(r, renderer.ThemeConfig, vehiclesMenu) viewstate := map[string]any{ diff --git a/themes/default/config.yaml b/themes/default/config.yaml index 5fc9839..49aa1f9 100644 --- a/themes/default/config.yaml +++ b/themes/default/config.yaml @@ -114,9 +114,24 @@ views: groups: files: - web/layouts/auth/groups.html + lost_password: + init: + files: + - web/layouts/auth/lost-password-init.html + recover: + form: + files: + - web/layouts/auth/lost-password-recover.html + ko: + files: + - web/layouts/auth/lost-password-recover-ko.html onboarding: - files: - - web/layouts/auth/onboarding.html + form: + files: + - web/layouts/auth/onboarding.html + ko: + files: + - web/layouts/auth/onboarding-ko.html icons: svg: @@ -125,6 +140,7 @@ icons: hero:outline/calendar: hero:outline/chevron-right: hero:outline/cog: + hero:outline/document-arrow-down: hero:outline/document-text: hero:outline/home: hero:outline/map: @@ -164,4 +180,9 @@ emails: files: - emails/layout.html - emails/onboarding/existing-member.html - \ No newline at end of file + auth: + retrieve_password: + subject: PAROURSMOB - Réinitialisez votre mot de passe + files: + - emails/layout.html + - emails/auth/retrieve-password.html \ No newline at end of file diff --git a/themes/default/emails/auth/retrieve-password.html b/themes/default/emails/auth/retrieve-password.html new file mode 100644 index 0000000..0bc2260 --- /dev/null +++ b/themes/default/emails/auth/retrieve-password.html @@ -0,0 +1,5 @@ +{{define "content"}} +

Bonjour,

+

Vous avez demandé à réinitialiser votre mot de passe pour {{.username}}

+

Pour créer votre nouveau mot de passe, cliquez sur le lien suivant : http://localhost:9000/auth/lost-password/recover?key={{.key}}

+{{end}} \ No newline at end of file diff --git a/themes/default/emails/onboarding/new-administrator.html b/themes/default/emails/onboarding/new-administrator.html index f2e62d2..53687a3 100644 --- a/themes/default/emails/onboarding/new-administrator.html +++ b/themes/default/emails/onboarding/new-administrator.html @@ -1,5 +1,5 @@ {{define "content"}}

Vous avez été ajouté comme administrateur de l'organisation {{.group}} sur PARCOURSMOB.

Vous devez créer votre compte pour y accéder.

-

Pour créer votre compte PARCOURSMOB, cliquez sur : http://localhost:9000/onboarding?key={{.key}}

+

Pour créer votre compte PARCOURSMOB, cliquez sur : http://localhost:9000/auth/onboarding?key={{.key}}

{{end}} \ No newline at end of file diff --git a/themes/default/emails/onboarding/new-member.html b/themes/default/emails/onboarding/new-member.html index 845d612..3c230aa 100644 --- a/themes/default/emails/onboarding/new-member.html +++ b/themes/default/emails/onboarding/new-member.html @@ -1,5 +1,5 @@ {{define "content"}}

Vous avez été ajouté à l'organisation {{.group}} sur PARCOURSMOB.

Vous devez créer votre compte pour y accéder.

-

Pour créer votre compte PARCOURSMOB, cliquez sur : http://localhost:9000/onboarding?key={{.key}}

+

Pour créer votre compte PARCOURSMOB, cliquez sur : http://localhost:9000/auth/onboarding?key={{.key}}

{{end}} \ No newline at end of file diff --git a/themes/default/web/layouts/_partials/address_autocomplete.html b/themes/default/web/layouts/_partials/address_autocomplete.html index 174aa1c..3ffd1b2 100644 --- a/themes/default/web/layouts/_partials/address_autocomplete.html +++ b/themes/default/web/layouts/_partials/address_autocomplete.html @@ -3,7 +3,7 @@
+ + {{template "groups_members" .}} +
+
+
+ +
+ + +
+ +
diff --git a/themes/default/web/layouts/auth/lost-password-init.html b/themes/default/web/layouts/auth/lost-password-init.html new file mode 100644 index 0000000..996fcbd --- /dev/null +++ b/themes/default/web/layouts/auth/lost-password-init.html @@ -0,0 +1,40 @@ +{{define "main"}} + + + PARCOURSMOB - Identification + + + +
+ + + +
+
+ +

Réinitialiser votre mot de passe PARCOURSMOB

+
+ +
+
+
+ +
+ +
+
+ +

Si votre compte existe, vous allez recevoir un mot de passe par email contenant un lien pour réinitialiser votre mot de passe. Celui-ci sera actif pendant 72h.

+ +
+ +
+ + +
+
+
+
+ + +{{end}} \ No newline at end of file diff --git a/themes/default/web/layouts/auth/lost-password-recover-ko.html b/themes/default/web/layouts/auth/lost-password-recover-ko.html new file mode 100644 index 0000000..90b6928 --- /dev/null +++ b/themes/default/web/layouts/auth/lost-password-recover-ko.html @@ -0,0 +1,26 @@ +{{define "main"}} + + + PARCOURSMOB - Identification + + + + + + +
+
+ +

Réinitialiser votre mot de passe PARCOURSMOB

+

Ce lien de réinitialisation n'est plus actif. Vous l'avez déjà utilisé ou il a expiré. Vous pouvez redemander un nouveau mot de passe ou réessayer de vous connecter directement à PARCOURSMOB.

+
+ + + +
+
+ +
+ + +{{end}} \ No newline at end of file diff --git a/themes/default/web/layouts/auth/lost-password-recover.html b/themes/default/web/layouts/auth/lost-password-recover.html new file mode 100644 index 0000000..4286fd5 --- /dev/null +++ b/themes/default/web/layouts/auth/lost-password-recover.html @@ -0,0 +1,41 @@ +{{define "main"}} + + + PARCOURSMOB - Identification + + + +
+ + + +
+
+ +

Réinitialisez votre mot de passe PARCOURSMOB

+
+ +
+
+ +

Vous avez demandé à réinitialiser votre mot de passe pour {{.ViewState.recover.username}}

+ +
+ +
+ +
+
+ +
+ +
+ + +
+
+
+
+ + +{{end}} \ No newline at end of file diff --git a/themes/default/web/layouts/auth/onboarding-ko.html b/themes/default/web/layouts/auth/onboarding-ko.html new file mode 100644 index 0000000..12d3972 --- /dev/null +++ b/themes/default/web/layouts/auth/onboarding-ko.html @@ -0,0 +1,26 @@ +{{define "main"}} + + + PARCOURSMOB - Identification + + + + + + +
+
+ +

Inscription à PARCOURSMOB

+

Ce lien d'inscription n'est plus actif. Vous avez peut être déjà créé votre compte. Si ce n'est pas le cas, le lien a pu expirer : veuillez en demander un nouveau à l'administrateur PARCOURSMOB de votre structure.

+
+ + + +
+
+ +
+ + +{{end}} \ No newline at end of file diff --git a/themes/default/web/layouts/beneficiaries/create.html b/themes/default/web/layouts/beneficiaries/create.html index d42bf43..36ba876 100644 --- a/themes/default/web/layouts/beneficiaries/create.html +++ b/themes/default/web/layouts/beneficiaries/create.html @@ -11,14 +11,16 @@ last_name: null, email: null, phone_number: null, - birthdate: null + birthdate: null, + file_number: null }, rules: { first_name: ['required'], last_name: ['required'], email: ['required', 'email'], phone_number: ['required', 'regexMatch:^((\\+)33|0)[1-9](\\d{2}){4}$'], - birthdate: ['optional'], + birthdate: ['required'], + file_number: ['optional'], }, formValidation: { valid: false, @@ -28,6 +30,7 @@ email: {valid: null}, phone_number: {valid: null}, birthdate: {valid: null}, + file_number: {valid: null}, } }, isFormValid: true, @@ -72,7 +75,7 @@ :class="formValidation.fields.last_name.valid == false ? 'border-co-red border-2' : 'border-gray-300'"> -
+
-
+
+ +
+ + +
@@ -103,12 +115,11 @@
- - Numéro de dossier (allocataire, ...) + + x-model="fields.file_number" @blur="validateField('file_number')" + :class="formValidation.fields.file_number.valid == false ? 'border-co-red border-2' : 'border-gray-300'">
diff --git a/themes/default/web/layouts/beneficiaries/list.html b/themes/default/web/layouts/beneficiaries/list.html index 0b4e116..da8bbad 100644 --- a/themes/default/web/layouts/beneficiaries/list.html +++ b/themes/default/web/layouts/beneficiaries/list.html @@ -7,6 +7,13 @@

+ + +
-
+
-
+
+ +
+ + +
@@ -104,12 +116,11 @@
- - + +
diff --git a/themes/default/web/layouts/journeys/_partials/journeys-all.html b/themes/default/web/layouts/journeys/_partials/journeys-all.html index 92ab184..c340373 100644 --- a/themes/default/web/layouts/journeys/_partials/journeys-all.html +++ b/themes/default/web/layouts/journeys/_partials/journeys-all.html @@ -53,7 +53,7 @@ {{end}} - + {{ if gt (len .ViewState.journeys.Journeys) 0}}
{{.IconSet.Icon "tabler-icons:bus" "h-6 w-6 inline-flex mr-4"}} @@ -76,7 +76,7 @@ {{end}} {{if eq .Type "public_transport"}} - {{if eq .Display.Network "Antibes"}}Envibus{{else}}{{.Display.Network}}{{end}} Ligne {{.Display.Label}} + {{if eq .Display.Network "Antibes - Envibus"}}Envibus{{else}}{{.Display.Network}}{{end}} Ligne {{.Display.Label}} {{$.IconSet.Icon "hero:outline/chevron-right" "h-3 w-3 stroke-gray-800 m-2"}} {{end}} @@ -88,6 +88,8 @@
+ {{end}} +
diff --git a/themes/default/web/layouts/vehicles/search.html b/themes/default/web/layouts/vehicles/search.html index 073d824..db6d7e6 100644 --- a/themes/default/web/layouts/vehicles/search.html +++ b/themes/default/web/layouts/vehicles/search.html @@ -10,7 +10,7 @@
diff --git a/themes/default/web/public/css/main.css b/themes/default/web/public/css/main.css index 2ef3532..ee473ab 100644 --- a/themes/default/web/public/css/main.css +++ b/themes/default/web/public/css/main.css @@ -1507,11 +1507,6 @@ html { border-bottom-left-radius: 1rem; } -.rounded-l { - border-top-left-radius: 0.25rem; - border-bottom-left-radius: 0.25rem; -} - .border { border-width: 1px; } @@ -1670,10 +1665,6 @@ html { padding: 0.375rem; } -.p-3 { - padding: 0.75rem; -} - .px-4 { padding-left: 1rem; padding-right: 1rem; @@ -1714,6 +1705,11 @@ html { padding-bottom: 1rem; } +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + .px-3 { padding-left: 0.75rem; padding-right: 0.75rem; @@ -1734,11 +1730,6 @@ html { padding-bottom: 3rem; } -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - .py-8 { padding-top: 2rem; padding-bottom: 2rem; @@ -1777,6 +1768,10 @@ html { padding-right: 2.25rem; } +.pr-2 { + padding-right: 0.5rem; +} + .pr-12 { padding-right: 3rem; } @@ -1801,10 +1796,6 @@ html { padding-top: 2rem; } -.pr-2 { - padding-right: 0.5rem; -} - .pt-4 { padding-top: 1rem; } @@ -2532,16 +2523,16 @@ html { padding-right: 1.5rem; } - .sm\:py-5 { - padding-top: 1.25rem; - padding-bottom: 1.25rem; - } - .sm\:px-10 { padding-left: 2.5rem; padding-right: 2.5rem; } + .sm\:py-5 { + padding-top: 1.25rem; + padding-bottom: 1.25rem; + } + .sm\:pl-6 { padding-left: 1.5rem; } diff --git a/utils/cache/cache.go b/utils/cache/cache.go deleted file mode 100644 index dd1ea12..0000000 --- a/utils/cache/cache.go +++ /dev/null @@ -1,104 +0,0 @@ -package cache - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/spf13/viper" - clientv3 "go.etcd.io/etcd/client/v3" - "go.etcd.io/etcd/client/v3/namespace" -) - -type CacheHandler struct { - *clientv3.Client -} - -func NewCacheHandler(cfg *viper.Viper) (*CacheHandler, error) { - var ( - endpoints = cfg.GetStringSlice("cache.storage.etcd.endpoints") - prefix = cfg.GetString("cache.storage.etcd.prefix") - ) - - cli, err := clientv3.New(clientv3.Config{ - Endpoints: endpoints, - DialTimeout: 5 * time.Second, - }) - if err != nil { - return nil, err - } - - cli.KV = namespace.NewKV(cli.KV, prefix) - cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix) - cli.Lease = namespace.NewLease(cli.Lease, prefix) - - return &CacheHandler{ - Client: cli, - }, nil -} - -func (s *CacheHandler) Put(k string, v any) error { - data, err := json.Marshal(v) - if err != nil { - return err - } - // _, err = s.Client.KV.Put(context.TODO(), k, data.String()) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - _, err = s.Client.KV.Put(ctx, k, string(data)) - cancel() - if err != nil { - return err - } - return nil -} - -func (s *CacheHandler) PutWithTTL(k string, v any, duration time.Duration) error { - lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds())) - if err != nil { - return err - } - - data, err := json.Marshal(v) - if err != nil { - return err - } - // _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID)) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - _, err = s.Client.KV.Put(ctx, k, string(data), clientv3.WithLease(lease.ID)) - cancel() - if err != nil { - return err - } - return nil -} - -func (s *CacheHandler) Get(k string) (any, error) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - resp, err := s.Client.KV.Get(ctx, k) - cancel() - if err != nil { - return nil, err - } - for _, v := range resp.Kvs { - var data any - err := json.Unmarshal([]byte(v.Value), &data) - if err != nil { - return nil, err - } - // We return directly as we want to last revision of value - return data, nil - } - return nil, errors.New(fmt.Sprintf("no value %v", k)) -} - -func (s *CacheHandler) Delete(k string) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - _, err := s.Client.KV.Delete(ctx, k) - cancel() - if err != nil { - return err - } - return nil -} diff --git a/utils/identification/groups.go b/utils/identification/groups.go index 79b9b7a..52c19e5 100644 --- a/utils/identification/groups.go +++ b/utils/identification/groups.go @@ -19,6 +19,7 @@ func (p *IdentificationProvider) GroupsMiddleware(next http.Handler) http.Handle o, ok := session.Values["organization"] if !ok || o == nil { + fmt.Println("no organization") http.Redirect(w, r, "/auth/groups/", http.StatusFound) return } diff --git a/utils/identification/oidc.go b/utils/identification/oidc.go index 5e60c9a..9adf208 100644 --- a/utils/identification/oidc.go +++ b/utils/identification/oidc.go @@ -9,6 +9,7 @@ import ( "net/http" "git.coopgo.io/coopgo-apps/parcoursmob/services" + "git.coopgo.io/coopgo-apps/parcoursmob/utils/storage" "github.com/coreos/go-oidc" "github.com/gorilla/sessions" "github.com/spf13/viper" @@ -28,7 +29,7 @@ type IdentificationProvider struct { Services *services.ServicesHandler } -func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHandler) (*IdentificationProvider, error) { +func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHandler, kv storage.KVHandler) (*IdentificationProvider, error) { var ( providerURL = cfg.GetString("identification.oidc.provider") clientID = cfg.GetString("identification.oidc.client_id") @@ -54,7 +55,7 @@ func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHand Scopes: []string{oidc.ScopeOpenID, "groups", "first_name", "last_name", "display_name"}, } - var store = sessions.NewCookieStore([]byte(sessionsSecret)) + store := storage.NewSessionStore(kv, []byte(sessionsSecret)) verifier := provider.Verifier(&oidc.Config{ClientID: oauth2Config.ClientID}) return &IdentificationProvider{ diff --git a/utils/storage/cache.go b/utils/storage/cache.go new file mode 100644 index 0000000..3b3420d --- /dev/null +++ b/utils/storage/cache.go @@ -0,0 +1,11 @@ +package storage + +import ( + "github.com/spf13/viper" +) + +type CacheHandler KVHandler + +func NewCacheHandler(cfg *viper.Viper) (CacheHandler, error) { + return NewKVHandler(cfg) +} diff --git a/utils/storage/etcd.go b/utils/storage/etcd.go new file mode 100644 index 0000000..5dfeec2 --- /dev/null +++ b/utils/storage/etcd.go @@ -0,0 +1,149 @@ +package storage + +import ( + "bytes" + "context" + "encoding/gob" + "encoding/json" + "fmt" + "time" + + "github.com/spf13/viper" + clientv3 "go.etcd.io/etcd/client/v3" + "go.etcd.io/etcd/client/v3/namespace" +) + +type EtcdSerializer interface { + Deserialize(d []byte, m *any) error + Serialize(m any) ([]byte, error) +} + +type JSONEtcdSerializer struct{} + +// Serialize to JSON. Will err if there are unmarshalable key values +func (s JSONEtcdSerializer) Serialize(m any) ([]byte, error) { + return json.Marshal(m) +} + +// Deserialize back to map[string]interface{} +func (s JSONEtcdSerializer) Deserialize(d []byte, m *any) (err error) { + err = json.Unmarshal(d, &m) + if err != nil { + fmt.Printf("JSONSerializer.deserialize() Error: %v", err) + return err + } + return +} + +// GobEtcdSerializer uses gob package to encode the session map +type GobEtcdSerializer struct{} + +// Serialize using gob +func (s GobEtcdSerializer) Serialize(m any) ([]byte, error) { + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + err := enc.Encode(m) + if err == nil { + return buf.Bytes(), nil + } + return nil, err +} + +// Deserialize back to map[interface{}]interface{} +func (s GobEtcdSerializer) Deserialize(d []byte, m any) error { + dec := gob.NewDecoder(bytes.NewBuffer(d)) + return dec.Decode(&m) +} + +type EtcdHandler struct { + *clientv3.Client + serializer EtcdSerializer +} + +func NewEtcdHandler(cfg *viper.Viper) (*EtcdHandler, error) { + var ( + endpoints = cfg.GetStringSlice("storage.kv.etcd.endpoints") + prefix = cfg.GetString("storage.kv.etcd.prefix") + ) + + cli, err := clientv3.New(clientv3.Config{ + Endpoints: endpoints, + DialTimeout: 5 * time.Second, + }) + if err != nil { + return nil, err + } + + cli.KV = namespace.NewKV(cli.KV, prefix) + cli.Watcher = namespace.NewWatcher(cli.Watcher, prefix) + cli.Lease = namespace.NewLease(cli.Lease, prefix) + + return &EtcdHandler{ + Client: cli, + serializer: JSONEtcdSerializer{}, + }, nil +} + +func (s *EtcdHandler) Put(k string, v any) error { + data, err := s.serializer.Serialize(v) + if err != nil { + return err + } + // _, err = s.Client.KV.Put(context.TODO(), k, data.String()) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _, err = s.Client.KV.Put(ctx, k, string(data)) + cancel() + if err != nil { + return err + } + return nil +} + +func (s *EtcdHandler) PutWithTTL(k string, v any, duration time.Duration) error { + lease, err := s.Client.Lease.Grant(context.TODO(), int64(duration.Seconds())) + if err != nil { + return err + } + + data, err := s.serializer.Serialize(v) + if err != nil { + return err + } + // _, err = s.Client.KV.Put(context.TODO(), k, data.String(), clientv3.WithLease(lease.ID)) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _, err = s.Client.KV.Put(ctx, k, string(data), clientv3.WithLease(lease.ID)) + cancel() + if err != nil { + return err + } + return nil +} + +func (s *EtcdHandler) Get(k string) (any, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + resp, err := s.Client.KV.Get(ctx, k) + cancel() + if err != nil { + return nil, err + } + for _, v := range resp.Kvs { + var data any + err := s.serializer.Deserialize([]byte(v.Value), &data) + if err != nil { + return nil, err + } + // We return directly as we want to last revision of value + return data, nil + } + return nil, fmt.Errorf("no value %v", k) +} + +func (s *EtcdHandler) Delete(k string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _, err := s.Client.KV.Delete(ctx, k) + cancel() + if err != nil { + return err + } + return nil +} diff --git a/utils/storage/kv.go b/utils/storage/kv.go new file mode 100644 index 0000000..f422a7b --- /dev/null +++ b/utils/storage/kv.go @@ -0,0 +1,18 @@ +package storage + +import ( + "time" + + "github.com/spf13/viper" +) + +type KVHandler interface { + Put(k string, v any) error + PutWithTTL(k string, v any, duration time.Duration) error + Get(k string) (any, error) + Delete(k string) error +} + +func NewKVHandler(cfg *viper.Viper) (KVHandler, error) { + return NewEtcdHandler(cfg) +} diff --git a/utils/storage/sessions.go b/utils/storage/sessions.go new file mode 100644 index 0000000..05cf5e6 --- /dev/null +++ b/utils/storage/sessions.go @@ -0,0 +1,144 @@ +package storage + +import ( + "context" + "encoding/base32" + "errors" + "fmt" + "net/http" + "strings" + "time" + + "github.com/gorilla/securecookie" + "github.com/gorilla/sessions" +) + +// Amount of time for cookies/kv keys to expire. +var sessionExpire = 86400 * 30 + +type SessionStore struct { + KV KVHandler + Codecs []securecookie.Codec + options *sessions.Options // default configuration + DefaultMaxAge int // default TiKV TTL for a MaxAge == 0 session + //maxLength int + keyPrefix string + //serializer SessionSerializer +} + +func NewSessionStore(client KVHandler, keyPairs ...[]byte) *SessionStore { + es := &SessionStore{ + KV: client, + Codecs: securecookie.CodecsFromPairs(keyPairs...), + options: &sessions.Options{ + Path: "/", + MaxAge: sessionExpire, + }, + DefaultMaxAge: 60 * 20, // 20 minutes seems like a reasonable default + //maxLength: 4096, + keyPrefix: "session/", + } + + return es +} + +func (s *SessionStore) Get(r *http.Request, name string) (*sessions.Session, error) { + // session := sessions.NewSession(s, name) + // ok, err := s.load(r.Context(), session) + // if !(err == nil && ok) { + // if err == nil { + // err = errors.New("key does not exist") + // } + // } + // return session, err + return sessions.GetRegistry(r).Get(s, name) +} + +func (s *SessionStore) New(r *http.Request, name string) (*sessions.Session, error) { + session := sessions.NewSession(s, name) + options := *s.options + session.Options = &options + session.IsNew = true + if c, errCookie := r.Cookie(name); errCookie == nil { + err := securecookie.DecodeMulti(name, c.Value, &session.ID, s.Codecs...) + if err != nil { + return session, err + } + ok, err := s.load(r.Context(), session) + session.IsNew = !(err == nil && ok) // not new if no error and data available + } + return session, nil +} + +// Save adds a single session to the response. +func (s *SessionStore) Save(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { + // Marked for deletion. + if session.Options.MaxAge <= 0 { + if err := s.delete(r.Context(), session); err != nil { + return err + } + http.SetCookie(w, sessions.NewCookie(session.Name(), "", session.Options)) + } else { + // Build an alphanumeric key for the kv store. + if session.ID == "" { + session.ID = strings.TrimRight(base32.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(32)), "=") + } + if err := s.save(r.Context(), session); err != nil { + return err + } + encoded, err := securecookie.EncodeMulti(session.Name(), session.ID, s.Codecs...) + if err != nil { + return err + } + http.SetCookie(w, sessions.NewCookie(session.Name(), encoded, session.Options)) + } + return nil +} + +// save stores the session in kv. +func (s *SessionStore) save(ctx context.Context, session *sessions.Session) error { + m := make(map[string]interface{}, len(session.Values)) + for k, v := range session.Values { + ks, ok := k.(string) + if !ok { + err := fmt.Errorf("non-string key value, cannot serialize session: %v", k) + return err + } + m[ks] = v + } + + age := session.Options.MaxAge + if age == 0 { + age = s.DefaultMaxAge + } + + return s.KV.PutWithTTL(s.keyPrefix+session.ID, m, time.Duration(age)*time.Second) +} + +// load reads the session from kv store. +// returns true if there is a sessoin data in DB +func (s *SessionStore) load(ctx context.Context, session *sessions.Session) (bool, error) { + + data, err := s.KV.Get(s.keyPrefix + session.ID) + if err != nil { + return false, err + } + + if data == nil && err == nil { + return false, errors.New("key does not exist") + } + + for k, v := range data.(map[string]any) { + session.Values[k] = v + } + + return true, nil +} + +// delete removes keys from tikv if MaxAge<0 +func (s *SessionStore) delete(ctx context.Context, session *sessions.Session) error { + if err := s.KV.Delete(s.keyPrefix + session.ID); err != nil { + return err + } + return nil +}