refactor: Update comments to English for consistency across models

This commit is contained in:
Jean Jacques Avril 2025-03-10 10:11:04 +00:00
parent 7f275c774e
commit d1720ea33d
8 changed files with 275 additions and 275 deletions

View File

@ -8,32 +8,32 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Activity repräsentiert eine Aktivität im System // Activity represents an activity in the system
type Activity struct { type Activity struct {
EntityBase EntityBase
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
BillingRate float64 `gorm:"column:billing_rate"` 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 { func (Activity) TableName() string {
return "activities" return "activities"
} }
// ActivityUpdate enthält die aktualisierbaren Felder einer Activity // ActivityUpdate contains the updatable fields of an Activity
type ActivityUpdate struct { 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"` Name *string `gorm:"column:name"`
BillingRate *float64 `gorm:"column:billing_rate"` 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 { type ActivityCreate struct {
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
BillingRate float64 `gorm:"column:billing_rate"` 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) { func GetActivityByID(ctx context.Context, id ulid.ULID) (*Activity, error) {
var activity Activity var activity Activity
result := GetEngine(ctx).Where("id = ?", id).First(&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 return &activity, nil
} }
// GetAllActivities gibt alle Activities zurück // GetAllActivities returns all Activities
func GetAllActivities(ctx context.Context) ([]Activity, error) { func GetAllActivities(ctx context.Context) ([]Activity, error) {
var activities []Activity var activities []Activity
result := GetEngine(ctx).Find(&activities) result := GetEngine(ctx).Find(&activities)
@ -56,7 +56,7 @@ func GetAllActivities(ctx context.Context) ([]Activity, error) {
return activities, nil return activities, nil
} }
// CreateActivity erstellt eine neue Activity // CreateActivity creates a new Activity
func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, error) { func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, error) {
activity := Activity{ activity := Activity{
Name: create.Name, Name: create.Name,
@ -70,26 +70,26 @@ func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, erro
return &activity, nil return &activity, nil
} }
// UpdateActivity aktualisiert eine bestehende Activity // UpdateActivity updates an existing Activity
func UpdateActivity(ctx context.Context, update ActivityUpdate) (*Activity, error) { func UpdateActivity(ctx context.Context, update ActivityUpdate) (*Activity, error) {
activity, err := GetActivityByID(ctx, update.ID) activity, err := GetActivityByID(ctx, update.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if activity == nil { 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 { if err := UpdateModel(ctx, activity, update); err != nil {
return nil, err return nil, err
} }
// Aktualisierte Daten aus der Datenbank laden // Load updated data from the database
return GetActivityByID(ctx, update.ID) 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 { func DeleteActivity(ctx context.Context, id ulid.ULID) error {
result := GetEngine(ctx).Delete(&Activity{}, id) result := GetEngine(ctx).Delete(&Activity{}, id)
return result.Error return result.Error

View File

@ -15,10 +15,10 @@ type EntityBase struct {
DeletedAt gorm.DeletedAt `gorm:"index"` 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 { func (eb *EntityBase) BeforeCreate(tx *gorm.DB) error {
if eb.ID.Compare(ulid.ULID{}) == 0 { // Wenn ID leer ist if eb.ID.Compare(ulid.ULID{}) == 0 { // If ID is empty
// Generiere eine neue ULID // Generate a new ULID
entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0) entropy := ulid.Monotonic(rand.New(rand.NewSource(time.Now().UnixNano())), 0)
eb.ID = ulid.MustNew(ulid.Timestamp(time.Now()), entropy) eb.ID = ulid.MustNew(ulid.Timestamp(time.Now()), entropy)
} }

View File

@ -8,29 +8,29 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Company repräsentiert ein Unternehmen im System // Company represents a company in the system
type Company struct { type Company struct {
EntityBase EntityBase
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
} }
// TableName gibt den Tabellennamen für GORM an // TableName specifies the table name for GORM
func (Company) TableName() string { func (Company) TableName() string {
return "companies" return "companies"
} }
// CompanyCreate enthält die Felder zum Erstellen eines neuen Unternehmens // CompanyCreate contains the fields for creating a new company
type CompanyCreate struct { type CompanyCreate struct {
Name string Name string
} }
// CompanyUpdate enthält die aktualisierbaren Felder eines Unternehmens // CompanyUpdate contains the updatable fields of a company
type CompanyUpdate struct { type CompanyUpdate struct {
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates ID ulid.ULID `gorm:"-"` // Exclude from updates
Name *string `gorm:"column:name"` 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) { func GetCompanyByID(ctx context.Context, id ulid.ULID) (*Company, error) {
var company Company var company Company
result := GetEngine(ctx).Where("id = ?", id).First(&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 return &company, nil
} }
// GetAllCompanies gibt alle Unternehmen zurück // GetAllCompanies returns all companies
func GetAllCompanies(ctx context.Context) ([]Company, error) { func GetAllCompanies(ctx context.Context) ([]Company, error) {
var companies []Company var companies []Company
result := GetEngine(ctx).Find(&companies) result := GetEngine(ctx).Find(&companies)
@ -62,7 +62,7 @@ func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, er
return customers, nil return customers, nil
} }
// CreateCompany erstellt ein neues Unternehmen // CreateCompany creates a new company
func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) { func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) {
company := Company{ company := Company{
Name: create.Name, Name: create.Name,
@ -75,26 +75,26 @@ func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error)
return &company, nil return &company, nil
} }
// UpdateCompany aktualisiert ein bestehendes Unternehmen // UpdateCompany updates an existing company
func UpdateCompany(ctx context.Context, update CompanyUpdate) (*Company, error) { func UpdateCompany(ctx context.Context, update CompanyUpdate) (*Company, error) {
company, err := GetCompanyByID(ctx, update.ID) company, err := GetCompanyByID(ctx, update.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if company == nil { 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 { if err := UpdateModel(ctx, company, update); err != nil {
return nil, err return nil, err
} }
// Aktualisierte Daten aus der Datenbank laden // Load updated data from the database
return GetCompanyByID(ctx, update.ID) 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 { func DeleteCompany(ctx context.Context, id ulid.ULID) error {
result := GetEngine(ctx).Delete(&Company{}, id) result := GetEngine(ctx).Delete(&Company{}, id)
return result.Error return result.Error

View File

@ -8,32 +8,32 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Customer repräsentiert einen Kunden im System // Customer represents a customer in the system
type Customer struct { type Customer struct {
EntityBase EntityBase
Name string `gorm:"column:name"` Name string `gorm:"column:name"`
CompanyID int `gorm:"column:company_id"` 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 { func (Customer) TableName() string {
return "customers" return "customers"
} }
// CustomerCreate enthält die Felder zum Erstellen eines neuen Kunden // CustomerCreate contains the fields for creating a new customer
type CustomerCreate struct { type CustomerCreate struct {
Name string Name string
CompanyID int CompanyID int
} }
// CustomerUpdate enthält die aktualisierbaren Felder eines Kunden // CustomerUpdate contains the updatable fields of a customer
type CustomerUpdate struct { type CustomerUpdate struct {
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates ID ulid.ULID `gorm:"-"` // Exclude from updates
Name *string `gorm:"column:name"` Name *string `gorm:"column:name"`
CompanyID *int `gorm:"column:company_id"` 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) { func GetCustomerByID(ctx context.Context, id ulid.ULID) (*Customer, error) {
var customer Customer var customer Customer
result := GetEngine(ctx).Where("id = ?", id).First(&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 return &customer, nil
} }
// GetAllCustomers gibt alle Kunden zurück // GetAllCustomers returns all customers
func GetAllCustomers(ctx context.Context) ([]Customer, error) { func GetAllCustomers(ctx context.Context) ([]Customer, error) {
var customers []Customer var customers []Customer
result := GetEngine(ctx).Find(&customers) result := GetEngine(ctx).Find(&customers)
@ -56,7 +56,7 @@ func GetAllCustomers(ctx context.Context) ([]Customer, error) {
return customers, nil return customers, nil
} }
// CreateCustomer erstellt einen neuen Kunden // CreateCustomer creates a new customer
func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, error) { func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, error) {
customer := Customer{ customer := Customer{
Name: create.Name, Name: create.Name,
@ -70,26 +70,26 @@ func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, erro
return &customer, nil return &customer, nil
} }
// UpdateCustomer aktualisiert einen bestehenden Kunden // UpdateCustomer updates an existing customer
func UpdateCustomer(ctx context.Context, update CustomerUpdate) (*Customer, error) { func UpdateCustomer(ctx context.Context, update CustomerUpdate) (*Customer, error) {
customer, err := GetCustomerByID(ctx, update.ID) customer, err := GetCustomerByID(ctx, update.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if customer == nil { 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 { if err := UpdateModel(ctx, customer, update); err != nil {
return nil, err return nil, err
} }
// Aktualisierte Daten aus der Datenbank laden // Load updated data from the database
return GetCustomerByID(ctx, update.ID) 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 { func DeleteCustomer(ctx context.Context, id ulid.ULID) error {
result := GetEngine(ctx).Delete(&Customer{}, id) result := GetEngine(ctx).Delete(&Customer{}, id)
return result.Error return result.Error

View File

@ -7,14 +7,14 @@ import (
"reflect" "reflect"
"strings" "strings"
"gorm.io/driver/postgres" // Für PostgreSQL "gorm.io/driver/postgres" // For PostgreSQL
"gorm.io/gorm" "gorm.io/gorm"
) )
// Globale Variable für die DB-Verbindung // Global variable for the DB connection
var defaultDB *gorm.DB 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 { type DatabaseConfig struct {
Host string Host string
Port int Port int
@ -24,68 +24,68 @@ type DatabaseConfig struct {
SSLMode string SSLMode string
} }
// InitDB initialisiert die Datenbankverbindung (einmalig beim Start) // InitDB initializes the database connection (once at startup)
// mit der übergebenen Konfiguration // with the provided configuration
func InitDB(config DatabaseConfig) error { 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", 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) 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{}) db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil { 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 defaultDB = db
return nil 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 { 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) 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 { func UpdateModel(ctx context.Context, model any, updates any) error {
updateValue := reflect.ValueOf(updates) 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 { if updateValue.Kind() == reflect.Ptr {
updateValue = updateValue.Elem() updateValue = updateValue.Elem()
} }
// Stelle sicher, dass updates eine Struktur ist // Make sure updates is a struct
if updateValue.Kind() != reflect.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() updateType := updateValue.Type()
updateMap := make(map[string]any) updateMap := make(map[string]any)
// Durch alle Felder iterieren // Iterate through all fields
for i := 0; i < updateValue.NumField(); i++ { for i := 0; i < updateValue.NumField(); i++ {
field := updateValue.Field(i) field := updateValue.Field(i)
fieldType := updateType.Field(i) fieldType := updateType.Field(i)
// Überspringen von unexportierten Feldern // Skip unexported fields
if !fieldType.IsExported() { if !fieldType.IsExported() {
continue continue
} }
// Spezialfall: ID-Feld überspringen (nur für Updates verwenden) // Special case: Skip ID field (use only for updates)
if fieldType.Name == "ID" { if fieldType.Name == "ID" {
continue 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() { 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 fieldName := fieldType.Name
if tag, ok := fieldType.Tag.Lookup("gorm"); ok { if tag, ok := fieldType.Tag.Lookup("gorm"); ok {
// Tag-Optionen trennen // Separate tag options
options := strings.Split(tag, ";") options := strings.Split(tag, ";")
for _, option := range options { for _, option := range options {
if strings.HasPrefix(option, "column:") { 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() updateMap[fieldName] = field.Elem().Interface()
} }
} }
if len(updateMap) == 0 { if len(updateMap) == 0 {
return nil // Nichts zu aktualisieren return nil // Nothing to update
} }
return GetEngine(ctx).Model(model).Updates(updateMap).Error return GetEngine(ctx).Model(model).Updates(updateMap).Error

View File

@ -9,55 +9,55 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Project repräsentiert ein Projekt im System // Project represents a project in the system
type Project struct { type Project struct {
EntityBase EntityBase
Name string `gorm:"column:name;not null"` Name string `gorm:"column:name;not null"`
CustomerID ulid.ULID `gorm:"column:customer_id;type:uuid;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"` Customer *Customer `gorm:"foreignKey:CustomerID"`
} }
// TableName gibt den Tabellennamen für GORM an // TableName specifies the table name for GORM
func (Project) TableName() string { func (Project) TableName() string {
return "projects" return "projects"
} }
// ProjectCreate enthält die Felder zum Erstellen eines neuen Projekts // ProjectCreate contains the fields for creating a new project
type ProjectCreate struct { type ProjectCreate struct {
Name string Name string
CustomerID ulid.ULID CustomerID ulid.ULID
} }
// ProjectUpdate enthält die aktualisierbaren Felder eines Projekts // ProjectUpdate contains the updatable fields of a project
type ProjectUpdate struct { type ProjectUpdate struct {
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates ID ulid.ULID `gorm:"-"` // Exclude from updates
Name *string `gorm:"column:name"` Name *string `gorm:"column:name"`
CustomerID *ulid.ULID `gorm:"column:customer_id"` 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 { func (pc *ProjectCreate) Validate() error {
if pc.Name == "" { 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 { if pc.CustomerID.Compare(ulid.ULID{}) == 0 {
return errors.New("customerID darf nicht leer sein") return errors.New("customerID cannot be empty")
} }
return nil 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 { func (pu *ProjectUpdate) Validate() error {
if pu.Name != nil && *pu.Name == "" { 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 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) { func GetProjectByID(ctx context.Context, id ulid.ULID) (*Project, error) {
var project Project var project Project
result := GetEngine(ctx).Where("id = ?", id).First(&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 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) { func GetProjectWithCustomer(ctx context.Context, id ulid.ULID) (*Project, error) {
var project Project var project Project
result := GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&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 return &project, nil
} }
// GetAllProjects gibt alle Projekte zurück // GetAllProjects returns all projects
func GetAllProjects(ctx context.Context) ([]Project, error) { func GetAllProjects(ctx context.Context) ([]Project, error) {
var projects []Project var projects []Project
result := GetEngine(ctx).Find(&projects) result := GetEngine(ctx).Find(&projects)
@ -93,7 +93,7 @@ func GetAllProjects(ctx context.Context) ([]Project, error) {
return projects, nil 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) { func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) {
var projects []Project var projects []Project
result := GetEngine(ctx).Preload("Customer").Find(&projects) result := GetEngine(ctx).Preload("Customer").Find(&projects)
@ -103,7 +103,7 @@ func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) {
return projects, nil 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) { func GetProjectsByCustomerID(ctx context.Context, customerID ulid.ULID) ([]Project, error) {
var projects []Project var projects []Project
result := GetEngine(ctx).Where("customer_id = ?", customerID).Find(&projects) 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 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) { func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) {
// Validierung // Validation
if err := create.Validate(); err != nil { 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) customer, err := GetCustomerByID(ctx, create.CustomerID)
if err != nil { 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 { if customer == nil {
return nil, errors.New("der angegebene Kunde existiert nicht") return nil, errors.New("the specified customer does not exist")
} }
project := Project{ project := Project{
@ -136,16 +136,16 @@ func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error)
result := GetEngine(ctx).Create(&project) result := GetEngine(ctx).Create(&project)
if result.Error != nil { 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 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) { func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) {
// Validierung // Validation
if err := update.Validate(); err != nil { 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) project, err := GetProjectByID(ctx, update.ID)
@ -153,60 +153,60 @@ func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error)
return nil, err return nil, err
} }
if project == nil { 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 { if update.CustomerID != nil {
customer, err := GetCustomerByID(ctx, *update.CustomerID) customer, err := GetCustomerByID(ctx, *update.CustomerID)
if err != nil { 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 { 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 { 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) 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 { 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) result := GetEngine(ctx).Delete(&Project{}, id)
if result.Error != nil { 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 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) { func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*Project, error) {
// Validierung // Validation
if err := create.Validate(); err != nil { if err := create.Validate(); err != nil {
return nil, fmt.Errorf("validierungsfehler: %w", err) return nil, fmt.Errorf("validation error: %w", err)
} }
var project *Project var project *Project
// Transaktion starten // Start transaction
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
// Kundenprüfung innerhalb der Transaktion // Customer check within the transaction
var customer Customer var customer Customer
if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil { if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { 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 return err
} }
// Projekt erstellen // Create project
newProject := Project{ newProject := Project{
Name: create.Name, Name: create.Name,
CustomerID: create.CustomerID, CustomerID: create.CustomerID,
@ -216,14 +216,14 @@ func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*P
return err return err
} }
// Projekt für die Rückgabe speichern // Save project for return
project = &newProject project = &newProject
return nil return nil
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("transaktionsfehler: %w", err) return nil, fmt.Errorf("transaction error: %w", err)
} }
return project, nil return project, nil

View File

@ -10,7 +10,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// TimeEntry repräsentiert einen Zeiteintrag im System // TimeEntry represents a time entry in the system
type TimeEntry struct { type TimeEntry struct {
EntityBase EntityBase
UserID ulid.ULID `gorm:"column:user_id;type:uuid;not null;index"` UserID ulid.ULID `gorm:"column:user_id;type:uuid;not null;index"`
@ -21,18 +21,18 @@ type TimeEntry struct {
Description string `gorm:"column:description"` Description string `gorm:"column:description"`
Billable int `gorm:"column:billable"` // Percentage (0-100) Billable int `gorm:"column:billable"` // Percentage (0-100)
// Beziehungen für Eager Loading // Relationships for Eager Loading
User *User `gorm:"foreignKey:UserID"` User *User `gorm:"foreignKey:UserID"`
Project *Project `gorm:"foreignKey:ProjectID"` Project *Project `gorm:"foreignKey:ProjectID"`
Activity *Activity `gorm:"foreignKey:ActivityID"` Activity *Activity `gorm:"foreignKey:ActivityID"`
} }
// TableName gibt den Tabellennamen für GORM an // TableName specifies the table name for GORM
func (TimeEntry) TableName() string { func (TimeEntry) TableName() string {
return "time_entries" 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 { type TimeEntryCreate struct {
UserID ulid.ULID UserID ulid.ULID
ProjectID ulid.ULID ProjectID ulid.ULID
@ -43,9 +43,9 @@ type TimeEntryCreate struct {
Billable int // Percentage (0-100) 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 { 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"` UserID *ulid.ULID `gorm:"column:user_id"`
ProjectID *ulid.ULID `gorm:"column:project_id"` ProjectID *ulid.ULID `gorm:"column:project_id"`
ActivityID *ulid.ULID `gorm:"column:activity_id"` ActivityID *ulid.ULID `gorm:"column:activity_id"`
@ -55,54 +55,54 @@ type TimeEntryUpdate struct {
Billable *int `gorm:"column:billable"` 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 { func (tc *TimeEntryCreate) Validate() error {
// Prüfung auf leere IDs // Check for empty IDs
if tc.UserID.Compare(ulid.ULID{}) == 0 { 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 { 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 { 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() { if tc.Start.IsZero() {
return errors.New("startzeit darf nicht leer sein") return errors.New("start time cannot be empty")
} }
if tc.End.IsZero() { 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) { 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 { 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 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 { func (tu *TimeEntryUpdate) Validate() error {
// Billable-Prozent Prüfung // Billable percentage check
if tu.Billable != nil && (*tu.Billable < 0 || *tu.Billable > 100) { 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) { 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 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) { func GetTimeEntryByID(ctx context.Context, id ulid.ULID) (*TimeEntry, error) {
var timeEntry TimeEntry var timeEntry TimeEntry
result := GetEngine(ctx).Where("id = ?", id).First(&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 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) { func GetTimeEntryWithRelations(ctx context.Context, id ulid.ULID) (*TimeEntry, error) {
var timeEntry TimeEntry var timeEntry TimeEntry
result := GetEngine(ctx). result := GetEngine(ctx).
Preload("User"). Preload("User").
Preload("Project"). Preload("Project").
Preload("Project.Customer"). // Verschachtelte Beziehung Preload("Project.Customer"). // Nested relationship
Preload("Activity"). Preload("Activity").
Where("id = ?", id). Where("id = ?", id).
First(&timeEntry) First(&timeEntry)
@ -135,7 +135,7 @@ func GetTimeEntryWithRelations(ctx context.Context, id ulid.ULID) (*TimeEntry, e
return &timeEntry, nil return &timeEntry, nil
} }
// GetAllTimeEntries gibt alle Zeiteinträge zurück // GetAllTimeEntries returns all time entries
func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) { func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
var timeEntries []TimeEntry var timeEntries []TimeEntry
result := GetEngine(ctx).Find(&timeEntries) result := GetEngine(ctx).Find(&timeEntries)
@ -145,7 +145,7 @@ func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
return timeEntries, nil 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) { func GetTimeEntriesByUserID(ctx context.Context, userID ulid.ULID) ([]TimeEntry, error) {
var timeEntries []TimeEntry var timeEntries []TimeEntry
result := GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries) 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 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) { func GetTimeEntriesByProjectID(ctx context.Context, projectID ulid.ULID) ([]TimeEntry, error) {
var timeEntries []TimeEntry var timeEntries []TimeEntry
result := GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries) 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 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) { func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) {
var timeEntries []TimeEntry var timeEntries []TimeEntry
// Suche nach Überschneidungen im Zeitraum // Search for overlaps in the time range
result := GetEngine(ctx). result := GetEngine(ctx).
Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)", Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)",
start, end, start, end). start, end, start, end).
@ -180,7 +180,7 @@ func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]Tim
return timeEntries, nil 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) { func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float64, error) {
type Result struct { type Result struct {
TotalHours float64 TotalHours float64
@ -188,7 +188,7 @@ func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float6
var result Result var result Result
// SQL-Berechnung der gewichteten Stunden // SQL calculation of weighted hours
err := GetEngine(ctx).Raw(` err := GetEngine(ctx).Raw(`
SELECT SUM( SELECT SUM(
EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0) 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 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) { func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, error) {
// Validierung // Validation
if err := create.Validate(); err != nil { 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 var timeEntry *TimeEntry
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { 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 { if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil {
return err return err
} }
// Zeiteintrag erstellen // Create time entry
newTimeEntry := TimeEntry{ newTimeEntry := TimeEntry{
UserID: create.UserID, UserID: create.UserID,
ProjectID: create.ProjectID, ProjectID: create.ProjectID,
@ -232,7 +232,7 @@ func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, e
} }
if err := tx.Create(&newTimeEntry).Error; err != nil { 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 timeEntry = &newTimeEntry
@ -246,59 +246,59 @@ func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, e
return timeEntry, nil 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 { func validateReferences(tx *gorm.DB, userID, projectID, activityID ulid.ULID) error {
// Benutzer prüfen // Check user
var userCount int64 var userCount int64
if err := tx.Model(&User{}).Where("id = ?", userID).Count(&userCount).Error; err != nil { 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 { 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 var projectCount int64
if err := tx.Model(&Project{}).Where("id = ?", projectID).Count(&projectCount).Error; err != nil { 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 { 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 var activityCount int64
if err := tx.Model(&Activity{}).Where("id = ?", activityID).Count(&activityCount).Error; err != nil { 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 { if activityCount == 0 {
return errors.New("die angegebene Aktivität existiert nicht") return errors.New("the specified activity does not exist")
} }
return nil 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) { func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, error) {
// Validierung // Validation
if err := update.Validate(); err != nil { 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) timeEntry, err := GetTimeEntryByID(ctx, update.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if timeEntry == nil { 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 { 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 { 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 userID := timeEntry.UserID
if update.UserID != nil { if update.UserID != nil {
userID = *update.UserID 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 start := timeEntry.Start
if update.Start != nil { if update.Start != nil {
start = *update.Start start = *update.Start
@ -331,12 +331,12 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e
} }
if end.Before(start) { 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 { 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 return nil
@ -346,15 +346,15 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e
return nil, err return nil, err
} }
// Aktualisierte Daten aus der Datenbank laden // Load updated data from the database
return GetTimeEntryByID(ctx, update.ID) 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 { func DeleteTimeEntry(ctx context.Context, id ulid.ULID) error {
result := GetEngine(ctx).Delete(&TimeEntry{}, id) result := GetEngine(ctx).Delete(&TimeEntry{}, id)
if result.Error != nil { 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 return nil
} }

View File

@ -16,9 +16,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
// Argon2 Parameter // Argon2 Parameters
const ( const (
// Empfohlene Werte für Argon2id // Recommended values for Argon2id
ArgonTime = 1 ArgonTime = 1
ArgonMemory = 64 * 1024 // 64MB ArgonMemory = 64 * 1024 // 64MB
ArgonThreads = 4 ArgonThreads = 4
@ -26,33 +26,33 @@ const (
SaltLength = 16 SaltLength = 16
) )
// Rollen-Konstanten // Role Constants
const ( const (
RoleAdmin = "admin" RoleAdmin = "admin"
RoleUser = "user" RoleUser = "user"
RoleViewer = "viewer" RoleViewer = "viewer"
) )
// User repräsentiert einen Benutzer im System // User represents a user in the system
type User struct { type User struct {
EntityBase EntityBase
Email string `gorm:"column:email;unique;not null"` Email string `gorm:"column:email;unique;not null"`
Salt string `gorm:"column:salt;not null;type:varchar(64)"` // Basis64-codierter Salt Salt string `gorm:"column:salt;not null;type:varchar(64)"` // Base64-encoded Salt
Hash string `gorm:"column:hash;not null;type:varchar(128)"` // Basis64-codierter Hash Hash string `gorm:"column:hash;not null;type:varchar(128)"` // Base64-encoded Hash
Role string `gorm:"column:role;not null;default:'user'"` Role string `gorm:"column:role;not null;default:'user'"`
CompanyID ulid.ULID `gorm:"column:company_id;type:uuid;not null;index"` CompanyID ulid.ULID `gorm:"column:company_id;type:uuid;not null;index"`
HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"` HourlyRate float64 `gorm:"column:hourly_rate;not null;default:0"`
// Beziehung für Eager Loading // Relationship for Eager Loading
Company *Company `gorm:"foreignKey:CompanyID"` Company *Company `gorm:"foreignKey:CompanyID"`
} }
// TableName gibt den Tabellennamen für GORM an // TableName provides the table name for GORM
func (User) TableName() string { func (User) TableName() string {
return "users" return "users"
} }
// UserCreate enthält die Felder zum Erstellen eines neuen Benutzers // UserCreate contains the fields for creating a new user
type UserCreate struct { type UserCreate struct {
Email string Email string
Password string Password string
@ -61,23 +61,23 @@ type UserCreate struct {
HourlyRate float64 HourlyRate float64
} }
// UserUpdate enthält die aktualisierbaren Felder eines Benutzers // UserUpdate contains the updatable fields of a user
type UserUpdate struct { type UserUpdate struct {
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates ID ulid.ULID `gorm:"-"` // Exclude from updates
Email *string `gorm:"column:email"` 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"` Role *string `gorm:"column:role"`
CompanyID *ulid.ULID `gorm:"column:company_id"` CompanyID *ulid.ULID `gorm:"column:company_id"`
HourlyRate *float64 `gorm:"column:hourly_rate"` 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 { type PasswordData struct {
Salt string Salt string
Hash string Hash string
} }
// GenerateSalt erzeugt einen kryptografisch sicheren Salt // GenerateSalt generates a cryptographically secure salt
func GenerateSalt() (string, error) { func GenerateSalt() (string, error) {
salt := make([]byte, SaltLength) salt := make([]byte, SaltLength)
_, err := rand.Read(salt) _, err := rand.Read(salt)
@ -87,20 +87,20 @@ func GenerateSalt() (string, error) {
return base64.StdEncoding.EncodeToString(salt), nil 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) { func HashPassword(password string) (PasswordData, error) {
// Erzeugen eines kryptografisch sicheren Salts // Generate a cryptographically secure salt
saltStr, err := GenerateSalt() saltStr, err := GenerateSalt()
if err != nil { 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) salt, err := base64.StdEncoding.DecodeString(saltStr)
if err != nil { 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) hash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen)
hashStr := base64.StdEncoding.EncodeToString(hash) hashStr := base64.StdEncoding.EncodeToString(hash)
@ -110,26 +110,26 @@ func HashPassword(password string) (PasswordData, error) {
}, nil }, 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) { func VerifyPassword(password, saltStr, hashStr string) (bool, error) {
salt, err := base64.StdEncoding.DecodeString(saltStr) salt, err := base64.StdEncoding.DecodeString(saltStr)
if err != nil { 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) hash, err := base64.StdEncoding.DecodeString(hashStr)
if err != nil { 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) 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 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 { func hmacEqual(a, b []byte) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false
@ -143,28 +143,28 @@ func hmacEqual(a, b []byte) bool {
return result == 0 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 { func (uc *UserCreate) Validate() error {
if uc.Email == "" { 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,}$`) emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(uc.Email) { if !emailRegex.MatchString(uc.Email) {
return errors.New("ungültiges email-format") return errors.New("invalid email format")
} }
if uc.Password == "" { 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 { 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 ( var (
hasUpper = false hasUpper = false
hasLower = false hasLower = false
@ -187,57 +187,57 @@ func (uc *UserCreate) Validate() error {
} }
if !hasUpper || !hasLower || !hasNumber || !hasSpecial { 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 == "" { if uc.Role == "" {
uc.Role = RoleUser // Standardrolle setzen uc.Role = RoleUser // Set default role
} else { } else {
validRoles := []string{RoleAdmin, RoleUser, RoleViewer} validRoles := []string{RoleAdmin, RoleUser, RoleViewer}
isValid := slices.Contains(validRoles, uc.Role) isValid := slices.Contains(validRoles, uc.Role)
if !isValid { 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, ", ")) uc.Role, strings.Join(validRoles, ", "))
} }
} }
if uc.CompanyID.Compare(ulid.ULID{}) == 0 { 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 { if uc.HourlyRate < 0 {
return errors.New("stundensatz darf nicht negativ sein") return errors.New("hourly rate cannot be negative")
} }
return nil 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 { func (uu *UserUpdate) Validate() error {
if uu.Email != nil && *uu.Email == "" { 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 { if uu.Email != nil {
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`) emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(*uu.Email) { 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 != nil {
if *uu.Password == "" { 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 { 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 ( var (
hasUpper = false hasUpper = false
hasLower = false hasLower = false
@ -260,11 +260,11 @@ func (uu *UserUpdate) Validate() error {
} }
if !hasUpper || !hasLower || !hasNumber || !hasSpecial { 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 { if uu.Role != nil {
validRoles := []string{RoleAdmin, RoleUser, RoleViewer} validRoles := []string{RoleAdmin, RoleUser, RoleViewer}
isValid := false isValid := false
@ -275,19 +275,19 @@ func (uu *UserUpdate) Validate() error {
} }
} }
if !isValid { 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, ", ")) *uu.Role, strings.Join(validRoles, ", "))
} }
} }
if uu.HourlyRate != nil && *uu.HourlyRate < 0 { 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 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) { func GetUserByID(ctx context.Context, id ulid.ULID) (*User, error) {
var user User var user User
result := GetEngine(ctx).Where("id = ?", id).First(&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 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) { func GetUserByEmail(ctx context.Context, email string) (*User, error) {
var user User var user User
result := GetEngine(ctx).Where("email = ?", email).First(&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 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) { func GetUserWithCompany(ctx context.Context, id ulid.ULID) (*User, error) {
var user User var user User
result := GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&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 return &user, nil
} }
// GetAllUsers gibt alle Benutzer zurück // GetAllUsers returns all users
func GetAllUsers(ctx context.Context) ([]User, error) { func GetAllUsers(ctx context.Context) ([]User, error) {
var users []User var users []User
result := GetEngine(ctx).Find(&users) result := GetEngine(ctx).Find(&users)
@ -336,7 +336,7 @@ func GetAllUsers(ctx context.Context) ([]User, error) {
return users, nil 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) { func GetUsersByCompanyID(ctx context.Context, companyID ulid.ULID) ([]User, error) {
var users []User var users []User
result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&users) 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 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) { func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
// Validierung // Validation
if err := create.Validate(); err != nil { 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 var user *User
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
// Prüfen, ob Email bereits existiert // Check if email already exists
var count int64 var count int64
if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil { 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 { 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 var companyCount int64
if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil { 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 { 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) pwData, err := HashPassword(create.Password)
if err != nil { 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{ newUser := User{
Email: create.Email, Email: create.Email,
Salt: pwData.Salt, Salt: pwData.Salt,
@ -392,7 +392,7 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
} }
if err := tx.Create(&newUser).Error; err != nil { 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 user = &newUser
@ -406,66 +406,66 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
return user, nil return user, nil
} }
// UpdateUser aktualisiert einen bestehenden Benutzer // UpdateUser updates an existing user
func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) { func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) {
// Validierung // Validation
if err := update.Validate(); err != nil { 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) user, err := GetUserByID(ctx, update.ID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user == nil { 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 { 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 { if update.Email != nil && *update.Email != user.Email {
var count int64 var count int64
if err := tx.Model(&User{}).Where("email = ? AND id != ?", *update.Email, update.ID).Count(&count).Error; err != nil { 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 { 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 { if update.CompanyID != nil && update.CompanyID.Compare(user.CompanyID) != 0 {
var companyCount int64 var companyCount int64
if err := tx.Model(&Company{}).Where("id = ?", *update.CompanyID).Count(&companyCount).Error; err != nil { 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 { 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 { if update.Password != nil {
pwData, err := HashPassword(*update.Password) pwData, err := HashPassword(*update.Password)
if err != nil { 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{}{ if err := tx.Model(user).Updates(map[string]interface{}{
"salt": pwData.Salt, "salt": pwData.Salt,
"hash": pwData.Hash, "hash": pwData.Hash,
}).Error; err != nil { }).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{}) 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 { if update.Email != nil {
updates["email"] = *update.Email updates["email"] = *update.Email
} }
@ -479,10 +479,10 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) {
updates["hourly_rate"] = *update.HourlyRate 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 len(updates) > 0 {
if err := tx.Model(user).Updates(updates).Error; err != nil { 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 return nil, err
} }
// Aktualisierte Daten aus der Datenbank laden // Load updated data from the database
return GetUserByID(ctx, update.ID) 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 { func DeleteUser(ctx context.Context, id ulid.ULID) error {
// Hier könnte man prüfen, ob abhängige Entitäten existieren // Here one could check if dependent entities exist
// z.B. nicht löschen, wenn noch Zeiteinträge vorhanden sind // e.g., don't delete if time entries still exist
result := GetEngine(ctx).Delete(&User{}, id) result := GetEngine(ctx).Delete(&User{}, id)
if result.Error != nil { 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 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) { func AuthenticateUser(ctx context.Context, email, password string) (*User, error) {
user, err := GetUserByEmail(ctx, email) user, err := GetUserByEmail(ctx, email)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user == nil { if user == nil {
// Gleiche Fehlermeldung, um keine Informationen über existierende Accounts preiszugeben // Same error message to avoid revealing information about existing accounts
return nil, errors.New("ungültige Anmeldeinformationen") 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) isValid, err := VerifyPassword(password, user.Salt, user.Hash)
if err != nil { 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 { if !isValid {
return nil, errors.New("ungültige Anmeldeinformationen") return nil, errors.New("invalid login credentials")
} }
return user, nil return user, nil