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