diff --git a/.env.development b/.env.development index 6b1fae5..d412a4b 100644 --- a/.env.development +++ b/.env.development @@ -4,3 +4,5 @@ GRAPH_PORT = 3009 GRAPH_URL = http://localhost:3009 VITE_GRAPH_URL = http://localhost:3009 DB_URL = postgresql://root:root@127.0.0.1/rano_dev?search_path=public&sslmode=disable +MAILER_TEMPLATES_DIR = mailer/templates +MAILER_FROM_ADDRESS = NoReply diff --git a/cmd/migrate-up/main.go b/cmd/migrate-up/main.go index ba7aece..bdea489 100644 --- a/cmd/migrate-up/main.go +++ b/cmd/migrate-up/main.go @@ -13,7 +13,7 @@ import ( "gitserver.in/patialtech/rano/db" entMigrate "gitserver.in/patialtech/rano/db/ent/migrate" "gitserver.in/patialtech/rano/db/migrations" - "gitserver.in/patialtech/rano/pkg/logger" + "gitserver.in/patialtech/rano/util/logger" ) func main() { diff --git a/config/config.go b/config/config.go index 60445be..09ffe0d 100644 --- a/config/config.go +++ b/config/config.go @@ -9,7 +9,7 @@ import ( "strings" "gitserver.in/patialtech/rano/config/dotenv" - "gitserver.in/patialtech/rano/pkg/logger" + "gitserver.in/patialtech/rano/util/logger" ) const ( @@ -30,33 +30,35 @@ var ( type ( Env string Config struct { - WebPort int `env:"WEB_PORT"` - WebURL string `env:"WEB_URL"` - GraphPort int `env:"GRAPH_PORT"` - GrapURL string `env:"GRAPH_URL"` - DbURL string `env:"DB_URL"` + basePath string + WebPort int `env:"WEB_PORT"` + WebURL string `env:"WEB_URL"` + GraphPort int `env:"GRAPH_PORT"` + GrapURL string `env:"GRAPH_URL"` + DbURL string `env:"DB_URL"` + MailerTplDir string `env:"MAILER_TEMPLATES_DIR"` + MailerFrom string `env:"MAILER_FROM_ADDRESS"` } ) func init() { - wd, _ := os.Getwd() - - // In dev env we run test and other program for diff dir locations under project root, - // this makes reading env file harder. - // Let's add a hack to make sure we fallback to root dir in dev env + var base string if AppEnv == EnvDev { + wd, _ := os.Getwd() idx := strings.Index(wd, projDir) if idx > -1 { - wd = filepath.Join(wd[:idx], projDir) + base = filepath.Join(wd[:idx], projDir) } + } else { + base, _ = os.Executable() } - envVar, err := dotenv.Read(wd, fmt.Sprintf(".env.%s", AppEnv)) + envVar, err := dotenv.Read(base, fmt.Sprintf(".env.%s", AppEnv)) if err != nil { panic(err) } - conf = &Config{} + conf = &Config{basePath: base} conf.loadEnv(envVar) } diff --git a/config/dotenv/read.go b/config/dotenv/read.go index fa3ce1c..d2b833f 100644 --- a/config/dotenv/read.go +++ b/config/dotenv/read.go @@ -6,7 +6,7 @@ import ( "os" "path/filepath" - "gitserver.in/patialtech/rano/pkg/logger" + "gitserver.in/patialtech/rano/util/logger" ) // diff --git a/db/client.go b/db/client.go index b68bd8a..cef61c8 100644 --- a/db/client.go +++ b/db/client.go @@ -12,7 +12,7 @@ import ( pgx "github.com/jackc/pgx/v5/stdlib" "gitserver.in/patialtech/rano/config" "gitserver.in/patialtech/rano/db/ent" - "gitserver.in/patialtech/rano/pkg/logger" + "gitserver.in/patialtech/rano/util/logger" ) type connector struct { @@ -56,17 +56,20 @@ func Client() *ent.Client { // A AuditHook is an example for audit-log hook. func AuditHook(next ent.Mutator) ent.Mutator { - return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { + return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (v ent.Value, err error) { start := time.Now() defer func() { - saveAudit(ctx, m.Type(), m.Op().String(), time.Since(start)) + saveAudit(ctx, m.Type(), m.Op().String(), time.Since(start), err) }() - return next.Mutate(ctx, m) + + v, err = next.Mutate(ctx, m) + logger.Info("** %v", v) + return }) } -func saveAudit(_ context.Context, entT, op string, d time.Duration) { - logger.Info("audit %s %s %s", entT, op, d) +func saveAudit(_ context.Context, entT, op string, d time.Duration, err error) { + logger.Info("audit: %s %s %s %v", entT, op, d, err) // ml.SetCreatedAt(time.Now()) // if usr := auth.CtxUser(ctx); usr != nil { // ml.SetByID(usr.ID) diff --git a/db/ent/migrate/schema.go b/db/ent/migrate/schema.go index 8f8a510..0229076 100644 --- a/db/ent/migrate/schema.go +++ b/db/ent/migrate/schema.go @@ -99,10 +99,10 @@ var ( {Name: "updated_at", Type: field.TypeTime}, {Name: "email", Type: field.TypeString, Unique: true}, {Name: "email_verified", Type: field.TypeBool, Default: false}, - {Name: "phone", Type: field.TypeString, Size: 20}, + {Name: "phone", Type: field.TypeString, Nullable: true, Size: 20}, {Name: "phone_verified", Type: field.TypeBool, Default: false}, - {Name: "pwd_salt", Type: field.TypeString}, - {Name: "pwd_hash", Type: field.TypeString}, + {Name: "pwd_salt", Type: field.TypeString, Size: 250}, + {Name: "pwd_hash", Type: field.TypeString, Size: 250}, {Name: "login_failed_count", Type: field.TypeUint8, Nullable: true, Default: 0}, {Name: "login_attempt_on", Type: field.TypeTime, Nullable: true}, {Name: "login_locked_until", Type: field.TypeTime, Nullable: true}, diff --git a/db/ent/mutation.go b/db/ent/mutation.go index b44c399..c21d33b 100644 --- a/db/ent/mutation.go +++ b/db/ent/mutation.go @@ -2509,9 +2509,22 @@ func (m *UserMutation) OldPhone(ctx context.Context) (v string, err error) { return oldValue.Phone, nil } +// ClearPhone clears the value of the "phone" field. +func (m *UserMutation) ClearPhone() { + m.phone = nil + m.clearedFields[user.FieldPhone] = struct{}{} +} + +// PhoneCleared returns if the "phone" field was cleared in this mutation. +func (m *UserMutation) PhoneCleared() bool { + _, ok := m.clearedFields[user.FieldPhone] + return ok +} + // ResetPhone resets all changes to the "phone" field. func (m *UserMutation) ResetPhone() { m.phone = nil + delete(m.clearedFields, user.FieldPhone) } // SetPhoneVerified sets the "phone_verified" field. @@ -3358,6 +3371,9 @@ func (m *UserMutation) AddField(name string, value ent.Value) error { // mutation. func (m *UserMutation) ClearedFields() []string { var fields []string + if m.FieldCleared(user.FieldPhone) { + fields = append(fields, user.FieldPhone) + } if m.FieldCleared(user.FieldLoginFailedCount) { fields = append(fields, user.FieldLoginFailedCount) } @@ -3381,6 +3397,9 @@ func (m *UserMutation) FieldCleared(name string) bool { // error if the field is not defined in the schema. func (m *UserMutation) ClearField(name string) error { switch name { + case user.FieldPhone: + m.ClearPhone() + return nil case user.FieldLoginFailedCount: m.ClearLoginFailedCount() return nil diff --git a/db/ent/runtime.go b/db/ent/runtime.go index 7e4b4bd..f921994 100644 --- a/db/ent/runtime.go +++ b/db/ent/runtime.go @@ -113,11 +113,39 @@ func init() { // userDescPwdSalt is the schema descriptor for pwd_salt field. userDescPwdSalt := userFields[7].Descriptor() // user.PwdSaltValidator is a validator for the "pwd_salt" field. It is called by the builders before save. - user.PwdSaltValidator = userDescPwdSalt.Validators[0].(func(string) error) + user.PwdSaltValidator = func() func(string) error { + validators := userDescPwdSalt.Validators + fns := [...]func(string) error{ + validators[0].(func(string) error), + validators[1].(func(string) error), + } + return func(pwd_salt string) error { + for _, fn := range fns { + if err := fn(pwd_salt); err != nil { + return err + } + } + return nil + } + }() // userDescPwdHash is the schema descriptor for pwd_hash field. userDescPwdHash := userFields[8].Descriptor() // user.PwdHashValidator is a validator for the "pwd_hash" field. It is called by the builders before save. - user.PwdHashValidator = userDescPwdHash.Validators[0].(func(string) error) + user.PwdHashValidator = func() func(string) error { + validators := userDescPwdHash.Validators + fns := [...]func(string) error{ + validators[0].(func(string) error), + validators[1].(func(string) error), + } + return func(pwd_hash string) error { + for _, fn := range fns { + if err := fn(pwd_hash); err != nil { + return err + } + } + return nil + } + }() // userDescLoginFailedCount is the schema descriptor for login_failed_count field. userDescLoginFailedCount := userFields[9].Descriptor() // user.DefaultLoginFailedCount holds the default value on creation for the login_failed_count field. diff --git a/db/ent/schema/user.go b/db/ent/schema/user.go index 5fdf2ba..7e219b1 100644 --- a/db/ent/schema/user.go +++ b/db/ent/schema/user.go @@ -21,10 +21,10 @@ func (User) Fields() []ent.Field { fieldUpdated, field.String("email").Unique().NotEmpty(), field.Bool("email_verified").Default(false), - field.String("phone").MaxLen(20), + field.String("phone").MaxLen(20).Optional(), field.Bool("phone_verified").Default(false), - field.String("pwd_salt").NotEmpty(), - field.String("pwd_hash").NotEmpty(), + field.String("pwd_salt").MaxLen(250).NotEmpty(), + field.String("pwd_hash").MaxLen(250).NotEmpty(), field.Uint8("login_failed_count").Optional().Default(0), field.Time("login_attempt_on").Optional().Nillable(), field.Time("login_locked_until").Optional().Nillable(), diff --git a/db/ent/user/where.go b/db/ent/user/where.go index fb77b1d..9659403 100644 --- a/db/ent/user/where.go +++ b/db/ent/user/where.go @@ -335,6 +335,16 @@ func PhoneHasSuffix(v string) predicate.User { return predicate.User(sql.FieldHasSuffix(FieldPhone, v)) } +// PhoneIsNil applies the IsNil predicate on the "phone" field. +func PhoneIsNil() predicate.User { + return predicate.User(sql.FieldIsNull(FieldPhone)) +} + +// PhoneNotNil applies the NotNil predicate on the "phone" field. +func PhoneNotNil() predicate.User { + return predicate.User(sql.FieldNotNull(FieldPhone)) +} + // PhoneEqualFold applies the EqualFold predicate on the "phone" field. func PhoneEqualFold(v string) predicate.User { return predicate.User(sql.FieldEqualFold(FieldPhone, v)) diff --git a/db/ent/user_create.go b/db/ent/user_create.go index a7978f4..f3e7871 100644 --- a/db/ent/user_create.go +++ b/db/ent/user_create.go @@ -76,6 +76,14 @@ func (uc *UserCreate) SetPhone(s string) *UserCreate { return uc } +// SetNillablePhone sets the "phone" field if the given value is not nil. +func (uc *UserCreate) SetNillablePhone(s *string) *UserCreate { + if s != nil { + uc.SetPhone(*s) + } + return uc +} + // SetPhoneVerified sets the "phone_verified" field. func (uc *UserCreate) SetPhoneVerified(b bool) *UserCreate { uc.mutation.SetPhoneVerified(b) @@ -292,9 +300,6 @@ func (uc *UserCreate) check() error { if _, ok := uc.mutation.EmailVerified(); !ok { return &ValidationError{Name: "email_verified", err: errors.New(`ent: missing required field "User.email_verified"`)} } - if _, ok := uc.mutation.Phone(); !ok { - return &ValidationError{Name: "phone", err: errors.New(`ent: missing required field "User.phone"`)} - } if v, ok := uc.mutation.Phone(); ok { if err := user.PhoneValidator(v); err != nil { return &ValidationError{Name: "phone", err: fmt.Errorf(`ent: validator failed for field "User.phone": %w`, err)} diff --git a/db/ent/user_update.go b/db/ent/user_update.go index 871d498..4ae205f 100644 --- a/db/ent/user_update.go +++ b/db/ent/user_update.go @@ -78,6 +78,12 @@ func (uu *UserUpdate) SetNillablePhone(s *string) *UserUpdate { return uu } +// ClearPhone clears the value of the "phone" field. +func (uu *UserUpdate) ClearPhone() *UserUpdate { + uu.mutation.ClearPhone() + return uu +} + // SetPhoneVerified sets the "phone_verified" field. func (uu *UserUpdate) SetPhoneVerified(b bool) *UserUpdate { uu.mutation.SetPhoneVerified(b) @@ -425,6 +431,9 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) { if value, ok := uu.mutation.Phone(); ok { _spec.SetField(user.FieldPhone, field.TypeString, value) } + if uu.mutation.PhoneCleared() { + _spec.ClearField(user.FieldPhone, field.TypeString) + } if value, ok := uu.mutation.PhoneVerified(); ok { _spec.SetField(user.FieldPhoneVerified, field.TypeBool, value) } @@ -625,6 +634,12 @@ func (uuo *UserUpdateOne) SetNillablePhone(s *string) *UserUpdateOne { return uuo } +// ClearPhone clears the value of the "phone" field. +func (uuo *UserUpdateOne) ClearPhone() *UserUpdateOne { + uuo.mutation.ClearPhone() + return uuo +} + // SetPhoneVerified sets the "phone_verified" field. func (uuo *UserUpdateOne) SetPhoneVerified(b bool) *UserUpdateOne { uuo.mutation.SetPhoneVerified(b) @@ -1002,6 +1017,9 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) if value, ok := uuo.mutation.Phone(); ok { _spec.SetField(user.FieldPhone, field.TypeString, value) } + if uuo.mutation.PhoneCleared() { + _spec.ClearField(user.FieldPhone, field.TypeString) + } if value, ok := uuo.mutation.PhoneVerified(); ok { _spec.SetField(user.FieldPhoneVerified, field.TypeBool, value) } diff --git a/go.mod b/go.mod index 54df1fe..b1322af 100644 --- a/go.mod +++ b/go.mod @@ -7,15 +7,18 @@ require ( github.com/jackc/pgx/v5 v5.7.1 github.com/vektah/gqlparser/v2 v2.5.19 gitserver.in/patialtech/mux v0.3.1 + golang.org/x/crypto v0.29.0 ) require ( ariga.io/atlas v0.28.1 // indirect github.com/agext/levenshtein v1.2.3 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/bmatcuk/doublestar v1.3.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-openapi/inflect v0.21.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -25,6 +28,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect @@ -32,8 +36,9 @@ require ( github.com/zclconf/go-cty v1.15.0 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.29.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect + golang.org/x/net v0.31.0 // indirect + golang.org/x/sys v0.27.0 // indirect ) require ( @@ -42,12 +47,12 @@ require ( entgo.io/ent v0.14.1 github.com/agnivade/levenshtein v1.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/go-playground/validator/v10 v10.22.1 github.com/golang-migrate/migrate/v4 v4.18.1 github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/lib/pq v1.10.9 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sosodev/duration v1.3.1 // indirect github.com/urfave/cli/v2 v2.27.5 // indirect diff --git a/go.sum b/go.sum index a4f40be..2ea02da 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -ariga.io/atlas v0.25.1-0.20240717145915-af51d3945208 h1:ixs1c/fAXGS3mTdalyKQrtvfkFjgChih/unX66YTzYk= -ariga.io/atlas v0.25.1-0.20240717145915-af51d3945208/go.mod h1:KPLc7Zj+nzoXfWshrcY1RwlOh94dsATQEy4UPrF2RkM= ariga.io/atlas v0.28.1 h1:cNE0FYmoYs1u4KF+FGnp2on1srhM6FDpjaCgL7Rd8/c= ariga.io/atlas v0.28.1/go.mod h1:LOOp18LCL9r+VifvVlJqgYJwYl271rrXD9/wIyzJ8sw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -9,8 +7,6 @@ entgo.io/contrib v0.6.0 h1:xfo4TbJE7sJZWx7BV7YrpSz7IPFvS8MzL3fnfzZjKvQ= entgo.io/contrib v0.6.0/go.mod h1:3qWIseJ/9Wx2Hu5zVh15FDzv7d/UvKNcYKdViywWCQg= entgo.io/ent v0.14.1 h1:fUERL506Pqr92EPHJqr8EYxbPioflJo6PudkrEA8a/s= entgo.io/ent v0.14.1/go.mod h1:MH6XLG0KXpkcDQhKiHfANZSzR55TJyPL5IGNpI8wpco= -github.com/99designs/gqlgen v0.17.55 h1:3vzrNWYyzSZjGDFo68e5j9sSauLxfKvLp+6ioRokVtM= -github.com/99designs/gqlgen v0.17.55/go.mod h1:3Bq768f8hgVPGZxL8aY9MaYmbxa6llPM/qu1IGH1EJo= github.com/99designs/gqlgen v0.17.56 h1:+J42ARAHvnysH6klO9Wq+tCsGF32cpAgU3SyF0VRJtI= github.com/99designs/gqlgen v0.17.56/go.mod h1:rmB6vLvtL8uf9F9w0/irJ5alBkD8DJvj35ET31BKbtY= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= @@ -22,8 +18,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/PuerkitoBio/goquery v1.9.3 h1:mpJr/ikUA9/GNJB/DBZcGeFDXUtosHRyRrwh7KGdTG0= github.com/PuerkitoBio/goquery v1.9.3/go.mod h1:1ndLHPdTz+DyQPICCWYlYQMPl0oXZj0G6D4LCYA6u4U= -github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= -github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= @@ -32,8 +26,6 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= @@ -66,14 +58,22 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= -github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk= github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= @@ -116,8 +116,6 @@ github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+l github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= -github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= github.com/hashicorp/hcl/v2 v2.22.0 h1:hkZ3nCtqeJsDhPRFz5EA9iwcG1hNWGePOTw6oyul12M= github.com/hashicorp/hcl/v2 v2.22.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= @@ -132,20 +130,16 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= -github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= @@ -181,7 +175,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -190,22 +183,18 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= -github.com/vektah/gqlparser/v2 v2.5.18 h1:zSND3GtutylAQ1JpWnTHcqtaRZjl+y3NROeW8vuNo6Y= -github.com/vektah/gqlparser/v2 v2.5.18/go.mod h1:6HLzf7JKv9Fi3APymudztFQNmLXR5qJeEo6BOFcXVfc= github.com/vektah/gqlparser/v2 v2.5.19 h1:bhCPCX1D4WWzCDvkPl4+TP1N8/kLrWnp43egplt7iSg= github.com/vektah/gqlparser/v2 v2.5.19/go.mod h1:y7kvl5bBlDeuWIvLtA9849ncyvx6/lj06RsMrEjVy3U= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= -github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= -github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= gitserver.in/patialtech/mux v0.3.1 h1:lbhQVr2vBvTcUp64Qjd2+4/s2lQXiDtsl8c+PpZvnDE= gitserver.in/patialtech/mux v0.3.1/go.mod h1:/pYaLBNkRiMuxMKn9e2X0BIWt1bvHM19yQE/cJsm0q0= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -218,26 +207,18 @@ go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2 go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= -go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0 h1:pVgRXcIictcr+lBQIFeiwuwtDIs4eL21OuM9nyAADmo= -golang.org/x/exp v0.0.0-20230315142452-642cacee5cc0/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= -golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -246,28 +227,22 @@ golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= -golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= +golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= +golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -275,8 +250,6 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= -golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -302,8 +275,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gqlgen.yml b/gqlgen.yml index 50857ef..b778b38 100644 --- a/gqlgen.yml +++ b/gqlgen.yml @@ -1,6 +1,6 @@ # Where are all the schema files located? globs are supported eg src/**/*.graphqls schema: - - graph/*.graphql + - graph/**/*.graphql # Where should the generated server code go? exec: diff --git a/graph/auth.graphql b/graph/account.graphql similarity index 63% rename from graph/auth.graphql rename to graph/account.graphql index f5ba67c..40ca5f2 100644 --- a/graph/auth.graphql +++ b/graph/account.graphql @@ -1,12 +1,9 @@ extend type Mutation { - login(username: String!, email: String!): Boolean! + login(email: String!, pwd: String!): AuthUser! logout: Boolean! } extend type Query { - """ - me, is current AuthUser info - """ me: AuthUser } diff --git a/graph/auth.resolvers.go b/graph/account.resolvers.go similarity index 87% rename from graph/auth.resolvers.go rename to graph/account.resolvers.go index a6d112c..dcaf3b9 100644 --- a/graph/auth.resolvers.go +++ b/graph/account.resolvers.go @@ -2,7 +2,7 @@ package graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.55 +// Code generated by github.com/99designs/gqlgen version v0.17.56 import ( "context" @@ -12,7 +12,7 @@ import ( ) // Login is the resolver for the login field. -func (r *mutationResolver) Login(ctx context.Context, username string, email string) (bool, error) { +func (r *mutationResolver) Login(ctx context.Context, email string, pwd string) (*model.AuthUser, error) { panic(fmt.Errorf("not implemented: Login - login")) } diff --git a/graph/generated/auth.generated.go b/graph/generated/account.generated.go similarity index 93% rename from graph/generated/auth.generated.go rename to graph/generated/account.generated.go index ba5c963..04eed33 100644 --- a/graph/generated/auth.generated.go +++ b/graph/generated/account.generated.go @@ -273,6 +273,20 @@ func (ec *executionContext) _AuthUser(ctx context.Context, sel ast.SelectionSet, // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNAuthUser2gitserverᚗinᚋpatialtechᚋranoᚋgraphᚋmodelᚐAuthUser(ctx context.Context, sel ast.SelectionSet, v model.AuthUser) graphql.Marshaler { + return ec._AuthUser(ctx, sel, &v) +} + +func (ec *executionContext) marshalNAuthUser2ᚖgitserverᚗinᚋpatialtechᚋranoᚋgraphᚋmodelᚐAuthUser(ctx context.Context, sel ast.SelectionSet, v *model.AuthUser) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._AuthUser(ctx, sel, v) +} + func (ec *executionContext) marshalOAuthUser2ᚖgitserverᚗinᚋpatialtechᚋranoᚋgraphᚋmodelᚐAuthUser(ctx context.Context, sel ast.SelectionSet, v *model.AuthUser) graphql.Marshaler { if v == nil { return graphql.Null diff --git a/graph/generated/index.generated.go b/graph/generated/root.generated.go similarity index 85% rename from graph/generated/index.generated.go rename to graph/generated/root.generated.go index bab7de2..77a8853 100644 --- a/graph/generated/index.generated.go +++ b/graph/generated/root.generated.go @@ -18,11 +18,10 @@ import ( // region ************************** generated!.gotpl ************************** type MutationResolver interface { - Login(ctx context.Context, username string, email string) (bool, error) + Login(ctx context.Context, email string, pwd string) (*model.AuthUser, error) Logout(ctx context.Context) (bool, error) } type QueryResolver interface { - HeartBeat(ctx context.Context) (bool, error) Me(ctx context.Context) (*model.AuthUser, error) } @@ -33,24 +32,24 @@ type QueryResolver interface { func (ec *executionContext) field_Mutation_login_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) { var err error args := map[string]interface{}{} - arg0, err := ec.field_Mutation_login_argsUsername(ctx, rawArgs) + arg0, err := ec.field_Mutation_login_argsEmail(ctx, rawArgs) if err != nil { return nil, err } - args["username"] = arg0 - arg1, err := ec.field_Mutation_login_argsEmail(ctx, rawArgs) + args["email"] = arg0 + arg1, err := ec.field_Mutation_login_argsPwd(ctx, rawArgs) if err != nil { return nil, err } - args["email"] = arg1 + args["pwd"] = arg1 return args, nil } -func (ec *executionContext) field_Mutation_login_argsUsername( +func (ec *executionContext) field_Mutation_login_argsEmail( ctx context.Context, rawArgs map[string]interface{}, ) (string, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("username")) - if tmp, ok := rawArgs["username"]; ok { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) + if tmp, ok := rawArgs["email"]; ok { return ec.unmarshalNString2string(ctx, tmp) } @@ -58,12 +57,12 @@ func (ec *executionContext) field_Mutation_login_argsUsername( return zeroVal, nil } -func (ec *executionContext) field_Mutation_login_argsEmail( +func (ec *executionContext) field_Mutation_login_argsPwd( ctx context.Context, rawArgs map[string]interface{}, ) (string, error) { - ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("email")) - if tmp, ok := rawArgs["email"]; ok { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("pwd")) + if tmp, ok := rawArgs["pwd"]; ok { return ec.unmarshalNString2string(ctx, tmp) } @@ -116,7 +115,7 @@ func (ec *executionContext) _Mutation_login(ctx context.Context, field graphql.C }() resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { ctx = rctx // use context from middleware stack in children - return ec.resolvers.Mutation().Login(rctx, fc.Args["username"].(string), fc.Args["email"].(string)) + return ec.resolvers.Mutation().Login(rctx, fc.Args["email"].(string), fc.Args["pwd"].(string)) }) if err != nil { ec.Error(ctx, err) @@ -128,9 +127,9 @@ func (ec *executionContext) _Mutation_login(ctx context.Context, field graphql.C } return graphql.Null } - res := resTmp.(bool) + res := resTmp.(*model.AuthUser) fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) + return ec.marshalNAuthUser2ᚖgitserverᚗinᚋpatialtechᚋranoᚋgraphᚋmodelᚐAuthUser(ctx, field.Selections, res) } func (ec *executionContext) fieldContext_Mutation_login(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { @@ -140,7 +139,17 @@ func (ec *executionContext) fieldContext_Mutation_login(ctx context.Context, fie IsMethod: true, IsResolver: true, Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") + switch field.Name { + case "id": + return ec.fieldContext_AuthUser_id(ctx, field) + case "email": + return ec.fieldContext_AuthUser_email(ctx, field) + case "displayName": + return ec.fieldContext_AuthUser_displayName(ctx, field) + case "roleID": + return ec.fieldContext_AuthUser_roleID(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type AuthUser", field.Name) }, } defer func() { @@ -201,50 +210,6 @@ func (ec *executionContext) fieldContext_Mutation_logout(_ context.Context, fiel return fc, nil } -func (ec *executionContext) _Query_heartBeat(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { - fc, err := ec.fieldContext_Query_heartBeat(ctx, field) - if err != nil { - return graphql.Null - } - ctx = graphql.WithFieldContext(ctx, fc) - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return ec.resolvers.Query().HeartBeat(rctx) - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } - return graphql.Null - } - res := resTmp.(bool) - fc.Result = res - return ec.marshalNBoolean2bool(ctx, field.Selections, res) -} - -func (ec *executionContext) fieldContext_Query_heartBeat(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { - fc = &graphql.FieldContext{ - Object: "Query", - Field: field, - IsMethod: true, - IsResolver: true, - Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { - return nil, errors.New("field of type Boolean does not have child fields") - }, - } - return fc, nil -} - func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_me(ctx, field) if err != nil { @@ -512,28 +477,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr switch field.Name { case "__typename": out.Values[i] = graphql.MarshalString("Query") - case "heartBeat": - field := field - - innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - } - }() - res = ec._Query_heartBeat(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&fs.Invalids, 1) - } - return res - } - - rrm := func(ctx context.Context) graphql.Marshaler { - return ec.OperationContext.RootResolverMiddleware(ctx, - func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) - } - - out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "me": field := field diff --git a/graph/generated/root_.generated.go b/graph/generated/root_.generated.go index 64f0f35..850127c 100644 --- a/graph/generated/root_.generated.go +++ b/graph/generated/root_.generated.go @@ -48,13 +48,12 @@ type ComplexityRoot struct { } Mutation struct { - Login func(childComplexity int, username string, email string) int + Login func(childComplexity int, email string, pwd string) int Logout func(childComplexity int) int } Query struct { - HeartBeat func(childComplexity int) int - Me func(childComplexity int) int + Me func(childComplexity int) int } } @@ -115,7 +114,7 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return 0, false } - return e.complexity.Mutation.Login(childComplexity, args["username"].(string), args["email"].(string)), true + return e.complexity.Mutation.Login(childComplexity, args["email"].(string), args["pwd"].(string)), true case "Mutation.logout": if e.complexity.Mutation.Logout == nil { @@ -124,13 +123,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Mutation.Logout(childComplexity), true - case "Query.heartBeat": - if e.complexity.Query.HeartBeat == nil { - break - } - - return e.complexity.Query.HeartBeat(childComplexity), true - case "Query.me": if e.complexity.Query.Me == nil { break @@ -143,12 +135,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in } func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { - rc := graphql.GetOperationContext(ctx) - ec := executionContext{rc, e, 0, 0, make(chan graphql.DeferredResult)} + opCtx := graphql.GetOperationContext(ctx) + ec := executionContext{opCtx, e, 0, 0, make(chan graphql.DeferredResult)} inputUnmarshalMap := graphql.BuildUnmarshalerMap() first := true - switch rc.Operation.Operation { + switch opCtx.Operation.Operation { case ast.Query: return func(ctx context.Context) *graphql.Response { var response graphql.Response @@ -156,7 +148,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { if first { first = false ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) - data = ec._Query(ctx, rc.Operation.SelectionSet) + data = ec._Query(ctx, opCtx.Operation.SelectionSet) } else { if atomic.LoadInt32(&ec.pendingDeferred) > 0 { result := <-ec.deferredResults @@ -186,7 +178,7 @@ func (e *executableSchema) Exec(ctx context.Context) graphql.ResponseHandler { } first = false ctx = graphql.WithUnmarshalerMap(ctx, inputUnmarshalMap) - data := ec._Mutation(ctx, rc.Operation.SelectionSet) + data := ec._Mutation(ctx, opCtx.Operation.SelectionSet) var buf bytes.Buffer data.MarshalGQL(&buf) @@ -242,15 +234,12 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er } var sources = []*ast.Source{ - {Name: "../auth.graphql", Input: `extend type Mutation { - login(username: String!, email: String!): Boolean! + {Name: "../account.graphql", Input: `extend type Mutation { + login(email: String!, pwd: String!): AuthUser! logout: Boolean! } extend type Query { - """ - me, is current AuthUser info - """ me: AuthUser } @@ -261,15 +250,13 @@ type AuthUser { roleID: Int! } `, BuiltIn: false}, - {Name: "../index.graphql", Input: `# GraphQL schema example + {Name: "../root.graphql", Input: `# GraphQL schema example # # https://gqlgen.com/getting-started/ type Mutation -type Query { - heartBeat: Boolean! -} +type Query """ Maps a Time GraphQL scalar to a Go time.Time struct. diff --git a/graph/index.resolvers.go b/graph/index.resolvers.go deleted file mode 100644 index 81afcac..0000000 --- a/graph/index.resolvers.go +++ /dev/null @@ -1,28 +0,0 @@ -package graph - -// This file will be automatically regenerated based on the schema, any resolver implementations -// will be copied through when generating and any unknown code will be moved to the end. -// Code generated by github.com/99designs/gqlgen version v0.17.55 - -import ( - "context" - - graph "gitserver.in/patialtech/rano/graph/generated" -) - -// HeartBeat is the resolver for the heartBeat field. -func (r *queryResolver) HeartBeat(ctx context.Context) (bool, error) { - // do needful checkup - // - - return true, nil -} - -// Mutation returns graph.MutationResolver implementation. -func (r *Resolver) Mutation() graph.MutationResolver { return &mutationResolver{r} } - -// Query returns graph.QueryResolver implementation. -func (r *Resolver) Query() graph.QueryResolver { return &queryResolver{r} } - -type mutationResolver struct{ *Resolver } -type queryResolver struct{ *Resolver } diff --git a/graph/resolver.go b/graph/resolver.go index 0ed1972..268c83f 100644 --- a/graph/resolver.go +++ b/graph/resolver.go @@ -12,7 +12,7 @@ import ( "github.com/99designs/gqlgen/graphql/playground" "github.com/vektah/gqlparser/v2/gqlerror" "gitserver.in/patialtech/rano/graph/generated" - "gitserver.in/patialtech/rano/pkg/logger" + "gitserver.in/patialtech/rano/util/logger" ) // This file will not be regenerated automatically. diff --git a/graph/index.graphql b/graph/root.graphql similarity index 96% rename from graph/index.graphql rename to graph/root.graphql index 013c062..ef350ea 100644 --- a/graph/index.graphql +++ b/graph/root.graphql @@ -4,9 +4,7 @@ type Mutation -type Query { - heartBeat: Boolean! -} +type Query """ Maps a Time GraphQL scalar to a Go time.Time struct. diff --git a/graph/root.resolvers.go b/graph/root.resolvers.go new file mode 100644 index 0000000..5b6eecd --- /dev/null +++ b/graph/root.resolvers.go @@ -0,0 +1,18 @@ +package graph + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.56 + +import ( + "gitserver.in/patialtech/rano/graph/generated" +) + +// Mutation returns generated.MutationResolver implementation. +func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } + +// Query returns generated.QueryResolver implementation. +func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } + +type mutationResolver struct{ *Resolver } +type queryResolver struct{ *Resolver } diff --git a/graph/server/main.go b/graph/server/main.go index 349f9a0..ecbae71 100644 --- a/graph/server/main.go +++ b/graph/server/main.go @@ -8,7 +8,7 @@ import ( "gitserver.in/patialtech/mux/middleware" "gitserver.in/patialtech/rano/config" "gitserver.in/patialtech/rano/graph" - "gitserver.in/patialtech/rano/pkg/logger" + "gitserver.in/patialtech/rano/util/logger" ) func main() { diff --git a/mailer/mailer.go b/mailer/mailer.go new file mode 100644 index 0000000..81f99de --- /dev/null +++ b/mailer/mailer.go @@ -0,0 +1,72 @@ +package mailer + +import ( + "errors" + "net/mail" + + "gitserver.in/patialtech/rano/config" +) + +type ( + transporter interface { + send(*message) error + } + + Recipients struct { + To []mail.Address `json:"to"` + Cc []mail.Address `json:"cc"` + Bcc []mail.Address `json:"bcc"` + } + + message struct { + From string `json:"from" validate:"required"` + Recipients Recipients `json:"recipients" validate:"required"` + Subject string `json:"subject" validate:"required"` + HtmlBody string `json:"htmlBody" validate:"required"` + ReplyTo *mail.Address `json:"replyTo"` + } + + Template interface { + Subject() string + HtmlBody() (string, error) + } +) + +func Send(to []mail.Address, tpl Template) error { + return send(Recipients{To: to}, tpl) +} + +func SendCC(subject string, to, cc []mail.Address, tpl Template) error { + return send(Recipients{To: to, Cc: cc}, tpl) +} + +func send(recipients Recipients, tpl Template) error { + if tpl == nil { + return errors.New("mailer: email template is nil") + } + + if len(recipients.To) == 0 { + return errors.New("mailer: no recipient found") + } + + // TODO remove recepient with bounce hiostory + + body, err := tpl.HtmlBody() + if err != nil { + return err + } + // get ENV based transporter and send mail + return getTransporter().send(&message{ + From: config.Read().MailerFrom, + Recipients: recipients, + Subject: tpl.Subject(), + HtmlBody: body, + }) +} + +func getTransporter() transporter { + switch config.AppEnv { + default: + return transportDev{} + } +} diff --git a/mailer/message/_layout.html b/mailer/message/_layout.html new file mode 100644 index 0000000..0a3bb24 --- /dev/null +++ b/mailer/message/_layout.html @@ -0,0 +1,120 @@ +{{define "layout"}} + + + + + {{.Title}} + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + + + + + + +
+ logo + +   +
+
+
+ {{ template "content" .}} +

Thank You!

+
+
+
+ + + + + + +
+

+ {{.websiteDomain}} +

+ {{if not .AllowReply}} +

+ + ** This is a system generated email, please do not reply to it ** + +

+ {{end}} +
+
+ + +{{end}} diff --git a/mailer/message/render.go b/mailer/message/render.go new file mode 100644 index 0000000..f180d06 --- /dev/null +++ b/mailer/message/render.go @@ -0,0 +1,48 @@ +package message + +import ( + "bytes" + _ "embed" + "html/template" + + "gitserver.in/patialtech/rano/config" + "gitserver.in/patialtech/rano/mailer" + "gitserver.in/patialtech/rano/util/structs" +) + +//go:embed _layout.html +var layout string + +type TplData struct { + WebAssetsURL string + mailer.Template +} + +// render data in give HTML layout and content templates +func render(layout string, content string, data mailer.Template) (string, error) { + // layout + tpl, err := template.New("layout").Parse(layout) + if err != nil { + return "", err + } + + // content + _, err = tpl.New("content").Parse(content) + if err != nil { + return "", err + } + + // excute layout + content temaplte and render data + buf := new(bytes.Buffer) + d := structs.Map(data) + d["Title"] = "My App" + d["WebAssetsURL"] = config.Read().WebURL + d["AllowReply"] = false + + err = tpl.ExecuteTemplate(buf, "layout", d) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/mailer/message/render_test.go b/mailer/message/render_test.go new file mode 100644 index 0000000..2ee6a12 --- /dev/null +++ b/mailer/message/render_test.go @@ -0,0 +1,31 @@ +package message + +import ( + "strings" + "testing" +) + +type testmail struct { + Message string +} + +func (t testmail) Subject() string { + return "Test Test" +} + +func (t testmail) HtmlBody() (string, error) { + content := `

{{.Message}}

` + return render(layout, content, t) +} + +func TestRender(t *testing.T) { + tpl := testmail{ + Message: "some mesage", + } + + if b, err := tpl.HtmlBody(); err != nil { + t.Error(err) + } else if !strings.Contains(b, tpl.Message) { + t.Error("supposed to contain:", tpl.Message) + } +} diff --git a/mailer/message/welcome.go b/mailer/message/welcome.go new file mode 100644 index 0000000..974950d --- /dev/null +++ b/mailer/message/welcome.go @@ -0,0 +1,16 @@ +package message + +type Welcome struct { + Name string +} + +func (e *Welcome) Subject() string { + return "Welcome" +} + +func (e *Welcome) HtmlBody() (string, error) { + content := ` +

Welcome {{.Name}}

+` + return render(layout, content, e) +} diff --git a/mailer/transport_dev.go b/mailer/transport_dev.go new file mode 100644 index 0000000..be6a2b7 --- /dev/null +++ b/mailer/transport_dev.go @@ -0,0 +1,23 @@ +package mailer + +import ( + "os" + "path/filepath" + "time" + + "gitserver.in/patialtech/rano/util/open" +) + +type transportDev struct{} + +func (transportDev) send(msg *message) error { + dir := os.TempDir() + id := time.Now().Format("20060102T150405999") + file := filepath.Join(dir, id+".html") + + if err := os.WriteFile(file, []byte(msg.HtmlBody), 0440); err != nil { + return err + } + + return open.WithDefaultApp(file) +} diff --git a/pkg/user/create.go b/pkg/user/create.go new file mode 100644 index 0000000..c2b6d06 --- /dev/null +++ b/pkg/user/create.go @@ -0,0 +1,97 @@ +package user + +import ( + "context" + "errors" + "fmt" + "log/slog" + "net/mail" + "strings" + + "gitserver.in/patialtech/rano/db" + "gitserver.in/patialtech/rano/mailer" + "gitserver.in/patialtech/rano/mailer/message" + "gitserver.in/patialtech/rano/util/crypto" + "gitserver.in/patialtech/rano/util/logger" + "gitserver.in/patialtech/rano/util/validate" +) + +type CreateInput struct { + Email string `validate:"email"` + Phone string + Pwd string `validate:"required"` + ConfirmPwd string `validate:"required"` + FirstName string `validate:"required"` + MiddleName string + LastName string + RoleID uint8 `validate:"required"` +} + +var ( + ErrCreateInpNil = errors.New("user: create input is nil") + ErrWrongConfirmPwd = errors.New("user: confirm password does not match") +) + +// Create user record in DB +// +// will return created userID on success +func Create(ctx context.Context, inp *CreateInput) (int64, error) { + // check for nil inp + if inp == nil { + return 0, ErrCreateInpNil + } + + // validate + if err := validate.Struct(inp); err != nil { + return 0, err + } + + // compare pwd and comparePwd + if inp.Pwd != inp.ConfirmPwd { + return 0, ErrWrongConfirmPwd + } + + h, salt, err := crypto.PasswordHash(inp.Pwd) + if err != nil { + return 0, err + } + + // save record to DB + client := db.Client() + u, err := client.User.Create(). + SetEmail(inp.Email). + SetPwdHash(h). + SetPwdSalt(salt). + SetFirstName(inp.FirstName). + SetMiddleName(inp.MiddleName). + SetLastName(inp.LastName). + Save(ctx) + if err != nil { + logger.Error(err, slog.String("ref", "user: create error")) + return 0, errors.New("failed to create user") + } + + // email + err = mailer.Send( + []mail.Address{ + {Name: inp.FullName(), Address: inp.Email}, + }, + &message.Welcome{ + Name: inp.FullName(), + }, + ) + if err != nil { + logger.Error(err, slog.String("ref", "user: send welcome email")) + } + + return u.ID, nil +} + +func (inp *CreateInput) FullName() string { + if inp == nil { + return "" + } + + name := fmt.Sprintf("%s %s %s", inp.FirstName, inp.MiddleName, inp.LastName) + return strings.Join(strings.Fields(name), " ") +} diff --git a/pkg/user/create_test.go b/pkg/user/create_test.go new file mode 100644 index 0000000..533e112 --- /dev/null +++ b/pkg/user/create_test.go @@ -0,0 +1,36 @@ +package user + +import ( + "context" + "testing" +) + +func TestCreate(t *testing.T) { + t.Run("check nil", func(t *testing.T) { + if _, err := Create(context.Background(), nil); err == nil { + t.Error("nil check error expected") + } + }) + + t.Run("trigger validation errors", func(t *testing.T) { + if _, err := Create(context.Background(), &CreateInput{}); err == nil { + t.Error("validation errors are expected") + } else { + t.Log(err) + } + }) + + t.Run("create", func(t *testing.T) { + if _, err := Create(context.Background(), &CreateInput{ + Email: "aa@aa.com", + Pwd: "pwd123", + ConfirmPwd: "pwd123", + FirstName: "Ankit", + MiddleName: "Singh", + LastName: "Patial", + RoleID: 1, + }); err != nil { + t.Error(err) + } + }) +} diff --git a/taskfile.yml b/taskfile.yml index ed4922c..a5c7493 100644 --- a/taskfile.yml +++ b/taskfile.yml @@ -6,39 +6,44 @@ env: dotenv: ['.env.{{.ENV}}'] tasks: - gen: - desc: use go generate, for graph files - preconditions: - - go mod tidy - cmds: - - go mod tidy - - go generate ./graph - - task: ent-gen - - check: - desc: perform go vuln check - cmds: - - govulncheck -show verbose ./... - install: desc: install packages cmds: - deno install --allow-scripts=npm:@sveltejs/kit - graph: + start-graph: desc: run graph server cmds: - cmd: go run ./graph/server - codegen: + start-web: + desc: run web in dev mode + cmd: deno task dev + + gen: + desc: use go generate, for graph files + preconditions: + - go mod tidy + cmds: + - task: graph-gen + - task: ent-gen + + vuln-check: + desc: perform go vuln check + cmds: + - govulncheck -show verbose ./... + + graph-gen: + desc: graph gen + cmds: + - go mod tidy + - go generate ./graph + + graph-codegen: desc: generate graph types cmds: - cmd: deno task codegen - web: - desc: run web in dev mode - cmd: deno task dev - ent-new: desc: create new db Emtity cmd: cd ./db && go run -mod=mod entgo.io/ent/cmd/ent new {{.name}} diff --git a/util/crypto/hash.go b/util/crypto/hash.go new file mode 100644 index 0000000..a1017a9 --- /dev/null +++ b/util/crypto/hash.go @@ -0,0 +1,75 @@ +package crypto + +import ( + "bytes" + "crypto/md5" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "log/slog" + "math/big" + + "gitserver.in/patialtech/rano/util/logger" + "golang.org/x/crypto/argon2" +) + +func MD5(b []byte) string { + hash := md5.Sum(b) + return hex.EncodeToString(hash[:]) +} + +func MD5Int(b []byte) uint64 { + n := new(big.Int) + n.SetString(MD5(b), 16) + return n.Uint64() +} + +// Password using Argon2id +// +// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id +func PasswordHash(pwd string) (hash, salt string, err error) { + var sl []byte + sl, err = randomSecret(32) + if err != nil { + return + } + + // Generate hash + h := argon2.IDKey([]byte(pwd), sl, 3, 12288, 1, 32) + + hash = base64.StdEncoding.EncodeToString(h) + salt = base64.StdEncoding.EncodeToString(sl) + + return +} + +// ComparePassword +func ComparePasswordHash(pwd, hash, salt string) bool { + var h, s []byte + var err error + + if h, err = base64.StdEncoding.DecodeString(hash); err != nil { + logger.Error(err, slog.String("ref", "util/crypto.ComparePasswordHash decode hash")) + } + + if s, err = base64.StdEncoding.DecodeString(salt); err != nil { + logger.Error(err, slog.String("ref", "util/crypto.ComparePasswordHash decode salt")) + } + + // Generate hash for comparison. + ph := argon2.IDKey([]byte(pwd), s, 3, 12288, 1, 32) + + // Compare the generated hash with the stored hash. + // If they don't match return error. + return bytes.Equal(h, ph) +} + +func randomSecret(length uint32) ([]byte, error) { + secret := make([]byte, length) + _, err := rand.Read(secret) + if err != nil { + return nil, err + } + + return secret, nil +} diff --git a/util/crypto/hash_test.go b/util/crypto/hash_test.go new file mode 100644 index 0000000..8de3ecf --- /dev/null +++ b/util/crypto/hash_test.go @@ -0,0 +1,22 @@ +package crypto + +import "testing" + +func TestPasswordHash(t *testing.T) { + pwd := "MY Bingo pwd" + hash, salt, err := PasswordHash(pwd) + if err != nil { + t.Error(err) + return + } + + if hash == "" || salt == "" { + t.Error("either hash or password is empty") + return + } + + if !ComparePasswordHash(pwd, string(hash), string(salt)) { + t.Error("supposed to match") + } + +} diff --git a/pkg/logger/logger.go b/util/logger/logger.go similarity index 99% rename from pkg/logger/logger.go rename to util/logger/logger.go index 9d4fa4a..5e7f083 100644 --- a/pkg/logger/logger.go +++ b/util/logger/logger.go @@ -40,5 +40,6 @@ func getArgs(args []any) ([]any, []any) { a = append(a, arg) } } + return a, b } diff --git a/util/open/darwin.go b/util/open/darwin.go new file mode 100644 index 0000000..06c891a --- /dev/null +++ b/util/open/darwin.go @@ -0,0 +1,15 @@ +//go:build darwin + +package open + +import ( + "os/exec" +) + +func open(input string) *exec.Cmd { + return exec.Command("open", input) +} + +func openWith(input string, appName string) *exec.Cmd { + return exec.Command("open", "-a", appName, input) +} diff --git a/util/open/linux.go b/util/open/linux.go new file mode 100644 index 0000000..a432b58 --- /dev/null +++ b/util/open/linux.go @@ -0,0 +1,17 @@ +//go:build linux + +package open + +import ( + "os/exec" +) + +// http://sources.debian.net/src/xdg-utils + +func open(input string) *exec.Cmd { + return exec.Command("xdg-open", input) +} + +func openWith(input string, appName string) *exec.Cmd { + return exec.Command(appName, input) +} diff --git a/util/open/open.go b/util/open/open.go new file mode 100644 index 0000000..0044286 --- /dev/null +++ b/util/open/open.go @@ -0,0 +1,12 @@ +package open + +// WithDefaultApp open a file, directory, or URI using the OS's default application for that object type. +func WithDefaultApp(input string) error { + cmd := open(input) + return cmd.Run() +} + +// WithApp will open a file directory, or URI using the specified application. +func App(input string, appName string) error { + return openWith(input, appName).Run() +} diff --git a/util/open/windows.go b/util/open/windows.go new file mode 100644 index 0000000..a0b0324 --- /dev/null +++ b/util/open/windows.go @@ -0,0 +1,32 @@ +//go:build windows + +package open + +import ( + "os" + "os/exec" + "path/filepath" + "strings" +) + +var ( + cmd = "url.dll,FileProtocolHandler" + runDll32 = filepath.Join(os.Getenv("SYSTEMROOT"), "System32", "rundll32.exe") +) + +func cleaninput(input string) string { + r := strings.NewReplacer("&", "^&") + return r.Replace(input) +} + +func open(input string) *exec.Cmd { + cmd := exec.Command(runDll32, cmd, input) + // cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + return cmd +} + +func openWith(input string, appName string) *exec.Cmd { + cmd := exec.Command("cmd", "/C", "start", "", appName, cleaninput(input)) + // cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + return cmd +} diff --git a/util/structs/structs.go b/util/structs/structs.go new file mode 100644 index 0000000..bf5f209 --- /dev/null +++ b/util/structs/structs.go @@ -0,0 +1,33 @@ +package structs + +import ( + "reflect" +) + +func Map(obj interface{}) map[string]interface{} { + result := make(map[string]interface{}) + + val := reflect.ValueOf(obj) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + typ := val.Type() + + for i := 0; i < val.NumField(); i++ { + fieldName := typ.Field(i).Name + fieldValueKind := val.Field(i).Kind() + var fieldValue interface{} + + if fieldValueKind == reflect.Struct { + fieldValue = Map(val.Field(i).Interface()) + } else { + fieldValue = val.Field(i).Interface() + } + + result[fieldName] = fieldValue + } + + return result +} diff --git a/util/validate/validate.go b/util/validate/validate.go new file mode 100644 index 0000000..20c9516 --- /dev/null +++ b/util/validate/validate.go @@ -0,0 +1,26 @@ +package validate + +import ( + "reflect" + "strings" + + "github.com/go-playground/validator/v10" +) + +var validate *validator.Validate + +func init() { + validate = validator.New() + // register function to get tag name from json tags. + validate.RegisterTagNameFunc(func(fld reflect.StructField) string { + name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + return name + }) +} + +func Struct(s any) error { + return validate.Struct(s) +} diff --git a/web/lib/gql/graph.d.ts b/web/lib/gql/graph.d.ts index fac50d7..16206f5 100644 --- a/web/lib/gql/graph.d.ts +++ b/web/lib/gql/graph.d.ts @@ -28,20 +28,18 @@ export type AuthUser = { export type Mutation = { __typename?: 'Mutation'; - login: Scalars['Boolean']['output']; + login: AuthUser; logout: Scalars['Boolean']['output']; }; export type MutationLoginArgs = { email: Scalars['String']['input']; - username: Scalars['String']['input']; + pwd: Scalars['String']['input']; }; export type Query = { __typename?: 'Query'; - heartBeat: Scalars['Boolean']['output']; - /** me, is current AuthUser info */ me?: Maybe; }; diff --git a/web/public/mailer-logo.png b/web/public/mailer-logo.png new file mode 100644 index 0000000..c4c9dbf Binary files /dev/null and b/web/public/mailer-logo.png differ