refactor: Remove unused entity and datasource files; add AuthDto for authentication, simplification
This commit is contained in:
parent
3b0b2b4340
commit
7f275c774e
@ -2,13 +2,14 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
ginSwagger "github.com/swaggo/gin-swagger"
|
ginSwagger "github.com/swaggo/gin-swagger"
|
||||||
_ "github.com/timetracker/backend/docs" // This line is important for swag to work
|
_ "github.com/timetracker/backend/docs" // This line is important for swag to work
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db"
|
"github.com/timetracker/backend/internal/models"
|
||||||
_ "gorm.io/driver/postgres"
|
_ "gorm.io/driver/postgres"
|
||||||
// GORM IMPORTS MARKER
|
// GORM IMPORTS MARKER
|
||||||
)
|
)
|
||||||
@ -31,15 +32,19 @@ func helloHandler(c *gin.Context) {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|
||||||
db, _ := db.NewDatasourceContainer(db.DatabaseConfig{
|
dbConfig := models.DatabaseConfig{
|
||||||
Host: "localhost",
|
Host: "localhost",
|
||||||
Port: 5432,
|
Port: 5432,
|
||||||
User: "timetracker",
|
User: "postgres",
|
||||||
Password: "timetracker",
|
Password: "password",
|
||||||
DBName: "timetracker",
|
DBName: "mydatabase",
|
||||||
SSLMode: "disable",
|
SSLMode: "disable", // Für Entwicklungsumgebung
|
||||||
})
|
}
|
||||||
|
|
||||||
|
// Datenbank initialisieren
|
||||||
|
if err := models.InitDB(dbConfig); err != nil {
|
||||||
|
log.Fatalf("Fehler bei der DB-Initialisierung: %v", err)
|
||||||
|
}
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
r.GET("/", helloHandler)
|
r.GET("/", helloHandler)
|
||||||
|
@ -48,7 +48,7 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
golang.org/x/arch v0.15.0 // indirect
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
golang.org/x/crypto v0.36.0 // indirect
|
golang.org/x/crypto v0.36.0
|
||||||
golang.org/x/net v0.37.0 // indirect
|
golang.org/x/net v0.37.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
@ -1,22 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Activity struct {
|
|
||||||
EntityBase
|
|
||||||
Name string
|
|
||||||
BillingRate float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActivityUpdate struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
Name *string
|
|
||||||
BillingRate *float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type ActivityCreate struct {
|
|
||||||
Name string
|
|
||||||
BillingRate float64
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type EntityBase struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
CreatedAt time.Time
|
|
||||||
UpdatedAt time.Time
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "github.com/oklog/ulid/v2"
|
|
||||||
|
|
||||||
type Company struct {
|
|
||||||
EntityBase
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CompanyCreate struct {
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
type CompanyUpdate struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
Name *string
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "github.com/oklog/ulid/v2"
|
|
||||||
|
|
||||||
type Customer struct {
|
|
||||||
EntityBase
|
|
||||||
Name string
|
|
||||||
CompanyID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerCreate struct {
|
|
||||||
Name string
|
|
||||||
CompanyID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomerUpdate struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
Name *string
|
|
||||||
CompanyID *int
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "github.com/oklog/ulid/v2"
|
|
||||||
|
|
||||||
type Project struct {
|
|
||||||
EntityBase
|
|
||||||
Name string
|
|
||||||
CustomerID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectCreate struct {
|
|
||||||
Name string
|
|
||||||
CustomerID int
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProjectUpdate struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
Name *string
|
|
||||||
CustomerID *int
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeEntry struct {
|
|
||||||
EntityBase
|
|
||||||
UserID int
|
|
||||||
ProjectID int
|
|
||||||
ActivityID int
|
|
||||||
Start time.Time
|
|
||||||
End time.Time
|
|
||||||
Description string
|
|
||||||
Billable int // Percentage (0-100)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeEntryCreate struct {
|
|
||||||
UserID int
|
|
||||||
ProjectID int
|
|
||||||
ActivityID int
|
|
||||||
Start time.Time
|
|
||||||
End time.Time
|
|
||||||
Description string
|
|
||||||
Billable int // Percentage (0-100)
|
|
||||||
}
|
|
||||||
|
|
||||||
type TimeEntryUpdate struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
UserID *int
|
|
||||||
ProjectID *int
|
|
||||||
ActivityID *int
|
|
||||||
Start *time.Time
|
|
||||||
End *time.Time
|
|
||||||
Description *string
|
|
||||||
Billable *int // Percentage (0-100)
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package entities
|
|
||||||
|
|
||||||
import "github.com/oklog/ulid/v2"
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
EntityBase
|
|
||||||
Email string
|
|
||||||
Salt string
|
|
||||||
Role string
|
|
||||||
CompanyID int
|
|
||||||
HourlyRate float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserCreate struct {
|
|
||||||
Email string
|
|
||||||
Password string
|
|
||||||
Role string
|
|
||||||
CompanyID int
|
|
||||||
HourlyRate float64
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserUpdate struct {
|
|
||||||
ID ulid.ULID
|
|
||||||
Email *string
|
|
||||||
Password *string
|
|
||||||
Role *string
|
|
||||||
CompanyID *int
|
|
||||||
HourlyRate *float64
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActivityDatasource interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Activity, error)
|
|
||||||
Create(ctx context.Context, activity *entities.Activity) error
|
|
||||||
Update(ctx context.Context, activity *entities.Activity) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CompanyDatasource interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Company, error)
|
|
||||||
Create(ctx context.Context, company *entities.Company) error
|
|
||||||
Update(ctx context.Context, company *entities.Company) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CustomerDatasource interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Customer, error)
|
|
||||||
Create(ctx context.Context, customer *entities.Customer) error
|
|
||||||
Update(ctx context.Context, customer *entities.Customer) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProjectDatasource interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Project, error)
|
|
||||||
Create(ctx context.Context, project *entities.Project) error
|
|
||||||
Update(ctx context.Context, project *entities.Project) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeEntryDatasource interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.TimeEntry, error)
|
|
||||||
Create(ctx context.Context, timeEntry *entities.TimeEntry) error
|
|
||||||
Update(ctx context.Context, timeEntry *entities.TimeEntry) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
GetByRange(ctx context.Context, userID ulid.ULID, from time.Time, to time.Time) ([]*entities.TimeEntry, error)
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package persistence
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserDatasource interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.User, error)
|
|
||||||
Create(ctx context.Context, user *entities.User, passwordHash string, salt string) error
|
|
||||||
Update(ctx context.Context, user *entities.User, passwordHash *string) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
GetByEmail(ctx context.Context, email string) (*entities.User, error)
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActivityRepository interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Activity, error)
|
|
||||||
Create(ctx context.Context, activity *entities.ActivityCreate) error
|
|
||||||
Update(ctx context.Context, activity *entities.ActivityUpdate) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CompanyRepository interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Company, error)
|
|
||||||
Create(ctx context.Context, company *entities.CompanyCreate) error
|
|
||||||
Update(ctx context.Context, company *entities.CompanyUpdate) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CustomerRepository interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Customer, error)
|
|
||||||
Create(ctx context.Context, customer *entities.CustomerCreate) error
|
|
||||||
Update(ctx context.Context, customer *entities.CustomerUpdate) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProjectRepository interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.Project, error)
|
|
||||||
Create(ctx context.Context, project *entities.ProjectCreate) error
|
|
||||||
Update(ctx context.Context, project *entities.ProjectUpdate) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeEntryRepository interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.TimeEntry, error)
|
|
||||||
Create(ctx context.Context, timeEntry *entities.TimeEntryCreate) error
|
|
||||||
Update(ctx context.Context, timeEntry *entities.TimeEntryUpdate) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
GetByRange(ctx context.Context, userID ulid.ULID, from time.Time, to time.Time) ([]*entities.TimeEntry, error)
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
package repositories
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserRepository interface {
|
|
||||||
Get(ctx context.Context, id ulid.ULID) (*entities.User, error)
|
|
||||||
Create(ctx context.Context, user *entities.UserCreate) error
|
|
||||||
Update(ctx context.Context, user *entities.UserUpdate) error
|
|
||||||
Delete(ctx context.Context, id ulid.ULID) error
|
|
||||||
GetByUsername(ctx context.Context, email string) (*entities.User, error)
|
|
||||||
}
|
|
@ -1,99 +0,0 @@
|
|||||||
package db
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/ds"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DatabaseConfig enthält die Konfigurationsinformationen für die Datenbankverbindung
|
|
||||||
type DatabaseConfig struct {
|
|
||||||
Host string
|
|
||||||
Port int
|
|
||||||
User string
|
|
||||||
Password string
|
|
||||||
DBName string
|
|
||||||
SSLMode string
|
|
||||||
}
|
|
||||||
|
|
||||||
// DatasourceContainer enthält alle Datasource-Instanzen
|
|
||||||
type DatasourceContainer struct {
|
|
||||||
ActivityDatasource persistence.ActivityDatasource
|
|
||||||
CompanyDatasource persistence.CompanyDatasource
|
|
||||||
CustomerDatasource persistence.CustomerDatasource
|
|
||||||
ProjectDatasource persistence.ProjectDatasource
|
|
||||||
TimeEntryDatasource persistence.TimeEntryDatasource
|
|
||||||
UserDatasource persistence.UserDatasource
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDatasourceContainer erstellt und initialisiert alle Datasource-Instanzen
|
|
||||||
func NewDatasourceContainer(config DatabaseConfig) (*DatasourceContainer, error) {
|
|
||||||
// Erstelle DSN (Data Source Name) für die Datenbankverbindung
|
|
||||||
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)
|
|
||||||
|
|
||||||
// Erstelle SQL-Datenbankverbindung
|
|
||||||
sqlDB, err := sql.Open("pgx", dsn)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fehler beim Öffnen der SQL-Verbindung: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Konfiguriere Verbindungspool
|
|
||||||
sqlDB.SetMaxIdleConns(10)
|
|
||||||
sqlDB.SetMaxOpenConns(100)
|
|
||||||
|
|
||||||
// Initialisiere GORM mit der SQL-Verbindung
|
|
||||||
gormDB, err := gorm.Open(postgres.New(postgres.Config{
|
|
||||||
Conn: sqlDB,
|
|
||||||
}), &gorm.Config{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("fehler beim Initialisieren von GORM: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Erstelle alle Datasource-Instanzen
|
|
||||||
return &DatasourceContainer{
|
|
||||||
ActivityDatasource: ds.NewActivityDatasource(gormDB),
|
|
||||||
CompanyDatasource: ds.NewCompanyDatasource(gormDB),
|
|
||||||
CustomerDatasource: ds.NewCustomerDatasource(gormDB),
|
|
||||||
ProjectDatasource: ds.NewProjectDatasource(gormDB),
|
|
||||||
TimeEntryDatasource: ds.NewTimeEntryDatasource(gormDB),
|
|
||||||
UserDatasource: ds.NewUserDatasource(gormDB),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close schließt die Datenbankverbindung
|
|
||||||
func (r *DatasourceContainer) Close() error {
|
|
||||||
db, err := r.getGormDB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return sqlDB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper-Methode, um die GORM-DB aus einem der Repositories zu extrahieren
|
|
||||||
func (r *DatasourceContainer) getGormDB() (*gorm.DB, error) {
|
|
||||||
// Wir nehmen an, dass alle Repositories das gleiche DB-Handle verwenden
|
|
||||||
// Deshalb können wir einfach eines der Repositories nehmen
|
|
||||||
// Dies funktioniert nur, wenn wir Zugriff auf die interne DB haben oder eine Methode hinzufügen
|
|
||||||
// Hier müsste angepasst werden, wie Sie Zugriff auf die GORM-DB bekommen
|
|
||||||
|
|
||||||
// Beispiel (müsste angepasst werden):
|
|
||||||
// activityDS, ok := r.ActivityDatasource.(*ds.ActivityDatasource)
|
|
||||||
// if !ok {
|
|
||||||
// return nil, fmt.Errorf("Konnte GORM-DB nicht aus ActivityDatasource extrahieren")
|
|
||||||
// }
|
|
||||||
// return activityDS.GetDB(), nil
|
|
||||||
|
|
||||||
// Placeholder für die tatsächliche Implementierung:
|
|
||||||
return nil, fmt.Errorf("getGormDB() muss implementiert werden")
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package dbo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActivityDBO struct {
|
|
||||||
gorm.Model
|
|
||||||
ID ulid.ULID `gorm:"primaryKey;type:uuid"`
|
|
||||||
CreatedAt time.Time `gorm:"not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null"`
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
Name string `gorm:"type:varchar(255);not null"`
|
|
||||||
BillingRate float64 `gorm:"type:decimal(10,2)"`
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package dbo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CompanyDBO struct {
|
|
||||||
gorm.Model
|
|
||||||
ID ulid.ULID `gorm:"primaryKey;type:uuid"`
|
|
||||||
CreatedAt time.Time `gorm:"not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null"`
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
Name string `gorm:"type:varchar(255);not null"`
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package dbo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CustomerDBO struct {
|
|
||||||
gorm.Model
|
|
||||||
ID ulid.ULID `gorm:"primaryKey;type:uuid"`
|
|
||||||
CreatedAt time.Time `gorm:"not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null"`
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
Name string `gorm:"type:varchar(255);not null"`
|
|
||||||
CompanyID int
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package dbo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProjectDBO struct {
|
|
||||||
gorm.Model
|
|
||||||
ID ulid.ULID `gorm:"primaryKey;type:uuid"`
|
|
||||||
CreatedAt time.Time `gorm:"not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null"`
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
Name string `gorm:"type:varchar(255);not null"`
|
|
||||||
CustomerID int
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
package dbo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeEntryDBO struct {
|
|
||||||
gorm.Model
|
|
||||||
ID ulid.ULID `gorm:"primaryKey;type:uuid"`
|
|
||||||
CreatedAt time.Time `gorm:"not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null"`
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
UserID int
|
|
||||||
ProjectID int
|
|
||||||
ActivityID int
|
|
||||||
Start time.Time
|
|
||||||
End time.Time
|
|
||||||
Description string
|
|
||||||
Billable int // Percentage (0-100)
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
package dbo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserDBO struct {
|
|
||||||
gorm.Model
|
|
||||||
ID ulid.ULID `gorm:"primaryKey;type:uuid"`
|
|
||||||
CreatedAt time.Time `gorm:"not null"`
|
|
||||||
UpdatedAt time.Time `gorm:"not null"`
|
|
||||||
LastEditorID ulid.ULID
|
|
||||||
Email string
|
|
||||||
PasswordHash string
|
|
||||||
Salt string
|
|
||||||
Role string
|
|
||||||
CompanyID int
|
|
||||||
HourlyRate float64
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package ds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/dbo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ActivityDatasource struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewActivityDatasource(db *gorm.DB) persistence.ActivityDatasource {
|
|
||||||
return &ActivityDatasource{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ActivityDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.Activity, error) {
|
|
||||||
var activityDBO dbo.ActivityDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&activityDBO, "id = ?", id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
activity := &entities.Activity{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: activityDBO.ID,
|
|
||||||
CreatedAt: activityDBO.CreatedAt,
|
|
||||||
UpdatedAt: activityDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
Name: activityDBO.Name,
|
|
||||||
BillingRate: activityDBO.BillingRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return activity, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ActivityDatasource) Create(ctx context.Context, activity *entities.Activity) error {
|
|
||||||
activityDBO := dbo.ActivityDBO{
|
|
||||||
ID: activity.ID,
|
|
||||||
CreatedAt: activity.CreatedAt,
|
|
||||||
UpdatedAt: activity.UpdatedAt,
|
|
||||||
Name: activity.Name,
|
|
||||||
BillingRate: activity.BillingRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Create(&activityDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ActivityDatasource) Update(ctx context.Context, activity *entities.Activity) error {
|
|
||||||
activityDBO := dbo.ActivityDBO{
|
|
||||||
ID: activity.ID,
|
|
||||||
CreatedAt: activity.CreatedAt,
|
|
||||||
UpdatedAt: activity.UpdatedAt,
|
|
||||||
Name: activity.Name,
|
|
||||||
BillingRate: activity.BillingRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Save(&activityDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ActivityDatasource) Delete(ctx context.Context, id ulid.ULID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&dbo.ActivityDBO{}, "id = ?", id).Error
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
package ds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/dbo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CompanyyDatasource struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCompanyDatasource(db *gorm.DB) persistence.CompanyDatasource {
|
|
||||||
return &CompanyyDatasource{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CompanyyDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.Company, error) {
|
|
||||||
var companyDBO dbo.CompanyDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&companyDBO, "id = ?", id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
company := &entities.Company{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: companyDBO.ID,
|
|
||||||
CreatedAt: companyDBO.CreatedAt,
|
|
||||||
UpdatedAt: companyDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
Name: companyDBO.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
return company, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CompanyyDatasource) Create(ctx context.Context, company *entities.Company) error {
|
|
||||||
companyDBO := dbo.CompanyDBO{
|
|
||||||
ID: company.ID,
|
|
||||||
CreatedAt: company.CreatedAt,
|
|
||||||
UpdatedAt: company.UpdatedAt,
|
|
||||||
Name: company.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Create(&companyDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CompanyyDatasource) Update(ctx context.Context, company *entities.Company) error {
|
|
||||||
companyDBO := dbo.CompanyDBO{
|
|
||||||
ID: company.ID,
|
|
||||||
CreatedAt: company.CreatedAt,
|
|
||||||
UpdatedAt: company.UpdatedAt,
|
|
||||||
Name: company.Name,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Save(&companyDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CompanyyDatasource) Delete(ctx context.Context, id ulid.ULID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&dbo.CompanyDBO{}, "id = ?", id).Error
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package ds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/dbo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CustomerDatasource struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCustomerDatasource(db *gorm.DB) persistence.CustomerDatasource {
|
|
||||||
return &CustomerDatasource{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.Customer, error) {
|
|
||||||
var customerDBO dbo.CustomerDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&customerDBO, "id = ?", id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
customer := &entities.Customer{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: customerDBO.ID,
|
|
||||||
CreatedAt: customerDBO.CreatedAt,
|
|
||||||
UpdatedAt: customerDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
Name: customerDBO.Name,
|
|
||||||
CompanyID: customerDBO.CompanyID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return customer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerDatasource) Create(ctx context.Context, customer *entities.Customer) error {
|
|
||||||
customerDBO := dbo.CustomerDBO{
|
|
||||||
ID: customer.ID,
|
|
||||||
CreatedAt: customer.CreatedAt,
|
|
||||||
UpdatedAt: customer.UpdatedAt,
|
|
||||||
Name: customer.Name,
|
|
||||||
CompanyID: customer.CompanyID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Create(&customerDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerDatasource) Update(ctx context.Context, customer *entities.Customer) error {
|
|
||||||
customerDBO := dbo.CustomerDBO{
|
|
||||||
ID: customer.ID,
|
|
||||||
CreatedAt: customer.CreatedAt,
|
|
||||||
UpdatedAt: customer.UpdatedAt,
|
|
||||||
Name: customer.Name,
|
|
||||||
CompanyID: customer.CompanyID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Save(&customerDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *CustomerDatasource) Delete(ctx context.Context, id ulid.ULID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&dbo.CustomerDBO{}, "id = ?", id).Error
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package ds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/dbo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProjectDatasource struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProjectDatasource(db *gorm.DB) persistence.ProjectDatasource {
|
|
||||||
return &ProjectDatasource{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.Project, error) {
|
|
||||||
var projectDBO dbo.ProjectDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&projectDBO, "id = ?", id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
project := &entities.Project{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: projectDBO.ID,
|
|
||||||
CreatedAt: projectDBO.CreatedAt,
|
|
||||||
UpdatedAt: projectDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
Name: projectDBO.Name,
|
|
||||||
CustomerID: projectDBO.CustomerID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return project, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectDatasource) Create(ctx context.Context, project *entities.Project) error {
|
|
||||||
projectDBO := dbo.ProjectDBO{
|
|
||||||
ID: project.ID,
|
|
||||||
CreatedAt: project.CreatedAt,
|
|
||||||
UpdatedAt: project.UpdatedAt,
|
|
||||||
Name: project.Name,
|
|
||||||
CustomerID: project.CustomerID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Create(&projectDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectDatasource) Update(ctx context.Context, project *entities.Project) error {
|
|
||||||
projectDBO := dbo.ProjectDBO{
|
|
||||||
ID: project.ID,
|
|
||||||
CreatedAt: project.CreatedAt,
|
|
||||||
UpdatedAt: project.UpdatedAt,
|
|
||||||
Name: project.Name,
|
|
||||||
CustomerID: project.CustomerID,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Save(&projectDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *ProjectDatasource) Delete(ctx context.Context, id ulid.ULID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&dbo.ProjectDBO{}, "id = ?", id).Error
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
package ds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/dbo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TimeEntryDatasource struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTimeEntryDatasource(db *gorm.DB) persistence.TimeEntryDatasource {
|
|
||||||
return &TimeEntryDatasource{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimeEntryDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.TimeEntry, error) {
|
|
||||||
var timeEntryDBO dbo.TimeEntryDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&timeEntryDBO, "id = ?", id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
timeEntry := &entities.TimeEntry{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: timeEntryDBO.ID,
|
|
||||||
CreatedAt: timeEntryDBO.CreatedAt,
|
|
||||||
UpdatedAt: timeEntryDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
UserID: timeEntryDBO.UserID,
|
|
||||||
ProjectID: timeEntryDBO.ProjectID,
|
|
||||||
ActivityID: timeEntryDBO.ActivityID,
|
|
||||||
Start: timeEntryDBO.Start,
|
|
||||||
End: timeEntryDBO.End,
|
|
||||||
Description: timeEntryDBO.Description,
|
|
||||||
Billable: timeEntryDBO.Billable,
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimeEntryDatasource) Create(ctx context.Context, timeEntry *entities.TimeEntry) error {
|
|
||||||
timeEntryDBO := dbo.TimeEntryDBO{
|
|
||||||
ID: timeEntry.ID,
|
|
||||||
CreatedAt: timeEntry.CreatedAt,
|
|
||||||
UpdatedAt: timeEntry.UpdatedAt,
|
|
||||||
UserID: timeEntry.UserID,
|
|
||||||
ProjectID: timeEntry.ProjectID,
|
|
||||||
ActivityID: timeEntry.ActivityID,
|
|
||||||
Start: timeEntry.Start,
|
|
||||||
End: timeEntry.End,
|
|
||||||
Description: timeEntry.Description,
|
|
||||||
Billable: timeEntry.Billable,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Create(&timeEntryDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimeEntryDatasource) Update(ctx context.Context, timeEntry *entities.TimeEntry) error {
|
|
||||||
var existingEntry dbo.TimeEntryDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&existingEntry, "id = ?", timeEntry.ID).Error; err != nil {
|
|
||||||
return entities.ErrTimeEntryNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
updateData := map[string]any{
|
|
||||||
"user_id": timeEntry.UserID,
|
|
||||||
"project_id": timeEntry.ProjectID,
|
|
||||||
"activity_id": timeEntry.ActivityID,
|
|
||||||
"start": timeEntry.Start,
|
|
||||||
"end": timeEntry.End,
|
|
||||||
"description": timeEntry.Description,
|
|
||||||
"billable": timeEntry.Billable,
|
|
||||||
"updated_at": gorm.Expr("NOW()"), // Optional: Automatisches Update-Datum
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Model(&dbo.TimeEntryDBO{}).Where("id = ?", timeEntry.ID).Updates(updateData).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimeEntryDatasource) Delete(ctx context.Context, id ulid.ULID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&dbo.TimeEntryDBO{}, "id = ?", id).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *TimeEntryDatasource) GetByRange(ctx context.Context, userID ulid.ULID, from time.Time, to time.Time) ([]*entities.TimeEntry, error) {
|
|
||||||
var timeEntryDBOs []*dbo.TimeEntryDBO
|
|
||||||
if err := r.db.WithContext(ctx).Where("user_id = ? AND start_time >= ? AND end_time <= ?", userID, from, to).Find(&timeEntryDBOs).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
timeEntries := make([]*entities.TimeEntry, len(timeEntryDBOs))
|
|
||||||
for i, timeEntryDBO := range timeEntryDBOs {
|
|
||||||
timeEntries[i] = &entities.TimeEntry{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: timeEntryDBO.ID,
|
|
||||||
CreatedAt: timeEntryDBO.CreatedAt,
|
|
||||||
UpdatedAt: timeEntryDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
UserID: timeEntryDBO.UserID,
|
|
||||||
ProjectID: timeEntryDBO.ProjectID,
|
|
||||||
ActivityID: timeEntryDBO.ActivityID,
|
|
||||||
Start: timeEntryDBO.Start,
|
|
||||||
End: timeEntryDBO.End,
|
|
||||||
Description: timeEntryDBO.Description,
|
|
||||||
Billable: timeEntryDBO.Billable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return timeEntries, nil
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
package ds
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/domain/entities"
|
|
||||||
"github.com/timetracker/backend/internal/domain/persistence"
|
|
||||||
"github.com/timetracker/backend/internal/infrastructure/persistence/db/dbo"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
type UserDatasource struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserDatasource(db *gorm.DB) persistence.UserDatasource {
|
|
||||||
return &UserDatasource{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserDatasource) Get(ctx context.Context, id ulid.ULID) (*entities.User, error) {
|
|
||||||
var userDBO dbo.UserDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&userDBO, "id = ?", id).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &entities.User{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: userDBO.ID,
|
|
||||||
CreatedAt: userDBO.CreatedAt,
|
|
||||||
UpdatedAt: userDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
Email: userDBO.Email,
|
|
||||||
Salt: userDBO.Salt,
|
|
||||||
Role: userDBO.Role,
|
|
||||||
CompanyID: userDBO.CompanyID,
|
|
||||||
HourlyRate: userDBO.HourlyRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserDatasource) Create(ctx context.Context, user *entities.User, passwordHash string, salt string) error {
|
|
||||||
|
|
||||||
old := r.db.WithContext(ctx).First(&dbo.UserDBO{}, "email = ?", user.Email)
|
|
||||||
if old.Error == nil {
|
|
||||||
return entities.ErrUserAlreadyExists
|
|
||||||
}
|
|
||||||
|
|
||||||
userDBO := dbo.UserDBO{
|
|
||||||
ID: user.ID,
|
|
||||||
CreatedAt: user.CreatedAt,
|
|
||||||
UpdatedAt: user.UpdatedAt,
|
|
||||||
Email: user.Email,
|
|
||||||
PasswordHash: passwordHash,
|
|
||||||
Salt: salt,
|
|
||||||
Role: user.Role,
|
|
||||||
CompanyID: user.CompanyID,
|
|
||||||
HourlyRate: user.HourlyRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Create(&userDBO).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserDatasource) Update(ctx context.Context, user *entities.User, passwordHash *string) error {
|
|
||||||
var existingUser dbo.UserDBO
|
|
||||||
if err := r.db.WithContext(ctx).First(&existingUser, "id = ?", user.ID).Error; err != nil {
|
|
||||||
return entities.ErrUserNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nur relevante Felder aktualisieren
|
|
||||||
updateData := map[string]interface{}{
|
|
||||||
"email": user.Email,
|
|
||||||
"role": user.Role,
|
|
||||||
"company_id": user.CompanyID,
|
|
||||||
"hourly_rate": user.HourlyRate,
|
|
||||||
"updated_at": gorm.Expr("NOW()"), // Optional: Automatisch das Update-Datum setzen
|
|
||||||
}
|
|
||||||
|
|
||||||
if passwordHash != nil {
|
|
||||||
updateData["password_hash"] = *passwordHash
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.db.WithContext(ctx).Model(&dbo.UserDBO{}).Where("id = ?", user.ID).Updates(updateData).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserDatasource) Delete(ctx context.Context, id ulid.ULID) error {
|
|
||||||
return r.db.WithContext(ctx).Delete(&dbo.UserDBO{}, "id = ?", id).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *UserDatasource) GetByEmail(ctx context.Context, email string) (*entities.User, error) {
|
|
||||||
var userDBO dbo.UserDBO
|
|
||||||
if err := r.db.WithContext(ctx).Where("email = ?", email).First(&userDBO).Error; err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user := &entities.User{
|
|
||||||
EntityBase: entities.EntityBase{
|
|
||||||
ID: userDBO.ID,
|
|
||||||
CreatedAt: userDBO.CreatedAt,
|
|
||||||
UpdatedAt: userDBO.UpdatedAt,
|
|
||||||
},
|
|
||||||
Email: userDBO.Email,
|
|
||||||
Salt: userDBO.Salt,
|
|
||||||
Role: userDBO.Role,
|
|
||||||
CompanyID: userDBO.CompanyID,
|
|
||||||
HourlyRate: userDBO.HourlyRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
return user, nil
|
|
||||||
}
|
|
96
backend/internal/models/activity.go
Normal file
96
backend/internal/models/activity.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Activity repräsentiert eine Aktivität im System
|
||||||
|
type Activity struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
BillingRate float64 `gorm:"column:billing_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName gibt den Tabellennamen für GORM an
|
||||||
|
func (Activity) TableName() string {
|
||||||
|
return "activities"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActivityUpdate enthält die aktualisierbaren Felder einer Activity
|
||||||
|
type ActivityUpdate struct {
|
||||||
|
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 enthält die Felder zum Erstellen einer neuen Activity
|
||||||
|
type ActivityCreate struct {
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
BillingRate float64 `gorm:"column:billing_rate"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllActivities gibt alle Activities zurück
|
||||||
|
func GetAllActivities(ctx context.Context) ([]Activity, error) {
|
||||||
|
var activities []Activity
|
||||||
|
result := GetEngine(ctx).Find(&activities)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return activities, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateActivity erstellt eine neue Activity
|
||||||
|
func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, error) {
|
||||||
|
activity := Activity{
|
||||||
|
Name: create.Name,
|
||||||
|
BillingRate: create.BillingRate,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&activity)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &activity, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 nicht gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generische Update-Funktion verwenden
|
||||||
|
if err := UpdateModel(ctx, activity, update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Daten aus der Datenbank laden
|
||||||
|
return GetActivityByID(ctx, update.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
|
||||||
|
}
|
26
backend/internal/models/base.go
Normal file
26
backend/internal/models/base.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EntityBase struct {
|
||||||
|
ID ulid.ULID `gorm:"type:uuid;primaryKey"`
|
||||||
|
CreatedAt time.Time `gorm:"index"`
|
||||||
|
UpdatedAt time.Time `gorm:"index"`
|
||||||
|
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 { // 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)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
101
backend/internal/models/company.go
Normal file
101
backend/internal/models/company.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Company repräsentiert ein Unternehmen im System
|
||||||
|
type Company struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName gibt den Tabellennamen für GORM an
|
||||||
|
func (Company) TableName() string {
|
||||||
|
return "companies"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompanyCreate enthält die Felder zum Erstellen eines neuen Unternehmens
|
||||||
|
type CompanyCreate struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompanyUpdate enthält die aktualisierbaren Felder eines Unternehmens
|
||||||
|
type CompanyUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &company, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCompanies gibt alle Unternehmen zurück
|
||||||
|
func GetAllCompanies(ctx context.Context) ([]Company, error) {
|
||||||
|
var companies []Company
|
||||||
|
result := GetEngine(ctx).Find(&companies)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return companies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, error) {
|
||||||
|
var customers []Customer
|
||||||
|
result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&customers)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return customers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCompany erstellt ein neues Unternehmen
|
||||||
|
func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) {
|
||||||
|
company := Company{
|
||||||
|
Name: create.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&company)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &company, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 nicht gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generische Update-Funktion verwenden
|
||||||
|
if err := UpdateModel(ctx, company, update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Daten aus der Datenbank laden
|
||||||
|
return GetCompanyByID(ctx, update.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
|
||||||
|
}
|
96
backend/internal/models/customer.go
Normal file
96
backend/internal/models/customer.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Customer repräsentiert einen Kunden im System
|
||||||
|
type Customer struct {
|
||||||
|
EntityBase
|
||||||
|
Name string `gorm:"column:name"`
|
||||||
|
CompanyID int `gorm:"column:company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName gibt den Tabellennamen für GORM an
|
||||||
|
func (Customer) TableName() string {
|
||||||
|
return "customers"
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomerCreate enthält die Felder zum Erstellen eines neuen Kunden
|
||||||
|
type CustomerCreate struct {
|
||||||
|
Name string
|
||||||
|
CompanyID int
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomerUpdate enthält die aktualisierbaren Felder eines Kunden
|
||||||
|
type CustomerUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
CompanyID *int `gorm:"column:company_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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &customer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllCustomers gibt alle Kunden zurück
|
||||||
|
func GetAllCustomers(ctx context.Context) ([]Customer, error) {
|
||||||
|
var customers []Customer
|
||||||
|
result := GetEngine(ctx).Find(&customers)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return customers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateCustomer erstellt einen neuen Kunden
|
||||||
|
func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, error) {
|
||||||
|
customer := Customer{
|
||||||
|
Name: create.Name,
|
||||||
|
CompanyID: create.CompanyID,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&customer)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &customer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 nicht gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generische Update-Funktion verwenden
|
||||||
|
if err := UpdateModel(ctx, customer, update); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Daten aus der Datenbank laden
|
||||||
|
return GetCustomerByID(ctx, update.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
|
||||||
|
}
|
108
backend/internal/models/db.go
Normal file
108
backend/internal/models/db.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/driver/postgres" // Für PostgreSQL
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Globale Variable für die DB-Verbindung
|
||||||
|
var defaultDB *gorm.DB
|
||||||
|
|
||||||
|
// DatabaseConfig enthält die Konfigurationsdaten für die Datenbankverbindung
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
DBName string
|
||||||
|
SSLMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitDB initialisiert die Datenbankverbindung (einmalig beim Start)
|
||||||
|
// mit der übergebenen Konfiguration
|
||||||
|
func InitDB(config DatabaseConfig) error {
|
||||||
|
// 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)
|
||||||
|
|
||||||
|
// Datenbankverbindung herstellen
|
||||||
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Verbinden zur Datenbank: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultDB = db
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEngine gibt die DB-Instanz zurück, ggf. mit context
|
||||||
|
func GetEngine(ctx context.Context) *gorm.DB {
|
||||||
|
// Falls in ctx eine spezielle Transaktion steckt, könnte man das hier prüfen
|
||||||
|
return defaultDB.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateModel aktualisiert ein Modell anhand der gesetzten Pointer-Felder
|
||||||
|
func UpdateModel(ctx context.Context, model any, updates any) error {
|
||||||
|
updateValue := reflect.ValueOf(updates)
|
||||||
|
|
||||||
|
// Wenn updates ein Pointer ist, den Wert dahinter verwenden
|
||||||
|
if updateValue.Kind() == reflect.Ptr {
|
||||||
|
updateValue = updateValue.Elem()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stelle sicher, dass updates eine Struktur ist
|
||||||
|
if updateValue.Kind() != reflect.Struct {
|
||||||
|
return errors.New("updates muss eine Struktur sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateType := updateValue.Type()
|
||||||
|
updateMap := make(map[string]any)
|
||||||
|
|
||||||
|
// Durch alle Felder iterieren
|
||||||
|
for i := 0; i < updateValue.NumField(); i++ {
|
||||||
|
field := updateValue.Field(i)
|
||||||
|
fieldType := updateType.Field(i)
|
||||||
|
|
||||||
|
// Überspringen von unexportierten Feldern
|
||||||
|
if !fieldType.IsExported() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spezialfall: ID-Feld überspringen (nur für Updates verwenden)
|
||||||
|
if fieldType.Name == "ID" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Für Pointer-Typen prüfen, ob sie nicht nil sind
|
||||||
|
if field.Kind() == reflect.Ptr && !field.IsNil() {
|
||||||
|
// Feldname aus GORM-Tag extrahieren oder Standard-Feldnamen verwenden
|
||||||
|
fieldName := fieldType.Name
|
||||||
|
|
||||||
|
if tag, ok := fieldType.Tag.Lookup("gorm"); ok {
|
||||||
|
// Tag-Optionen trennen
|
||||||
|
options := strings.Split(tag, ";")
|
||||||
|
for _, option := range options {
|
||||||
|
if strings.HasPrefix(option, "column:") {
|
||||||
|
fieldName = strings.TrimPrefix(option, "column:")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Den Wert hinter dem Pointer verwenden
|
||||||
|
updateMap[fieldName] = field.Elem().Interface()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(updateMap) == 0 {
|
||||||
|
return nil // Nichts zu aktualisieren
|
||||||
|
}
|
||||||
|
|
||||||
|
return GetEngine(ctx).Model(model).Updates(updateMap).Error
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package entities
|
package models
|
||||||
|
|
||||||
import "errors"
|
import "errors"
|
||||||
|
|
230
backend/internal/models/project.go
Normal file
230
backend/internal/models/project.go
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
|
// Beziehungen (für Eager Loading)
|
||||||
|
Customer *Customer `gorm:"foreignKey:CustomerID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName gibt den Tabellennamen für GORM an
|
||||||
|
func (Project) TableName() string {
|
||||||
|
return "projects"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectCreate enthält die Felder zum Erstellen eines neuen Projekts
|
||||||
|
type ProjectCreate struct {
|
||||||
|
Name string
|
||||||
|
CustomerID ulid.ULID
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProjectUpdate enthält die aktualisierbaren Felder eines Projekts
|
||||||
|
type ProjectUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates
|
||||||
|
Name *string `gorm:"column:name"`
|
||||||
|
CustomerID *ulid.ULID `gorm:"column:customer_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prüft, ob die Create-Struktur gültige Daten enthält
|
||||||
|
func (pc *ProjectCreate) Validate() error {
|
||||||
|
if pc.Name == "" {
|
||||||
|
return errors.New("project name darf nicht leer sein")
|
||||||
|
}
|
||||||
|
// Prüfung auf gültige CustomerID
|
||||||
|
if pc.CustomerID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("customerID darf nicht leer sein")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 darf nicht leer sein")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllProjects gibt alle Projekte zurück
|
||||||
|
func GetAllProjects(ctx context.Context) ([]Project, error) {
|
||||||
|
var projects []Project
|
||||||
|
result := GetEngine(ctx).Find(&projects)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return projects, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProject erstellt ein neues Projekt mit Validierung
|
||||||
|
func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prüfen, ob der Kunde existiert
|
||||||
|
customer, err := GetCustomerByID(ctx, create.CustomerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fehler beim Prüfen des Kunden: %w", err)
|
||||||
|
}
|
||||||
|
if customer == nil {
|
||||||
|
return nil, errors.New("der angegebene Kunde existiert nicht")
|
||||||
|
}
|
||||||
|
|
||||||
|
project := Project{
|
||||||
|
Name: create.Name,
|
||||||
|
CustomerID: create.CustomerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Create(&project)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, fmt.Errorf("fehler beim Erstellen des Projekts: %w", result.Error)
|
||||||
|
}
|
||||||
|
return &project, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateProject aktualisiert ein bestehendes Projekt mit Validierung
|
||||||
|
func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := update.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
project, err := GetProjectByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if project == nil {
|
||||||
|
return nil, errors.New("project nicht gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("fehler beim Prüfen des Kunden: %w", err)
|
||||||
|
}
|
||||||
|
if customer == nil {
|
||||||
|
return nil, errors.New("der angegebene Kunde existiert nicht")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generische Update-Funktion verwenden
|
||||||
|
if err := UpdateModel(ctx, project, update); err != nil {
|
||||||
|
return nil, fmt.Errorf("fehler beim Aktualisieren des Projekts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Daten aus der Datenbank laden
|
||||||
|
return GetProjectByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteProject löscht ein Projekt anhand seiner ID
|
||||||
|
func DeleteProject(ctx context.Context, id ulid.ULID) error {
|
||||||
|
// 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("fehler beim Löschen des Projekts: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateProjectWithTransaction erstellt ein Projekt innerhalb einer Transaktion
|
||||||
|
func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*Project, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var project *Project
|
||||||
|
|
||||||
|
// Transaktion starten
|
||||||
|
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 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("der angegebene Kunde existiert nicht")
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projekt erstellen
|
||||||
|
newProject := Project{
|
||||||
|
Name: create.Name,
|
||||||
|
CustomerID: create.CustomerID,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newProject).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projekt für die Rückgabe speichern
|
||||||
|
project = &newProject
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("transaktionsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return project, nil
|
||||||
|
}
|
360
backend/internal/models/timeentry.go
Normal file
360
backend/internal/models/timeentry.go
Normal file
@ -0,0 +1,360 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TimeEntry repräsentiert einen Zeiteintrag im System
|
||||||
|
type TimeEntry struct {
|
||||||
|
EntityBase
|
||||||
|
UserID ulid.ULID `gorm:"column:user_id;type:uuid;not null;index"`
|
||||||
|
ProjectID ulid.ULID `gorm:"column:project_id;type:uuid;not null;index"`
|
||||||
|
ActivityID ulid.ULID `gorm:"column:activity_id;type:uuid;not null;index"`
|
||||||
|
Start time.Time `gorm:"column:start;not null"`
|
||||||
|
End time.Time `gorm:"column:end;not null"`
|
||||||
|
Description string `gorm:"column:description"`
|
||||||
|
Billable int `gorm:"column:billable"` // Percentage (0-100)
|
||||||
|
|
||||||
|
// Beziehungen für Eager Loading
|
||||||
|
User *User `gorm:"foreignKey:UserID"`
|
||||||
|
Project *Project `gorm:"foreignKey:ProjectID"`
|
||||||
|
Activity *Activity `gorm:"foreignKey:ActivityID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName gibt den Tabellennamen für GORM an
|
||||||
|
func (TimeEntry) TableName() string {
|
||||||
|
return "time_entries"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeEntryCreate enthält die Felder zum Erstellen eines neuen Zeiteintrags
|
||||||
|
type TimeEntryCreate struct {
|
||||||
|
UserID ulid.ULID
|
||||||
|
ProjectID ulid.ULID
|
||||||
|
ActivityID ulid.ULID
|
||||||
|
Start time.Time
|
||||||
|
End time.Time
|
||||||
|
Description string
|
||||||
|
Billable int // Percentage (0-100)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TimeEntryUpdate enthält die aktualisierbaren Felder eines Zeiteintrags
|
||||||
|
type TimeEntryUpdate struct {
|
||||||
|
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"`
|
||||||
|
Start *time.Time `gorm:"column:start"`
|
||||||
|
End *time.Time `gorm:"column:end"`
|
||||||
|
Description *string `gorm:"column:description"`
|
||||||
|
Billable *int `gorm:"column:billable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prüft, ob die Create-Struktur gültige Daten enthält
|
||||||
|
func (tc *TimeEntryCreate) Validate() error {
|
||||||
|
// Prüfung auf leere IDs
|
||||||
|
if tc.UserID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("userID darf nicht leer sein")
|
||||||
|
}
|
||||||
|
if tc.ProjectID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("projectID darf nicht leer sein")
|
||||||
|
}
|
||||||
|
if tc.ActivityID.Compare(ulid.ULID{}) == 0 {
|
||||||
|
return errors.New("activityID darf nicht leer sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeitprüfungen
|
||||||
|
if tc.Start.IsZero() {
|
||||||
|
return errors.New("startzeit darf nicht leer sein")
|
||||||
|
}
|
||||||
|
if tc.End.IsZero() {
|
||||||
|
return errors.New("endzeit darf nicht leer sein")
|
||||||
|
}
|
||||||
|
if tc.End.Before(tc.Start) {
|
||||||
|
return errors.New("endzeit kann nicht vor der startzeit liegen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Billable-Prozent Prüfung
|
||||||
|
if tc.Billable < 0 || tc.Billable > 100 {
|
||||||
|
return errors.New("billable muss zwischen 0 und 100 liegen")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prüft, ob die Update-Struktur gültige Daten enthält
|
||||||
|
func (tu *TimeEntryUpdate) Validate() error {
|
||||||
|
// Billable-Prozent Prüfung
|
||||||
|
if tu.Billable != nil && (*tu.Billable < 0 || *tu.Billable > 100) {
|
||||||
|
return errors.New("billable muss zwischen 0 und 100 liegen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeitprüfungen
|
||||||
|
if tu.Start != nil && tu.End != nil && tu.End.Before(*tu.Start) {
|
||||||
|
return errors.New("endzeit kann nicht vor der startzeit liegen")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &timeEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"). // Verschachtelte Beziehung
|
||||||
|
Preload("Activity").
|
||||||
|
Where("id = ?", id).
|
||||||
|
First(&timeEntry)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &timeEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllTimeEntries gibt alle Zeiteinträge zurück
|
||||||
|
func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) {
|
||||||
|
var timeEntries []TimeEntry
|
||||||
|
result := GetEngine(ctx).Find(&timeEntries)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTimeEntriesByDateRange gibt alle Zeiteinträge in einem Zeitraum zurück
|
||||||
|
func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) {
|
||||||
|
var timeEntries []TimeEntry
|
||||||
|
// Suche nach Überschneidungen im Zeitraum
|
||||||
|
result := GetEngine(ctx).
|
||||||
|
Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)",
|
||||||
|
start, end, start, end).
|
||||||
|
Find(&timeEntries)
|
||||||
|
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return timeEntries, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SumBillableHoursByProject berechnet die abrechenbaren Stunden pro Projekt
|
||||||
|
func SumBillableHoursByProject(ctx context.Context, projectID ulid.ULID) (float64, error) {
|
||||||
|
type Result struct {
|
||||||
|
TotalHours float64
|
||||||
|
}
|
||||||
|
|
||||||
|
var result Result
|
||||||
|
|
||||||
|
// SQL-Berechnung der gewichteten Stunden
|
||||||
|
err := GetEngine(ctx).Raw(`
|
||||||
|
SELECT SUM(
|
||||||
|
EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0)
|
||||||
|
) as total_hours
|
||||||
|
FROM time_entries
|
||||||
|
WHERE project_id = ?
|
||||||
|
`, projectID).Scan(&result).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.TotalHours, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateTimeEntry erstellt einen neuen Zeiteintrag mit Validierung
|
||||||
|
func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starten einer Transaktion
|
||||||
|
var timeEntry *TimeEntry
|
||||||
|
|
||||||
|
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Verweise prüfen
|
||||||
|
if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeiteintrag erstellen
|
||||||
|
newTimeEntry := TimeEntry{
|
||||||
|
UserID: create.UserID,
|
||||||
|
ProjectID: create.ProjectID,
|
||||||
|
ActivityID: create.ActivityID,
|
||||||
|
Start: create.Start,
|
||||||
|
End: create.End,
|
||||||
|
Description: create.Description,
|
||||||
|
Billable: create.Billable,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newTimeEntry).Error; err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Erstellen des Zeiteintrags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timeEntry = &newTimeEntry
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateReferences prüft, ob alle referenzierten Entitäten existieren
|
||||||
|
func validateReferences(tx *gorm.DB, userID, projectID, activityID ulid.ULID) error {
|
||||||
|
// Benutzer prüfen
|
||||||
|
var userCount int64
|
||||||
|
if err := tx.Model(&User{}).Where("id = ?", userID).Count(&userCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Prüfen des Benutzers: %w", err)
|
||||||
|
}
|
||||||
|
if userCount == 0 {
|
||||||
|
return errors.New("der angegebene Benutzer existiert nicht")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projekt prüfen
|
||||||
|
var projectCount int64
|
||||||
|
if err := tx.Model(&Project{}).Where("id = ?", projectID).Count(&projectCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Prüfen des Projekts: %w", err)
|
||||||
|
}
|
||||||
|
if projectCount == 0 {
|
||||||
|
return errors.New("das angegebene Projekt existiert nicht")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktivität prüfen
|
||||||
|
var activityCount int64
|
||||||
|
if err := tx.Model(&Activity{}).Where("id = ?", activityID).Count(&activityCount).Error; err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Prüfen der Aktivität: %w", err)
|
||||||
|
}
|
||||||
|
if activityCount == 0 {
|
||||||
|
return errors.New("die angegebene Aktivität existiert nicht")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTimeEntry aktualisiert einen bestehenden Zeiteintrag mit Validierung
|
||||||
|
func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := update.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeiteintrag suchen
|
||||||
|
timeEntry, err := GetTimeEntryByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if timeEntry == nil {
|
||||||
|
return nil, errors.New("zeiteintrag nicht gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starten einer Transaktion für das Update
|
||||||
|
err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// Referenzen prüfen, falls sie aktualisiert werden
|
||||||
|
if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil {
|
||||||
|
// Aktuelle Werte verwenden, wenn nicht aktualisiert
|
||||||
|
userID := timeEntry.UserID
|
||||||
|
if update.UserID != nil {
|
||||||
|
userID = *update.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
projectID := timeEntry.ProjectID
|
||||||
|
if update.ProjectID != nil {
|
||||||
|
projectID = *update.ProjectID
|
||||||
|
}
|
||||||
|
|
||||||
|
activityID := timeEntry.ActivityID
|
||||||
|
if update.ActivityID != nil {
|
||||||
|
activityID = *update.ActivityID
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateReferences(tx, userID, projectID, activityID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zeitkonsistenz prüfen
|
||||||
|
start := timeEntry.Start
|
||||||
|
if update.Start != nil {
|
||||||
|
start = *update.Start
|
||||||
|
}
|
||||||
|
|
||||||
|
end := timeEntry.End
|
||||||
|
if update.End != nil {
|
||||||
|
end = *update.End
|
||||||
|
}
|
||||||
|
|
||||||
|
if end.Before(start) {
|
||||||
|
return errors.New("endzeit kann nicht vor der startzeit liegen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generisches Update verwenden
|
||||||
|
if err := UpdateModel(ctx, timeEntry, update); err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Aktualisieren des Zeiteintrags: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Daten aus der Datenbank laden
|
||||||
|
return GetTimeEntryByID(ctx, update.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("fehler beim Löschen des Zeiteintrags: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
534
backend/internal/models/user.go
Normal file
534
backend/internal/models/user.go
Normal file
@ -0,0 +1,534 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/oklog/ulid/v2"
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Argon2 Parameter
|
||||||
|
const (
|
||||||
|
// Empfohlene Werte für Argon2id
|
||||||
|
ArgonTime = 1
|
||||||
|
ArgonMemory = 64 * 1024 // 64MB
|
||||||
|
ArgonThreads = 4
|
||||||
|
ArgonKeyLen = 32
|
||||||
|
SaltLength = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// Rollen-Konstanten
|
||||||
|
const (
|
||||||
|
RoleAdmin = "admin"
|
||||||
|
RoleUser = "user"
|
||||||
|
RoleViewer = "viewer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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)"` // 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"`
|
||||||
|
|
||||||
|
// Beziehung für Eager Loading
|
||||||
|
Company *Company `gorm:"foreignKey:CompanyID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TableName gibt den Tabellennamen für GORM an
|
||||||
|
func (User) TableName() string {
|
||||||
|
return "users"
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCreate enthält die Felder zum Erstellen eines neuen Benutzers
|
||||||
|
type UserCreate struct {
|
||||||
|
Email string
|
||||||
|
Password string
|
||||||
|
Role string
|
||||||
|
CompanyID ulid.ULID
|
||||||
|
HourlyRate float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserUpdate enthält die aktualisierbaren Felder eines Benutzers
|
||||||
|
type UserUpdate struct {
|
||||||
|
ID ulid.ULID `gorm:"-"` // Ausschließen von Updates
|
||||||
|
Email *string `gorm:"column:email"`
|
||||||
|
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 enthält die Daten für Passwort-Hash und Salt
|
||||||
|
type PasswordData struct {
|
||||||
|
Salt string
|
||||||
|
Hash string
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateSalt erzeugt einen kryptografisch sicheren Salt
|
||||||
|
func GenerateSalt() (string, error) {
|
||||||
|
salt := make([]byte, SaltLength)
|
||||||
|
_, err := rand.Read(salt)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(salt), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword erstellt einen sicheren Passwort-Hash mit Argon2id und einem zufälligen Salt
|
||||||
|
func HashPassword(password string) (PasswordData, error) {
|
||||||
|
// Erzeugen eines kryptografisch sicheren Salts
|
||||||
|
saltStr, err := GenerateSalt()
|
||||||
|
if err != nil {
|
||||||
|
return PasswordData{}, fmt.Errorf("fehler beim Generieren des Salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
salt, err := base64.StdEncoding.DecodeString(saltStr)
|
||||||
|
if err != nil {
|
||||||
|
return PasswordData{}, fmt.Errorf("fehler beim Dekodieren des Salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash mit Argon2id erstellen (moderne, sichere Hash-Funktion)
|
||||||
|
hash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen)
|
||||||
|
hashStr := base64.StdEncoding.EncodeToString(hash)
|
||||||
|
|
||||||
|
return PasswordData{
|
||||||
|
Salt: saltStr,
|
||||||
|
Hash: hashStr,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("fehler beim Dekodieren des Salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hash, err := base64.StdEncoding.DecodeString(hashStr)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("fehler beim Dekodieren des Hash: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash mit gleichem Salt berechnen
|
||||||
|
computedHash := argon2.IDKey([]byte(password), salt, ArgonTime, ArgonMemory, ArgonThreads, ArgonKeyLen)
|
||||||
|
|
||||||
|
// Konstante Zeit-Vergleich, um Timing-Angriffe zu vermeiden
|
||||||
|
return hmacEqual(hash, computedHash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// hmacEqual führt einen konstante-Zeit Vergleich durch (verhindert Timing-Attacken)
|
||||||
|
func hmacEqual(a, b []byte) bool {
|
||||||
|
if len(a) != len(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var result byte
|
||||||
|
for i := 0; i < len(a); i++ {
|
||||||
|
result |= a[i] ^ b[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate prüft, ob die Create-Struktur gültige Daten enthält
|
||||||
|
func (uc *UserCreate) Validate() error {
|
||||||
|
if uc.Email == "" {
|
||||||
|
return errors.New("email darf nicht leer sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("ungültiges email-format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.Password == "" {
|
||||||
|
return errors.New("passwort darf nicht leer sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort-Komplexität prüfen
|
||||||
|
if len(uc.Password) < 10 {
|
||||||
|
return errors.New("passwort muss mindestens 10 Zeichen lang sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komplexere Passwortvalidierung
|
||||||
|
var (
|
||||||
|
hasUpper = false
|
||||||
|
hasLower = false
|
||||||
|
hasNumber = false
|
||||||
|
hasSpecial = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, char := range uc.Password {
|
||||||
|
switch {
|
||||||
|
case 'A' <= char && char <= 'Z':
|
||||||
|
hasUpper = true
|
||||||
|
case 'a' <= char && char <= 'z':
|
||||||
|
hasLower = true
|
||||||
|
case '0' <= char && char <= '9':
|
||||||
|
hasNumber = true
|
||||||
|
case char == '!' || char == '@' || char == '#' || char == '$' ||
|
||||||
|
char == '%' || char == '^' || char == '&' || char == '*':
|
||||||
|
hasSpecial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
|
||||||
|
return errors.New("passwort muss Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rolle prüfen
|
||||||
|
if uc.Role == "" {
|
||||||
|
uc.Role = RoleUser // Standardrolle setzen
|
||||||
|
} else {
|
||||||
|
validRoles := []string{RoleAdmin, RoleUser, RoleViewer}
|
||||||
|
isValid := slices.Contains(validRoles, uc.Role)
|
||||||
|
if !isValid {
|
||||||
|
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 darf nicht leer sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
if uc.HourlyRate < 0 {
|
||||||
|
return errors.New("stundensatz darf nicht negativ sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 darf nicht leer sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("ungültiges email-format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if uu.Password != nil {
|
||||||
|
if *uu.Password == "" {
|
||||||
|
return errors.New("passwort darf nicht leer sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort-Komplexität prüfen
|
||||||
|
if len(*uu.Password) < 10 {
|
||||||
|
return errors.New("passwort muss mindestens 10 Zeichen lang sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Komplexere Passwortvalidierung
|
||||||
|
var (
|
||||||
|
hasUpper = false
|
||||||
|
hasLower = false
|
||||||
|
hasNumber = false
|
||||||
|
hasSpecial = false
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, char := range *uu.Password {
|
||||||
|
switch {
|
||||||
|
case 'A' <= char && char <= 'Z':
|
||||||
|
hasUpper = true
|
||||||
|
case 'a' <= char && char <= 'z':
|
||||||
|
hasLower = true
|
||||||
|
case '0' <= char && char <= '9':
|
||||||
|
hasNumber = true
|
||||||
|
case char == '!' || char == '@' || char == '#' || char == '$' ||
|
||||||
|
char == '%' || char == '^' || char == '&' || char == '*':
|
||||||
|
hasSpecial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasUpper || !hasLower || !hasNumber || !hasSpecial {
|
||||||
|
return errors.New("passwort muss Großbuchstaben, Kleinbuchstaben, Zahlen und Sonderzeichen enthalten")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rolle prüfen
|
||||||
|
if uu.Role != nil {
|
||||||
|
validRoles := []string{RoleAdmin, RoleUser, RoleViewer}
|
||||||
|
isValid := false
|
||||||
|
for _, role := range validRoles {
|
||||||
|
if *uu.Role == role {
|
||||||
|
isValid = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isValid {
|
||||||
|
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("stundensatz darf nicht negativ sein")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllUsers gibt alle Benutzer zurück
|
||||||
|
func GetAllUsers(ctx context.Context) ([]User, error) {
|
||||||
|
var users []User
|
||||||
|
result := GetEngine(ctx).Find(&users)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if result.Error != nil {
|
||||||
|
return nil, result.Error
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser erstellt einen neuen Benutzer mit Validierung und sicherem Passwort-Hashing
|
||||||
|
func CreateUser(ctx context.Context, create UserCreate) (*User, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := create.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starten einer Transaktion
|
||||||
|
var user *User
|
||||||
|
|
||||||
|
err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 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("fehler beim Prüfen der Email: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("email wird bereits verwendet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("fehler beim Prüfen der Firma: %w", err)
|
||||||
|
}
|
||||||
|
if companyCount == 0 {
|
||||||
|
return errors.New("die angegebene Firma existiert nicht")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort hashen mit einzigartigem Salt
|
||||||
|
pwData, err := HashPassword(create.Password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Hashen des Passworts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benutzer erstellen mit Salt und Hash getrennt gespeichert
|
||||||
|
newUser := User{
|
||||||
|
Email: create.Email,
|
||||||
|
Salt: pwData.Salt,
|
||||||
|
Hash: pwData.Hash,
|
||||||
|
Role: create.Role,
|
||||||
|
CompanyID: create.CompanyID,
|
||||||
|
HourlyRate: create.HourlyRate,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := tx.Create(&newUser).Error; err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Erstellen des Benutzers: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
user = &newUser
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser aktualisiert einen bestehenden Benutzer
|
||||||
|
func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) {
|
||||||
|
// Validierung
|
||||||
|
if err := update.Validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("validierungsfehler: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benutzer suchen
|
||||||
|
user, err := GetUserByID(ctx, update.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
|
return nil, errors.New("benutzer nicht gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starten einer Transaktion für das Update
|
||||||
|
err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 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("fehler beim Prüfen der Email: %w", err)
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("email wird bereits verwendet")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("fehler beim Prüfen der Firma: %w", err)
|
||||||
|
}
|
||||||
|
if companyCount == 0 {
|
||||||
|
return errors.New("die angegebene Firma existiert nicht")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wenn Passwort aktualisiert wird, neu hashen mit neuem Salt
|
||||||
|
if update.Password != nil {
|
||||||
|
pwData, err := HashPassword(*update.Password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fehler beim Hashen des Passworts: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("fehler beim Aktualisieren des Passworts: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map für generisches Update erstellen
|
||||||
|
updates := make(map[string]interface{})
|
||||||
|
|
||||||
|
// Nur nicht-Passwort-Felder dem Update hinzufügen
|
||||||
|
if update.Email != nil {
|
||||||
|
updates["email"] = *update.Email
|
||||||
|
}
|
||||||
|
if update.Role != nil {
|
||||||
|
updates["role"] = *update.Role
|
||||||
|
}
|
||||||
|
if update.CompanyID != nil {
|
||||||
|
updates["company_id"] = *update.CompanyID
|
||||||
|
}
|
||||||
|
if update.HourlyRate != nil {
|
||||||
|
updates["hourly_rate"] = *update.HourlyRate
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("fehler beim Aktualisieren des Benutzers: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aktualisierte Daten aus der Datenbank laden
|
||||||
|
return GetUserByID(ctx, update.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser löscht einen Benutzer anhand seiner ID
|
||||||
|
func DeleteUser(ctx context.Context, id ulid.ULID) error {
|
||||||
|
// Hier könnte man prüfen, ob abhängige Entitäten existieren
|
||||||
|
// z.B. nicht löschen, wenn noch Zeiteinträge vorhanden sind
|
||||||
|
|
||||||
|
result := GetEngine(ctx).Delete(&User{}, id)
|
||||||
|
if result.Error != nil {
|
||||||
|
return fmt.Errorf("fehler beim Löschen des Benutzers: %w", result.Error)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// Gleiche Fehlermeldung, um keine Informationen über existierende Accounts preiszugeben
|
||||||
|
return nil, errors.New("ungültige Anmeldeinformationen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Passwort überprüfen mit dem gespeicherten Salt
|
||||||
|
isValid, err := VerifyPassword(password, user.Salt, user.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fehler bei der Passwortüberprüfung: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isValid {
|
||||||
|
return nil, errors.New("ungültige Anmeldeinformationen")
|
||||||
|
}
|
||||||
|
|
||||||
|
return user, nil
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package entities
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
@ -43,7 +43,7 @@ Here's a guide to finding information within the project:
|
|||||||
- **Code Examples:**
|
- **Code Examples:**
|
||||||
- `docu/code_examples/react_component.tsx`: Example React component.
|
- `docu/code_examples/react_component.tsx`: Example React component.
|
||||||
|
|
||||||
**Important Note about Code Examples:** The files in `docu/code_examples/` are for illustrative purposes *only*. They do *not* represent a runnable project structure. Treat each file as an isolated example. The package declarations within these files (e.g., `package entities`, `package repositories`, `package main`) are conceptual and should be interpreted in the context of the described architecture, *not* as a literal directory structure. Do not attempt to run `go get` or similar commands based on these examples, as the necessary project structure and dependencies are not present.
|
**Important Note about Code Examples:** The files in `docu/code_examples/` are for illustrative purposes *only*. They do *not* represent a runnable project structure. Treat each file as an isolated example. The package declarations within these files (e.g., `package models`, `package repositories`, `package main`) are conceptual and should be interpreted in the context of the described architecture, *not* as a literal directory structure. Do not attempt to run `go get` or similar commands based on these examples, as the necessary project structure and dependencies are not present.
|
||||||
|
|
||||||
## Rules and Guidelines
|
## Rules and Guidelines
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user