Compare commits

..

7 Commits

Author SHA1 Message Date
$(pass /github/name)
f99ff57275 minor 2024-09-07 08:56:33 +02:00
$(pass /github/name)
c36af961f3 fix auth routes 2024-09-07 08:56:26 +02:00
$(pass /github/name)
b3dc134c8c add admin user creation 2024-09-07 08:56:15 +02:00
$(pass /github/name)
066419e546 fix: user model json handling; user_controller_test debug logging, user_controller 2024-09-07 08:55:39 +02:00
$(pass /github/name)
ff7c83671f fix: auth && auth_test 2024-09-07 08:53:57 +02:00
$(pass /github/name)
ef5e771998 chg: cors allowOrigins now configurable 2024-09-07 08:52:48 +02:00
$(pass /github/name)
8f3d73af90 add: AllowOrigins config && changed config format 2024-09-07 08:51:58 +02:00
15 changed files with 267 additions and 133 deletions

View File

@@ -18,7 +18,7 @@ func main() {
config.LoadConfig()
err := database.Open(config.DB.Path)
err := database.Open(config.DB.Path, config.Recipients.AdminEmail)
if err != nil {
logger.Error.Fatalf("Couldn't init database: %v", err)
}

View File

@@ -1,6 +1,9 @@
{
"WebsiteTitle": "My Carsharing Site",
"BaseURL": "https://domain.de",
"site": {
"WebsiteTitle": "My Carsharing Site",
"BaseUrl": "https://domain.de",
"AllowOrigins": "https://domain.de"
},
"Environment": "dev",
"db": {
"Path": "data/db.sqlite3"

View File

@@ -23,6 +23,11 @@ type DatabaseConfig struct {
Path string `json:"Path" default:"data/db.sqlite3" envconfig:"DB_PATH"`
}
type SiteConfig struct {
AllowOrigins string `json:"AllowOrigins" envconfig:"ALLOW_ORIGINS"`
WebsiteTitle string `json:"WebsiteTitle" envconfig:"WEBSITE_TITLE"`
BaseURL string `json:"BaseUrl" envconfig:"BASE_URL"`
}
type AuthenticationConfig struct {
JWTSecret string
CSRFSecret string
@@ -57,11 +62,10 @@ type SecurityConfig struct {
}
type Config struct {
Auth AuthenticationConfig `json:"auth"`
Site SiteConfig `json:"site"`
Templates TemplateConfig `json:"templates"`
Recipients RecipientsConfig `json:"recipients"`
ConfigFilePath string `json:"config_file_path" envconfig:"CONFIG_FILE_PATH"`
WebsiteTitle string `json:"WebsiteTitle" envconfig:"WEBSITE_TITLE"`
BaseURL string `json:"BaseUrl" envconfig:"BASE_URL"`
Env string `json:"Environment" default:"development" envconfig:"ENV"`
DB DatabaseConfig `json:"db"`
SMTP SMTPConfig `json:"smtp"`
@@ -69,17 +73,16 @@ type Config struct {
}
var (
BaseURL string
WebsiteTitle string
CFGPath string
CFG Config
Auth AuthenticationConfig
DB DatabaseConfig
Templates TemplateConfig
SMTP SMTPConfig
Recipients RecipientsConfig
Env string
Security SecurityConfig
Site SiteConfig
CFGPath string
CFG Config
Auth AuthenticationConfig
DB DatabaseConfig
Templates TemplateConfig
SMTP SMTPConfig
Recipients RecipientsConfig
Env string
Security SecurityConfig
)
var environmentOptions map[string]bool = map[string]bool{
"development": true,
@@ -115,11 +118,10 @@ func LoadConfig() {
DB = CFG.DB
Templates = CFG.Templates
SMTP = CFG.SMTP
BaseURL = CFG.BaseURL
Recipients = CFG.Recipients
Security = CFG.Security
Env = CFG.Env
WebsiteTitle = CFG.WebsiteTitle
Site = CFG.Site
logger.Info.Printf("Config loaded: %#v", CFG)
}

View File

@@ -44,9 +44,6 @@ var (
func TestSuite(t *testing.T) {
_ = deleteTestDB("test.db")
if err := database.Open("test.db"); err != nil {
log.Fatalf("Failed to create DB: %#v", err)
}
cwd, err := os.Getwd()
if err != nil {
@@ -77,6 +74,9 @@ func TestSuite(t *testing.T) {
log.Fatalf("Error setting environment variable: %v", err)
}
config.LoadConfig()
if err := database.Open("test.db", config.Recipients.AdminEmail); err != nil {
log.Fatalf("Failed to create DB: %#v", err)
}
utils.SMTPStart(Host, Port)
emailService := services.NewEmailService(config.SMTP.Host, config.SMTP.Port, config.SMTP.User, config.SMTP.Password)
var consentRepo repositories.ConsentRepositoryInterface = &repositories.ConsentRepository{}

View File

@@ -24,17 +24,19 @@ type UserController struct {
}
type RegistrationData struct {
User models.User `json:"user"`
User models.User `json:"user"`
Password string `json:"password"`
}
func (uc *UserController) CurrentUserHandler(c *gin.Context) {
userID, err := middlewares.GetUserIDFromContext(c)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Failed to authenticate user"})
c.Abort()
return
userIDString, ok := c.Get("user_id")
if !ok || userIDString == nil {
logger.Error.Printf("Error getting user_id from header")
}
user, err := uc.Service.GetUserByID(userID)
userID := userIDString.(float64)
logger.Error.Printf("UserIDINt64: %v", userID)
logger.Error.Printf("c.Get Value: %v", userIDString)
user, err := uc.Service.GetUserByID(int64(userID))
if err != nil {
logger.Error.Printf("Error retrieving valid user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving user."})
@@ -124,6 +126,7 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
// logger.Info.Printf("REGISTERING user: %#v", regData.User)
regData.User.RoleID = constants.Roles.Member
regData.User.Password = regData.Password
// Register User
id, token, err := uc.Service.RegisterUser(&regData.User)

View File

@@ -18,11 +18,17 @@ import (
"GoMembership/internal/config"
"GoMembership/internal/constants"
"GoMembership/internal/middlewares"
"GoMembership/internal/models"
"GoMembership/internal/utils"
"GoMembership/pkg/logger"
)
type loginInput struct {
Email string `json:"email"`
Password string `json:"password"`
}
type RegisterUserTest struct {
WantDBData map[string]interface{}
Name string
@@ -61,11 +67,13 @@ func testUserController(t *testing.T) {
}
})
}
testLoginUser(t)
testCurrentUserHandler(t)
}
func testLoginUser(t *testing.T) {
func testLoginUser(t *testing.T) (string, http.Cookie) {
// This test should run after the user registration test
var loginCookie http.Cookie
var loginInput loginInput
t.Run("LoginUser", func(t *testing.T) {
// Test cases
tests := []struct {
@@ -104,6 +112,9 @@ func testLoginUser(t *testing.T) {
}
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("Testing : %v", tt.name)
logger.Error.Print("==============================================================")
t.Run(tt.name, func(t *testing.T) {
// Setup
c, w, _ := GetMockedJSONContext([]byte(tt.input), "/login")
@@ -122,13 +133,113 @@ func testLoginUser(t *testing.T) {
logger.Info.Printf("Response: %#v", response)
assert.Contains(t, response, "set-token")
assert.NotEmpty(t, response["set-token"])
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
loginCookie = *cookie
err = json.Unmarshal([]byte(tt.input), &loginInput)
assert.NoError(t, err, "Failed to unmarshal input JSON")
break
}
}
assert.NotEmpty(t, loginCookie)
} else {
assert.NotContains(t, response, "set-token")
}
})
}
})
return loginInput.Email, loginCookie
}
func testCurrentUserHandler(t *testing.T) {
loginEmail, loginCookie := testLoginUser(t)
// This test should run after the user login test
invalidCookie := http.Cookie{
Name: "jwt",
Value: "invalid.token.here",
}
tests := []struct {
name string
setupCookie func(*http.Request)
expectedUserMail string
expectedStatus int
}{
{
name: "With valid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
},
expectedUserMail: loginEmail,
expectedStatus: http.StatusOK,
},
{
name: "Without cookie",
setupCookie: func(req *http.Request) {},
expectedStatus: http.StatusUnauthorized,
},
{
name: "With invalid cookie",
setupCookie: func(req *http.Request) {
req.AddCookie(&invalidCookie)
},
expectedStatus: http.StatusUnauthorized,
},
}
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("Testing : %v", tt.name)
logger.Error.Print("==============================================================")
if tt.expectedStatus == http.StatusOK {
time.Sleep(time.Second) // Small delay to ensure different timestamps to get a different JWT token
}
t.Run(tt.name, func(t *testing.T) {
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(middlewares.AuthMiddleware())
router.GET("/current-user", Uc.CurrentUserHandler)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/current-user", nil)
tt.setupCookie(req)
router.ServeHTTP(w, req)
assert.Equal(t, tt.expectedStatus, w.Code)
if tt.expectedStatus == http.StatusOK {
var response models.User
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, tt.expectedUserMail, response.Email)
var newCookie *http.Cookie
for _, cookie := range w.Result().Cookies() {
if cookie.Name == "jwt" {
newCookie = cookie
break
}
}
assert.NotNil(t, newCookie, "Cookie should be renewed")
assert.NotEqual(t, loginCookie.Value, newCookie.Value, "Cookie value should be different")
assert.True(t, newCookie.MaxAge > 0, "New cookie should not be expired")
} else {
// For unauthorized requests, check for an error message
var errorResponse map[string]string
err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
assert.NoError(t, err)
assert.Contains(t, errorResponse, "error")
assert.NotEmpty(t, errorResponse["error"])
}
})
}
}
func validateUser(assert bool, wantDBData map[string]interface{}) error {
users, err := Uc.Service.GetUsers(wantDBData)
if err != nil {
@@ -195,14 +306,14 @@ func checkWelcomeMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, fmt.Sprintf("Mitgliedsnummer</strong>: %v", user.Membership.ID)) {
return fmt.Errorf("Users membership Id(%v) has not been rendered in registration mail.", user.Membership.ID)
}
if !strings.Contains(message.Body, config.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.BaseURL)
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.Site.BaseURL)
}
if !strings.Contains(message.Body, config.BaseURL+config.Templates.LogoURI) {
return fmt.Errorf("Logo Url (%v) has not been rendered in registration mail.", config.BaseURL+config.WebsiteTitle)
if !strings.Contains(message.Body, config.Site.BaseURL+config.Templates.LogoURI) {
return fmt.Errorf("Logo Url (%v) has not been rendered in registration mail.", config.Site.BaseURL+config.Site.WebsiteTitle)
}
if !strings.Contains(message.Body, config.WebsiteTitle) {
return fmt.Errorf("Website title (%v) has not been rendered in registration mail.", config.WebsiteTitle)
if !strings.Contains(message.Body, config.Site.WebsiteTitle) {
return fmt.Errorf("Website title (%v) has not been rendered in registration mail.", config.Site.WebsiteTitle)
}
return nil
}
@@ -249,8 +360,8 @@ func checkRegistrationMail(message *utils.Email, user *models.User) error {
if !strings.Contains(message.Body, user.BankAccount.IBAN) {
return fmt.Errorf("Users IBAN(%v) has not been rendered in registration mail.", user.BankAccount.IBAN)
}
if !strings.Contains(message.Body, config.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.BaseURL)
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in registration mail.", config.Site.BaseURL)
}
return nil
}
@@ -267,8 +378,8 @@ func checkVerificationMail(message *utils.Email, user *models.User) error {
return fmt.Errorf("Users Verification link token(%v) has not been rendered in email verification mail. %v", user.Verification.VerificationToken, verificationURL)
}
if !strings.Contains(message.Body, config.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.BaseURL)
if !strings.Contains(message.Body, config.Site.BaseURL) {
return fmt.Errorf("Base Url (%v) has not been rendered in email verification mail.", config.Site.BaseURL)
}
// open the provided link:
if err := verifyMail(verificationURL); err != nil {
@@ -349,7 +460,7 @@ func getBaseUser() models.User {
func customizeInput(customize func(models.User) models.User) *RegistrationData {
user := getBaseUser()
user = customize(user) // Apply the customization
return &RegistrationData{User: user}
return &RegistrationData{User: user, Password: user.Password}
}
func getTestUsers() []RegisterUserTest {

View File

@@ -5,7 +5,7 @@ import (
"GoMembership/internal/models"
"GoMembership/pkg/logger"
"crypto/rand"
"encoding/hex"
"encoding/base64"
"time"
"github.com/alexedwards/argon2id"
@@ -15,7 +15,7 @@ import (
var DB *gorm.DB
func Open(dbPath string) error {
func Open(dbPath string, adminMail string) error {
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
@@ -34,12 +34,60 @@ func Open(dbPath string) error {
DB = db
logger.Info.Print("Opened DB")
if err := seedDatabase(); err != nil {
return err
var count int64
db.Model(&models.User{}).Count(&count)
if count == 0 {
admin, err := seedAdmin(adminMail)
if err != nil {
return err
}
result := db.Create(&admin)
if result.Error != nil {
return result.Error
}
}
return nil
}
// TODO: Landing page to create an admin
func seedAdmin(userMail string) (*models.User, error) {
passwordBytes := make([]byte, 12)
_, err := rand.Read(passwordBytes)
if err != nil {
return nil, err
}
// Encode into a URL-safe base64 string
password, err := base64.URLEncoding.EncodeToString(passwordBytes)[:12], nil
if err != nil {
return nil, err
}
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
return nil, err
}
logger.Error.Print("==============================================================")
logger.Error.Printf("Admin Email: %v", userMail)
logger.Error.Printf("Admin Password: %v", password)
logger.Error.Print("==============================================================")
return &models.User{
FirstName: "ad",
LastName: "min",
DateOfBirth: time.Now(),
Password: hash,
Address: "Downhill 4",
ZipCode: "9999",
City: "TechTown",
Email: userMail,
Status: constants.ActiveStatus,
RoleID: constants.Roles.Editor,
}, nil
}
func Close() error {
logger.Info.Print("Closing DB")
db, err := DB.DB()
@@ -48,42 +96,3 @@ func Close() error {
}
return db.Close()
}
func seedDatabase() error {
var count int64
DB.Model(&models.User{}).Count(&count)
if count == 0 {
bytes := make([]byte, 12)
_, err := rand.Read(bytes)
if err != nil {
return err
}
password := hex.EncodeToString(bytes)
hash, err := argon2id.CreateHash(password, argon2id.DefaultParams)
if err != nil {
return err
}
admin := models.User{
FirstName: "ad",
LastName: "min",
DateOfBirth: time.Now(),
Password: hash,
Address: "Downhill 4",
ZipCode: "9999",
City: "TechTown",
Status: constants.ActiveStatus,
RoleID: constants.Roles.Editor,
}
result := DB.Create(&admin)
if result.Error != nil {
return result.Error
}
logger.Error.Print("==============================================================")
logger.Error.Printf("Admin Password: %v", password)
logger.Error.Print("==============================================================")
}
return nil
}

View File

@@ -28,6 +28,9 @@ func GenerateToken(userID int64) (string, error) {
}
func verifyToken(tokenString string) (*jwt.Token, error) {
if tokenString == "" {
return nil, fmt.Errorf("Authorization token is required")
}
token, err := jwtParser.Parse(tokenString, func(_ *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
@@ -36,55 +39,53 @@ func verifyToken(tokenString string) (*jwt.Token, error) {
return nil, err
}
return token, nil
}
func GetUserIDFromContext(c *gin.Context) (int64, error) {
tokenString, err := c.Cookie("jwt")
if err != nil {
logger.Error.Printf("Error getting cookie: %v\n", err)
return 0, err
}
if tokenString == "" {
logger.Error.Printf("Token is empty: %v\n", err)
return 0, fmt.Errorf("Authorization token is required")
}
token, err := verifyToken(tokenString)
if err != nil || !token.Valid {
logger.Error.Printf("Token is invalid: %v\n", err)
return 0, fmt.Errorf("Token not valid!")
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
logger.Error.Printf("claims userid: %v", claims["user_id"].(float64))
if !ok {
logger.Error.Printf("Invalid Token claims")
return 0, fmt.Errorf("Invalid token claims")
return nil, fmt.Errorf("invalid token claims")
}
userID, ok := claims["user_id"].(float64)
exp, ok := claims["exp"].(float64)
if !ok {
return nil, fmt.Errorf("invalid expiration claim")
}
userID, ok := claims["user_id"].(float64)
if !ok {
logger.Error.Printf("Invalid user ID: %v", userID)
return 0, fmt.Errorf("Invalid user ID")
return nil, fmt.Errorf("Invalid user ID")
}
return int64(userID), nil
if time.Now().Unix() > int64(exp) {
return nil, fmt.Errorf("token expired")
}
return token, nil
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
userID, err := GetUserIDFromContext(c)
tokenString, err := c.Cookie("jwt")
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
logger.Error.Printf("No Auth token: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "No Auth token"})
c.Abort()
return
}
token, err := verifyToken(tokenString)
if err != nil {
logger.Error.Printf("Token is invalid: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Auth token invalid"})
c.Abort()
return
}
claims, _ := token.Claims.(jwt.MapClaims)
userID, _ := claims["user_id"].(float64)
// Generate a new token
newToken, err := GenerateToken(int64(userID))
if err != nil {

View File

@@ -111,6 +111,9 @@ func TestAuthMiddleware(t *testing.T) {
}
for _, tt := range tests {
logger.Error.Print("==============================================================")
logger.Error.Printf("Testing : %v", tt.name)
logger.Error.Print("==============================================================")
t.Run(tt.name, func(t *testing.T) {
// Setup
r := gin.New()

View File

@@ -11,8 +11,8 @@ import (
func CORSMiddleware() gin.HandlerFunc {
logger.Info.Print("Applying CORS")
return cors.New(cors.Config{
AllowOrigins: []string{config.BaseURL, "http://localhost:8080"}, // Add your frontend URL(s)
AllowMethods: []string{"GET", "POST"}, // "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowOrigins: []string{config.Site.AllowOrigins},
AllowMethods: []string{"GET", "POST"}, // "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Requested-With"},
// ExposeHeaders: []string{"Content-Length"},
AllowCredentials: true,

View File

@@ -69,10 +69,10 @@ func TestCORSMiddleware(t *testing.T) {
}{
{
name: "Allowed origin",
origin: config.BaseURL,
origin: config.Site.AllowOrigins,
expectedStatus: http.StatusOK,
expectedHeaders: map[string]string{
"Access-Control-Allow-Origin": config.BaseURL,
"Access-Control-Allow-Origin": config.Site.AllowOrigins,
"Content-Type": "text/plain; charset=utf-8",
"Access-Control-Allow-Credentials": "true",
},

View File

@@ -16,7 +16,7 @@ type User struct {
Phone string `json:"phone" validate:"omitempty,omitnil"`
Notes *string `json:"notes"`
FirstName string `gorm:"not null" json:"first_name" validate:"required"`
Password string `json:"password" required_unless=RoleID 0`
Password string `json:"-" required_unless=RoleID 0`
Email string `gorm:"unique;not null" json:"email" validate:"required,email"`
LastName string `gorm:"not null" json:"last_name" validate:"required"`
ProfilePicture string `json:"profile_picture" validate:"omitempty,omitnil,image"`
@@ -27,7 +27,7 @@ type User struct {
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"`
Verification Verification `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;"`
Membership Membership `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"membership"`
ID int64 `gorm:"primaryKey"`
ID int64 `gorm:"primaryKey" json:"id"`
PaymentStatus int8 `json:"payment_status"`
Status int8 `json:"status"`
RoleID int8 `json:"role_id"`

View File

@@ -12,6 +12,7 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
router.POST("/users/register", userController.RegisterUser)
router.POST("/users/contact", contactController.RelayContactRequest)
router.POST("/users/login", userController.LoginUser)
router.POST("/csp-report", middlewares.CSPReportHandling)
// create subrouter for teh authenticated area /account
@@ -26,8 +27,8 @@ func RegisterRoutes(router *gin.Engine, userController *controllers.UserControll
apiRouter.Use(middlewares.APIKeyMiddleware())
authRouter := router.Group("/users/backend")
{
router.POST("/currentUser", userController.CurrentUserHandler)
}
authRouter.Use(middlewares.AuthMiddleware())
{
authRouter.POST("/currentUser", userController.CurrentUserHandler)
}
}

View File

@@ -70,7 +70,7 @@ func (s *EmailService) SendVerificationEmail(user *models.User, token *string) e
FirstName: user.FirstName,
LastName: user.LastName,
Token: *token,
BASEURL: config.BaseURL,
BASEURL: config.Site.BaseURL,
}
subject := constants.MailVerificationSubject
@@ -102,8 +102,8 @@ func (s *EmailService) SendWelcomeEmail(user *models.User) error {
MembershipID: user.Membership.ID,
MembershipFee: float32(user.Membership.SubscriptionModel.MonthlyFee),
RentalFee: float32(user.Membership.SubscriptionModel.HourlyRate),
BASEURL: config.BaseURL,
WebsiteTitle: config.WebsiteTitle,
BASEURL: config.Site.BaseURL,
WebsiteTitle: config.Site.WebsiteTitle,
Logo: config.Templates.LogoURI,
}
@@ -151,9 +151,9 @@ func (s *EmailService) SendRegistrationNotification(user *models.User) error {
Email: user.Email,
Phone: user.Phone,
IBAN: user.BankAccount.IBAN,
BASEURL: config.BaseURL,
BASEURL: config.Site.BaseURL,
Logo: config.Templates.LogoURI,
WebsiteTitle: config.WebsiteTitle,
WebsiteTitle: config.Site.WebsiteTitle,
}
subject := constants.MailRegistrationSubject
@@ -175,9 +175,9 @@ func (s *EmailService) RelayContactFormMessage(sender string, name string, messa
}{
Message: message,
Name: name,
BASEURL: config.BaseURL,
BASEURL: config.Site.BaseURL,
Logo: config.Templates.LogoURI,
WebsiteTitle: config.WebsiteTitle,
WebsiteTitle: config.Site.WebsiteTitle,
}
subject := constants.MailContactSubject
body, err := ParseTemplate("mail_contact_form.tmpl", data)

View File

@@ -6,12 +6,13 @@ import (
"GoMembership/internal/database"
"GoMembership/internal/models"
"GoMembership/pkg/logger"
"github.com/go-playground/validator/v10"
"github.com/jbub/banking/iban"
"github.com/jbub/banking/swift"
"reflect"
"slices"
"time"
"github.com/go-playground/validator/v10"
"github.com/jbub/banking/iban"
"github.com/jbub/banking/swift"
)
//