diff --git a/backend/internal/models/activity.go b/backend/internal/models/activity.go index f72a968..395091a 100644 --- a/backend/internal/models/activity.go +++ b/backend/internal/models/activity.go @@ -8,32 +8,32 @@ import ( "gorm.io/gorm" ) -// Activity repräsentiert eine Aktivität im System +// Activity represents an activity in the system type Activity struct { EntityBase Name string `gorm:"column:name"` BillingRate float64 `gorm:"column:billing_rate"` } -// TableName gibt den Tabellennamen für GORM an +// TableName specifies the table name for GORM func (Activity) TableName() string { return "activities" } -// ActivityUpdate enthält die aktualisierbaren Felder einer Activity +// ActivityUpdate contains the updatable fields of an Activity type ActivityUpdate struct { - ID ulid.ULID `gorm:"-"` // Verwenden Sie "-" um anzuzeigen, dass dieses Feld ignoriert werden soll + ID ulid.ULID `gorm:"-"` // Use "-" to indicate that this field should be ignored Name *string `gorm:"column:name"` BillingRate *float64 `gorm:"column:billing_rate"` } -// ActivityCreate enthält die Felder zum Erstellen einer neuen Activity +// ActivityCreate contains the fields for creating a new Activity type ActivityCreate struct { Name string `gorm:"column:name"` BillingRate float64 `gorm:"column:billing_rate"` } -// GetActivityByID sucht eine Activity anhand ihrer ID +// GetActivityByID finds an Activity by its ID func GetActivityByID(ctx context.Context, id ulid.ULID) (*Activity, error) { var activity Activity result := GetEngine(ctx).Where("id = ?", id).First(&activity) @@ -46,7 +46,7 @@ func GetActivityByID(ctx context.Context, id ulid.ULID) (*Activity, error) { return &activity, nil } -// GetAllActivities gibt alle Activities zurück +// GetAllActivities returns all Activities func GetAllActivities(ctx context.Context) ([]Activity, error) { var activities []Activity result := GetEngine(ctx).Find(&activities) @@ -56,7 +56,7 @@ func GetAllActivities(ctx context.Context) ([]Activity, error) { return activities, nil } -// CreateActivity erstellt eine neue Activity +// CreateActivity creates a new Activity func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, error) { activity := Activity{ Name: create.Name, @@ -70,26 +70,26 @@ func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, erro return &activity, nil } -// UpdateActivity aktualisiert eine bestehende Activity +// UpdateActivity updates an existing Activity func UpdateActivity(ctx context.Context, update ActivityUpdate) (*Activity, error) { activity, err := GetActivityByID(ctx, update.ID) if err != nil { return nil, err } if activity == nil { - return nil, errors.New("activity nicht gefunden") + return nil, errors.New("activity not found") } - // Generische Update-Funktion verwenden + // Use generic update function if err := UpdateModel(ctx, activity, update); err != nil { return nil, err } - // Aktualisierte Daten aus der Datenbank laden + // Load updated data from the database return GetActivityByID(ctx, update.ID) } -// DeleteActivity löscht eine Activity anhand ihrer ID +// DeleteActivity deletes an Activity by its ID func DeleteActivity(ctx context.Context, id ulid.ULID) error { result := GetEngine(ctx).Delete(&Activity{}, id) return result.Error diff --git a/backend/internal/models/base.go b/backend/internal/models/base.go index 4c46168..722f133 100644 --- a/backend/internal/models/base.go +++ b/backend/internal/models/base.go @@ -15,10 +15,10 @@ type EntityBase struct { DeletedAt gorm.DeletedAt `gorm:"index"` } -// BeforeCreate wird von GORM vor dem Erstellen eines Datensatzes aufgerufen +// BeforeCreate is called by GORM before creating a record func (eb *EntityBase) BeforeCreate(tx *gorm.DB) error { - if eb.ID.Compare(ulid.ULID{}) == 0 { // Wenn ID leer ist - // Generiere eine neue ULID + if eb.ID.Compare(ulid.ULID{}) == 0 { // If ID is empty + // Generate a new ULID entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) eb.ID = ulid.MustNew(ulid.Timestamp(time.Now()), entropy) } diff --git a/backend/internal/models/company.go b/backend/internal/models/company.go index f40d3f1..0756cd4 100644 --- a/backend/internal/models/company.go +++ b/backend/internal/models/company.go @@ -8,29 +8,29 @@ import ( "gorm.io/gorm" ) -// Company repräsentiert ein Unternehmen im System +// Company represents a company in the system type Company struct { EntityBase Name string `gorm:"column:name"` } -// TableName gibt den Tabellennamen für GORM an +// TableName specifies the table name for GORM func (Company) TableName() string { return "companies" } -// CompanyCreate enthält die Felder zum Erstellen eines neuen Unternehmens +// CompanyCreate contains the fields for creating a new company type CompanyCreate struct { Name string } -// CompanyUpdate enthält die aktualisierbaren Felder eines Unternehmens +// CompanyUpdate contains the updatable fields of a company type CompanyUpdate struct { - ID ulid.ULID `gorm:"-"` // Ausschließen von Updates + ID ulid.ULID `gorm:"-"` // Exclude from updates Name *string `gorm:"column:name"` } -// GetCompanyByID sucht ein Unternehmen anhand seiner ID +// GetCompanyByID finds a company by its ID func GetCompanyByID(ctx context.Context, id ulid.ULID) (*Company, error) { var company Company result := GetEngine(ctx).Where("id = ?", id).First(&company) @@ -43,7 +43,7 @@ func GetCompanyByID(ctx context.Context, id ulid.ULID) (*Company, error) { return &company, nil } -// GetAllCompanies gibt alle Unternehmen zurück +// GetAllCompanies returns all companies func GetAllCompanies(ctx context.Context) ([]Company, error) { var companies []Company result := GetEngine(ctx).Find(&companies) @@ -62,7 +62,7 @@ func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, er return customers, nil } -// CreateCompany erstellt ein neues Unternehmen +// CreateCompany creates a new company func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) { company := Company{ Name: create.Name, @@ -75,26 +75,26 @@ func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) return &company, nil } -// UpdateCompany aktualisiert ein bestehendes Unternehmen +// UpdateCompany updates an existing company func UpdateCompany(ctx context.Context, update CompanyUpdate) (*Company, error) { company, err := GetCompanyByID(ctx, update.ID) if err != nil { return nil, err } if company == nil { - return nil, errors.New("company nicht gefunden") + return nil, errors.New("company not found") } - // Generische Update-Funktion verwenden + // Use generic update function if err := UpdateModel(ctx, company, update); err != nil { return nil, err } - // Aktualisierte Daten aus der Datenbank laden + // Load updated data from the database return GetCompanyByID(ctx, update.ID) } -// DeleteCompany löscht ein Unternehmen anhand seiner ID +// DeleteCompany deletes a company by its ID func DeleteCompany(ctx context.Context, id ulid.ULID) error { result := GetEngine(ctx).Delete(&Company{}, id) return result.Error diff --git a/backend/internal/models/customer.go b/backend/internal/models/customer.go index 3e20175..c86e26a 100644 --- a/backend/internal/models/customer.go +++ b/backend/internal/models/customer.go @@ -8,32 +8,32 @@ import ( "gorm.io/gorm" ) -// Customer repräsentiert einen Kunden im System +// Customer represents a customer in the system type Customer struct { EntityBase Name string `gorm:"column:name"` CompanyID int `gorm:"column:company_id"` } -// TableName gibt den Tabellennamen für GORM an +// TableName specifies the table name for GORM func (Customer) TableName() string { return "customers" } -// CustomerCreate enthält die Felder zum Erstellen eines neuen Kunden +// CustomerCreate contains the fields for creating a new customer type CustomerCreate struct { Name string CompanyID int } -// CustomerUpdate enthält die aktualisierbaren Felder eines Kunden +// CustomerUpdate contains the updatable fields of a customer type CustomerUpdate struct { - ID ulid.ULID `gorm:"-"` // Ausschließen von Updates + ID ulid.ULID `gorm:"-"` // Exclude from updates Name *string `gorm:"column:name"` CompanyID *int `gorm:"column:company_id"` } -// GetCustomerByID sucht einen Kunden anhand seiner ID +// GetCustomerByID finds a customer by its ID func GetCustomerByID(ctx context.Context, id ulid.ULID) (*Customer, error) { var customer Customer result := GetEngine(ctx).Where("id = ?", id).First(&customer) @@ -46,7 +46,7 @@ func GetCustomerByID(ctx context.Context, id ulid.ULID) (*Customer, error) { return &customer, nil } -// GetAllCustomers gibt alle Kunden zurück +// GetAllCustomers returns all customers func GetAllCustomers(ctx context.Context) ([]Customer, error) { var customers []Customer result := GetEngine(ctx).Find(&customers) @@ -56,7 +56,7 @@ func GetAllCustomers(ctx context.Context) ([]Customer, error) { return customers, nil } -// CreateCustomer erstellt einen neuen Kunden +// CreateCustomer creates a new customer func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, error) { customer := Customer{ Name: create.Name, @@ -70,26 +70,26 @@ func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, erro return &customer, nil } -// UpdateCustomer aktualisiert einen bestehenden Kunden +// UpdateCustomer updates an existing customer func UpdateCustomer(ctx context.Context, update CustomerUpdate) (*Customer, error) { customer, err := GetCustomerByID(ctx, update.ID) if err != nil { return nil, err } if customer == nil { - return nil, errors.New("customer nicht gefunden") + return nil, errors.New("customer not found") } - // Generische Update-Funktion verwenden + // Use generic update function if err := UpdateModel(ctx, customer, update); err != nil { return nil, err } - // Aktualisierte Daten aus der Datenbank laden + // Load updated data from the database return GetCustomerByID(ctx, update.ID) } -// DeleteCustomer löscht einen Kunden anhand seiner ID +// DeleteCustomer deletes a customer by its ID func DeleteCustomer(ctx context.Context, id ulid.ULID) error { result := GetEngine(ctx).Delete(&Customer{}, id) return result.Error diff --git a/backend/internal/models/db.go b/backend/internal/models/db.go index a41bc83..0672549 100644 --- a/backend/internal/models/db.go +++ b/backend/internal/models/db.go @@ -7,14 +7,14 @@ import ( "reflect" "strings" - "gorm.io/driver/postgres" // Für PostgreSQL + "gorm.io/driver/postgres" // For PostgreSQL "gorm.io/gorm" ) -// Globale Variable für die DB-Verbindung +// Global variable for the DB connection var defaultDB *gorm.DB -// DatabaseConfig enthält die Konfigurationsdaten für die Datenbankverbindung +// DatabaseConfig contains the configuration data for the database connection type DatabaseConfig struct { Host string Port int @@ -24,68 +24,68 @@ type DatabaseConfig struct { SSLMode string } -// InitDB initialisiert die Datenbankverbindung (einmalig beim Start) -// mit der übergebenen Konfiguration +// InitDB initializes the database connection (once at startup) +// with the provided configuration func InitDB(config DatabaseConfig) error { - // DSN (Data Source Name) erstellen + // Create DSN (Data Source Name) dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode) - // Datenbankverbindung herstellen + // Establish database connection db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) if err != nil { - return fmt.Errorf("fehler beim Verbinden zur Datenbank: %w", err) + return fmt.Errorf("error connecting to the database: %w", err) } defaultDB = db return nil } -// GetEngine gibt die DB-Instanz zurück, ggf. mit context +// GetEngine returns the DB instance, possibly with context func GetEngine(ctx context.Context) *gorm.DB { - // Falls in ctx eine spezielle Transaktion steckt, könnte man das hier prüfen + // If a special transaction is in ctx, you could check it here return defaultDB.WithContext(ctx) } -// UpdateModel aktualisiert ein Modell anhand der gesetzten Pointer-Felder +// UpdateModel updates a model based on the set pointer fields func UpdateModel(ctx context.Context, model any, updates any) error { updateValue := reflect.ValueOf(updates) - // Wenn updates ein Pointer ist, den Wert dahinter verwenden + // If updates is a pointer, use the value behind it if updateValue.Kind() == reflect.Ptr { updateValue = updateValue.Elem() } - // Stelle sicher, dass updates eine Struktur ist + // Make sure updates is a struct if updateValue.Kind() != reflect.Struct { - return errors.New("updates muss eine Struktur sein") + return errors.New("updates must be a struct") } updateType := updateValue.Type() updateMap := make(map[string]any) - // Durch alle Felder iterieren + // Iterate through all fields for i := 0; i < updateValue.NumField(); i++ { field := updateValue.Field(i) fieldType := updateType.Field(i) - // Überspringen von unexportierten Feldern + // Skip unexported fields if !fieldType.IsExported() { continue } - // Spezialfall: ID-Feld überspringen (nur für Updates verwenden) + // Special case: Skip ID field (use only for updates) if fieldType.Name == "ID" { continue } - // Für Pointer-Typen prüfen, ob sie nicht nil sind + // For pointer types, check if they are not nil if field.Kind() == reflect.Ptr && !field.IsNil() { - // Feldname aus GORM-Tag extrahieren oder Standard-Feldnamen verwenden + // Extract field name from GORM tag or use default field name fieldName := fieldType.Name if tag, ok := fieldType.Tag.Lookup("gorm"); ok { - // Tag-Optionen trennen + // Separate tag options options := strings.Split(tag, ";") for _, option := range options { if strings.HasPrefix(option, "column:") { @@ -95,13 +95,13 @@ func UpdateModel(ctx context.Context, model any, updates any) error { } } - // Den Wert hinter dem Pointer verwenden + // Use the value behind the pointer updateMap[fieldName] = field.Elem().Interface() } } if len(updateMap) == 0 { - return nil // Nichts zu aktualisieren + return nil // Nothing to update } return GetEngine(ctx).Model(model).Updates(updateMap).Error diff --git a/backend/internal/models/project.go b/backend/internal/models/project.go index ecca730..7afaeff 100644 --- a/backend/internal/models/project.go +++ b/backend/internal/models/project.go @@ -9,55 +9,55 @@ import ( "gorm.io/gorm" ) -// Project repräsentiert ein Projekt im System +// Project represents a project in the system type Project struct { EntityBase Name string `gorm:"column:name;not null"` CustomerID ulid.ULID `gorm:"column:customer_id;type:uuid;not null"` - // Beziehungen (für Eager Loading) + // Relationships (for Eager Loading) Customer *Customer `gorm:"foreignKey:CustomerID"` } -// TableName gibt den Tabellennamen für GORM an +// TableName specifies the table name for GORM func (Project) TableName() string { return "projects" } -// ProjectCreate enthält die Felder zum Erstellen eines neuen Projekts +// ProjectCreate contains the fields for creating a new project type ProjectCreate struct { Name string CustomerID ulid.ULID } -// ProjectUpdate enthält die aktualisierbaren Felder eines Projekts +// ProjectUpdate contains the updatable fields of a project type ProjectUpdate struct { - ID ulid.ULID `gorm:"-"` // Ausschließen von Updates + ID ulid.ULID `gorm:"-"` // Exclude from updates Name *string `gorm:"column:name"` CustomerID *ulid.ULID `gorm:"column:customer_id"` } -// Validate prüft, ob die Create-Struktur gültige Daten enthält +// Validate checks if the Create struct contains valid data func (pc *ProjectCreate) Validate() error { if pc.Name == "" { - return errors.New("project name darf nicht leer sein") + return errors.New("project name cannot be empty") } - // Prüfung auf gültige CustomerID + // Check for valid CustomerID if pc.CustomerID.Compare(ulid.ULID{}) == 0 { - return errors.New("customerID darf nicht leer sein") + return errors.New("customerID cannot be empty") } return nil } -// Validate prüft, ob die Update-Struktur gültige Daten enthält +// Validate checks if the Update struct contains valid data func (pu *ProjectUpdate) Validate() error { if pu.Name != nil && *pu.Name == "" { - return errors.New("project name darf nicht leer sein") + return errors.New("project name cannot be empty") } return nil } -// GetProjectByID sucht ein Projekt anhand seiner ID +// GetProjectByID finds a project by its ID func GetProjectByID(ctx context.Context, id ulid.ULID) (*Project, error) { var project Project result := GetEngine(ctx).Where("id = ?", id).First(&project) @@ -70,7 +70,7 @@ func GetProjectByID(ctx context.Context, id ulid.ULID) (*Project, error) { return &project, nil } -// GetProjectWithCustomer lädt ein Projekt mit den zugehörigen Kundeninformationen +// GetProjectWithCustomer loads a project with the associated customer information func GetProjectWithCustomer(ctx context.Context, id ulid.ULID) (*Project, error) { var project Project result := GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&project) @@ -83,7 +83,7 @@ func GetProjectWithCustomer(ctx context.Context, id ulid.ULID) (*Project, error) return &project, nil } -// GetAllProjects gibt alle Projekte zurück +// GetAllProjects returns all projects func GetAllProjects(ctx context.Context) ([]Project, error) { var projects []Project result := GetEngine(ctx).Find(&projects) @@ -93,7 +93,7 @@ func GetAllProjects(ctx context.Context) ([]Project, error) { return projects, nil } -// GetAllProjectsWithCustomers gibt alle Projekte mit Kundeninformationen zurück +// GetAllProjectsWithCustomers returns all projects with customer information func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) { var projects []Project result := GetEngine(ctx).Preload("Customer").Find(&projects) @@ -103,7 +103,7 @@ func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) { return projects, nil } -// GetProjectsByCustomerID gibt alle Projekte eines bestimmten Kunden zurück +// GetProjectsByCustomerID returns all projects of a specific customer func GetProjectsByCustomerID(ctx context.Context, customerID ulid.ULID) ([]Project, error) { var projects []Project result := GetEngine(ctx).Where("customer_id = ?", customerID).Find(&projects) @@ -113,20 +113,20 @@ func GetProjectsByCustomerID(ctx context.Context, customerID ulid.ULID) ([]Proje return projects, nil } -// CreateProject erstellt ein neues Projekt mit Validierung +// CreateProject creates a new project with validation func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) { - // Validierung + // Validation if err := create.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } - // Prüfen, ob der Kunde existiert + // Check if the customer exists customer, err := GetCustomerByID(ctx, create.CustomerID) if err != nil { - return nil, fmt.Errorf("fehler beim Prüfen des Kunden: %w", err) + return nil, fmt.Errorf("error checking the customer: %w", err) } if customer == nil { - return nil, errors.New("der angegebene Kunde existiert nicht") + return nil, errors.New("the specified customer does not exist") } project := Project{ @@ -136,16 +136,16 @@ func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) result := GetEngine(ctx).Create(&project) if result.Error != nil { - return nil, fmt.Errorf("fehler beim Erstellen des Projekts: %w", result.Error) + return nil, fmt.Errorf("error creating the project: %w", result.Error) } return &project, nil } -// UpdateProject aktualisiert ein bestehendes Projekt mit Validierung +// UpdateProject updates an existing project with validation func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) { - // Validierung + // Validation if err := update.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } project, err := GetProjectByID(ctx, update.ID) @@ -153,60 +153,60 @@ func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) return nil, err } if project == nil { - return nil, errors.New("project nicht gefunden") + return nil, errors.New("project not found") } - // Wenn CustomerID aktualisiert wird, prüfen ob der Kunde existiert + // If CustomerID is updated, check if the customer exists if update.CustomerID != nil { customer, err := GetCustomerByID(ctx, *update.CustomerID) if err != nil { - return nil, fmt.Errorf("fehler beim Prüfen des Kunden: %w", err) + return nil, fmt.Errorf("error checking the customer: %w", err) } if customer == nil { - return nil, errors.New("der angegebene Kunde existiert nicht") + return nil, errors.New("the specified customer does not exist") } } - // Generische Update-Funktion verwenden + // Use generic update function if err := UpdateModel(ctx, project, update); err != nil { - return nil, fmt.Errorf("fehler beim Aktualisieren des Projekts: %w", err) + return nil, fmt.Errorf("error updating the project: %w", err) } - // Aktualisierte Daten aus der Datenbank laden + // Load updated data from the database return GetProjectByID(ctx, update.ID) } -// DeleteProject löscht ein Projekt anhand seiner ID +// DeleteProject deletes a project by its ID func DeleteProject(ctx context.Context, id ulid.ULID) error { - // Hier könnte man prüfen, ob abhängige Entitäten existieren + // Here you could check if dependent entities exist result := GetEngine(ctx).Delete(&Project{}, id) if result.Error != nil { - return fmt.Errorf("fehler beim Löschen des Projekts: %w", result.Error) + return fmt.Errorf("error deleting the project: %w", result.Error) } return nil } -// CreateProjectWithTransaction erstellt ein Projekt innerhalb einer Transaktion +// CreateProjectWithTransaction creates a project within a transaction func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*Project, error) { - // Validierung + // Validation if err := create.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } var project *Project - // Transaktion starten + // Start transaction err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { - // Kundenprüfung innerhalb der Transaktion + // Customer check within the transaction var customer Customer if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return errors.New("der angegebene Kunde existiert nicht") + return errors.New("the specified customer does not exist") } return err } - // Projekt erstellen + // Create project newProject := Project{ Name: create.Name, CustomerID: create.CustomerID, @@ -216,14 +216,14 @@ func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*P return err } - // Projekt für die Rückgabe speichern + // Save project for return project = &newProject return nil }) if err != nil { - return nil, fmt.Errorf("transaktionsfehler: %w", err) + return nil, fmt.Errorf("transaction error: %w", err) } return project, nil diff --git a/backend/internal/models/timeentry.go b/backend/internal/models/timeentry.go index 14190d8..d97b7c7 100644 --- a/backend/internal/models/timeentry.go +++ b/backend/internal/models/timeentry.go @@ -10,7 +10,7 @@ import ( "gorm.io/gorm" ) -// TimeEntry repräsentiert einen Zeiteintrag im System +// TimeEntry represents a time entry in the system type TimeEntry struct { EntityBase UserID ulid.ULID `gorm:"column:user_id;type:uuid;not null;index"` @@ -21,18 +21,18 @@ type TimeEntry struct { Description string `gorm:"column:description"` Billable int `gorm:"column:billable"` // Percentage (0-100) - // Beziehungen für Eager Loading + // Relationships for Eager Loading User *User `gorm:"foreignKey:UserID"` Project *Project `gorm:"foreignKey:ProjectID"` Activity *Activity `gorm:"foreignKey:ActivityID"` } -// TableName gibt den Tabellennamen für GORM an +// TableName specifies the table name for GORM func (TimeEntry) TableName() string { return "time_entries" } -// TimeEntryCreate enthält die Felder zum Erstellen eines neuen Zeiteintrags +// TimeEntryCreate contains the fields for creating a new time entry type TimeEntryCreate struct { UserID ulid.ULID ProjectID ulid.ULID @@ -43,9 +43,9 @@ type TimeEntryCreate struct { Billable int // Percentage (0-100) } -// TimeEntryUpdate enthält die aktualisierbaren Felder eines Zeiteintrags +// TimeEntryUpdate contains the updatable fields of a time entry type TimeEntryUpdate struct { - ID ulid.ULID `gorm:"-"` // Ausschließen von Updates + ID ulid.ULID `gorm:"-"` // Exclude from updates UserID *ulid.ULID `gorm:"column:user_id"` ProjectID *ulid.ULID `gorm:"column:project_id"` ActivityID *ulid.ULID `gorm:"column:activity_id"` @@ -55,54 +55,54 @@ type TimeEntryUpdate struct { Billable *int `gorm:"column:billable"` } -// Validate prüft, ob die Create-Struktur gültige Daten enthält +// Validate checks if the Create struct contains valid data func (tc *TimeEntryCreate) Validate() error { - // Prüfung auf leere IDs + // Check for empty IDs if tc.UserID.Compare(ulid.ULID{}) == 0 { - return errors.New("userID darf nicht leer sein") + return errors.New("userID cannot be empty") } if tc.ProjectID.Compare(ulid.ULID{}) == 0 { - return errors.New("projectID darf nicht leer sein") + return errors.New("projectID cannot be empty") } if tc.ActivityID.Compare(ulid.ULID{}) == 0 { - return errors.New("activityID darf nicht leer sein") + return errors.New("activityID cannot be empty") } - // Zeitprüfungen + // Time checks if tc.Start.IsZero() { - return errors.New("startzeit darf nicht leer sein") + return errors.New("start time cannot be empty") } if tc.End.IsZero() { - return errors.New("endzeit darf nicht leer sein") + return errors.New("end time cannot be empty") } if tc.End.Before(tc.Start) { - return errors.New("endzeit kann nicht vor der startzeit liegen") + return errors.New("end time cannot be before start time") } - // Billable-Prozent Prüfung + // Billable percentage check if tc.Billable < 0 || tc.Billable > 100 { - return errors.New("billable muss zwischen 0 und 100 liegen") + return errors.New("billable must be between 0 and 100") } return nil } -// Validate prüft, ob die Update-Struktur gültige Daten enthält +// Validate checks if the Update struct contains valid data func (tu *TimeEntryUpdate) Validate() error { - // Billable-Prozent Prüfung + // Billable percentage check if tu.Billable != nil && (*tu.Billable < 0 || *tu.Billable > 100) { - return errors.New("billable muss zwischen 0 und 100 liegen") + return errors.New("billable must be between 0 and 100") } - // Zeitprüfungen + // Time checks if tu.Start != nil && tu.End != nil && tu.End.Before(*tu.Start) { - return errors.New("endzeit kann nicht vor der startzeit liegen") + return errors.New("end time cannot be before start time") } return nil } -// GetTimeEntryByID sucht einen Zeiteintrag anhand seiner ID +// GetTimeEntryByID finds a time entry by its ID func GetTimeEntryByID(ctx context.Context, id ulid.ULID) (*TimeEntry, error) { var timeEntry TimeEntry result := GetEngine(ctx).Where("id = ?", id).First(&timeEntry) @@ -115,13 +115,13 @@ func GetTimeEntryByID(ctx context.Context, id ulid.ULID) (*TimeEntry, error) { return &timeEntry, nil } -// GetTimeEntryWithRelations lädt einen Zeiteintrag mit allen zugehörigen Daten +// GetTimeEntryWithRelations loads a time entry with all associated data func GetTimeEntryWithRelations(ctx context.Context, id ulid.ULID) (*TimeEntry, error) { var timeEntry TimeEntry result := GetEngine(ctx). Preload("User"). Preload("Project"). - Preload("Project.Customer"). // Verschachtelte Beziehung + Preload("Project.Customer"). // Nested relationship Preload("Activity"). Where("id = ?", id). First(&timeEntry) @@ -135,7 +135,7 @@ func GetTimeEntryWithRelations(ctx context.Context, id ulid.ULID) (*TimeEntry, e return &timeEntry, nil } -// GetAllTimeEntries gibt alle Zeiteinträge zurück +// GetAllTimeEntries returns all time entries func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) { var timeEntries []TimeEntry result := GetEngine(ctx).Find(&timeEntries) @@ -145,7 +145,7 @@ func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) { return timeEntries, nil } -// GetTimeEntriesByUserID gibt alle Zeiteinträge eines Benutzers zurück +// GetTimeEntriesByUserID returns all time entries of a user func GetTimeEntriesByUserID(ctx context.Context, userID ulid.ULID) ([]TimeEntry, error) { var timeEntries []TimeEntry result := GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries) @@ -155,7 +155,7 @@ func GetTimeEntriesByUserID(ctx context.Context, userID ulid.ULID) ([]TimeEntry, return timeEntries, nil } -// GetTimeEntriesByProjectID gibt alle Zeiteinträge eines Projekts zurück +// GetTimeEntriesByProjectID returns all time entries of a project func GetTimeEntriesByProjectID(ctx context.Context, projectID ulid.ULID) ([]TimeEntry, error) { var timeEntries []TimeEntry result := GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries) @@ -165,10 +165,10 @@ func GetTimeEntriesByProjectID(ctx context.Context, projectID ulid.ULID) ([]Time return timeEntries, nil } -// GetTimeEntriesByDateRange gibt alle Zeiteinträge in einem Zeitraum zurück +// GetTimeEntriesByDateRange returns all time entries within a time range func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) { var timeEntries []TimeEntry - // Suche nach Überschneidungen im Zeitraum + // Search for overlaps in the time range result := GetEngine(ctx). Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)", start, end, start, end). @@ -180,7 +180,7 @@ func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]Tim return timeEntries, nil } -// SumBillableHoursByProject berechnet die abrechenbaren Stunden pro Projekt +// SumBillableHoursByProject calculates the billable hours per project func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float64, error) { type Result struct { TotalHours float64 @@ -188,7 +188,7 @@ func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float6 var result Result - // SQL-Berechnung der gewichteten Stunden + // SQL calculation of weighted hours err := GetEngine(ctx).Raw(` SELECT SUM( EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0) @@ -204,23 +204,23 @@ func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float6 return result.TotalHours, nil } -// CreateTimeEntry erstellt einen neuen Zeiteintrag mit Validierung +// CreateTimeEntry creates a new time entry with validation func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, error) { - // Validierung + // Validation if err := create.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } - // Starten einer Transaktion + // Start a transaction var timeEntry *TimeEntry err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { - // Verweise prüfen + // Check references if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil { return err } - // Zeiteintrag erstellen + // Create time entry newTimeEntry := TimeEntry{ UserID: create.UserID, ProjectID: create.ProjectID, @@ -232,7 +232,7 @@ func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, e } if err := tx.Create(&newTimeEntry).Error; err != nil { - return fmt.Errorf("fehler beim Erstellen des Zeiteintrags: %w", err) + return fmt.Errorf("error creating the time entry: %w", err) } timeEntry = &newTimeEntry @@ -246,59 +246,59 @@ func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, e return timeEntry, nil } -// validateReferences prüft, ob alle referenzierten Entitäten existieren +// validateReferences checks if all referenced entities exist func validateReferences(tx *gorm.DB, userID, projectID, activityID ulid.ULID) error { - // Benutzer prüfen + // Check user var userCount int64 if err := tx.Model(&User{}).Where("id = ?", userID).Count(&userCount).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen des Benutzers: %w", err) + return fmt.Errorf("error checking the user: %w", err) } if userCount == 0 { - return errors.New("der angegebene Benutzer existiert nicht") + return errors.New("the specified user does not exist") } - // Projekt prüfen + // Check project var projectCount int64 if err := tx.Model(&Project{}).Where("id = ?", projectID).Count(&projectCount).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen des Projekts: %w", err) + return fmt.Errorf("error checking the project: %w", err) } if projectCount == 0 { - return errors.New("das angegebene Projekt existiert nicht") + return errors.New("the specified project does not exist") } - // Aktivität prüfen + // Check activity var activityCount int64 if err := tx.Model(&Activity{}).Where("id = ?", activityID).Count(&activityCount).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen der Aktivität: %w", err) + return fmt.Errorf("error checking the activity: %w", err) } if activityCount == 0 { - return errors.New("die angegebene Aktivität existiert nicht") + return errors.New("the specified activity does not exist") } return nil } -// UpdateTimeEntry aktualisiert einen bestehenden Zeiteintrag mit Validierung +// UpdateTimeEntry updates an existing time entry with validation func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, error) { - // Validierung + // Validation if err := update.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } - // Zeiteintrag suchen + // Find time entry timeEntry, err := GetTimeEntryByID(ctx, update.ID) if err != nil { return nil, err } if timeEntry == nil { - return nil, errors.New("zeiteintrag nicht gefunden") + return nil, errors.New("time entry not found") } - // Starten einer Transaktion für das Update + // Start a transaction for the update err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error { - // Referenzen prüfen, falls sie aktualisiert werden + // Check references if they are updated if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil { - // Aktuelle Werte verwenden, wenn nicht aktualisiert + // Use current values if not updated userID := timeEntry.UserID if update.UserID != nil { userID = *update.UserID @@ -319,7 +319,7 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e } } - // Zeitkonsistenz prüfen + // Check time consistency start := timeEntry.Start if update.Start != nil { start = *update.Start @@ -331,12 +331,12 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e } if end.Before(start) { - return errors.New("endzeit kann nicht vor der startzeit liegen") + return errors.New("end time cannot be before start time") } - // Generisches Update verwenden + // Use generic update if err := UpdateModel(ctx, timeEntry, update); err != nil { - return fmt.Errorf("fehler beim Aktualisieren des Zeiteintrags: %w", err) + return fmt.Errorf("error updating the time entry: %w", err) } return nil @@ -346,15 +346,15 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e return nil, err } - // Aktualisierte Daten aus der Datenbank laden + // Load updated data from the database return GetTimeEntryByID(ctx, update.ID) } -// DeleteTimeEntry löscht einen Zeiteintrag anhand seiner ID +// DeleteTimeEntry deletes a time entry by its ID func DeleteTimeEntry(ctx context.Context, id ulid.ULID) error { result := GetEngine(ctx).Delete(&TimeEntry{}, id) if result.Error != nil { - return fmt.Errorf("fehler beim Löschen des Zeiteintrags: %w", result.Error) + return fmt.Errorf("error deleting the time entry: %w", result.Error) } return nil } diff --git a/backend/internal/models/user.go b/backend/internal/models/user.go index af36b34..06db8dc 100644 --- a/backend/internal/models/user.go +++ b/backend/internal/models/user.go @@ -16,9 +16,9 @@ import ( "gorm.io/gorm" ) -// Argon2 Parameter +// Argon2 Parameters const ( - // Empfohlene Werte für Argon2id + // Recommended values for Argon2id ArgonTime = 1 ArgonMemory = 64 * 1024 // 64MB ArgonThreads = 4 @@ -26,33 +26,33 @@ const ( SaltLength = 16 ) -// Rollen-Konstanten +// Role Constants const ( RoleAdmin = "admin" RoleUser = "user" RoleViewer = "viewer" ) -// User repräsentiert einen Benutzer im System +// User represents a user in the system type User struct { EntityBase Email string `gorm:"column:email;unique;not null"` - Salt string `gorm:"column:salt;not null;type:varchar(64)"` // Basis64-codierter Salt - Hash string `gorm:"column:hash;not null;type:varchar(128)"` // Basis64-codierter Hash + Salt string `gorm:"column:salt;not null;type:varchar(64)"` // Base64-encoded Salt + Hash string `gorm:"column:hash;not null;type:varchar(128)"` // Base64-encoded Hash Role string `gorm:"column:role;not null;default:'user'"` CompanyID ulid.ULID `gorm:"column:company_id;type:uuid;not null;index"` HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"` - // Beziehung für Eager Loading + // Relationship for Eager Loading Company *Company `gorm:"foreignKey:CompanyID"` } -// TableName gibt den Tabellennamen für GORM an +// TableName provides the table name for GORM func (User) TableName() string { return "users" } -// UserCreate enthält die Felder zum Erstellen eines neuen Benutzers +// UserCreate contains the fields for creating a new user type UserCreate struct { Email string Password string @@ -61,23 +61,23 @@ type UserCreate struct { HourlyRate float64 } -// UserUpdate enthält die aktualisierbaren Felder eines Benutzers +// UserUpdate contains the updatable fields of a user type UserUpdate struct { - ID ulid.ULID `gorm:"-"` // Ausschließen von Updates + ID ulid.ULID `gorm:"-"` // Exclude from updates Email *string `gorm:"column:email"` - Password *string `gorm:"-"` // Nicht direkt in DB speichern + Password *string `gorm:"-"` // Not stored directly in DB Role *string `gorm:"column:role"` CompanyID *ulid.ULID `gorm:"column:company_id"` HourlyRate *float64 `gorm:"column:hourly_rate"` } -// PasswordData enthält die Daten für Passwort-Hash und Salt +// PasswordData contains the data for password hash and salt type PasswordData struct { Salt string Hash string } -// GenerateSalt erzeugt einen kryptografisch sicheren Salt +// GenerateSalt generates a cryptographically secure salt func GenerateSalt() (string, error) { salt := make([]byte, SaltLength) _, err := rand.Read(salt) @@ -87,20 +87,20 @@ func GenerateSalt() (string, error) { return base64.StdEncoding.EncodeToString(salt), nil } -// HashPassword erstellt einen sicheren Passwort-Hash mit Argon2id und einem zufälligen Salt +// HashPassword creates a secure password hash with Argon2id and a random salt func HashPassword(password string) (PasswordData, error) { - // Erzeugen eines kryptografisch sicheren Salts + // Generate a cryptographically secure salt saltStr, err := GenerateSalt() if err != nil { - return PasswordData{}, fmt.Errorf("fehler beim Generieren des Salt: %w", err) + return PasswordData{}, fmt.Errorf("error generating salt: %w", err) } salt, err := base64.StdEncoding.DecodeString(saltStr) if err != nil { - return PasswordData{}, fmt.Errorf("fehler beim Dekodieren des Salt: %w", err) + return PasswordData{}, fmt.Errorf("error decoding salt: %w", err) } - // Hash mit Argon2id erstellen (moderne, sichere Hash-Funktion) + // Create hash with Argon2id (modern, secure hash function) hash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen) hashStr := base64.StdEncoding.EncodeToString(hash) @@ -110,26 +110,26 @@ func HashPassword(password string) (PasswordData, error) { }, nil } -// VerifyPassword prüft, ob ein Passwort mit dem Hash übereinstimmt +// VerifyPassword checks if a password matches the hash func VerifyPassword(password, saltStr, hashStr string) (bool, error) { salt, err := base64.StdEncoding.DecodeString(saltStr) if err != nil { - return false, fmt.Errorf("fehler beim Dekodieren des Salt: %w", err) + return false, fmt.Errorf("error decoding salt: %w", err) } hash, err := base64.StdEncoding.DecodeString(hashStr) if err != nil { - return false, fmt.Errorf("fehler beim Dekodieren des Hash: %w", err) + return false, fmt.Errorf("error decoding hash: %w", err) } - // Hash mit gleichem Salt berechnen + // Calculate hash with the same salt computedHash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen) - // Konstante Zeit-Vergleich, um Timing-Angriffe zu vermeiden + // Constant time comparison to prevent timing attacks return hmacEqual(hash, computedHash), nil } -// hmacEqual führt einen konstante-Zeit Vergleich durch (verhindert Timing-Attacken) +// hmacEqual performs a constant-time comparison (prevents timing attacks) func hmacEqual(a, b []byte) bool { if len(a) != len(b) { return false @@ -143,28 +143,28 @@ func hmacEqual(a, b []byte) bool { return result == 0 } -// Validate prüft, ob die Create-Struktur gültige Daten enthält +// Validate checks if the Create structure contains valid data func (uc *UserCreate) Validate() error { if uc.Email == "" { - return errors.New("email darf nicht leer sein") + return errors.New("email cannot be empty") } - // Email-Format prüfen + // Check email format emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) if !emailRegex.MatchString(uc.Email) { - return errors.New("ungültiges email-format") + return errors.New("invalid email format") } if uc.Password == "" { - return errors.New("passwort darf nicht leer sein") + return errors.New("password cannot be empty") } - // Passwort-Komplexität prüfen + // Check password complexity if len(uc.Password) < 10 { - return errors.New("passwort muss mindestens 10 Zeichen lang sein") + return errors.New("password must be at least 10 characters long") } - // Komplexere Passwortvalidierung + // More complex password validation var ( hasUpper = false hasLower = false @@ -187,57 +187,57 @@ func (uc *UserCreate) Validate() error { } if !hasUpper || !hasLower || !hasNumber || !hasSpecial { - return errors.New("passwort muss Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten") + return errors.New("password must contain uppercase letters, lowercase letters, numbers, and special characters") } - // Rolle prüfen + // Check role if uc.Role == "" { - uc.Role = RoleUser // Standardrolle setzen + uc.Role = RoleUser // Set default role } else { validRoles := []string{RoleAdmin, RoleUser, RoleViewer} isValid := slices.Contains(validRoles, uc.Role) if !isValid { - return fmt.Errorf("ungültige rolle: %s, erlaubt sind: %s", + return fmt.Errorf("invalid role: %s, allowed are: %s", uc.Role, strings.Join(validRoles, ", ")) } } if uc.CompanyID.Compare(ulid.ULID{}) == 0 { - return errors.New("companyID darf nicht leer sein") + return errors.New("companyID cannot be empty") } if uc.HourlyRate < 0 { - return errors.New("stundensatz darf nicht negativ sein") + return errors.New("hourly rate cannot be negative") } return nil } -// Validate prüft, ob die Update-Struktur gültige Daten enthält +// Validate checks if the Update structure contains valid data func (uu *UserUpdate) Validate() error { if uu.Email != nil && *uu.Email == "" { - return errors.New("email darf nicht leer sein") + return errors.New("email cannot be empty") } - // Email-Format prüfen + // Check email format if uu.Email != nil { emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) if !emailRegex.MatchString(*uu.Email) { - return errors.New("ungültiges email-format") + return errors.New("invalid email format") } } if uu.Password != nil { if *uu.Password == "" { - return errors.New("passwort darf nicht leer sein") + return errors.New("password cannot be empty") } - // Passwort-Komplexität prüfen + // Check password complexity if len(*uu.Password) < 10 { - return errors.New("passwort muss mindestens 10 Zeichen lang sein") + return errors.New("password must be at least 10 characters long") } - // Komplexere Passwortvalidierung + // More complex password validation var ( hasUpper = false hasLower = false @@ -260,11 +260,11 @@ func (uu *UserUpdate) Validate() error { } if !hasUpper || !hasLower || !hasNumber || !hasSpecial { - return errors.New("passwort muss Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten") + return errors.New("password must contain uppercase letters, lowercase letters, numbers, and special characters") } } - // Rolle prüfen + // Check role if uu.Role != nil { validRoles := []string{RoleAdmin, RoleUser, RoleViewer} isValid := false @@ -275,19 +275,19 @@ func (uu *UserUpdate) Validate() error { } } if !isValid { - return fmt.Errorf("ungültige rolle: %s, erlaubt sind: %s", + return fmt.Errorf("invalid role: %s, allowed are: %s", *uu.Role, strings.Join(validRoles, ", ")) } } if uu.HourlyRate != nil && *uu.HourlyRate < 0 { - return errors.New("stundensatz darf nicht negativ sein") + return errors.New("hourly rate cannot be negative") } return nil } -// GetUserByID sucht einen Benutzer anhand seiner ID +// GetUserByID finds a user by their ID func GetUserByID(ctx context.Context, id ulid.ULID) (*User, error) { var user User result := GetEngine(ctx).Where("id = ?", id).First(&user) @@ -300,7 +300,7 @@ func GetUserByID(ctx context.Context, id ulid.ULID) (*User, error) { return &user, nil } -// GetUserByEmail sucht einen Benutzer anhand seiner Email +// GetUserByEmail finds a user by their email func GetUserByEmail(ctx context.Context, email string) (*User, error) { var user User result := GetEngine(ctx).Where("email = ?", email).First(&user) @@ -313,7 +313,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { return &user, nil } -// GetUserWithCompany lädt einen Benutzer mit seiner Firma +// GetUserWithCompany loads a user with their company func GetUserWithCompany(ctx context.Context, id ulid.ULID) (*User, error) { var user User result := GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&user) @@ -326,7 +326,7 @@ func GetUserWithCompany(ctx context.Context, id ulid.ULID) (*User, error) { return &user, nil } -// GetAllUsers gibt alle Benutzer zurück +// GetAllUsers returns all users func GetAllUsers(ctx context.Context) ([]User, error) { var users []User result := GetEngine(ctx).Find(&users) @@ -336,7 +336,7 @@ func GetAllUsers(ctx context.Context) ([]User, error) { return users, nil } -// GetUsersByCompanyID gibt alle Benutzer einer Firma zurück +// GetUsersByCompanyID returns all users of a company func GetUsersByCompanyID(ctx context.Context, companyID ulid.ULID) ([]User, error) { var users []User result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&users) @@ -346,42 +346,42 @@ func GetUsersByCompanyID(ctx context.Context, companyID ulid.ULID) ([]User, erro return users, nil } -// CreateUser erstellt einen neuen Benutzer mit Validierung und sicherem Passwort-Hashing +// CreateUser creates a new user with validation and secure password hashing func CreateUser(ctx context.Context, create UserCreate) (*User, error) { - // Validierung + // Validation if err := create.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } - // Starten einer Transaktion + // Start a transaction var user *User err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { - // Prüfen, ob Email bereits existiert + // Check if email already exists var count int64 if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen der Email: %w", err) + return fmt.Errorf("error checking email: %w", err) } if count > 0 { - return errors.New("email wird bereits verwendet") + return errors.New("email is already in use") } - // Prüfen, ob Company existiert + // Check if company exists var companyCount int64 if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen der Firma: %w", err) + return fmt.Errorf("error checking company: %w", err) } if companyCount == 0 { - return errors.New("die angegebene Firma existiert nicht") + return errors.New("the specified company does not exist") } - // Passwort hashen mit einzigartigem Salt + // Hash password with unique salt pwData, err := HashPassword(create.Password) if err != nil { - return fmt.Errorf("fehler beim Hashen des Passworts: %w", err) + return fmt.Errorf("error hashing password: %w", err) } - // Benutzer erstellen mit Salt und Hash getrennt gespeichert + // Create user with salt and hash stored separately newUser := User{ Email: create.Email, Salt: pwData.Salt, @@ -392,7 +392,7 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) { } if err := tx.Create(&newUser).Error; err != nil { - return fmt.Errorf("fehler beim Erstellen des Benutzers: %w", err) + return fmt.Errorf("error creating user: %w", err) } user = &newUser @@ -406,66 +406,66 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) { return user, nil } -// UpdateUser aktualisiert einen bestehenden Benutzer +// UpdateUser updates an existing user func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) { - // Validierung + // Validation if err := update.Validate(); err != nil { - return nil, fmt.Errorf("validierungsfehler: %w", err) + return nil, fmt.Errorf("validation error: %w", err) } - // Benutzer suchen + // Find user user, err := GetUserByID(ctx, update.ID) if err != nil { return nil, err } if user == nil { - return nil, errors.New("benutzer nicht gefunden") + return nil, errors.New("user not found") } - // Starten einer Transaktion für das Update + // Start a transaction for the update err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error { - // Wenn Email aktualisiert wird, prüfen ob sie bereits verwendet wird + // If email is updated, check if it's already in use if update.Email != nil && *update.Email != user.Email { var count int64 if err := tx.Model(&User{}).Where("email = ? AND id != ?", *update.Email, update.ID).Count(&count).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen der Email: %w", err) + return fmt.Errorf("error checking email: %w", err) } if count > 0 { - return errors.New("email wird bereits verwendet") + return errors.New("email is already in use") } } - // Wenn CompanyID aktualisiert wird, prüfen ob sie existiert + // If CompanyID is updated, check if it exists if update.CompanyID != nil && update.CompanyID.Compare(user.CompanyID) != 0 { var companyCount int64 if err := tx.Model(&Company{}).Where("id = ?", *update.CompanyID).Count(&companyCount).Error; err != nil { - return fmt.Errorf("fehler beim Prüfen der Firma: %w", err) + return fmt.Errorf("error checking company: %w", err) } if companyCount == 0 { - return errors.New("die angegebene Firma existiert nicht") + return errors.New("the specified company does not exist") } } - // Wenn Passwort aktualisiert wird, neu hashen mit neuem Salt + // If password is updated, rehash with new salt if update.Password != nil { pwData, err := HashPassword(*update.Password) if err != nil { - return fmt.Errorf("fehler beim Hashen des Passworts: %w", err) + return fmt.Errorf("error hashing password: %w", err) } - // Salt und Hash direkt im Modell aktualisieren + // Update salt and hash directly in the model if err := tx.Model(user).Updates(map[string]interface{}{ "salt": pwData.Salt, "hash": pwData.Hash, }).Error; err != nil { - return fmt.Errorf("fehler beim Aktualisieren des Passworts: %w", err) + return fmt.Errorf("error updating password: %w", err) } } - // Map für generisches Update erstellen + // Create map for generic update updates := make(map[string]interface{}) - // Nur nicht-Passwort-Felder dem Update hinzufügen + // Add only non-password fields to the update if update.Email != nil { updates["email"] = *update.Email } @@ -479,10 +479,10 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) { updates["hourly_rate"] = *update.HourlyRate } - // Generisches Update nur ausführen, wenn es Änderungen gibt + // Only execute generic update if there are changes if len(updates) > 0 { if err := tx.Model(user).Updates(updates).Error; err != nil { - return fmt.Errorf("fehler beim Aktualisieren des Benutzers: %w", err) + return fmt.Errorf("error updating user: %w", err) } } @@ -493,41 +493,41 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) { return nil, err } - // Aktualisierte Daten aus der Datenbank laden + // Load updated data from the database return GetUserByID(ctx, update.ID) } -// DeleteUser löscht einen Benutzer anhand seiner ID +// DeleteUser deletes a user by their ID func DeleteUser(ctx context.Context, id ulid.ULID) error { - // Hier könnte man prüfen, ob abhängige Entitäten existieren - // z.B. nicht löschen, wenn noch Zeiteinträge vorhanden sind + // Here one could check if dependent entities exist + // e.g., don't delete if time entries still exist result := GetEngine(ctx).Delete(&User{}, id) if result.Error != nil { - return fmt.Errorf("fehler beim Löschen des Benutzers: %w", result.Error) + return fmt.Errorf("error deleting user: %w", result.Error) } return nil } -// AuthenticateUser authentifiziert einen Benutzer mit Email und Passwort +// AuthenticateUser authenticates a user with email and password func AuthenticateUser(ctx context.Context, email, password string) (*User, error) { user, err := GetUserByEmail(ctx, email) if err != nil { return nil, err } if user == nil { - // Gleiche Fehlermeldung, um keine Informationen über existierende Accounts preiszugeben - return nil, errors.New("ungültige Anmeldeinformationen") + // Same error message to avoid revealing information about existing accounts + return nil, errors.New("invalid login credentials") } - // Passwort überprüfen mit dem gespeicherten Salt + // Verify password with the stored salt isValid, err := VerifyPassword(password, user.Salt, user.Hash) if err != nil { - return nil, fmt.Errorf("fehler bei der Passwortüberprüfung: %w", err) + return nil, fmt.Errorf("error verifying password: %w", err) } if !isValid { - return nil, errors.New("ungültige Anmeldeinformationen") + return nil, errors.New("invalid login credentials") } return user, nil