working on auth.

mailer, basic setup with html template and a dev treansport
This commit is contained in:
Ankit Patial 2024-11-15 21:42:15 +05:30
parent b0db98452a
commit 26a00c9f7c
45 changed files with 923 additions and 252 deletions

View File

@ -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<no-reply@my-app.com>

View File

@ -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() {

View File

@ -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 {
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)
}

View File

@ -6,7 +6,7 @@ import (
"os"
"path/filepath"
"gitserver.in/patialtech/rano/pkg/logger"
"gitserver.in/patialtech/rano/util/logger"
)
//

View File

@ -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)

View File

@ -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},

View File

@ -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

View File

@ -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.

View File

@ -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(),

View File

@ -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))

View File

@ -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)}

View File

@ -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)
}

11
go.mod
View File

@ -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

61
go.sum
View File

@ -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=

View File

@ -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:

View File

@ -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
}

View File

@ -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"))
}

View File

@ -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

View File

@ -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

View File

@ -48,12 +48,11 @@ 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
}
}
@ -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.

View File

@ -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 }

View File

@ -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.

View File

@ -4,9 +4,7 @@
type Mutation
type Query {
heartBeat: Boolean!
}
type Query
"""
Maps a Time GraphQL scalar to a Go time.Time struct.

18
graph/root.resolvers.go Normal file
View File

@ -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 }

View File

@ -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() {

72
mailer/mailer.go Normal file
View File

@ -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{}
}
}

120
mailer/message/_layout.html Normal file
View File

@ -0,0 +1,120 @@
{{define "layout"}}
<html lang="en">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>{{.Title}}</title>
</head>
<body
style="
background-color: #f2f2f2;
margin: 0;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 16px;
"
>
<table cellpadding="0" cellspacing="0" border="0" bgcolor="#ffffff" align="center" style="width: 100%">
<tbody>
<tr style="border-collapse: collapse">
<td align="center" bgcolor="#f2f2f2" style="border-collapse: collapse; padding: 15px">
<table
cellpadding="0"
cellspacing="0"
border="0"
bgcolor="#ffffff"
style="
width: 640px;
border: 2px solid #77808a;
background-color: #ffffff;
text-align: left;
padding: 0;
margin: 0;
"
>
<tbody>
<tr style="border-collapse: collapse">
<td style="border-collapse: collapse; padding: 15px">
<table>
<tbody>
<tr style="border-collapse: collapse">
<td style="border-collapse: collapse; padding: 0; vertical-align: top; width: 200px">
<img
src="{{.WebAssetsURL}}/mailer-logo.png"
alt="logo"
title="{{.Title}}"
style="outline: none; text-decoration: none; display: block; max-width: 175px"
/>
</td>
<td
style="
border-collapse: collapse;
padding: 0;
vertical-align: top;
text-align: right;
width: 100%;
"
>
&nbsp;
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr class="m_-6736051552126560803borderRow" style="border-collapse: collapse">
<td
style="
border-collapse: collapse;
padding: 0;
background-color: #e2e2e2;
height: 1px;
line-height: 0;
"
></td>
</tr>
<tr style="border-collapse: collapse">
<td style="border-collapse: collapse; padding: 15px; overflow-wrap: anywhere">
<div style="font-size: 1em; line-height: 1.4em">
{{ template "content" .}}
<p>Thank You!</p>
</div>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr style="border-collapse: collapse">
<td align="center" bgcolor="#f2f2f2" style="border-collapse: collapse; padding: 15px">
<table
width="630"
cellpadding="5px"
cellspacing="0"
border="0"
style="width: 640px; font-size: 0.75em; color: #aaaaaa; text-align: left"
>
<tbody>
<tr style="border-collapse: collapse">
<td style="border-collapse: collapse; padding: 15px; padding-top: 0">
<p style="color: #333; font-size: 0.875em; line-height: 1.4em; margin: 0 0 0.75em">
<a href="{{.websiteURL}}" target="_blank" rel="noopener noreferrer">{{.websiteDomain}}</a>
</p>
{{if not .AllowReply}}
<p>
<small>
<strong>** This is a system generated email, please do not reply to it **</strong>
</small>
</p>
{{end}}
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
{{end}}

48
mailer/message/render.go Normal file
View File

@ -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
}

View File

@ -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 := `<p>{{.Message}}</p>`
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)
}
}

16
mailer/message/welcome.go Normal file
View File

@ -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 := `
<p>Welcome {{.Name}}</p>
`
return render(layout, content, e)
}

23
mailer/transport_dev.go Normal file
View File

@ -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)
}

97
pkg/user/create.go Normal file
View File

@ -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), " ")
}

36
pkg/user/create_test.go Normal file
View File

@ -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)
}
})
}

View File

@ -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}}

75
util/crypto/hash.go Normal file
View File

@ -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
}

22
util/crypto/hash_test.go Normal file
View File

@ -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")
}
}

View File

@ -40,5 +40,6 @@ func getArgs(args []any) ([]any, []any) {
a = append(a, arg)
}
}
return a, b
}

15
util/open/darwin.go Normal file
View File

@ -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)
}

17
util/open/linux.go Normal file
View File

@ -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)
}

12
util/open/open.go Normal file
View File

@ -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()
}

32
util/open/windows.go Normal file
View File

@ -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
}

33
util/structs/structs.go Normal file
View File

@ -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
}

26
util/validate/validate.go Normal file
View File

@ -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)
}

View File

@ -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<AuthUser>;
};

BIN
web/public/mailer-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB