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

View File

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

View File

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

View File

@@ -44,18 +44,9 @@ export const actions = {
console.log("Login response headers:", Object.fromEntries(res.headers));
if (!res.ok) {
let errorMessage = `HTTP error! status: ${res.status}`;
try {
const errorData = await res.json();
errorMessage = errorData.error || errorMessage;
} catch (parseError) {
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 errorData = await res.json();
const errors = formatError(errorData.errors);
return fail(res.status, { errors });
}
const responseBody = await res.json();
@@ -81,12 +72,4 @@ export const actions = {
console.log("Redirecting to:", 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 }}
out:send={{ key: error.id }}
>
{error.error}
{$t(error.key)}
</h4>
{/each}
{/if}

View File

@@ -26,6 +26,6 @@ func testXSSAttempt(t *testing.T) {
w := httptest.NewRecorder()
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)
}

View File

@@ -22,6 +22,7 @@ import (
"GoMembership/internal/repositories"
"GoMembership/internal/services"
"GoMembership/internal/utils"
"GoMembership/internal/validation"
"GoMembership/pkg/logger"
)
@@ -102,7 +103,9 @@ func TestSuite(t *testing.T) {
var userRepo repositories.UserRepositoryInterface = &repositories.UserRepository{}
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}
Cc = &ContactController{EmailService: emailService}
@@ -110,6 +113,10 @@ func TestSuite(t *testing.T) {
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) {
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 {
subscriptions := []models.SubscriptionModel{
{

View File

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

View File

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

View File

@@ -7,10 +7,12 @@ import (
"GoMembership/internal/models"
"GoMembership/internal/services"
"GoMembership/internal/utils"
"strings"
"net/http"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
@@ -33,13 +35,34 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
var user models.User
if err := c.ShouldBindJSON(&user); err != nil {
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
}
logger.Error.Print("Continuing...")
tokenString, err := c.Cookie("jwt")
if err != nil {
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()
return
}
@@ -47,7 +70,10 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
if err != nil {
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
}
jwtUserID := uint((*claims)["user_id"].(float64))
@@ -55,22 +81,27 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
if user.ID == 0 {
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
}
if user.ID != jwtUserID && userRole < constants.Roles.Editor {
c.JSON(http.StatusForbidden, gin.H{"error": "You are not authorized to update this user"})
return
}
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"})
c.JSON(http.StatusForbidden, gin.H{"errors": []gin.H{{
"field": "general",
"key": "server.error.unauthorized_update",
}}})
return
}
selectedModel, err := uc.MembershipService.GetModelByName(&user.Membership.SubscriptionModel.Name)
if err != nil {
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
}
user.Membership.SubscriptionModel = *selectedModel
@@ -84,20 +115,28 @@ func (uc *UserController) UpdateHandler(c *gin.Context) {
// user.Email = existingUser.Email
// user.RoleID = existingUser.RoleID
// }
updatedUser, err := uc.Service.UpdateUser(&user, userRole)
if err != nil {
switch err {
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:
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:
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})
}
@@ -106,37 +145,51 @@ func (uc *UserController) CurrentUserHandler(c *gin.Context) {
userIDInterface, ok := c.Get("user_id")
if !ok || userIDInterface == nil {
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
}
userID, ok := userIDInterface.(uint)
if !ok {
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
}
user, err := uc.Service.GetUserByID(uint(userID))
if err != nil {
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
}
subscriptions, err := uc.MembershipService.GetSubscriptions(nil)
if err != nil {
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
}
licenceCategories, err := uc.DriversLicenceService.GetAllCategories()
if err != nil {
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
}
logger.Error.Printf("licenceCategories: %#v", licenceCategories)
c.JSON(http.StatusOK, gin.H{
"user": user.Safe(),
"subscriptions": subscriptions,
@@ -164,14 +217,20 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
if err := c.ShouldBindJSON(&input); err != nil {
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
}
user, err := uc.Service.GetUserByEmail(input.Email)
if err != nil {
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
}
@@ -179,20 +238,29 @@ func (uc *UserController) LoginHandler(c *gin.Context) {
if err != nil {
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
}
if !ok {
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
}
logger.Error.Printf("jwtsevret: %v", config.Auth.JWTSecret)
token, err := middlewares.GenerateToken(config.Auth.JWTSecret, user, "")
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
}
@@ -209,18 +277,31 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
if err := c.ShouldBindJSON(&regData); err != nil {
logger.Error.Printf("Couldn't decode Userdata: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "Couldn't decode userdata"})
return
}
if regData.User.Membership.SubscriptionModel.Name == "" {
logger.Error.Printf("No subscription model provided: %v", regData.User.Email)
c.JSON(http.StatusNotAcceptable, gin.H{"error": "No subscription model provided"})
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",
})
}
c.JSON(http.StatusBadRequest, gin.H{"error": validationErrors})
return
}
selectedModel, err := uc.MembershipService.GetModelByName(&regData.User.Membership.SubscriptionModel.Name)
if err != nil {
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
}
regData.User.Membership.SubscriptionModel = *selectedModel
@@ -231,7 +312,18 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
id, token, err := uc.Service.RegisterUser(&regData.User)
if err != nil {
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
}
regData.User.ID = id
@@ -255,7 +347,10 @@ func (uc *UserController) RegisterUser(c *gin.Context) {
_, err = uc.ConsentService.RegisterConsent(&consent)
if err != nil {
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
}
}

View File

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

View File

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

View File

@@ -78,7 +78,11 @@ func AuthMiddleware() gin.HandlerFunc {
tokenString, err := c.Cookie("jwt")
if err != nil {
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()
return
}
@@ -91,7 +95,11 @@ func AuthMiddleware() gin.HandlerFunc {
return
}
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()
return
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,13 +10,13 @@ import (
"GoMembership/internal/models"
"GoMembership/pkg/errors"
"GoMembership/pkg/logger"
)
type UserRepositoryInterface interface {
CreateUser(user *models.User) (uint, error)
UpdateUser(user *models.User) (*models.User, error)
GetUsers(where map[string]interface{}) (*[]models.User, error)
GetUserByID(userID *uint) (*models.User, error)
GetUserByEmail(email string) (*models.User, error)
SetVerificationToken(verification *models.Verification) (uint, error)
IsVerified(userID *uint) (bool, error)
@@ -28,6 +28,7 @@ type UserRepository struct{}
func (ur *UserRepository) CreateUser(user *models.User) (uint, error) {
result := database.DB.Create(user)
if result.Error != nil {
logger.Error.Printf("Create User error: %#v", result.Error)
return 0, result.Error
}
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 {
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
}
// Update the user's main fields
result := tx.Session(&gorm.Session{FullSaveAssociations: true}).Updates(user)
if result.Error != nil {
return result.Error
@@ -50,6 +55,29 @@ func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
if result.RowsAffected == 0 {
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
})
@@ -58,10 +86,11 @@ func (ur *UserRepository) UpdateUser(user *models.User) (*models.User, error) {
}
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 &updatedUser, nil
}
@@ -81,7 +110,7 @@ func (ur *UserRepository) GetUsers(where map[string]interface{}) (*[]models.User
return &users, nil
}
func (ur *UserRepository) GetUserByID(userID *uint) (*models.User, error) {
func GetUserByID(userID *uint) (*models.User, error) {
var user models.User
result := database.DB.
Preload(clause.Associations).

View File

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

View File

@@ -1,15 +1,10 @@
package services
import (
"slices"
"time"
"github.com/go-playground/validator/v10"
"GoMembership/internal/models"
"GoMembership/internal/repositories"
"GoMembership/internal/utils"
"GoMembership/pkg/errors"
)
type MembershipServiceInterface interface {
@@ -37,9 +32,6 @@ func (service *MembershipService) FindMembershipByUserID(userID uint) (*models.M
// Membership_Subscriptions
func (service *MembershipService) RegisterSubscription(subscription *models.SubscriptionModel) (uint, error) {
if err := validateSubscriptionData(subscription); err != nil {
return 0, err
}
return service.SubscriptionRepo.CreateSubscriptionModel(subscription)
}
@@ -48,15 +40,7 @@ func (service *MembershipService) GetMembershipModelNames() ([]string, error) {
}
func (service *MembershipService) GetModelByName(modelname *string) (*models.SubscriptionModel, error) {
sModelNames, err := service.SubscriptionRepo.GetMembershipModelNames()
if err != nil {
return nil, err
}
if !slices.Contains(sModelNames, *modelname) {
return nil, errors.ErrNotFound
}
return service.SubscriptionRepo.GetModelByName(modelname)
return repositories.GetModelByName(modelname)
}
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)
}
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"
"github.com/alexedwards/argon2id"
"github.com/go-playground/validator/v10"
"gorm.io/gorm"
"time"
@@ -34,12 +33,6 @@ type UserService struct {
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 != "" {
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) {
if err := validateUserData(user, user.RoleID); err != nil {
return http.StatusNotAcceptable, "", err
}
setPassword(user.Password, user)
@@ -76,21 +66,19 @@ func (service *UserService) RegisterUser(user *models.User) (uint, string, error
user.CreatedAt = time.Now()
user.UpdatedAt = time.Now()
user.PaymentStatus = constants.AwaitingPaymentStatus
// user.DriversLicence.Status = constants.UnverifiedStatus
user.DriversLicence.Status = constants.UnverifiedStatus
user.BankAccount.MandateDateSigned = time.Now()
id, err := service.Repo.CreateUser(user)
if err != nil && strings.Contains(err.Error(), "UNIQUE constraint failed") {
return http.StatusConflict, "", err
} else if err != nil {
return http.StatusInternalServerError, "", err
if err != nil {
return 0, "", err
}
user.ID = id
token, err := utils.GenerateVerificationToken()
if err != nil {
return http.StatusInternalServerError, "", err
return 0, "", err
}
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
verified, err := service.Repo.IsVerified(&user.ID)
if err != nil {
return http.StatusInternalServerError, "", err
return 0, "", err
}
if verified {
return http.StatusAlreadyReported, "", errors.ErrAlreadyVerified
return 0, "", errors.ErrAlreadyVerified
}
// 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) {
return service.Repo.GetUserByID(&id)
return repositories.GetUserByID(&id)
}
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 {
return nil, err
}
user, err := service.Repo.GetUserByID(&verification.UserID)
user, err := repositories.GetUserByID(&verification.UserID)
if err != nil {
return nil, err
}
@@ -159,27 +147,6 @@ func (service *UserService) VerifyUser(token *string) (*models.User, error) {
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 {
hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
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()
// }