Big refactoring of PARCOURSMOB - Initial commit
This commit is contained in:
104
utils/cache/cache.go
vendored
Normal file
104
utils/cache/cache.go
vendored
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
}
|
||||
14
utils/form-validators/form-validators.go
Normal file
14
utils/form-validators/form-validators.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package formvalidators
|
||||
|
||||
import "github.com/go-playground/validator/v10"
|
||||
|
||||
type FormValidator struct {
|
||||
*validator.Validate
|
||||
}
|
||||
|
||||
func New() (f FormValidator) {
|
||||
validate := validator.New()
|
||||
validate.RegisterValidation("phoneNumber", PhoneNumber)
|
||||
f.Validate = validate
|
||||
return
|
||||
}
|
||||
15
utils/form-validators/phone-numbers.go
Normal file
15
utils/form-validators/phone-numbers.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package formvalidators
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
const phoneNumberRegexStreing = "^((\\+)33|0)[1-9](\\d{2}){4}$"
|
||||
|
||||
var phoneNumberRegex = regexp.MustCompile(phoneNumberRegexStreing)
|
||||
|
||||
func PhoneNumber(fl validator.FieldLevel) bool {
|
||||
return phoneNumberRegex.MatchString(fl.Field().String())
|
||||
}
|
||||
26
utils/icons/svg-icons.go
Normal file
26
utils/icons/svg-icons.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package icons
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
type IconSet struct {
|
||||
Icons map[string]string
|
||||
}
|
||||
|
||||
func NewIconSet(set map[string]string) IconSet {
|
||||
return IconSet{
|
||||
Icons: set,
|
||||
}
|
||||
}
|
||||
|
||||
func (i IconSet) Icon(name string, classes string) template.HTML {
|
||||
icon, ok := i.Icons[name]
|
||||
|
||||
if !ok {
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
return template.HTML(fmt.Sprintf(icon, classes))
|
||||
}
|
||||
74
utils/identification/groups.go
Normal file
74
utils/identification/groups.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package identification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
||||
)
|
||||
|
||||
const GroupKey ContextKey = "group"
|
||||
const RolesKey ContextKey = "roles"
|
||||
|
||||
func (p *IdentificationProvider) GroupsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
claims := r.Context().Value(ClaimsKey).(map[string]any)
|
||||
|
||||
session, _ := p.SessionsStore.Get(r, "parcoursmob_session")
|
||||
|
||||
o, ok := session.Values["organization"]
|
||||
if !ok || o == nil {
|
||||
http.Redirect(w, r, "/auth/groups/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
org := o.(string)
|
||||
|
||||
claimgroups, ok := claims["groups"].([]any)
|
||||
|
||||
if !ok {
|
||||
fmt.Println("cast issue")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
for _, group := range claimgroups {
|
||||
if group == org {
|
||||
|
||||
request := &groupsmanagement.GetGroupRequest{
|
||||
Id: group.(string),
|
||||
}
|
||||
|
||||
resp, err := p.Services.GRPC.GroupsManagement.GetGroup(context.TODO(), request)
|
||||
if err != nil {
|
||||
delete(session.Values, "organization")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/auth/groups/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), GroupKey, resp.Group.ToStorageType())
|
||||
|
||||
roles := map[string]bool{}
|
||||
|
||||
for _, role := range claimgroups {
|
||||
//TODO handle flexible roles / roles discovery
|
||||
if role == fmt.Sprintf("%s:admin", org) {
|
||||
roles[role.(string)] = true
|
||||
}
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, RolesKey, roles)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Session organization is not in the available groups
|
||||
delete(session.Values, "organization")
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, "/auth/groups/", http.StatusFound)
|
||||
})
|
||||
}
|
||||
117
utils/identification/oidc.go
Normal file
117
utils/identification/oidc.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package identification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/services"
|
||||
"github.com/coreos/go-oidc"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/spf13/viper"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type ContextKey string
|
||||
|
||||
const IdtokenKey ContextKey = "idtoken"
|
||||
const ClaimsKey ContextKey = "claims"
|
||||
|
||||
type IdentificationProvider struct {
|
||||
SessionsStore sessions.Store
|
||||
Provider *oidc.Provider
|
||||
OAuth2Config oauth2.Config
|
||||
TokenVerifier *oidc.IDTokenVerifier
|
||||
Services *services.ServicesHandler
|
||||
}
|
||||
|
||||
func NewIdentificationProvider(cfg *viper.Viper, services *services.ServicesHandler) (*IdentificationProvider, error) {
|
||||
var (
|
||||
providerURL = cfg.GetString("identification.oidc.provider")
|
||||
clientID = cfg.GetString("identification.oidc.client_id")
|
||||
clientSecret = cfg.GetString("identification.oidc.client_secret")
|
||||
redirectURL = cfg.GetString("identification.oidc.redirect_url")
|
||||
sessionsSecret = cfg.GetString("identification.sessions.secret")
|
||||
)
|
||||
|
||||
provider, err := oidc.NewProvider(context.Background(), providerURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
oauth2Config := oauth2.Config{
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
RedirectURL: redirectURL,
|
||||
|
||||
// Discovery returns the OAuth2 endpoints.
|
||||
Endpoint: provider.Endpoint(),
|
||||
|
||||
// "openid" is a required scope for OpenID Connect flows.
|
||||
Scopes: []string{oidc.ScopeOpenID, "groups", "profile"},
|
||||
}
|
||||
|
||||
var store = sessions.NewCookieStore([]byte(sessionsSecret))
|
||||
verifier := provider.Verifier(&oidc.Config{ClientID: oauth2Config.ClientID})
|
||||
|
||||
return &IdentificationProvider{
|
||||
SessionsStore: store,
|
||||
Provider: provider,
|
||||
OAuth2Config: oauth2Config,
|
||||
TokenVerifier: verifier,
|
||||
Services: services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *IdentificationProvider) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
session, _ := p.SessionsStore.Get(r, "parcoursmob_session")
|
||||
|
||||
if session.Values["idtoken"] == nil || session.Values["idtoken"] == "" {
|
||||
|
||||
state, err := newState()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
session.Values["state"] = state
|
||||
session.Save(r, w)
|
||||
http.Redirect(w, r, p.OAuth2Config.AuthCodeURL(state), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
idtoken, err := p.TokenVerifier.Verify(context.Background(), session.Values["idtoken"].(string))
|
||||
if err != nil {
|
||||
state, err := newState()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
session.Values["state"] = state
|
||||
delete(session.Values, "idtoken")
|
||||
http.Redirect(w, r, p.OAuth2Config.AuthCodeURL(state), http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
var claims map[string]any
|
||||
|
||||
err = idtoken.Claims(&claims)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), IdtokenKey, idtoken)
|
||||
ctx = context.WithValue(ctx, ClaimsKey, claims)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func newState() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(b), nil
|
||||
}
|
||||
37
utils/profile-pictures/profile-pictures.go
Normal file
37
utils/profile-pictures/profile-pictures.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package profilepictures
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
|
||||
"github.com/fogleman/gg"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
func DefaultProfilePicture(initials string) *image.RGBA {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 300, 300))
|
||||
col := color.RGBA{36, 56, 135, 255}
|
||||
white := color.RGBA{255, 255, 255, 255}
|
||||
point := fixed.Point26_6{fixed.I(40), fixed.I(200)}
|
||||
|
||||
draw.Draw(img, img.Bounds(), &image.Uniform{col}, image.Point{X: 0, Y: 0}, draw.Src)
|
||||
|
||||
ff, err := gg.LoadFontFace("themes/default/fonts/bitter.ttf", 150.0)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return img
|
||||
}
|
||||
|
||||
d := &font.Drawer{
|
||||
Dst: img,
|
||||
Src: image.NewUniform(white),
|
||||
Face: ff,
|
||||
Dot: point,
|
||||
}
|
||||
d.DrawString(initials)
|
||||
|
||||
return img
|
||||
}
|
||||
Reference in New Issue
Block a user