feat: Refactor User entity and datasource to use email and password hashing with salt

This commit is contained in:
Jean Jacques Avril 2025-03-10 08:05:48 +00:00
parent f567d086ec
commit 3193204dac
9 changed files with 110 additions and 56 deletions

View File

@ -0,0 +1,31 @@
package entities
import "errors"
var ErrUserAlreadyExists = errors.New("user already exists")
var ErrUserNotFound = errors.New("user not found")
var ErrActivityNotFound = errors.New("activity not found")
var ErrActivityAlreadyExists = errors.New("activity already exists")
var ErrInvalidPassword = errors.New("invalid password")
var ErrInvalidEmail = errors.New("invalid email")
var ErrInvalidUsername = errors.New("invalid username")
var ErrInvalidRole = errors.New("invalid role")
var ErrInvalidCompanyID = errors.New("invalid company id")
var ErrInvalidHourlyRate = errors.New("invalid hourly rate")
var ErrInvalidID = errors.New("invalid id")
var ErrTimeEntryNotFound = errors.New("time entry not found")
var ErrTimeEntryAlreadyExists = errors.New("time entry already exists")
var ErrInvalidDuration = errors.New("invalid duration")
var ErrInvalidDescription = errors.New("invalid description")
var ErrInvalidStartTime = errors.New("invalid start time")
var ErrInvalidEndTime = errors.New("invalid end time")
var ErrInvalidBillable = errors.New("invalid billable")
var ErrInvalidProjectID = errors.New("invalid project id")
var ErrProjectNotFound = errors.New("project not found")
var ErrProjectAlreadyExists = errors.New("project already exists")
var ErrInvalidName = errors.New("invalid name")
var ErrInvalidClientID = errors.New("invalid client id")
var ErrClientNotFound = errors.New("client not found")
var ErrClientAlreadyExists = errors.New("client already exists")
var ErrInvalidAddress = errors.New("invalid address")
var ErrInvalidPhone = errors.New("invalid phone")

View File

@ -4,15 +4,15 @@ import "github.com/oklog/ulid/v2"
type User struct { type User struct {
EntityBase EntityBase
Username string Email string
Password string Salt string
Role string Role string
CompanyID int CompanyID int
HourlyRate float64 HourlyRate float64
} }
type UserCreate struct { type UserCreate struct {
Username string Email string
Password string Password string
Role string Role string
CompanyID int CompanyID int
@ -21,7 +21,7 @@ type UserCreate struct {
type UserUpdate struct { type UserUpdate struct {
ID ulid.ULID ID ulid.ULID
Username *string Email *string
Password *string Password *string
Role *string Role *string
CompanyID *int CompanyID *int

View File

@ -9,8 +9,8 @@ import (
type UserDatasource interface { type UserDatasource interface {
Get(ctx context.Context, id ulid.ULID) (*entities.User, error) Get(ctx context.Context, id ulid.ULID) (*entities.User, error)
Create(ctx context.Context, user *entities.User) error Create(ctx context.Context, user *entities.User, passwordHash string, salt string) error
Update(ctx context.Context, user *entities.User) error Update(ctx context.Context, user *entities.User, passwordHash *string) error
Delete(ctx context.Context, id ulid.ULID) error Delete(ctx context.Context, id ulid.ULID) error
GetByUsername(ctx context.Context, username string) (*entities.User, error) GetByEmail(ctx context.Context, email string) (*entities.User, error)
} }

View File

@ -13,8 +13,9 @@ type UserDBO struct {
CreatedAt time.Time `gorm:"not null"` CreatedAt time.Time `gorm:"not null"`
UpdatedAt time.Time `gorm:"not null"` UpdatedAt time.Time `gorm:"not null"`
LastEditorID ulid.ULID LastEditorID ulid.ULID
Username string Email string
Password string PasswordHash string
Salt string
Role string Role string
CompanyID int CompanyID int
HourlyRate float64 HourlyRate float64

View File

@ -61,20 +61,23 @@ func (r *TimeEntryDatasource) Create(ctx context.Context, timeEntry *entities.Ti
} }
func (r *TimeEntryDatasource) Update(ctx context.Context, timeEntry *entities.TimeEntry) error { func (r *TimeEntryDatasource) Update(ctx context.Context, timeEntry *entities.TimeEntry) error {
timeEntryDBO := dbo.TimeEntryDBO{ var existingEntry dbo.TimeEntryDBO
ID: timeEntry.ID, if err := r.db.WithContext(ctx).First(&existingEntry, "id = ?", timeEntry.ID).Error; err != nil {
CreatedAt: timeEntry.CreatedAt, return entities.ErrTimeEntryNotFound
UpdatedAt: timeEntry.UpdatedAt,
UserID: timeEntry.UserID,
ProjectID: timeEntry.ProjectID,
ActivityID: timeEntry.ActivityID,
Start: timeEntry.Start,
End: timeEntry.End,
Description: timeEntry.Description,
Billable: timeEntry.Billable,
} }
return r.db.WithContext(ctx).Save(&timeEntryDBO).Error updateData := map[string]any{
"user_id": timeEntry.UserID,
"project_id": timeEntry.ProjectID,
"activity_id": timeEntry.ActivityID,
"start": timeEntry.Start,
"end": timeEntry.End,
"description": timeEntry.Description,
"billable": timeEntry.Billable,
"updated_at": gorm.Expr("NOW()"), // Optional: Automatisches Update-Datum
}
return r.db.WithContext(ctx).Model(&dbo.TimeEntryDBO{}).Where("id = ?", timeEntry.ID).Updates(updateData).Error
} }
func (r *TimeEntryDatasource) Delete(ctx context.Context, id ulid.ULID) error { func (r *TimeEntryDatasource) Delete(ctx context.Context, id ulid.ULID) error {

View File

@ -30,8 +30,8 @@ func (r *UserDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.User,
CreatedAt: userDBO.CreatedAt, CreatedAt: userDBO.CreatedAt,
UpdatedAt: userDBO.UpdatedAt, UpdatedAt: userDBO.UpdatedAt,
}, },
Username: userDBO.Username, Email: userDBO.Email,
Password: userDBO.Password, Salt: userDBO.Salt,
Role: userDBO.Role, Role: userDBO.Role,
CompanyID: userDBO.CompanyID, CompanyID: userDBO.CompanyID,
HourlyRate: userDBO.HourlyRate, HourlyRate: userDBO.HourlyRate,
@ -40,43 +40,57 @@ func (r *UserDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.User,
return user, nil return user, nil
} }
func (r *UserDatasource) Create(ctx context.Context, user *entities.User) error { func (r *UserDatasource) Create(ctx context.Context, user *entities.User, passwordHash string, salt string) error {
old := r.db.WithContext(ctx).First(&dbo.UserDBO{}, "email = ?", user.Email)
if old.Error == nil {
return entities.ErrUserAlreadyExists
}
userDBO := dbo.UserDBO{ userDBO := dbo.UserDBO{
ID: user.ID, ID: user.ID,
CreatedAt: user.CreatedAt, CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt, UpdatedAt: user.UpdatedAt,
Username: user.Username, Email: user.Email,
Password: user.Password, PasswordHash: passwordHash,
Role: user.Role, Salt: salt,
CompanyID: user.CompanyID, Role: user.Role,
HourlyRate: user.HourlyRate, CompanyID: user.CompanyID,
HourlyRate: user.HourlyRate,
} }
return r.db.WithContext(ctx).Create(&userDBO).Error return r.db.WithContext(ctx).Create(&userDBO).Error
} }
func (r *UserDatasource) Update(ctx context.Context, user *entities.User) error { func (r *UserDatasource) Update(ctx context.Context, user *entities.User, passwordHash *string) error {
userDBO := dbo.UserDBO{ var existingUser dbo.UserDBO
ID: user.ID, if err := r.db.WithContext(ctx).First(&existingUser, "id = ?", user.ID).Error; err != nil {
CreatedAt: user.CreatedAt, return entities.ErrUserNotFound
UpdatedAt: user.UpdatedAt,
Username: user.Username,
Password: user.Password,
Role: user.Role,
CompanyID: user.CompanyID,
HourlyRate: user.HourlyRate,
} }
return r.db.WithContext(ctx).Save(&userDBO).Error // Nur relevante Felder aktualisieren
updateData := map[string]interface{}{
"email": user.Email,
"role": user.Role,
"company_id": user.CompanyID,
"hourly_rate": user.HourlyRate,
"updated_at": gorm.Expr("NOW()"), // Optional: Automatisch das Update-Datum setzen
}
if passwordHash != nil {
updateData["password_hash"] = *passwordHash
}
return r.db.WithContext(ctx).Model(&dbo.UserDBO{}).Where("id = ?", user.ID).Updates(updateData).Error
} }
func (r *UserDatasource) Delete(ctx context.Context, id ulid.ULID) error { func (r *UserDatasource) Delete(ctx context.Context, id ulid.ULID) error {
return r.db.WithContext(ctx).Delete(&dbo.UserDBO{}, "id = ?", id).Error return r.db.WithContext(ctx).Delete(&dbo.UserDBO{}, "id = ?", id).Error
} }
func (r *UserDatasource) GetByUsername(ctx context.Context, username string) (*entities.User, error) { func (r *UserDatasource) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
var userDBO dbo.UserDBO var userDBO dbo.UserDBO
if err := r.db.WithContext(ctx).Where("username = ?", username).First(&userDBO).Error; err != nil { if err := r.db.WithContext(ctx).Where("email = ?", email).First(&userDBO).Error; err != nil {
return nil, err return nil, err
} }
@ -86,8 +100,8 @@ func (r *UserDatasource) GetByUsername(ctx context.Context, username string) (*e
CreatedAt: userDBO.CreatedAt, CreatedAt: userDBO.CreatedAt,
UpdatedAt: userDBO.UpdatedAt, UpdatedAt: userDBO.UpdatedAt,
}, },
Username: userDBO.Username, Email: userDBO.Email,
Password: userDBO.Password, Salt: userDBO.Salt,
Role: userDBO.Role, Role: userDBO.Role,
CompanyID: userDBO.CompanyID, CompanyID: userDBO.CompanyID,
HourlyRate: userDBO.HourlyRate, HourlyRate: userDBO.HourlyRate,

View File

@ -0,0 +1,6 @@
package dto
type AuthDto struct {
Email string `json:"email"`
Password string `json:"password"`
}

View File

@ -11,16 +11,15 @@ type UserDto struct {
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
LastEditorID ulid.ULID `json:"lastEditorID"` LastEditorID ulid.ULID `json:"lastEditorID"`
Username string `json:"username"` Email string `json:"email"`
Password string `json:"password"` // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
Role string `json:"role"` Role string `json:"role"`
CompanyID int `json:"companyId"` CompanyID int `json:"companyId"`
HourlyRate float64 `json:"hourlyRate"` HourlyRate float64 `json:"hourlyRate"`
} }
type UserCreateDto struct { type UserCreateDto struct {
Username string `json:"username"` Email string `json:"email"`
Password string `json:"password"` // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. Password string `json:"password"`
Role string `json:"role"` Role string `json:"role"`
CompanyID int `json:"companyId"` CompanyID int `json:"companyId"`
HourlyRate float64 `json:"hourlyRate"` HourlyRate float64 `json:"hourlyRate"`
@ -31,8 +30,8 @@ type UserUpdateDto struct {
CreatedAt *time.Time `json:"createdAt"` CreatedAt *time.Time `json:"createdAt"`
UpdatedAt *time.Time `json:"updatedAt"` UpdatedAt *time.Time `json:"updatedAt"`
LastEditorID *ulid.ULID `json:"lastEditorID"` LastEditorID *ulid.ULID `json:"lastEditorID"`
Username *string `json:"username"` Email *string `json:"email"`
Password *string `json:"password"` // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. Password *string `json:"password"`
Role *string `json:"role"` Role *string `json:"role"`
CompanyID *int `json:"companyId"` CompanyID *int `json:"companyId"`
HourlyRate *float64 `json:"hourlyRate"` HourlyRate *float64 `json:"hourlyRate"`

View File

@ -140,14 +140,14 @@ export interface UserDto {
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
lastEditorID: string; lastEditorID: string;
username: string; email: string;
password: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. password: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
role: string; role: string;
companyId: number /* int */; companyId: number /* int */;
hourlyRate: number /* float64 */; hourlyRate: number /* float64 */;
} }
export interface UserCreateDto { export interface UserCreateDto {
username: string; email: string;
password: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. password: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
role: string; role: string;
companyId: number /* int */; companyId: number /* int */;
@ -158,7 +158,7 @@ export interface UserUpdateDto {
createdAt?: string; createdAt?: string;
updatedAt?: string; updatedAt?: string;
lastEditorID?: string; lastEditorID?: string;
username?: string; email?: string;
password?: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. password?: string; // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration.
role?: string; role?: string;
companyId?: number /* int */; companyId?: number /* int */;