Compare commits

..

7 Commits

Author SHA1 Message Date
Alex
79ffb5051c chg: admin creation 2024-10-09 18:12:54 +02:00
Alex
5de2f31e5f chg: nested struct loading 2024-10-09 18:12:40 +02:00
Alex
b2e4947d37 add & moved to validations folder; del validator/v10 2024-10-09 18:12:20 +02:00
Alex
6aee416b63 chg: moved to gin binding instead of validator 2024-10-09 18:09:49 +02:00
Alex
02d75f0ab1 chg: backend: error struct 2024-10-09 18:08:33 +02:00
Alex
3b3dc9d251 chg: frontend: routes: errorhandling 2024-10-09 18:07:28 +02:00
Alex
c008bcad0b chg: frontend errorFormating 2024-10-09 18:05:27 +02:00
32 changed files with 630 additions and 391 deletions

View File

@@ -68,38 +68,33 @@ export function isEmpty(obj) {
} }
return true; return true;
} }
/**
* Test whether or not an object is empty.
* @param {any} obj - The object to test
* @returns `true` or `false`
*/
export function formatError(obj) { export function formatError(obj) {
const errors = []; const errors = [];
if (typeof obj === "object" && obj !== null) { if (typeof obj === "object" && obj !== null) {
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
obj.forEach((/** @type {Object} */ error) => { obj.forEach((error) => {
Object.keys(error).map((k) => {
errors.push({ errors.push({
error: error[k], field: error.field,
key: error.key,
id: Math.random() * 1000, id: Math.random() * 1000,
}); });
}); });
});
} else { } else {
Object.keys(obj).map((k) => { Object.keys(obj).forEach((field) => {
errors.push({ errors.push({
error: obj[k], field: field,
key: obj[field].key,
id: Math.random() * 1000, id: Math.random() * 1000,
}); });
}); });
} }
} else { } else {
errors.push({ errors.push({
error: obj.charAt(0).toUpperCase() + obj.slice(1), field: "general",
key: obj,
id: 0, id: 0,
}); });
} }
return errors; return errors;
} }

View File

@@ -90,7 +90,9 @@ export const actions = {
console.dir(formData); console.dir(formData);
console.dir(cleanUpdateData); console.dir(cleanUpdateData);
const apiURL = `${BASE_API_URI}/backend/users/update/`; const apiURL = `${BASE_API_URI}/backend/users/update/`;
const res = await fetch(apiURL, {
/** @type {RequestInit} */
const requestUpdateOptions = {
method: "PATCH", method: "PATCH",
credentials: "include", credentials: "include",
headers: { headers: {
@@ -98,11 +100,12 @@ export const actions = {
Cookie: `jwt=${cookies.get("jwt")}`, Cookie: `jwt=${cookies.get("jwt")}`,
}, },
body: JSON.stringify(cleanUpdateData), body: JSON.stringify(cleanUpdateData),
}); };
const res = await fetch(apiURL, requestUpdateOptions);
if (!res.ok) { if (!res.ok) {
const response = await res.json(); const response = await res.json();
const errors = formatError(response.error); const errors = formatError(response.errors);
return fail(400, { errors: errors }); return fail(400, { errors: errors });
} }
@@ -159,7 +162,7 @@ export const actions = {
if (!res.ok) { if (!res.ok) {
const response = await res.json(); const response = await res.json();
const errors = formatError(response.error); const errors = formatError(response.errors);
return fail(400, { errors: errors }); return fail(400, { errors: errors });
} }
@@ -195,7 +198,7 @@ export const actions = {
if (!res.ok) { if (!res.ok) {
const response = await res.json(); const response = await res.json();
const errors = formatError(response.error); const errors = formatError(response.errors);
return fail(400, { errors: errors }); return fail(400, { errors: errors });
} }

View File

@@ -99,7 +99,12 @@
password2 = ""; password2 = "";
const open = () => (showModal = true); const open = () => (showModal = true);
const close = () => (showModal = false); const close = () => {
showModal = false;
if (form) {
form.errors = undefined;
}
};
// const toggleAvatars = () => (showAvatars = !showAvatars); // const toggleAvatars = () => (showAvatars = !showAvatars);
$: selectedSubscriptionModel = $: selectedSubscriptionModel =
@@ -222,7 +227,7 @@
in:receive={{ key: error.id }} in:receive={{ key: error.id }}
out:send={{ key: error.id }} out:send={{ key: error.id }}
> >
{error.error} {$t(error.field) + ": " + $t(error.key)}
</h4> </h4>
{/each} {/each}
{/if} {/if}

View File

@@ -44,18 +44,9 @@ export const actions = {
console.log("Login response headers:", Object.fromEntries(res.headers)); console.log("Login response headers:", Object.fromEntries(res.headers));
if (!res.ok) { if (!res.ok) {
let errorMessage = `HTTP error! status: ${res.status}`;
try {
const errorData = await res.json(); const errorData = await res.json();
errorMessage = errorData.error || errorMessage; const errors = formatError(errorData.errors);
} catch (parseError) { return fail(res.status, { errors });
console.error("Failed to parse error response:", parseError);
errorMessage = await res.text(); // Get the raw response text if JSON parsing fails
}
console.error("Login failed:", errorMessage);
return fail(res.status, {
errors: [{ error: errorMessage, id: Date.now() }],
});
} }
const responseBody = await res.json(); const responseBody = await res.json();
@@ -81,12 +72,4 @@ export const actions = {
console.log("Redirecting to:", next || "/"); console.log("Redirecting to:", next || "/");
throw redirect(303, next || "/"); throw redirect(303, next || "/");
}, },
// if (!res.ok) {
// const response = await res.json();
// const errors = formatError(response.error);
// return fail(400, { errors: errors });
// }
// throw redirect(303, next || "/");
// },
}; };

View File

@@ -35,7 +35,7 @@
in:receive={{ key: error.id }} in:receive={{ key: error.id }}
out:send={{ key: error.id }} out:send={{ key: error.id }}
> >
{error.error} {$t(error.key)}
</h4> </h4>
{/each} {/each}
{/if} {/if}

View File

@@ -26,6 +26,6 @@ func testXSSAttempt(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotAcceptable, w.Code) assert.Equal(t, http.StatusBadRequest, w.Code)
assert.NotContains(t, w.Body.String(), xssPayload) assert.NotContains(t, w.Body.String(), xssPayload)
} }

View File

@@ -22,6 +22,7 @@ import (
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/internal/services" "GoMembership/internal/services"
"GoMembership/internal/utils" "GoMembership/internal/utils"
"GoMembership/internal/validation"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
) )
@@ -102,7 +103,9 @@ func TestSuite(t *testing.T) {
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{} var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
userService := &services.UserService{Repo: userRepo, Licences: licenceRepo} userService := &services.UserService{Repo: userRepo, Licences: licenceRepo}
Uc = &UserController{Service: userService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService} driversLicenceService := &services.DriversLicenceService{Repo: licenceRepo}
Uc = &UserController{Service: userService, DriversLicenceService: driversLicenceService, EmailService: emailService, ConsentService: consentService, BankAccountService: bankAccountService, MembershipService: membershipService}
Mc = &MembershipController{Service: *membershipService} Mc = &MembershipController{Service: *membershipService}
Cc = &ContactController{EmailService: emailService} Cc = &ContactController{EmailService: emailService}
@@ -110,6 +113,10 @@ func TestSuite(t *testing.T) {
log.Fatalf("Failed to init Subscription plans: %#v", err) log.Fatalf("Failed to init Subscription plans: %#v", err)
} }
if err := initLicenceCategories(); err != nil {
log.Fatalf("Failed to init Categories: %v", err)
}
validation.SetupValidators()
t.Run("userController", func(t *testing.T) { t.Run("userController", func(t *testing.T) {
testUserController(t) testUserController(t)
}) })
@@ -139,6 +146,35 @@ func TestSuite(t *testing.T) {
// } // }
} }
func initLicenceCategories() error {
categories := []models.LicenceCategory{
{Category: "AM"},
{Category: "A1"},
{Category: "A2"},
{Category: "A"},
{Category: "B"},
{Category: "C1"},
{Category: "C"},
{Category: "D1"},
{Category: "D"},
{Category: "BE"},
{Category: "C1E"},
{Category: "CE"},
{Category: "D1E"},
{Category: "DE"},
{Category: "T"},
{Category: "L"},
}
for _, category := range categories {
result := database.DB.Create(&category)
if result.Error != nil {
return result.Error
}
}
return nil
}
func initSubscriptionPlans() error { func initSubscriptionPlans() error {
subscriptions := []models.SubscriptionModel{ subscriptions := []models.SubscriptionModel{
{ {

View File

@@ -26,6 +26,7 @@ func (mc *MembershipController) RegisterSubscription(c *gin.Context) {
if err := c.ShouldBindJSON(&regData); err != nil { if err := c.ShouldBindJSON(&regData); err != nil {
logger.Error.Printf("Couln't decode subscription data: %v", err) logger.Error.Printf("Couln't decode subscription data: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode subscription data"}) c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode subscription data"})
return
} }
// Register Subscription // Register Subscription

View File

@@ -86,7 +86,7 @@ func getSubscriptionData() []RegisterSubscriptionTest {
return []RegisterSubscriptionTest{ return []RegisterSubscriptionTest{
{ {
Name: "Missing details should fail", Name: "Missing details should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Just a Subscription"}, WantDBData: map[string]interface{}{"name": "Just a Subscription"},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
@@ -97,7 +97,7 @@ func getSubscriptionData() []RegisterSubscriptionTest {
}, },
{ {
Name: "Missing model name should fail", Name: "Missing model name should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": ""}, WantDBData: map[string]interface{}{"name": ""},
Assert: false, Assert: false,
Input: GenerateInputJSON( Input: GenerateInputJSON(
@@ -108,7 +108,7 @@ func getSubscriptionData() []RegisterSubscriptionTest {
}, },
{ {
Name: "Negative monthly fee should fail", Name: "Negative monthly fee should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData { Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData {
@@ -118,7 +118,7 @@ func getSubscriptionData() []RegisterSubscriptionTest {
}, },
{ {
Name: "Negative hourly rate should fail", Name: "Negative hourly rate should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"name": "Premium"}, WantDBData: map[string]interface{}{"name": "Premium"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData { Input: GenerateInputJSON(customizeSubscription(func(sub MembershipData) MembershipData {

View File

@@ -7,10 +7,12 @@ import (
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/services" "GoMembership/internal/services"
"GoMembership/internal/utils" "GoMembership/internal/utils"
"strings"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"GoMembership/pkg/errors" "GoMembership/pkg/errors"
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
@@ -33,13 +35,34 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
var user models.User var user models.User
if err := c.ShouldBindJSON(&user); err != nil { if err := c.ShouldBindJSON(&user); err != nil {
logger.Error.Printf("Couldn't decode input: %v", err) logger.Error.Printf("Couldn't decode input: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode request data"}) var validationErrors []gin.H
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",
})
}
logger.Error.Printf("ValidationErrors: %#v", validationErrors)
c.JSON(http.StatusBadRequest, gin.H{"errors": validationErrors})
c.Abort()
return return
} }
logger.Error.Print("Continuing...")
tokenString, err := c.Cookie("jwt") tokenString, err := c.Cookie("jwt")
if err != nil { if err != nil {
logger.Error.Printf("No Auth token: %v\n", err) logger.Error.Printf("No Auth token: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "No Auth token"}) c.JSON(http.StatusUnauthorized, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.no_auth_token",
}}})
c.Abort() c.Abort()
return return
} }
@@ -47,7 +70,10 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
if err != nil { if err != nil {
logger.Error.Printf("Error retrieving token and claims from JWT") logger.Error.Printf("Error retrieving token and claims from JWT")
c.JSON(http.StatusInternalServerError, gin.H{"error": "JWT parsing error"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.jwt_parsing_error",
}}})
return return
} }
jwtUserID := uint((*claims)["user_id"].(float64)) jwtUserID := uint((*claims)["user_id"].(float64))
@@ -55,22 +81,27 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
if user.ID == 0 { if user.ID == 0 {
logger.Error.Printf("No User.ID in request from user with id: %v, aborting", jwtUserID) logger.Error.Printf("No User.ID in request from user with id: %v, aborting", jwtUserID)
c.JSON(http.StatusBadRequest, gin.H{"error": "No user id provided"}) c.JSON(http.StatusBadRequest, gin.H{"errors": []gin.H{{
"field": "id",
"key": "server.validation.no_user_id_provided",
}}})
return return
} }
if user.ID != jwtUserID && userRole < constants.Roles.Editor { if user.ID != jwtUserID && userRole < constants.Roles.Editor {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to update this user"}) c.JSON(http.StatusForbidden, gin.H{"errors": []gin.H{{
return "field": "general",
} "key": "server.error.unauthorized_update",
if user.Membership.SubscriptionModel.Name == "" { }}})
logger.Error.Printf("No subscription model provided: %v", user.Email)
c.JSON(http.StatusNotAcceptable, gin.H{"error": "No subscription model provided"})
return return
} }
selectedModel, err := uc.MembershipService.GetModelByName(&user.Membership.SubscriptionModel.Name) selectedModel, err := uc.MembershipService.GetModelByName(&user.Membership.SubscriptionModel.Name)
if err != nil { if err != nil {
logger.Error.Printf("%v:No subscription model found: %#v", user.Email, err) logger.Error.Printf("%v:No subscription model found: %#v", user.Email, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Not a valid subscription model"}) c.JSON(http.StatusNotFound, gin.H{"errors": []gin.H{{
"field": "subscription_model",
"key": "server.validation.invalid_subscription_model",
}}})
return return
} }
user.Membership.SubscriptionModel = *selectedModel user.Membership.SubscriptionModel = *selectedModel
@@ -84,21 +115,29 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
// user.Email = existingUser.Email // user.Email = existingUser.Email
// user.RoleID = existingUser.RoleID // user.RoleID = existingUser.RoleID
// } // }
updatedUser, err := uc.Service.UpdateUser(&user, userRole) updatedUser, err := uc.Service.UpdateUser(&user, userRole)
if err != nil { if err != nil {
switch err { switch err {
case errors.ErrUserNotFound: case errors.ErrUserNotFound:
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) c.JSON(http.StatusNotFound, gin.H{"errors": []gin.H{{
"field": user.FirstName + " " + user.LastName,
"key": "server.validation.user_not_found",
}}})
case errors.ErrInvalidUserData: case errors.ErrInvalidUserData:
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user data"}) c.JSON(http.StatusBadRequest, gin.H{"errors": []gin.H{{
"field": "user",
"key": "server.validation.invalid_user_data",
}}})
default: default:
logger.Error.Printf("Failed to update user: %v", err) logger.Error.Printf("Failed to update user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal Server error"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
} "field": "general",
"key": "server.error.internal_server_error",
}}})
return return
} }
}
c.JSON(http.StatusAccepted, gin.H{"message": "User updated successfully", "user": updatedUser}) c.JSON(http.StatusAccepted, gin.H{"message": "User updated successfully", "user": updatedUser})
} }
@@ -106,37 +145,51 @@ func (uc *UserController) CurrentUserHandler(c *gin.Context) {
userIDInterface, ok := c.Get("user_id") userIDInterface, ok := c.Get("user_id")
if !ok || userIDInterface == nil { if !ok || userIDInterface == nil {
logger.Error.Printf("Error getting user_id from header") logger.Error.Printf("Error getting user_id from header")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Missing or invalid user ID type"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.validation.no_user_id_provided",
}}})
return return
} }
userID, ok := userIDInterface.(uint) userID, ok := userIDInterface.(uint)
if !ok { if !ok {
logger.Error.Printf("Error: user_id is not of type uint") logger.Error.Printf("Error: user_id is not of type uint")
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "user",
"key": "server.error.internal_server_error",
}}})
return return
} }
user, err := uc.Service.GetUserByID(uint(userID)) user, err := uc.Service.GetUserByID(uint(userID))
if err != nil { if err != nil {
logger.Error.Printf("Error retrieving valid user: %v", err) logger.Error.Printf("Error retrieving valid user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving user."}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.internal_server_error",
}}})
return return
} }
subscriptions, err := uc.MembershipService.GetSubscriptions(nil) subscriptions, err := uc.MembershipService.GetSubscriptions(nil)
if err != nil { if err != nil {
logger.Error.Printf("Error retrieving subscriptions: %v", err) logger.Error.Printf("Error retrieving subscriptions: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving subscriptions."}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "validation.internal_server_error",
}}})
return return
} }
licenceCategories, err := uc.DriversLicenceService.GetAllCategories() licenceCategories, err := uc.DriversLicenceService.GetAllCategories()
if err != nil { if err != nil {
logger.Error.Printf("Error retrieving licence categories: %v", err) logger.Error.Printf("Error retrieving licence categories: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Error retrieving licence categories."}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "validation.internal_server_error",
}}})
return return
} }
logger.Error.Printf("licenceCategories: %#v", licenceCategories)
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"user": user.Safe(), "user": user.Safe(),
"subscriptions": subscriptions, "subscriptions": subscriptions,
@@ -164,14 +217,20 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
if err := c.ShouldBindJSON(&input); err != nil { if err := c.ShouldBindJSON(&input); err != nil {
logger.Error.Printf("Couldn't decode input: %v", err.Error()) logger.Error.Printf("Couldn't decode input: %v", err.Error())
c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode request data"}) c.JSON(http.StatusBadRequest, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.invalid_json",
}}})
return return
} }
user, err := uc.Service.GetUserByEmail(input.Email) user, err := uc.Service.GetUserByEmail(input.Email)
if err != nil { if err != nil {
logger.Error.Printf("Error during user(%v) retrieval: %v\n", input.Email, err) logger.Error.Printf("Error during user(%v) retrieval: %v\n", input.Email, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Couldn't find user"}) c.JSON(http.StatusNotFound, gin.H{"errors": []gin.H{{
"field": "login",
"key": "server.validation.user_not_found_or_wrong_password",
}}})
return return
} }
@@ -179,20 +238,29 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
if err != nil { if err != nil {
logger.Error.Printf("Error during Password comparison: %v", err.Error()) logger.Error.Printf("Error during Password comparison: %v", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{"error": "couldn't calculate match"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.internal_server_error",
}}})
return return
} }
if !ok { if !ok {
logger.Error.Printf("Wrong Password: %v %v", user.FirstName, user.LastName) logger.Error.Printf("Wrong Password: %v %v", user.FirstName, user.LastName)
c.JSON(http.StatusNotAcceptable, gin.H{"error": "Wrong Password"}) c.JSON(http.StatusNotAcceptable, gin.H{"errors": []gin.H{{
"field": "login",
"key": "server.validation.user_not_found_or_wrong_password",
}}})
return return
} }
logger.Error.Printf("jwtsevret: %v", config.Auth.JWTSecret) logger.Error.Printf("jwtsevret: %v", config.Auth.JWTSecret)
token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "") token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "")
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate JWT token"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.jwt_generation_failed",
}}})
return return
} }
@@ -209,18 +277,31 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
if err := c.ShouldBindJSON(&regData); err != nil { if err := c.ShouldBindJSON(&regData); err != nil {
logger.Error.Printf("Couldn't decode Userdata: %v", err) logger.Error.Printf("Couldn't decode Userdata: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode userdata"}) var validationErrors []gin.H
return if ve, ok := err.(validator.ValidationErrors); ok {
} for _, e := range ve {
if regData.User.Membership.SubscriptionModel.Name == "" { validationErrors = append(validationErrors, gin.H{
logger.Error.Printf("No subscription model provided: %v", regData.User.Email) "field": e.Field(),
c.JSON(http.StatusNotAcceptable, gin.H{"error": "No subscription model provided"}) "key": "server.validation." + e.Tag(),
})
}
} else {
validationErrors = append(validationErrors, gin.H{
"field": "general",
"key": "server.error.invalid_json",
})
}
c.JSON(http.StatusBadRequest, gin.H{"error": validationErrors})
return return
} }
selectedModel, err := uc.MembershipService.GetModelByName(&regData.User.Membership.SubscriptionModel.Name) selectedModel, err := uc.MembershipService.GetModelByName(&regData.User.Membership.SubscriptionModel.Name)
if err != nil { if err != nil {
logger.Error.Printf("%v:No subscription model found: %#v", regData.User.Email, err) logger.Error.Printf("%v:No subscription model found: %#v", regData.User.Email, err)
c.JSON(http.StatusNotFound, gin.H{"error": "Not a valid subscription model"}) c.JSON(http.StatusNotFound, gin.H{"errors": []gin.H{{
"field": "subscription_model",
"key": "server.validation.invalid_subscription_model",
}}})
return return
} }
regData.User.Membership.SubscriptionModel = *selectedModel regData.User.Membership.SubscriptionModel = *selectedModel
@@ -231,7 +312,18 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
id, token, err := uc.Service.RegisterUser(&regData.User) id, token, err := uc.Service.RegisterUser(&regData.User)
if err != nil { if err != nil {
logger.Error.Printf("Couldn't register User(%v): %v", regData.User.Email, err) logger.Error.Printf("Couldn't register User(%v): %v", regData.User.Email, err)
c.JSON(int(id), gin.H{"error": "Couldn't register User"}) if strings.Contains(err.Error(), "UNIQUE constraint failed: users.email") {
c.JSON(http.StatusConflict, gin.H{"errors": []gin.H{{
"field": "email",
"key": "server.validation.email_already_registered",
}}})
} else {
logger.Error.Printf("Failed to register user: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.internal_server_error",
}}})
}
return return
} }
regData.User.ID = id regData.User.ID = id
@@ -255,7 +347,10 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
_, err = uc.ConsentService.RegisterConsent(&consent) _, err = uc.ConsentService.RegisterConsent(&consent)
if err != nil { if err != nil {
logger.Error.Printf("%v, Couldn't register consent: %v", regData.User.Email, err) logger.Error.Printf("%v, Couldn't register consent: %v", regData.User.Email, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Couldn't register User-consent"}) c.JSON(http.StatusInternalServerError, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.internal_server_error",
}}})
return return
} }
} }

View File

@@ -243,6 +243,7 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
expectedUserMail string expectedUserMail string
expectedStatus int expectedStatus int
expectNewCookie bool expectNewCookie bool
expectedErrors []map[string]string
}{ }{
{ {
name: "With valid cookie", name: "With valid cookie",
@@ -274,6 +275,9 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
name: "Without cookie", name: "Without cookie",
setupCookie: func(req *http.Request) {}, setupCookie: func(req *http.Request) {},
expectedStatus: http.StatusUnauthorized, expectedStatus: http.StatusUnauthorized,
expectedErrors: []map[string]string{
{"field": "general", "key": "server.error.no_auth_token"},
},
}, },
{ {
name: "With invalid cookie", name: "With invalid cookie",
@@ -281,6 +285,9 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
req.AddCookie(&invalidCookie) req.AddCookie(&invalidCookie)
}, },
expectedStatus: http.StatusUnauthorized, expectedStatus: http.StatusUnauthorized,
expectedErrors: []map[string]string{
{"field": "general", "key": "server.error.no_auth_token"},
},
}, },
} }
@@ -327,12 +334,22 @@ func testCurrentUserHandler(t *testing.T, loginEmail string, loginCookie http.Co
assert.Nil(t, newCookie, "No new cookie should be set for non-expired token") assert.Nil(t, newCookie, "No new cookie should be set for non-expired token")
} }
} else { } else {
// For unauthorized requests, check for an error message // For unauthorized requests, check for the new error structure
var errorResponse map[string]string var errorResponse map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &errorResponse) err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
assert.NoError(t, err) assert.NoError(t, err)
assert.Contains(t, errorResponse, "error")
assert.NotEmpty(t, errorResponse["error"]) errors, ok := errorResponse["errors"].([]interface{})
assert.True(t, ok, "Expected 'errors' field in response")
assert.Len(t, errors, len(tt.expectedErrors), "Unexpected number of errors")
for i, expectedError := range tt.expectedErrors {
if i < len(errors) {
actualError := errors[i].(map[string]interface{})
assert.Equal(t, expectedError["field"], actualError["field"], "Mismatched error field")
assert.Equal(t, expectedError["key"], actualError["key"], "Mismatched error key")
}
}
} }
}) })
@@ -414,7 +431,7 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
setupCookie func(*http.Request) setupCookie func(*http.Request)
updateFunc func(*models.User) updateFunc func(*models.User)
expectedStatus int expectedStatus int
expectedError string expectedErrors []map[string]string
}{ }{
{ {
name: "Valid Update", name: "Valid Update",
@@ -441,7 +458,9 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
u.Phone = "01738484994" u.Phone = "01738484994"
}, },
expectedStatus: http.StatusUnauthorized, expectedStatus: http.StatusUnauthorized,
expectedError: "Auth token invalid", expectedErrors: []map[string]string{
{"field": "general", "key": "server.error.no_auth_token"},
},
}, },
{ {
name: "Invalid Email Update", name: "Invalid Email Update",
@@ -456,7 +475,9 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
u.Email = "invalid-email" u.Email = "invalid-email"
}, },
expectedStatus: http.StatusBadRequest, expectedStatus: http.StatusBadRequest,
expectedError: "Invalid user data", expectedErrors: []map[string]string{
{"field": "Email", "key": "server.validation.email"},
},
}, },
{ {
name: "Change LicenceNumber", name: "Change LicenceNumber",
@@ -491,7 +512,7 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
{ {
name: "Add 2 categories", name: "Delete 1 and add 1 category",
setupCookie: func(req *http.Request) { setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie) req.AddCookie(&loginCookie)
}, },
@@ -502,13 +523,31 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
u.Phone = "01738484994" u.Phone = "01738484994"
u.DriversLicence.LicenceNumber = "B072RRE2I50" u.DriversLicence.LicenceNumber = "B072RRE2I50"
var licenceRepo repositories.DriversLicenceInterface = &repositories.DriversLicenceRepository{} var licenceRepo repositories.DriversLicenceInterface = &repositories.DriversLicenceRepository{}
category, err := licenceRepo.FindCategoryByName("B") category, err := licenceRepo.FindCategoryByName("A")
category2, err := licenceRepo.FindCategoryByName("BE") category2, err := licenceRepo.FindCategoryByName("BE")
assert.NoError(t, err) assert.NoError(t, err)
u.DriversLicence.LicenceCategories = []models.LicenceCategory{category, category2} u.DriversLicence.LicenceCategories = []models.LicenceCategory{category, category2}
}, },
expectedStatus: http.StatusAccepted, expectedStatus: http.StatusAccepted,
}, },
{
name: "Delete 1 category",
setupCookie: func(req *http.Request) {
req.AddCookie(&loginCookie)
},
updateFunc: func(u *models.User) {
u.Password = ""
u.FirstName = "John Updated"
u.LastName = "Doe Updated"
u.Phone = "01738484994"
u.DriversLicence.LicenceNumber = "B072RRE2I50"
var licenceRepo repositories.DriversLicenceInterface = &repositories.DriversLicenceRepository{}
category, err := licenceRepo.FindCategoryByName("A")
assert.NoError(t, err)
u.DriversLicence.LicenceCategories = []models.LicenceCategory{category}
},
expectedStatus: http.StatusAccepted,
},
{ {
name: "Delete all categories", name: "Delete all categories",
setupCookie: func(req *http.Request) { setupCookie: func(req *http.Request) {
@@ -538,7 +577,9 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
u.FirstName = "John Missing ID" u.FirstName = "John Missing ID"
}, },
expectedStatus: http.StatusForbidden, expectedStatus: http.StatusForbidden,
expectedError: "You are not authorized to update this user", expectedErrors: []map[string]string{
{"field": "general", "key": "server.error.unauthorized_update"},
},
}, },
{ {
name: "Password Update", name: "Password Update",
@@ -582,7 +623,7 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
t.Fatalf("Failed to marshal user data: %v", err) t.Fatalf("Failed to marshal user data: %v", err)
} }
logger.Error.Printf("Updated User: %#v", updatedUser) // logger.Error.Printf("Updated User: %#v", updatedUser)
// Create request // Create request
req, _ := http.NewRequest("PUT", "/users/"+strconv.FormatUint(uint64(user.ID), 10), bytes.NewBuffer(jsonData)) req, _ := http.NewRequest("PUT", "/users/"+strconv.FormatUint(uint64(user.ID), 10), bytes.NewBuffer(jsonData))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
@@ -599,18 +640,38 @@ func testUpdateUser(t *testing.T, loginEmail string, loginCookie http.Cookie) {
// Perform request // Perform request
router.ServeHTTP(w, req) router.ServeHTTP(w, req)
bodyBytes, _ := io.ReadAll(w.Body)
t.Logf("Response Body: %s", string(bodyBytes))
// Check status code // Check status code
assert.Equal(t, tt.expectedStatus, w.Code) assert.Equal(t, tt.expectedStatus, w.Code)
// Parse response // Parse response
var response map[string]interface{} var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response) err = json.Unmarshal(bodyBytes, &response)
assert.NoError(t, err) if err != nil {
t.Fatalf("Failed to unmarshal response body: %v", err)
}
if tt.expectedError != "" { if tt.expectedErrors != nil {
assert.Equal(t, tt.expectedError, response["error"]) errors, ok := response["errors"].([]interface{})
if !ok {
t.Fatalf("Expected 'errors' field in response, got: %v", response)
}
assert.Len(t, errors, len(tt.expectedErrors), "Unexpected number of errors")
for i, expectedError := range tt.expectedErrors {
if i < len(errors) {
actualError := errors[i].(map[string]interface{})
assert.Equal(t, expectedError["field"], actualError["field"], "Mismatched error field")
assert.Equal(t, expectedError["key"], actualError["key"], "Mismatched error key")
}
}
} else { } else {
assert.Equal(t, "User updated successfully", response["message"]) // Check for success message
message, ok := response["message"].(string)
if !ok {
t.Fatalf("Expected 'message' field in response, got: %v", response)
}
assert.Equal(t, "User updated successfully", message)
// Verify the update in the database // Verify the update in the database
updatedUserFromDB, err := Uc.Service.GetUserByID(user.ID) updatedUserFromDB, err := Uc.Service.GetUserByID(user.ID)
@@ -844,7 +905,7 @@ func getTestUsers() []RegisterUserTest {
return []RegisterUserTest{ return []RegisterUserTest{
{ {
Name: "birthday < 18 should fail", Name: "birthday < 18 should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -854,7 +915,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "FirstName empty, should fail", Name: "FirstName empty, should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -864,7 +925,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "LastName Empty should fail", Name: "LastName Empty should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -874,7 +935,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "EMail wrong format should fail", Name: "EMail wrong format should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "johnexample.com"}, WantDBData: map[string]interface{}{"email": "johnexample.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -884,7 +945,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Missing Zip Code should fail", Name: "Missing Zip Code should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -894,7 +955,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Missing Address should fail", Name: "Missing Address should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -904,7 +965,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Missing City should fail", Name: "Missing City should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -914,7 +975,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Missing IBAN should fail", Name: "Missing IBAN should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -924,7 +985,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Invalid IBAN should fail", Name: "Invalid IBAN should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -934,7 +995,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Missing subscription plan should fail", Name: "Missing subscription plan should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -944,7 +1005,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Invalid subscription plan should fail", Name: "Invalid subscription plan should fail",
WantResponse: http.StatusNotFound, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -982,7 +1043,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Subscription constraints not entered; should fail", Name: "Subscription constraints not entered; should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -993,7 +1054,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "Subscription constraints wrong; should fail", Name: "Subscription constraints wrong; should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.junior.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {
@@ -1028,7 +1089,7 @@ func getTestUsers() []RegisterUserTest {
}, },
{ {
Name: "wrong driverslicence number, should fail", Name: "wrong driverslicence number, should fail",
WantResponse: http.StatusNotAcceptable, WantResponse: http.StatusBadRequest,
WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"}, WantDBData: map[string]interface{}{"email": "john.wronglicence.doe@example.com"},
Assert: false, Assert: false,
Input: GenerateInputJSON(customizeInput(func(user models.User) models.User { Input: GenerateInputJSON(customizeInput(func(user models.User) models.User {

View File

@@ -98,7 +98,7 @@ func createLicenceCategories() []models.LicenceCategory {
{Category: "CE"}, {Category: "CE"},
{Category: "D1E"}, {Category: "D1E"},
{Category: "DE"}, {Category: "DE"},
{Category: "R"}, {Category: "T"},
{Category: "L"}, {Category: "L"},
} }
} }
@@ -142,16 +142,12 @@ func createAdmin(userMail string, subscriptionModelID uint) (*models.User, error
StartDate: time.Now(), StartDate: time.Now(),
SubscriptionModelID: subscriptionModelID, SubscriptionModelID: subscriptionModelID,
}, },
BankAccount: models.BankAccount{ BankAccount: models.BankAccount{},
AccountHolderName: "",
Bank: "",
IBAN: "", //"DE49700500000008447644", //fake
},
DriversLicence: models.DriversLicence{ DriversLicence: models.DriversLicence{
Status: constants.UnverifiedStatus, Status: constants.UnverifiedStatus,
}, },
}, nil }, nil
//"DE49700500000008447644", //fake
} }
func Close() error { func Close() error {

View File

@@ -78,7 +78,11 @@ func AuthMiddleware() gin.HandlerFunc {
tokenString, err := c.Cookie("jwt") tokenString, err := c.Cookie("jwt")
if err != nil { if err != nil {
logger.Error.Printf("No Auth token: %v\n", err) logger.Error.Printf("No Auth token: %v\n", err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "No Auth token"}) c.JSON(http.StatusUnauthorized,
gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.no_auth_token",
}}})
c.Abort() c.Abort()
return return
} }
@@ -91,7 +95,11 @@ func AuthMiddleware() gin.HandlerFunc {
return return
} }
logger.Error.Printf("Token(%v) is invalid: %v\n", tokenString, err) logger.Error.Printf("Token(%v) is invalid: %v\n", tokenString, err)
c.JSON(http.StatusUnauthorized, gin.H{"error": "Auth token invalid"}) c.JSON(http.StatusUnauthorized,
gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.no_auth_token",
}}})
c.Abort() c.Abort()
return return
} }

View File

@@ -6,10 +6,10 @@ type BankAccount struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
MandateDateSigned time.Time `gorm:"not null" json:"mandate_date_signed"` MandateDateSigned time.Time `gorm:"not null" json:"mandate_date_signed"`
Bank string `json:"bank_name" validate:"omitempty,alphanumunicode,safe_content"` Bank string `json:"bank_name" binding:"omitempty,alphanumunicode,safe_content"`
AccountHolderName string `json:"account_holder_name" validate:"omitempty,alphaunicode,safe_content"` AccountHolderName string `json:"account_holder_name" binding:"omitempty,alphaunicode,safe_content"`
IBAN string `gorm:"not null" json:"iban" validate:"iban"` IBAN string `gorm:"not null" json:"iban" binding:"iban"`
BIC string `json:"bic" validate:"omitempty,bic"` BIC string `json:"bic" binding:"omitempty,bic"`
MandateReference string `gorm:"not null" json:"mandate_reference"` MandateReference string `gorm:"not null" json:"mandate_reference"`
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
} }

View File

@@ -7,10 +7,10 @@ import (
type Consent struct { type Consent struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
FirstName string `gorm:"not null" json:"first_name" validate:"safe_content"` FirstName string `gorm:"not null" json:"first_name" binding:"safe_content"`
LastName string `gorm:"not null" json:"last_name" validate:"safe_content"` LastName string `gorm:"not null" json:"last_name" binding:"safe_content"`
Email string `json:"email" validate:"email,safe_content"` Email string `json:"email" binding:"email,safe_content"`
ConsentType string `gorm:"not null" json:"consent_type" validate:"safe_content"` ConsentType string `gorm:"not null" json:"consent_type" binding:"safe_content"`
ID uint `gorm:"primaryKey"` ID uint `gorm:"primaryKey"`
User User User User
UserID uint UserID uint

View File

@@ -6,15 +6,15 @@ import (
type DriversLicence struct { type DriversLicence struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
Status int8 `json:"status" validate:"omitempty,number"` Status int8 `json:"status" binding:"omitempty,number"`
LicenceNumber string `json:"number" validate:"omitempty,euDriversLicence,safe_content"` LicenceNumber string `json:"number" binding:"omitempty,euDriversLicence,safe_content"`
IssuedDate time.Time `json:"issued_date" validate:"omitempty,lte"` IssuedDate time.Time `json:"issued_date" binding:"omitempty,lte"`
ExpirationDate time.Time `json:"expiration_date" validate:"omitempty,gt"` ExpirationDate time.Time `json:"expiration_date" binding:"omitempty,gt"`
IssuingCountry string `json:"country" validate:"safe_content"` IssuingCountry string `json:"country" binding:"safe_content"`
LicenceCategories []LicenceCategory `json:"licence_categories" gorm:"many2many:licence_2_categories"` LicenceCategories []LicenceCategory `json:"licence_categories" gorm:"many2many:licence_2_categories"`
} }
type LicenceCategory struct { type LicenceCategory struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
Category string `json:"category" validate:"safe_content"` Category string `json:"category" binding:"safe_content"`
} }

View File

@@ -7,9 +7,9 @@ type Membership struct {
UpdatedAt time.Time UpdatedAt time.Time
StartDate time.Time `json:"start_date"` StartDate time.Time `json:"start_date"`
EndDate time.Time `json:"end_date"` EndDate time.Time `json:"end_date"`
Status int8 `json:"status" validate:"safe_content"` Status int8 `json:"status" binding:"number,safe_content"`
SubscriptionModel SubscriptionModel `gorm:"foreignKey:SubscriptionModelID" json:"subscription_model"` SubscriptionModel SubscriptionModel `gorm:"foreignKey:SubscriptionModelID" json:"subscription_model"`
SubscriptionModelID uint `json:"subsription_model_id"` SubscriptionModelID uint `json:"subsription_model_id"`
ParentMembershipID uint `json:"parent_member_id" validate:"omitempty,omitnil,number"` ParentMembershipID uint `json:"parent_member_id" binding:"omitempty,omitnil,number"`
ID uint `json:"id"` ID uint `json:"id"`
} }

View File

@@ -7,13 +7,13 @@ import (
type SubscriptionModel struct { type SubscriptionModel struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
Name string `gorm:"unique" json:"name" validate:"required,subscriptionModel,safe_content"` Name string `gorm:"unique" json:"name" binding:"required"`
Details string `json:"details" validate:"required"` Details string `json:"details"`
Conditions string `json:"conditions"` Conditions string `json:"conditions"`
RequiredMembershipField string `json:"required_membership_field" validate:"membershipField"` RequiredMembershipField string `json:"required_membership_field"`
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
MonthlyFee float32 `json:"monthly_fee" validate:"number,gte=0"` MonthlyFee float32 `json:"monthly_fee"`
HourlyRate float32 `json:"hourly_rate" validate:"number,gte=0"` HourlyRate float32 `json:"hourly_rate"`
IncludedPerYear int16 `json:"included_hours_per_year" validate:"omitempty,number,gte=0"` IncludedPerYear int16 `json:"included_hours_per_year"`
IncludedPerMonth int16 `json:"included_hours_per_month" validate:"omitempty,number,gte=0"` IncludedPerMonth int16 `json:"included_hours_per_month"`
} }

View File

@@ -10,18 +10,18 @@ import (
type User struct { type User struct {
gorm.Model gorm.Model
DateOfBirth time.Time `gorm:"not null" json:"date_of_birth" validate:"required,age"` DateOfBirth time.Time `gorm:"not null" json:"date_of_birth" binding:"required,safe_content"`
Company string `json:"company" validate:"omitempty,omitnil,safe_content"` Company string `json:"company" binding:"omitempty,omitnil,safe_content"`
Phone string `json:"phone" validate:"omitempty,omitnil,safe_content"` Phone string `json:"phone" binding:"omitempty,omitnil,safe_content"`
Notes string `json:"notes" validate:"safe_content"` Notes string `json:"notes" binding:"safe_content"`
FirstName string `gorm:"not null" json:"first_name" validate:"required,safe_content"` FirstName string `gorm:"not null" json:"first_name" binding:"required,safe_content"`
Password string `json:"password" validate:"required_unless=RoleID 0,safe_content"` Password string `json:"password" binding:"required_unless=RoleID 0,safe_content"`
Email string `gorm:"unique;not null" json:"email" validate:"required,email,safe_content"` Email string `gorm:"unique;not null" json:"email" binding:"required,email,safe_content"`
LastName string `gorm:"not null" json:"last_name" validate:"required,safe_content"` LastName string `gorm:"not null" json:"last_name" binding:"required,safe_content"`
ProfilePicture string `json:"profile_picture" validate:"omitempty,omitnil,image,safe_content"` ProfilePicture string `json:"profile_picture" binding:"omitempty,omitnil,image,safe_content"`
Address string `gorm:"not null" json:"address" validate:"required,safe_content"` Address string `gorm:"not null" json:"address" binding:"required,safe_content"`
ZipCode string `gorm:"not null" json:"zip_code" validate:"required,alphanum,safe_content"` ZipCode string `gorm:"not null" json:"zip_code" binding:"required,alphanum,safe_content"`
City string `form:"not null" json:"city" validate:"required,alphaunicode,safe_content"` City string `form:"not null" json:"city" binding:"required,alphaunicode,safe_content"`
Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"` Consents []Consent `gorm:"constraint:OnUpdate:CASCADE"`
BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"` BankAccount BankAccount `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"bank_account"`
BankAccountID uint BankAccountID uint

View File

@@ -11,7 +11,6 @@ import (
type SubscriptionModelsRepositoryInterface interface { type SubscriptionModelsRepositoryInterface interface {
CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (uint, error) CreateSubscriptionModel(subscriptionModel *models.SubscriptionModel) (uint, error)
GetMembershipModelNames() ([]string, error) GetMembershipModelNames() ([]string, error)
GetModelByName(modelname *string) (*models.SubscriptionModel, error)
GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error)
} }
@@ -26,7 +25,7 @@ func (sr *SubscriptionModelsRepository) CreateSubscriptionModel(subscriptionMode
return subscriptionModel.ID, nil return subscriptionModel.ID, nil
} }
func (sr *SubscriptionModelsRepository) GetModelByName(modelname *string) (*models.SubscriptionModel, error) { func GetModelByName(modelname *string) (*models.SubscriptionModel, error) {
var model models.SubscriptionModel var model models.SubscriptionModel
if err := database.DB.Where("name = ?", modelname).First(&model).Error; err != nil { if err := database.DB.Where("name = ?", modelname).First(&model).Error; err != nil {
return nil, err return nil, err

View File

@@ -10,13 +10,13 @@ import (
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/pkg/errors" "GoMembership/pkg/errors"
"GoMembership/pkg/logger"
) )
type UserRepositoryInterface interface { type UserRepositoryInterface interface {
CreateUser(user *models.User) (uint, error) CreateUser(user *models.User) (uint, error)
UpdateUser(user *models.User) (*models.User, error) UpdateUser(user *models.User) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error) GetUsers(where map[string]interface{}) (*[]models.User, error)
GetUserByID(userID *uint) (*models.User, error)
GetUserByEmail(email string) (*models.User, error) GetUserByEmail(email string) (*models.User, error)
SetVerificationToken(verification *models.Verification) (uint, error) SetVerificationToken(verification *models.Verification) (uint, error)
IsVerified(userID *uint) (bool, error) IsVerified(userID *uint) (bool, error)
@@ -28,6 +28,7 @@ type UserRepository struct{}
func (ur *UserRepository) CreateUser(user *models.User) (uint, error) { func (ur *UserRepository) CreateUser(user *models.User) (uint, error) {
result := database.DB.Create(user) result := database.DB.Create(user)
if result.Error != nil { if result.Error != nil {
logger.Error.Printf("Create User error: %#v", result.Error)
return 0, result.Error return 0, result.Error
} }
return user.ID, nil return user.ID, nil
@@ -39,10 +40,14 @@ func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
} }
err := database.DB.Transaction(func(tx *gorm.DB) error { err := database.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.First(&models.User{}, user.ID).Error; err != nil { // Check if the user exists in the database
var existingUser models.User
if err := tx.Preload("DriversLicence.LicenceCategories").
Preload("Membership").
First(&existingUser, user.ID).Error; err != nil {
return err return err
} }
// Update the user's main fields
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(user) result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(user)
if result.Error != nil { if result.Error != nil {
return result.Error return result.Error
@@ -50,6 +55,29 @@ func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
if result.RowsAffected == 0 { if result.RowsAffected == 0 {
return errors.ErrNoRowsAffected return errors.ErrNoRowsAffected
} }
// Handle the update of the LicenceCategories explicitly
if user.DriversLicence.ID != 0 {
// Replace the LicenceCategories with the new list
if err := tx.Model(&existingUser.DriversLicence).Association("LicenceCategories").Replace(user.DriversLicence.LicenceCategories); err != nil {
return err
}
}
// Update the Membership if provided
if user.Membership.ID != 0 {
if err := tx.Model(&existingUser.Membership).Updates(user.Membership).Error; err != nil {
return err
}
}
// Update the DriversLicence fields if provided
if user.DriversLicence.ID != 0 {
if err := tx.Model(&existingUser.DriversLicence).Updates(user.DriversLicence).Error; err != nil {
return err
}
}
return nil return nil
}) })
@@ -58,10 +86,11 @@ func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
} }
var updatedUser models.User var updatedUser models.User
if err := database.DB.First(&updatedUser, user.ID).Error; err != nil { if err := database.DB.Preload("DriversLicence.LicenceCategories").
Preload("Membership").
First(&updatedUser, user.ID).Error; err != nil {
return nil, err return nil, err
} }
return &updatedUser, nil return &updatedUser, nil
} }
@@ -81,7 +110,7 @@ func (ur *UserRepository) GetUsers(where map[string]interface{}) (*[]models.User
return &users, nil return &users, nil
} }
func (ur *UserRepository) GetUserByID(userID *uint) (*models.User, error) { func GetUserByID(userID *uint) (*models.User, error) {
var user models.User var user models.User
result := database.DB. result := database.DB.
Preload(clause.Associations). Preload(clause.Associations).

View File

@@ -13,6 +13,7 @@ import (
"GoMembership/internal/controllers" "GoMembership/internal/controllers"
"GoMembership/internal/middlewares" "GoMembership/internal/middlewares"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/internal/validation"
"GoMembership/internal/routes" "GoMembership/internal/routes"
"GoMembership/internal/services" "GoMembership/internal/services"
@@ -63,6 +64,7 @@ func Run() {
router.Use(middlewares.RateLimitMiddleware(limiter)) router.Use(middlewares.RateLimitMiddleware(limiter))
routes.RegisterRoutes(router, userController, membershipController, contactController) routes.RegisterRoutes(router, userController, membershipController, contactController)
validation.SetupValidators()
logger.Info.Println("Starting server on :8080") logger.Info.Println("Starting server on :8080")
srv = &http.Server{ srv = &http.Server{

View File

@@ -1,15 +1,10 @@
package services package services
import ( import (
"slices"
"time" "time"
"github.com/go-playground/validator/v10"
"GoMembership/internal/models" "GoMembership/internal/models"
"GoMembership/internal/repositories" "GoMembership/internal/repositories"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
) )
type MembershipServiceInterface interface { type MembershipServiceInterface interface {
@@ -37,9 +32,6 @@ func (service *MembershipService) FindMembershipByUserID(userID uint) (*models.M
// Membership_Subscriptions // Membership_Subscriptions
func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) { func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) {
if err := validateSubscriptionData(subscription); err != nil {
return 0, err
}
return service.SubscriptionRepo.CreateSubscriptionModel(subscription) return service.SubscriptionRepo.CreateSubscriptionModel(subscription)
} }
@@ -48,15 +40,7 @@ func (service *MembershipService) GetMembershipModelNames() ([]string, error) {
} }
func (service *MembershipService) GetModelByName(modelname *string) (*models.SubscriptionModel, error) { func (service *MembershipService) GetModelByName(modelname *string) (*models.SubscriptionModel, error) {
sModelNames, err := service.SubscriptionRepo.GetMembershipModelNames() return repositories.GetModelByName(modelname)
if err != nil {
return nil, err
}
if !slices.Contains(sModelNames, *modelname) {
return nil, errors.ErrNotFound
}
return service.SubscriptionRepo.GetModelByName(modelname)
} }
func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) { func (service *MembershipService) GetSubscriptions(where map[string]interface{}) (*[]models.SubscriptionModel, error) {
@@ -65,12 +49,3 @@ func (service *MembershipService) GetSubscriptions(where map[string]interface{})
} }
return service.SubscriptionRepo.GetSubscriptions(where) return service.SubscriptionRepo.GetSubscriptions(where)
} }
func validateSubscriptionData(subscription *models.SubscriptionModel) error {
validate := validator.New()
// subscriptionModel and membershipField don't have to be evaluated if adding a new subscription
validate.RegisterValidation("subscriptionModel", func(fl validator.FieldLevel) bool { return true })
validate.RegisterValidation("membershipField", func(fl validator.FieldLevel) bool { return true })
validate.RegisterValidation("safe_content", utils.ValidateSafeContent)
return validate.Struct(subscription)
}

View File

@@ -12,7 +12,6 @@ import (
"GoMembership/pkg/logger" "GoMembership/pkg/logger"
"github.com/alexedwards/argon2id" "github.com/alexedwards/argon2id"
"github.com/go-playground/validator/v10"
"gorm.io/gorm" "gorm.io/gorm"
"time" "time"
@@ -34,12 +33,6 @@ type UserService struct {
func (service *UserService) UpdateUser(user *models.User, userRole int8) (*models.User, error) { func (service *UserService) UpdateUser(user *models.User, userRole int8) (*models.User, error) {
if err := validateUserData(user, userRole); err != nil {
logger.Info.Printf("UPDATING user: %#v", user)
logger.Error.Printf("Failed to validate user data: %v", err)
return nil, errors.ErrInvalidUserData
}
if user.Password != "" { if user.Password != "" {
setPassword(user.Password, user) setPassword(user.Password, user)
} }
@@ -66,9 +59,6 @@ func (service *UserService) UpdateUser(user *models.User, userRole int8) (*model
} }
func (service *UserService) RegisterUser(user *models.User) (uint, string, error) { func (service *UserService) RegisterUser(user *models.User) (uint, string, error) {
if err := validateUserData(user, user.RoleID); err != nil {
return http.StatusNotAcceptable, "", err
}
setPassword(user.Password, user) setPassword(user.Password, user)
@@ -76,21 +66,19 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
user.CreatedAt = time.Now() user.CreatedAt = time.Now()
user.UpdatedAt = time.Now() user.UpdatedAt = time.Now()
user.PaymentStatus = constants.AwaitingPaymentStatus user.PaymentStatus = constants.AwaitingPaymentStatus
// user.DriversLicence.Status = constants.UnverifiedStatus user.DriversLicence.Status = constants.UnverifiedStatus
user.BankAccount.MandateDateSigned = time.Now() user.BankAccount.MandateDateSigned = time.Now()
id, err := service.Repo.CreateUser(user) id, err := service.Repo.CreateUser(user)
if err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") { if err != nil {
return http.StatusConflict, "", err return 0, "", err
} else if err != nil {
return http.StatusInternalServerError, "", err
} }
user.ID = id user.ID = id
token, err := utils.GenerateVerificationToken() token, err := utils.GenerateVerificationToken()
if err != nil { if err != nil {
return http.StatusInternalServerError, "", err return 0, "", err
} }
logger.Info.Printf("TOKEN: %v", token) logger.Info.Printf("TOKEN: %v", token)
@@ -98,10 +86,10 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
// Check if user is already verified // Check if user is already verified
verified, err := service.Repo.IsVerified(&user.ID) verified, err := service.Repo.IsVerified(&user.ID)
if err != nil { if err != nil {
return http.StatusInternalServerError, "", err return 0, "", err
} }
if verified { if verified {
return http.StatusAlreadyReported, "", errors.ErrAlreadyVerified return 0, "", errors.ErrAlreadyVerified
} }
// Prepare the Verification record // Prepare the Verification record
@@ -119,7 +107,7 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
func (service *UserService) GetUserByID(id uint) (*models.User, error) { func (service *UserService) GetUserByID(id uint) (*models.User, error) {
return service.Repo.GetUserByID(&id) return repositories.GetUserByID(&id)
} }
func (service *UserService) GetUserByEmail(email string) (*models.User, error) { func (service *UserService) GetUserByEmail(email string) (*models.User, error) {
@@ -140,7 +128,7 @@ func (service *UserService) VerifyUser(token *string) (*models.User, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
user, err := service.Repo.GetUserByID(&verification.UserID) user, err := repositories.GetUserByID(&verification.UserID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -159,27 +147,6 @@ func (service *UserService) VerifyUser(token *string) (*models.User, error) {
return user, nil return user, nil
} }
func validateUserData(user *models.User, userRole int8) error {
validate := validator.New()
validate.RegisterValidation("safe_content", utils.ValidateSafeContent)
if userRole == constants.Roles.Admin {
validate.RegisterValidation("membershipField", utils.ValidateToTrue)
validate.RegisterValidation("age", utils.ValidateToTrue)
validate.RegisterValidation("bic", utils.ValidateToTrue)
validate.RegisterValidation("subscriptionModel", utils.ValidateToTrue)
validate.RegisterValidation("iban", utils.ValidateToTrue)
validate.RegisterValidation("euDriversLicence", utils.ValidateToTrue)
} else {
validate.RegisterValidation("membershipField", utils.ValidateRequiredMembershipField)
validate.RegisterValidation("age", utils.AgeValidator)
validate.RegisterValidation("bic", utils.BICValidator)
validate.RegisterValidation("subscriptionModel", utils.SubscriptionModelValidator)
validate.RegisterValidation("iban", utils.IBANValidator)
validate.RegisterValidation("euDriversLicence", utils.ValidateDriversLicence)
}
return validate.Struct(user)
}
func setPassword(plaintextPassword string, u *models.User) error { func setPassword(plaintextPassword string, u *models.User) error {
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams) hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
if err != nil { if err != nil {

View File

@@ -1,157 +0,0 @@
package utils
// import "regexp"
import (
"GoMembership/internal/database"
"GoMembership/internal/models"
"GoMembership/pkg/logger"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"time"
"github.com/go-playground/validator/v10"
"github.com/jbub/banking/iban"
"github.com/jbub/banking/swift"
)
var xssPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)<script`),
regexp.MustCompile(`(?i)javascript:`),
regexp.MustCompile(`(?i)on\w+\s*=`),
regexp.MustCompile(`(?i)(vbscript|data):`),
regexp.MustCompile(`(?i)<(iframe|object|embed|applet)`),
regexp.MustCompile(`(?i)expression\s*\(`),
regexp.MustCompile(`(?i)url\s*\(`),
regexp.MustCompile(`(?i)<\?`),
regexp.MustCompile(`(?i)<%`),
regexp.MustCompile(`(?i)<!\[CDATA\[`),
regexp.MustCompile(`(?i)<(svg|animate)`),
regexp.MustCompile(`(?i)<(audio|video|source)`),
regexp.MustCompile(`(?i)base64`),
}
func ValidateToTrue(fl validator.FieldLevel) bool {
return true
}
func AgeValidator(fl validator.FieldLevel) bool {
fieldValue := fl.Field()
dateOfBirth := fieldValue.Interface().(time.Time)
now := time.Now()
age := now.Year() - dateOfBirth.Year()
if now.YearDay() < dateOfBirth.YearDay() {
age-- // if birthday is in the future..
}
return age >= 18
}
func SubscriptionModelValidator(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
var names []string
if err := database.DB.Model(&models.SubscriptionModel{}).Pluck("name", &names).Error; err != nil {
logger.Error.Fatalf("Couldn't get SubscriptionModel names: %#v", err)
return false
}
return slices.Contains(names, fieldValue)
}
func IBANValidator(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
return iban.Validate(fieldValue) == nil
}
func ValidateRequiredMembershipField(fl validator.FieldLevel) bool {
user := fl.Top().Interface().(*models.User)
membership := user.Membership
subModel := membership.SubscriptionModel
// Get the field name specified in RequiredMembershipField
fieldName := subModel.RequiredMembershipField
if fieldName == "" {
return true
}
// Get the value of the field specified by RequiredMembershipField
fieldValue := reflect.ValueOf(membership).FieldByName(fieldName)
// Check if the fieldValue is valid
if !fieldValue.IsValid() {
return false
}
// Check if the fieldValue is a nil pointer
if fieldValue.Kind() == reflect.Ptr && fieldValue.IsNil() {
return false
}
// Ensure that the fieldValue is an uint
var fieldUint uint
if fieldValue.Kind() == reflect.Uint {
fieldUint = uint(fieldValue.Uint())
} else {
return false
}
var membershipIDs []uint
if err := database.DB.Model(&models.Membership{}).Pluck("id", &membershipIDs).Error; err != nil {
logger.Error.Fatalf("Couldn't get SubscriptionModel names: %#v", err)
return false
}
// Check if the field value is zero (empty)
return slices.Contains(membershipIDs, fieldUint)
}
func BICValidator(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
return swift.Validate(fieldValue) == nil
}
func ValidateSafeContent(fl validator.FieldLevel) bool {
input := strings.ToLower(fl.Field().String())
for _, pattern := range xssPatterns {
if pattern.MatchString(input) {
return false
}
}
return true
}
func ValidateDriversLicence(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
if len(fieldValue) != 11 {
return false
}
id, tenthChar := string(fieldValue[:9]), string(fieldValue[9])
if tenthChar == "X" {
tenthChar = "10"
}
tenthValue, _ := strconv.ParseInt(tenthChar, 10, 8)
// for readability
weights := []int{9, 8, 7, 6, 5, 4, 3, 2, 1}
sum := 0
for i := 0; i < 9; i++ {
char := string(id[i])
value, _ := strconv.ParseInt(char, 36, 64)
sum += int(value) * weights[i]
}
calcCheckDigit := sum % 11
if calcCheckDigit != int(tenthValue) {
return false
}
return true
}

View File

@@ -0,0 +1,38 @@
package validation
import (
"strconv"
"github.com/go-playground/validator/v10"
)
func ValidateDriversLicence(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
if len(fieldValue) != 11 {
return false
}
id, tenthChar := string(fieldValue[:9]), string(fieldValue[9])
if tenthChar == "X" {
tenthChar = "10"
}
tenthValue, _ := strconv.ParseInt(tenthChar, 10, 8)
// for readability
weights := []int{9, 8, 7, 6, 5, 4, 3, 2, 1}
sum := 0
for i := 0; i < 9; i++ {
char := string(id[i])
value, _ := strconv.ParseInt(char, 36, 64)
sum += int(value) * weights[i]
}
calcCheckDigit := sum % 11
if calcCheckDigit != int(tenthValue) {
return false
}
return true
}

View File

@@ -0,0 +1,19 @@
package validation
import (
"github.com/go-playground/validator/v10"
"github.com/jbub/banking/iban"
"github.com/jbub/banking/swift"
)
func IBANValidator(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
return iban.Validate(fieldValue) == nil
}
func BICValidator(fl validator.FieldLevel) bool {
fieldValue := fl.Field().String()
return swift.Validate(fieldValue) == nil
}

View File

@@ -0,0 +1,34 @@
package validation
import (
"regexp"
"strings"
"github.com/go-playground/validator/v10"
)
var xssPatterns = []*regexp.Regexp{
regexp.MustCompile(`(?i)<script`),
regexp.MustCompile(`(?i)javascript:`),
regexp.MustCompile(`(?i)on\w+\s*=`),
regexp.MustCompile(`(?i)(vbscript|data):`),
regexp.MustCompile(`(?i)<(iframe|object|embed|applet)`),
regexp.MustCompile(`(?i)expression\s*\(`),
regexp.MustCompile(`(?i)url\s*\(`),
regexp.MustCompile(`(?i)<\?`),
regexp.MustCompile(`(?i)<%`),
regexp.MustCompile(`(?i)<!\[CDATA\[`),
regexp.MustCompile(`(?i)<(svg|animate)`),
regexp.MustCompile(`(?i)<(audio|video|source)`),
regexp.MustCompile(`(?i)base64`),
}
func ValidateSafeContent(fl validator.FieldLevel) bool {
input := strings.ToLower(fl.Field().String())
for _, pattern := range xssPatterns {
if pattern.MatchString(input) {
return false
}
}
return true
}

View File

@@ -0,0 +1,30 @@
package validation
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"github.com/go-playground/validator/v10"
)
func validateMembership(sl validator.StructLevel, membership models.Membership) {
if membership.SubscriptionModel.RequiredMembershipField != "" {
switch membership.SubscriptionModel.RequiredMembershipField {
case "ParentMembershipID":
if membership.ParentMembershipID == 0 {
sl.ReportError(membership.ParentMembershipID, membership.SubscriptionModel.RequiredMembershipField,
"RequiredMembershipField", "required", "")
} else {
_, err := repositories.GetUserByID(&membership.ParentMembershipID)
if err != nil {
sl.ReportError(membership.ParentMembershipID, membership.SubscriptionModel.RequiredMembershipField,
"RequiredMembershipField", "user_id_not_found", "")
}
}
default:
sl.ReportError(membership.ParentMembershipID, membership.SubscriptionModel.RequiredMembershipField,
"RequiredMembershipField", "not_implemented", "")
}
}
}

View File

@@ -0,0 +1,23 @@
package validation
import (
"GoMembership/internal/models"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
func SetupValidators() {
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// Register custom validators
v.RegisterValidation("safe_content", ValidateSafeContent)
v.RegisterValidation("iban", IBANValidator)
v.RegisterValidation("bic", BICValidator)
v.RegisterValidation("euDriversLicence", ValidateDriversLicence)
// Register struct-level validations
v.RegisterStructValidation(validateUser, models.User{})
v.RegisterStructValidation(ValidateSubscription, models.SubscriptionModel{})
}
}

View File

@@ -0,0 +1,46 @@
package validation
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"github.com/go-playground/validator/v10"
)
// ValidateNewSubscription validates a new subscription model being created
func ValidateSubscription(sl validator.StructLevel) {
subscription := sl.Current().Interface().(models.SubscriptionModel)
if subscription.Name == "" {
sl.ReportError(subscription.Name, "Name", "name", "required", "")
}
if sl.Parent().Type().Name() == "MembershipData" {
// This is subscription only operation
if subscription.Details == "" {
sl.ReportError(subscription.Details, "Details", "details", "required", "")
}
if subscription.MonthlyFee < 0 {
sl.ReportError(subscription.MonthlyFee, "MonthlyFee", "monthly_fee", "gte", "0")
}
if subscription.HourlyRate < 0 {
sl.ReportError(subscription.HourlyRate, "HourlyRate", "hourly_rate", "gte", "0")
}
if subscription.IncludedPerYear < 0 {
sl.ReportError(subscription.IncludedPerYear, "IncludedPerYear", "included_hours_per_year", "gte", "0")
}
if subscription.IncludedPerMonth < 0 {
sl.ReportError(subscription.IncludedPerMonth, "IncludedPerMonth", "included_hours_per_month", "gte", "0")
}
} else {
// This is a nested probably user struct. We are only checking if the model exists
existingSubscription, err := repositories.GetModelByName(&subscription.Name)
if err != nil || existingSubscription == nil {
sl.ReportError(subscription.Name, "Name", "name", "exists", "")
}
}
}

View File

@@ -0,0 +1,51 @@
package validation
import (
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/pkg/logger"
"time"
"github.com/go-playground/validator/v10"
)
func validateUser(sl validator.StructLevel) {
user := sl.Current().Interface().(models.User)
if user.DateOfBirth.After(time.Now().AddDate(-18, 0, 0)) {
sl.ReportError(user.DateOfBirth, "DateOfBirth", "date_of_birth", "age", "")
}
if user.Membership.SubscriptionModel.Name == "" {
sl.ReportError(user.Membership.SubscriptionModel.Name, "SubscriptionModel.Name", "name", "required", "")
} else {
selectedModel, err := repositories.GetModelByName(&user.Membership.SubscriptionModel.Name)
if err != nil {
logger.Error.Printf("Error finding subscription model for user %v: %v", user.Email, err)
sl.ReportError(user.Membership.SubscriptionModel.Name, "SubscriptionModel.Name", "name", "invalid", "")
} else {
user.Membership.SubscriptionModel = *selectedModel
}
}
validateMembership(sl, user.Membership)
}
// func RequiredIfNotAdmin(fl validator.FieldLevel) bool {
// // Traverse up the struct hierarchy to find the IsAdmin field
// current := fl.Parent()
// // Check multiple levels of nesting to find userRole
// for current.IsValid() {
// if isRoleIDField := current.FieldByName("RoleID"); isRoleIDField.IsValid() {
// // If IsAdmin is found and is true, skip validation
// if isRoleIDField.Interface().(int8) == constants.Roles.Admin{
// return true
// }
// break
// }
// current = current.Parent() // Move to the next parent level
// }
// If not an admin, enforce that the field must have a non-zero value
// return !fl.Field().IsZero()
// }