diff --git a/dotenv/assign.go b/dotenv/assign.go index d8bc974..23e79ee 100644 --- a/dotenv/assign.go +++ b/dotenv/assign.go @@ -10,12 +10,10 @@ import ( // Assign env tag matching values from envMap func Assign[T any](to *T, envMap map[string]string) error { if to == nil { - slog.Warn(" arg 'to' is nil") return nil } if len(envMap) == 0 { - slog.Warn(" envMap is nil") return nil } @@ -58,7 +56,7 @@ func Assign[T any](to *T, envMap map[string]string) error { field.SetInt(v) } case reflect.Float32, reflect.Float64: - if v, err := strconv.ParseFloat(v, 10); err != nil { + if v, err := strconv.ParseFloat(v, 64); err != nil { return err } else { field.SetFloat(v) diff --git a/email/transport.go b/email/transport.go index 85811a4..eda0d0c 100644 --- a/email/transport.go +++ b/email/transport.go @@ -5,36 +5,6 @@ package email -import ( - "os" - "path/filepath" - "time" - - "code.patial.tech/go/appcore/open" -) - type Transport interface { Send(*Message) error } - -// DumpToTemp transport is for development environment to ensure emails are renderd as HTML ok -// -// once dump operation is done it will try to open the html with default app for html -type DumpToTemp struct{} - -func (DumpToTemp) Send(msg *Message) error { - // validate msg first - if err := msg.Validate(); err != nil { - return err - } - - 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/email/transport_dump.go b/email/transport_dump.go new file mode 100644 index 0000000..c32d041 --- /dev/null +++ b/email/transport_dump.go @@ -0,0 +1,30 @@ +package email + +import ( + "os" + "path/filepath" + + "code.patial.tech/go/appcore/open" + "github.com/google/uuid" +) + +// DumpToTemp transport is for development environment to ensure emails are renderd as HTML ok +// once dump operation is done it will try to open the html with default app for html +type DumpToTemp struct{} + +func (DumpToTemp) Send(msg *Message) error { + // validate msg first + if err := msg.Validate(); err != nil { + return err + } + + dir := os.TempDir() + id, _ := uuid.NewV7() + file := filepath.Join(dir, id.String()+".html") + + if err := os.WriteFile(file, []byte(msg.HtmlBody), 0440); err != nil { + return err + } + + return open.WithDefaultApp(file) +} diff --git a/gz/gz.go b/gz/gz.go index 7236a8e..d762a4d 100644 --- a/gz/gz.go +++ b/gz/gz.go @@ -8,7 +8,6 @@ package gz import ( "bytes" "compress/gzip" - "io" ) func Zip(data []byte) ([]byte, error) { @@ -31,7 +30,6 @@ func Zip(data []byte) ([]byte, error) { func UnZip(data []byte) ([]byte, error) { b := bytes.NewBuffer(data) - var r io.Reader r, err := gzip.NewReader(b) if err != nil { return nil, err diff --git a/jwt/jwt.go b/jwt/jwt.go index a95d1e5..2e2c2e9 100644 --- a/jwt/jwt.go +++ b/jwt/jwt.go @@ -8,6 +8,7 @@ package jwt import ( "crypto/ed25519" "errors" + "fmt" "log" "maps" "time" @@ -15,7 +16,18 @@ import ( "github.com/golang-jwt/jwt/v5" ) +// Sign using EdDSA func Sign(key ed25519.PrivateKey, claims map[string]any, issuer string, d time.Duration) (string, error) { + return SignEdDSA(key, claims, issuer, d) +} + +func Parse(key ed25519.PrivateKey, tokenString string, issuer string) (jwt.MapClaims, error) { + return ParseEdDSA(key, tokenString, issuer) +} + +// SignEdDSA (Edwards-curve Digital Signature Algorithm, typically Ed25519) is an excellent, +// modern choice for JWT signing—arguably safer and more efficient than both HS256 and traditional RSA/ECDSA. +func SignEdDSA(key ed25519.PrivateKey, claims map[string]any, issuer string, d time.Duration) (string, error) { cl := jwt.MapClaims{ "iss": issuer, "iat": jwt.NewNumericDate(time.Now().UTC()), @@ -27,7 +39,7 @@ func Sign(key ed25519.PrivateKey, claims map[string]any, issuer string, d time.D return t.SignedString(key) } -func Parse(key ed25519.PrivateKey, tokenString string, issuer string) (jwt.MapClaims, error) { +func ParseEdDSA(key ed25519.PrivateKey, tokenString string, issuer string) (jwt.MapClaims, error) { token, err := jwt.Parse( tokenString, func(token *jwt.Token) (any, error) { @@ -48,3 +60,40 @@ func Parse(key ed25519.PrivateKey, tokenString string, issuer string) (jwt.MapCl return nil, errors.New("no claims found") } } + +func SignHS256(secret []byte, claims map[string]any, issuer string, d time.Duration) (string, error) { + cl := jwt.MapClaims{ + "iss": issuer, + "iat": jwt.NewNumericDate(time.Now().UTC()), + "exp": jwt.NewNumericDate(time.Now().Add(d)), + } + maps.Copy(cl, claims) + + t := jwt.NewWithClaims(jwt.SigningMethodHS256, cl) + return t.SignedString(secret) +} + +func ParseHS256(secret []byte, tokenString string, issuer string) (jwt.MapClaims, error) { + token, err := jwt.Parse( + tokenString, + func(token *jwt.Token) (any, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + return secret, nil + }, + jwt.WithValidMethods([]string{jwt.SigningMethodHS256.Alg()}), + jwt.WithIssuer(issuer), + jwt.WithIssuedAt(), + jwt.WithExpirationRequired(), + ) + if err != nil { + log.Fatal(err) + } + + if claims, ok := token.Claims.(jwt.MapClaims); ok { + return claims, nil + } else { + return nil, errors.New("no claims found") + } +} diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go index daeb382..7d2fdca 100644 --- a/jwt/jwt_test.go +++ b/jwt/jwt_test.go @@ -60,3 +60,28 @@ MC4CAQAwBQYDK2VwBCIEIMMkYUKJ9P0gp+Rm9mR4i0KUBT9nFUzxzxjH7sC0xq/F fmt.Printf("%v", claims) } + +func TestHS256(t *testing.T) { + secret := []byte("c4c5fcb25e289e7a23763b013f04fd11b6b0247729216bb98d07f58332360aec") + claims := map[string]any{ + "id": 1, + "email": "aa@aa.com", + } + issuer := "pat" + + // Sign + jwt, err := SignHS256(secret, claims, issuer, time.Second) + if err != nil { + t.Error(err) + return + } + + t.Log("jwt", jwt) + + // Parse + _, err = ParseHS256(secret, jwt, issuer) + if err != nil { + t.Error(err) + return + } +} diff --git a/open/darwin.go b/open/open_darwin.go similarity index 92% rename from open/darwin.go rename to open/open_darwin.go index 06c891a..daacbb4 100644 --- a/open/darwin.go +++ b/open/open_darwin.go @@ -1,5 +1,3 @@ -//go:build darwin - package open import ( diff --git a/open/linux.go b/open/open_linux.go similarity index 93% rename from open/linux.go rename to open/open_linux.go index a432b58..d870e3d 100644 --- a/open/linux.go +++ b/open/open_linux.go @@ -1,5 +1,3 @@ -//go:build linux - package open import ( diff --git a/open/windows.go b/open/open_windows.go similarity index 97% rename from open/windows.go rename to open/open_windows.go index a0b0324..9513009 100644 --- a/open/windows.go +++ b/open/open_windows.go @@ -1,5 +1,3 @@ -//go:build windows - package open import ( diff --git a/ptr/ptr.go b/ptr/ptr.go index cd2e70a..9d5490c 100644 --- a/ptr/ptr.go +++ b/ptr/ptr.go @@ -7,15 +7,24 @@ package ptr import "strings" +func Ref[T string | bool | Num](v T) *T { + return &v +} + +func Deref[T any](v *T) T { + if v == nil { + var a T + return a + } + return *v +} + func Bool(v bool) *bool { return &v } func GetBool(v *bool) bool { - if v == nil { - return false - } - return *v + return Deref(v) } func Str(v string) *string { @@ -23,10 +32,7 @@ func Str(v string) *string { } func GetStr(v *string) string { - if v == nil { - return "" - } - return *v + return Deref(v) } func StrTrim(v *string) *string { @@ -38,17 +44,14 @@ func StrTrim(v *string) *string { return v } -type N interface { +type Num interface { uint8 | int8 | uint16 | int16 | uint32 | int32 | uint64 | int64 | uint | int | float32 | float64 } -func Number[T N](v T) *T { +func Number[T Num](v T) *T { return &v } -func GetNumber[T N](v *T) T { - if v == nil { - return 0 - } - return *v +func GetNumber[T Num](v *T) T { + return Deref(v) } diff --git a/ptr/ptr_test.go b/ptr/ptr_test.go new file mode 100644 index 0000000..af0678d --- /dev/null +++ b/ptr/ptr_test.go @@ -0,0 +1,36 @@ +package ptr + +import "testing" + +func TestRefDeref(t *testing.T) { + a := 10 + if Deref(Ref(a)) != a { + t.Log("a) had a issue") + return + } + + b := 10.1 + if Deref(Ref(b)) != b { + t.Log("b) had a issue") + return + } + + c := true + if Deref(Ref(c)) != c { + t.Log("c) had a issue") + return + } + + d := "hello there" + if Deref(Ref(d)) != d { + t.Log("d) had a issue") + return + } + + var e string + if Deref(Ref(e)) != e { + t.Log("e) had a issue") + return + } + +} diff --git a/response/reply.go b/response/reply.go index a109d32..8b0873c 100644 --- a/response/reply.go +++ b/response/reply.go @@ -8,6 +8,7 @@ package response import ( "encoding/json" "fmt" + "log/slog" "net/http" "code.patial.tech/go/appcore/request" @@ -49,7 +50,10 @@ func reply(w http.ResponseWriter, data any, p *request.Pager) { // if data is nil, let's pass it on as null if data == nil { w.WriteHeader(http.StatusOK) - w.Write([]byte("{\"data\":null}")) + _, writeErr := fmt.Fprint(w, "{\"data\":null}") + if writeErr != nil { + slog.Error(writeErr.Error()) + } return } @@ -64,30 +68,45 @@ func reply(w http.ResponseWriter, data any, p *request.Pager) { func BadRequest(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - w.Write(fmt.Appendf(nil, "{\"error\": %q}", err.Error())) + _, writeErr := fmt.Fprintf(w, "{\"error\": %q}", err.Error()) + if writeErr != nil { + slog.Error(writeErr.Error()) + } } func InternalServerError(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) - w.Write(fmt.Appendf(nil, "{\"error\": %q}", err.Error())) + _, writeErr := fmt.Fprintf(w, "{\"error\": %q}", err.Error()) + if writeErr != nil { + slog.Error(writeErr.Error()) + } } func SessionExpired(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte("{\"error\": \"Session is expired, please login again\"}")) + _, writeErr := fmt.Fprint(w, "{\"error\": \"Session is expired, please login again\"}") + if writeErr != nil { + slog.Error(writeErr.Error()) + } } func NotAutorized(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("{\"error\": \"You are not authorized to perform this action\"}")) + _, writeErr := fmt.Fprint(w, "{\"error\": \"You are not authorized to perform this action\"}") + if writeErr != nil { + slog.Error(writeErr.Error()) + } } // Forbidden response error func Forbidden(w http.ResponseWriter, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusForbidden) - w.Write(fmt.Appendf(nil, "{\"error\": %q}", err.Error())) + _, writeErr := fmt.Fprintf(w, "{\"error\": %q}", err.Error()) + if writeErr != nil { + slog.Error(writeErr.Error()) + } } diff --git a/validate/validate.go b/validate/validate.go index f8954ff..21eeeaf 100644 --- a/validate/validate.go +++ b/validate/validate.go @@ -6,6 +6,8 @@ package validate import ( + "errors" + "fmt" "reflect" "strings" @@ -24,8 +26,51 @@ func init() { } return name }) + } -func Struct(s any) error { - return validate.Struct(s) +// RegisterAlias for single/multuple tags +func RegisterAlias(alias, tags string) { + validate.RegisterAlias(alias, tags) +} + +func RegisterValidation(tagName string, fn validator.Func, callValidationEvenIfNull ...bool) { + validate.RegisterValidation(tagName, fn, callValidationEvenIfNull...) +} + +// Struct validator +func Struct(s any) error { + err := validate.Struct(s) + if IsInvalidValidationError(err) { + return err + } + + var valErrs validator.ValidationErrors + if !errors.As(err, &valErrs) { + return err + } + + var sb strings.Builder + for _, err := range valErrs { + switch err.Tag() { + case "required": + sb.WriteString(fmt.Sprintf("%s: is required.\n", err.Field())) + case "email": + sb.WriteString(fmt.Sprintf("%s: is invalid.\n", err.Field())) + default: + sb.WriteString(fmt.Sprintf("%s: %q validation failed.\n", err.Field(), err.Tag())) + } + } + + return errors.New(sb.String()) +} + +// Map validator +func Map(data map[string]any, rules map[string]any) map[string]any { + return validate.ValidateMap(data, rules) +} + +func IsInvalidValidationError(err error) bool { + var v *validator.InvalidValidationError + return errors.As(err, &v) } diff --git a/validate/validate_test.go b/validate/validate_test.go new file mode 100644 index 0000000..e76c876 --- /dev/null +++ b/validate/validate_test.go @@ -0,0 +1,30 @@ +package validate + +import "testing" + +func TestStruct(t *testing.T) { + type person struct { + FirstName string `validate:"required,max=10"` + LastName string `validate:"required"` + Email string `validate:"email"` + } + + var p *person + t.Log("check for nil value") + if err := Struct(p); err == nil { + t.Fatal("nil value must report and error") + } else { + t.Log(err.Error()) + } + + p = new(person) + t.Log("validation checks") + if err := Struct(p); err == nil { + t.Error(err) + } else { + t.Log(err) + } + + // Structure error string + +}