frontend: disabled button while processing password reset
This commit is contained in:
20
go-backend/internal/utils/cookies.go
Normal file
20
go-backend/internal/utils/cookies.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetCookie(c *gin.Context, token string) {
|
||||
c.SetSameSite(http.SameSiteLaxMode)
|
||||
c.SetCookie(
|
||||
"jwt",
|
||||
token,
|
||||
5*24*60*60, // 5 days
|
||||
"/",
|
||||
"",
|
||||
true,
|
||||
true,
|
||||
)
|
||||
}
|
||||
101
go-backend/internal/utils/crypto.go
Normal file
101
go-backend/internal/utils/crypto.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/quotedprintable"
|
||||
"net/mail"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
MimeVersion string
|
||||
Date string
|
||||
From string
|
||||
To string
|
||||
Subject string
|
||||
ContentType string
|
||||
Body string
|
||||
}
|
||||
|
||||
func GenerateRandomString(length int) (string, error) {
|
||||
bytes := make([]byte, length)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.URLEncoding.EncodeToString(bytes), nil
|
||||
}
|
||||
|
||||
func GenerateVerificationToken() (string, error) {
|
||||
return GenerateRandomString(32)
|
||||
}
|
||||
|
||||
func DecodeMail(message string) (*Email, error) {
|
||||
msg, err := mail.ReadMessage(strings.NewReader(message))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedBody, err := io.ReadAll(msg.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedBodyString, err := DecodeQuotedPrintable(string(decodedBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
decodedSubject, err := DecodeRFC2047(msg.Header.Get("Subject"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email := &Email{}
|
||||
|
||||
// Populate the headers
|
||||
email.MimeVersion = msg.Header.Get("Mime-Version")
|
||||
email.Date = msg.Header.Get("Date")
|
||||
email.From = msg.Header.Get("From")
|
||||
email.To = msg.Header.Get("To")
|
||||
email.Subject = decodedSubject
|
||||
email.Body = decodedBodyString
|
||||
email.ContentType = msg.Header.Get("Content-Type")
|
||||
|
||||
return email, nil
|
||||
}
|
||||
|
||||
func DecodeRFC2047(encoded string) (string, error) {
|
||||
decoder := new(mime.WordDecoder)
|
||||
decoded, err := decoder.DecodeHeader(encoded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
func DecodeQuotedPrintable(encodedString string) (string, error) {
|
||||
// Decode quoted-printable encoding
|
||||
reader := quotedprintable.NewReader(strings.NewReader(encodedString))
|
||||
decodedBytes := new(bytes.Buffer)
|
||||
_, err := decodedBytes.ReadFrom(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return decodedBytes.String(), nil
|
||||
}
|
||||
|
||||
func EncodeQuotedPrintable(s string) string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Use Quoted-Printable encoder
|
||||
qp := quotedprintable.NewWriter(&buf)
|
||||
|
||||
// Write the UTF-8 encoded string to the Quoted-Printable encoder
|
||||
qp.Write([]byte(s))
|
||||
qp.Close()
|
||||
|
||||
// Encode the result into a MIME header
|
||||
return mime.QEncoding.Encode("UTF-8", buf.String())
|
||||
}
|
||||
33
go-backend/internal/utils/mock_smtp.go
Normal file
33
go-backend/internal/utils/mock_smtp.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
smtpmock "github.com/mocktools/go-smtp-mock/v2"
|
||||
)
|
||||
|
||||
var Server smtpmock.Server
|
||||
|
||||
// StartMockSMTPServer starts a mock SMTP server for testing
|
||||
func SMTPStart(host string, port int) error {
|
||||
Server = *smtpmock.New(smtpmock.ConfigurationAttr{
|
||||
HostAddress: host,
|
||||
PortNumber: port,
|
||||
LogToStdout: false,
|
||||
LogServerActivity: false,
|
||||
})
|
||||
if err := Server.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SMTPGetMessages() []smtpmock.Message {
|
||||
return Server.MessagesAndPurge()
|
||||
}
|
||||
|
||||
func SMTPStop() error {
|
||||
|
||||
if err := Server.Stop(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
131
go-backend/internal/utils/priviliges.go
Normal file
131
go-backend/internal/utils/priviliges.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"GoMembership/internal/constants"
|
||||
"GoMembership/internal/models"
|
||||
"GoMembership/pkg/logger"
|
||||
"errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func HasPrivilige(user *models.User, privilige int8) bool {
|
||||
switch privilige {
|
||||
case constants.Priviliges.View:
|
||||
return user.RoleID >= constants.Roles.Viewer
|
||||
case constants.Priviliges.Update:
|
||||
return user.RoleID >= constants.Roles.Editor
|
||||
case constants.Priviliges.Create:
|
||||
return user.RoleID >= constants.Roles.Editor
|
||||
case constants.Priviliges.Delete:
|
||||
return user.RoleID >= constants.Roles.Editor
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// FilterAllowedStructFields filters allowed fields recursively in a struct and modifies structToModify in place.
|
||||
func FilterAllowedStructFields(input interface{}, existing interface{}, allowedFields map[string]bool, prefix string) error {
|
||||
v := reflect.ValueOf(input)
|
||||
origin := reflect.ValueOf(existing)
|
||||
|
||||
// Ensure both input and target are pointers to structs
|
||||
if v.Kind() != reflect.Ptr || origin.Kind() != reflect.Ptr {
|
||||
return errors.New("both input and existing must be pointers to structs")
|
||||
}
|
||||
|
||||
v = v.Elem()
|
||||
origin = origin.Elem()
|
||||
|
||||
if v.Kind() != reflect.Struct || origin.Kind() != reflect.Struct {
|
||||
return errors.New("both input and existing must be structs")
|
||||
}
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Type().Field(i)
|
||||
key := field.Name
|
||||
|
||||
// Skip unexported fields
|
||||
if !field.IsExported() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the full field path
|
||||
fullKey := key
|
||||
if prefix != "" {
|
||||
fullKey = prefix + "." + key
|
||||
}
|
||||
fieldValue := v.Field(i)
|
||||
originField := origin.Field(i)
|
||||
|
||||
// Handle nil pointers
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
if fieldValue.IsNil() {
|
||||
// If the field is nil, skip it or initialize it
|
||||
if !allowedFields[fullKey] {
|
||||
// If the field is not allowed, set it to the corresponding field from existing
|
||||
fieldValue.Set(originField)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Dereference the pointer for further processing
|
||||
fieldValue = fieldValue.Elem()
|
||||
originField = originField.Elem()
|
||||
}
|
||||
|
||||
// Handle slices
|
||||
if fieldValue.Kind() == reflect.Slice {
|
||||
if !allowedFields[fullKey] {
|
||||
// If the slice is not allowed, set it to the corresponding slice from existing
|
||||
fieldValue.Set(originField)
|
||||
continue
|
||||
} else {
|
||||
originField.Set(fieldValue)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle nested structs (including pointers to structs)
|
||||
if fieldValue.Kind() == reflect.Struct || (fieldValue.Kind() == reflect.Ptr && fieldValue.Type().Elem().Kind() == reflect.Struct) {
|
||||
if fieldValue.Kind() == reflect.Ptr {
|
||||
if fieldValue.IsNil() {
|
||||
continue
|
||||
}
|
||||
fieldValue = fieldValue.Elem()
|
||||
originField = originField.Elem() // May result in an invalid originField
|
||||
}
|
||||
|
||||
var originCopy reflect.Value
|
||||
|
||||
// Check if originField is valid (non-zero)
|
||||
if originField.IsValid() {
|
||||
originCopy = reflect.New(originField.Type()).Elem()
|
||||
originCopy.Set(originField)
|
||||
} else {
|
||||
// If originField is invalid (e.g., existing had a nil pointer),
|
||||
// create a new instance of the type from fieldValue
|
||||
originCopy = reflect.New(fieldValue.Type()).Elem()
|
||||
}
|
||||
|
||||
err := FilterAllowedStructFields(
|
||||
fieldValue.Addr().Interface(),
|
||||
originCopy.Addr().Interface(),
|
||||
allowedFields,
|
||||
fullKey,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Only allow whitelisted fields
|
||||
if !allowedFields[fullKey] {
|
||||
logger.Error.Printf("denying update of field: %#v", fullKey)
|
||||
fieldValue.Set(originField)
|
||||
} else {
|
||||
logger.Error.Printf("updating whitelisted field: %#v", fullKey)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
176
go-backend/internal/utils/priviliges_test.go
Normal file
176
go-backend/internal/utils/priviliges_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Name string
|
||||
Age int
|
||||
Address *Address
|
||||
Tags []string
|
||||
License License
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
City string
|
||||
Country string
|
||||
}
|
||||
|
||||
type License struct {
|
||||
ID string
|
||||
Categories []string
|
||||
}
|
||||
|
||||
func TestFilterAllowedStructFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input interface{}
|
||||
existing interface{}
|
||||
allowedFields map[string]bool
|
||||
expectedResult interface{}
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "Filter top-level fields",
|
||||
input: &User{
|
||||
Name: "Alice",
|
||||
Age: 30,
|
||||
},
|
||||
existing: &User{
|
||||
Name: "Bob",
|
||||
Age: 25,
|
||||
},
|
||||
allowedFields: map[string]bool{
|
||||
"Name": true,
|
||||
},
|
||||
expectedResult: &User{
|
||||
Name: "Alice", // Allowed field
|
||||
Age: 25, // Kept from existing
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Filter nested struct fields",
|
||||
input: &User{
|
||||
Name: "Alice",
|
||||
Address: &Address{
|
||||
City: "New York",
|
||||
Country: "USA",
|
||||
},
|
||||
},
|
||||
existing: &User{
|
||||
Name: "Bob",
|
||||
Address: &Address{
|
||||
City: "London",
|
||||
Country: "UK",
|
||||
},
|
||||
},
|
||||
allowedFields: map[string]bool{
|
||||
"Address.City": true,
|
||||
},
|
||||
expectedResult: &User{
|
||||
Name: "Bob", // Kept from existing
|
||||
Address: &Address{
|
||||
City: "New York", // Allowed field
|
||||
Country: "UK", // Kept from existing
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Filter slice fields",
|
||||
input: &User{
|
||||
Tags: []string{"admin", "user"},
|
||||
},
|
||||
existing: &User{
|
||||
Tags: []string{"guest"},
|
||||
},
|
||||
allowedFields: map[string]bool{
|
||||
"Tags": true,
|
||||
},
|
||||
expectedResult: &User{
|
||||
Tags: []string{"admin", "user"}, // Allowed slice
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Filter slice of structs",
|
||||
input: &User{
|
||||
License: License{
|
||||
ID: "123",
|
||||
Categories: []string{"A", "B"},
|
||||
},
|
||||
},
|
||||
existing: &User{
|
||||
License: License{
|
||||
ID: "456",
|
||||
Categories: []string{"C"},
|
||||
},
|
||||
},
|
||||
allowedFields: map[string]bool{
|
||||
"License.ID": true,
|
||||
},
|
||||
expectedResult: &User{
|
||||
License: License{
|
||||
ID: "123", // Allowed field
|
||||
Categories: []string{"C"}, // Kept from existing
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Filter pointer fields",
|
||||
input: &User{
|
||||
Address: &Address{
|
||||
City: "Paris",
|
||||
},
|
||||
},
|
||||
existing: &User{
|
||||
Address: &Address{
|
||||
City: "Berlin",
|
||||
Country: "Germany",
|
||||
},
|
||||
},
|
||||
allowedFields: map[string]bool{
|
||||
"Address.City": true,
|
||||
},
|
||||
expectedResult: &User{
|
||||
Address: &Address{
|
||||
City: "Paris", // Allowed field
|
||||
Country: "Germany", // Kept from existing
|
||||
},
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid input (non-pointer)",
|
||||
input: User{
|
||||
Name: "Alice",
|
||||
},
|
||||
existing: &User{
|
||||
Name: "Bob",
|
||||
},
|
||||
allowedFields: map[string]bool{
|
||||
"Name": true,
|
||||
},
|
||||
expectedResult: nil,
|
||||
expectError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := FilterAllowedStructFields(tt.input, tt.existing, tt.allowedFields, "")
|
||||
if (err != nil) != tt.expectError {
|
||||
t.Errorf("FilterAllowedStructFields() error = %v, expectError %v", err, tt.expectError)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.expectError && !reflect.DeepEqual(tt.input, tt.expectedResult) {
|
||||
t.Errorf("FilterAllowedStructFields() = %+v, expected %+v", tt.input, tt.expectedResult)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
50
go-backend/internal/utils/response_handler.go
Normal file
50
go-backend/internal/utils/response_handler.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"GoMembership/pkg/errors"
|
||||
"GoMembership/pkg/logger"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
func RespondWithError(c *gin.Context, err error, context string, code int, field string, key string) {
|
||||
logger.Error.Printf("Sending %v Error Response(Field: %v Key: %v) %v: %v", code, field, key, context, err.Error())
|
||||
c.JSON(code, gin.H{"errors": []gin.H{{
|
||||
"field": field,
|
||||
"key": key,
|
||||
}}})
|
||||
}
|
||||
|
||||
func HandleValidationError(c *gin.Context, err error) {
|
||||
var validationErrors []gin.H
|
||||
logger.Error.Printf("Sending validation error response Error %v", err.Error())
|
||||
if ve, ok := err.(validator.ValidationErrors); ok {
|
||||
for _, e := range ve {
|
||||
validationErrors = append(validationErrors, gin.H{
|
||||
"field": e.Field(),
|
||||
"key": "server.validation." + e.Tag(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
validationErrors = append(validationErrors, gin.H{
|
||||
"field": "general",
|
||||
"key": "server.error.invalid_json",
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{"errors": validationErrors})
|
||||
}
|
||||
|
||||
func HandleUserUpdateError(c *gin.Context, err error) {
|
||||
switch err {
|
||||
case errors.ErrUserNotFound:
|
||||
RespondWithError(c, err, "Error while updating user", http.StatusNotFound, "user.user", "server.validation.user_not_found")
|
||||
case errors.ErrInvalidUserData:
|
||||
RespondWithError(c, err, "Error while updating user", http.StatusBadRequest, "user.user", "server.validation.invalid_user_data")
|
||||
case errors.ErrSubscriptionNotFound:
|
||||
RespondWithError(c, err, "Error while updating user", http.StatusBadRequest, "subscription", "server.validation.subscription_data")
|
||||
default:
|
||||
RespondWithError(c, err, "Error while updating user", http.StatusInternalServerError, "user.user", "server.error.internal_server_error")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user