diff --git a/backend/internal/domain/entities/errors.go b/backend/internal/domain/entities/errors.go new file mode 100644 index 0000000..eec17e8 --- /dev/null +++ b/backend/internal/domain/entities/errors.go @@ -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") diff --git a/backend/internal/domain/entities/user.go b/backend/internal/domain/entities/user.go index 08a0d13..155e5b4 100644 --- a/backend/internal/domain/entities/user.go +++ b/backend/internal/domain/entities/user.go @@ -4,15 +4,15 @@ import "github.com/oklog/ulid/v2" type User struct { EntityBase - Username string - Password string + Email string + Salt string Role string CompanyID int HourlyRate float64 } type UserCreate struct { - Username string + Email string Password string Role string CompanyID int @@ -21,7 +21,7 @@ type UserCreate struct { type UserUpdate struct { ID ulid.ULID - Username *string + Email *string Password *string Role *string CompanyID *int diff --git a/backend/internal/domain/persistence/user_datasource.go b/backend/internal/domain/persistence/user_datasource.go index fea5cc8..58fd5dc 100644 --- a/backend/internal/domain/persistence/user_datasource.go +++ b/backend/internal/domain/persistence/user_datasource.go @@ -9,8 +9,8 @@ import ( type UserDatasource interface { Get(ctx context.Context, id ulid.ULID) (*entities.User, error) - Create(ctx context.Context, user *entities.User) error - Update(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, passwordHash *string) 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) } diff --git a/backend/internal/infrastructure/persistence/db/dbo/user_dbo.go b/backend/internal/infrastructure/persistence/db/dbo/user_dbo.go index b44879d..cbabc48 100644 --- a/backend/internal/infrastructure/persistence/db/dbo/user_dbo.go +++ b/backend/internal/infrastructure/persistence/db/dbo/user_dbo.go @@ -13,8 +13,9 @@ type UserDBO struct { CreatedAt time.Time `gorm:"not null"` UpdatedAt time.Time `gorm:"not null"` LastEditorID ulid.ULID - Username string - Password string + Email string + PasswordHash string + Salt string Role string CompanyID int HourlyRate float64 diff --git a/backend/internal/infrastructure/persistence/db/ds/timeentry_datasource.go b/backend/internal/infrastructure/persistence/db/ds/timeentry_datasource.go index 7268ef9..5223c91 100644 --- a/backend/internal/infrastructure/persistence/db/ds/timeentry_datasource.go +++ b/backend/internal/infrastructure/persistence/db/ds/timeentry_datasource.go @@ -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 { - timeEntryDBO := dbo.TimeEntryDBO{ - ID: timeEntry.ID, - CreatedAt: timeEntry.CreatedAt, - UpdatedAt: timeEntry.UpdatedAt, - UserID: timeEntry.UserID, - ProjectID: timeEntry.ProjectID, - ActivityID: timeEntry.ActivityID, - Start: timeEntry.Start, - End: timeEntry.End, - Description: timeEntry.Description, - Billable: timeEntry.Billable, + var existingEntry dbo.TimeEntryDBO + if err := r.db.WithContext(ctx).First(&existingEntry, "id = ?", timeEntry.ID).Error; err != nil { + return entities.ErrTimeEntryNotFound } - 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 { diff --git a/backend/internal/infrastructure/persistence/db/ds/user_datasource.go b/backend/internal/infrastructure/persistence/db/ds/user_datasource.go index 096f60c..1b4f859 100644 --- a/backend/internal/infrastructure/persistence/db/ds/user_datasource.go +++ b/backend/internal/infrastructure/persistence/db/ds/user_datasource.go @@ -30,8 +30,8 @@ func (r *UserDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.User, CreatedAt: userDBO.CreatedAt, UpdatedAt: userDBO.UpdatedAt, }, - Username: userDBO.Username, - Password: userDBO.Password, + Email: userDBO.Email, + Salt: userDBO.Salt, Role: userDBO.Role, CompanyID: userDBO.CompanyID, HourlyRate: userDBO.HourlyRate, @@ -40,43 +40,57 @@ func (r *UserDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.User, 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{ - ID: user.ID, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - Username: user.Username, - Password: user.Password, - Role: user.Role, - CompanyID: user.CompanyID, - HourlyRate: user.HourlyRate, + ID: user.ID, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + Email: user.Email, + PasswordHash: passwordHash, + Salt: salt, + Role: user.Role, + CompanyID: user.CompanyID, + HourlyRate: user.HourlyRate, } return r.db.WithContext(ctx).Create(&userDBO).Error } -func (r *UserDatasource) Update(ctx context.Context, user *entities.User) error { - userDBO := dbo.UserDBO{ - ID: user.ID, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - Username: user.Username, - Password: user.Password, - Role: user.Role, - CompanyID: user.CompanyID, - HourlyRate: user.HourlyRate, +func (r *UserDatasource) Update(ctx context.Context, user *entities.User, passwordHash *string) error { + var existingUser dbo.UserDBO + if err := r.db.WithContext(ctx).First(&existingUser, "id = ?", user.ID).Error; err != nil { + return entities.ErrUserNotFound } - 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 { 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 - 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 } @@ -86,8 +100,8 @@ func (r *UserDatasource) GetByUsername(ctx context.Context, username string) (*e CreatedAt: userDBO.CreatedAt, UpdatedAt: userDBO.UpdatedAt, }, - Username: userDBO.Username, - Password: userDBO.Password, + Email: userDBO.Email, + Salt: userDBO.Salt, Role: userDBO.Role, CompanyID: userDBO.CompanyID, HourlyRate: userDBO.HourlyRate, diff --git a/backend/internal/interfaces/http/dto/auth_dto.go b/backend/internal/interfaces/http/dto/auth_dto.go new file mode 100644 index 0000000..795e7b9 --- /dev/null +++ b/backend/internal/interfaces/http/dto/auth_dto.go @@ -0,0 +1,6 @@ +package dto + +type AuthDto struct { + Email string `json:"email"` + Password string `json:"password"` +} diff --git a/backend/internal/interfaces/http/dto/user_dto.go b/backend/internal/interfaces/http/dto/user_dto.go index 12b36c2..aeba5cc 100644 --- a/backend/internal/interfaces/http/dto/user_dto.go +++ b/backend/internal/interfaces/http/dto/user_dto.go @@ -11,16 +11,15 @@ type UserDto struct { CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` LastEditorID ulid.ULID `json:"lastEditorID"` - Username string `json:"username"` - Password string `json:"password"` // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. + Email string `json:"email"` Role string `json:"role"` CompanyID int `json:"companyId"` HourlyRate float64 `json:"hourlyRate"` } type UserCreateDto struct { - Username string `json:"username"` - Password string `json:"password"` // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. + Email string `json:"email"` + Password string `json:"password"` Role string `json:"role"` CompanyID int `json:"companyId"` HourlyRate float64 `json:"hourlyRate"` @@ -31,8 +30,8 @@ type UserUpdateDto struct { CreatedAt *time.Time `json:"createdAt"` UpdatedAt *time.Time `json:"updatedAt"` LastEditorID *ulid.ULID `json:"lastEditorID"` - Username *string `json:"username"` - Password *string `json:"password"` // Note: In a real application, you would NEVER send the password in a DTO. This is just for demonstration. + Email *string `json:"email"` + Password *string `json:"password"` Role *string `json:"role"` CompanyID *int `json:"companyId"` HourlyRate *float64 `json:"hourlyRate"` diff --git a/frontend/src/types/dto.ts b/frontend/src/types/dto.ts index 1140fd4..dca372f 100644 --- a/frontend/src/types/dto.ts +++ b/frontend/src/types/dto.ts @@ -140,14 +140,14 @@ export interface UserDto { createdAt: string; updatedAt: 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. role: string; companyId: number /* int */; hourlyRate: number /* float64 */; } 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. role: string; companyId: number /* int */; @@ -158,7 +158,7 @@ export interface UserUpdateDto { createdAt?: string; updatedAt?: 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. role?: string; companyId?: number /* int */;