Compare commits
	
		
			No commits in common. "main" and "ja/feat/init" have entirely different histories.
		
	
	
		
			main
			...
			ja/feat/in
		
	
		
| @ -1,19 +0,0 @@ | |||||||
| name: Gitea Actions Demo |  | ||||||
| run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀 |  | ||||||
| on: [push] |  | ||||||
| 
 |  | ||||||
| jobs: |  | ||||||
|   Explore-Gitea-Actions: |  | ||||||
|     runs-on: ubuntu-latest |  | ||||||
|     steps: |  | ||||||
|       - run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event." |  | ||||||
|       - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!" |  | ||||||
|       - run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}." |  | ||||||
|       - name: Check out repository code |  | ||||||
|         uses: actions/checkout@v4 |  | ||||||
|       - run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner." |  | ||||||
|       - run: echo "🖥️ The workflow is now ready to test your code on the runner." |  | ||||||
|       - name: List files in the repository |  | ||||||
|         run: | |  | ||||||
|           ls ${{ gitea.workspace }} |  | ||||||
|       - run: echo "🍏 This job's status is ${{ job.status }}." |  | ||||||
| @ -7,8 +7,6 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/config" |  | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/models" | 	"github.com/timetracker/backend/internal/models" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -17,15 +15,15 @@ func main() { | |||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	// Get database configuration with sensible defaults | 	// Get database configuration with sensible defaults | ||||||
| 	dbConfig := config.DefaultDatabaseConfig() | 	dbConfig := models.DefaultDatabaseConfig() | ||||||
| 
 | 
 | ||||||
| 	// Initialize database | 	// Initialize database | ||||||
| 	fmt.Println("Connecting to database...") | 	fmt.Println("Connecting to database...") | ||||||
| 	if err := db.InitDB(dbConfig); err != nil { | 	if err := models.InitDB(dbConfig); err != nil { | ||||||
| 		log.Fatalf("Error initializing database: %v", err) | 		log.Fatalf("Error initializing database: %v", err) | ||||||
| 	} | 	} | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		if err := db.CloseDB(); err != nil { | 		if err := models.CloseDB(); err != nil { | ||||||
| 			log.Printf("Error closing database connection: %v", err) | 			log.Printf("Error closing database connection: %v", err) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
| @ -36,7 +34,7 @@ func main() { | |||||||
| 	defer cancel() | 	defer cancel() | ||||||
| 
 | 
 | ||||||
| 	// Get the database engine | 	// Get the database engine | ||||||
| 	db := db.GetEngine(ctx) | 	db := models.GetEngine(ctx) | ||||||
| 
 | 
 | ||||||
| 	// Test database connection with a simple query | 	// Test database connection with a simple query | ||||||
| 	var result int | 	var result int | ||||||
|  | |||||||
| @ -6,8 +6,6 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 	"os" | 	"os" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/config" |  | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/models" | 	"github.com/timetracker/backend/internal/models" | ||||||
| 	"gorm.io/gorm/logger" | 	"gorm.io/gorm/logger" | ||||||
| ) | ) | ||||||
| @ -31,7 +29,7 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Get database configuration with sensible defaults | 	// Get database configuration with sensible defaults | ||||||
| 	dbConfig := config.DefaultDatabaseConfig() | 	dbConfig := models.DefaultDatabaseConfig() | ||||||
| 
 | 
 | ||||||
| 	// Override with environment variables if provided | 	// Override with environment variables if provided | ||||||
| 	if host := os.Getenv("DB_HOST"); host != "" { | 	if host := os.Getenv("DB_HOST"); host != "" { | ||||||
| @ -64,7 +62,7 @@ func main() { | |||||||
| 
 | 
 | ||||||
| 	var err error | 	var err error | ||||||
| 
 | 
 | ||||||
| 	gormDB, err := db.GetGormDB(dbConfig, "postgres") | 	gormDB, err := models.GetGormDB(dbConfig, "postgres") | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		log.Fatalf("Error getting gorm DB: %v", err) | 		log.Fatalf("Error getting gorm DB: %v", err) | ||||||
| 	} | 	} | ||||||
| @ -91,11 +89,11 @@ func main() { | |||||||
| 		fmt.Printf("✓ Database %s created successfully\n", dbConfig.DBName) | 		fmt.Printf("✓ Database %s created successfully\n", dbConfig.DBName) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if err = db.InitDB(dbConfig); err != nil { | 	if err = models.InitDB(dbConfig); err != nil { | ||||||
| 		log.Fatalf("Error initializing database: %v", err) | 		log.Fatalf("Error initializing database: %v", err) | ||||||
| 	} | 	} | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		if err := db.CloseDB(); err != nil { | 		if err := models.CloseDB(); err != nil { | ||||||
| 			log.Printf("Error closing database connection: %v", err) | 			log.Printf("Error closing database connection: %v", err) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  | |||||||
| @ -7,23 +7,21 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/oklog/ulid/v2" | 	"github.com/oklog/ulid/v2" | ||||||
| 	"github.com/timetracker/backend/internal/config" |  | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/models" | 	"github.com/timetracker/backend/internal/models" | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func main() { | func main() { | ||||||
| 	// Get database configuration with sensible defaults | 	// Get database configuration with sensible defaults | ||||||
| 	dbConfig := config.DefaultDatabaseConfig() | 	dbConfig := models.DefaultDatabaseConfig() | ||||||
| 
 | 
 | ||||||
| 	// Initialize database | 	// Initialize database | ||||||
| 	fmt.Println("Connecting to database...") | 	fmt.Println("Connecting to database...") | ||||||
| 	if err := db.InitDB(dbConfig); err != nil { | 	if err := models.InitDB(dbConfig); err != nil { | ||||||
| 		log.Fatalf("Error initializing database: %v", err) | 		log.Fatalf("Error initializing database: %v", err) | ||||||
| 	} | 	} | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		if err := db.CloseDB(); err != nil { | 		if err := models.CloseDB(); err != nil { | ||||||
| 			log.Printf("Error closing database connection: %v", err) | 			log.Printf("Error closing database connection: %v", err) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
|  | |||||||
| @ -7,7 +7,6 @@ import ( | |||||||
| 	"log" | 	"log" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/config" | 	"github.com/timetracker/backend/internal/config" | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/models" | 	"github.com/timetracker/backend/internal/models" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -24,11 +23,11 @@ func main() { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Initialize database | 	// Initialize database | ||||||
| 	if err := db.InitDB(cfg.Database); err != nil { | 	if err := models.InitDB(cfg.Database); err != nil { | ||||||
| 		log.Fatalf("Error initializing database: %v", err) | 		log.Fatalf("Error initializing database: %v", err) | ||||||
| 	} | 	} | ||||||
| 	defer func() { | 	defer func() { | ||||||
| 		if err := db.CloseDB(); err != nil { | 		if err := models.CloseDB(); err != nil { | ||||||
| 			log.Printf("Error closing database connection: %v", err) | 			log.Printf("Error closing database connection: %v", err) | ||||||
| 		} | 		} | ||||||
| 	}() | 	}() | ||||||
| @ -45,7 +44,7 @@ func main() { | |||||||
| func seedDatabase(ctx context.Context) error { | func seedDatabase(ctx context.Context) error { | ||||||
| 	// Check if seeding is needed | 	// Check if seeding is needed | ||||||
| 	var count int64 | 	var count int64 | ||||||
| 	if err := db.GetEngine(ctx).Model(&models.Company{}).Count(&count).Error; err != nil { | 	if err := models.GetEngine(ctx).Model(&models.Company{}).Count(&count).Error; err != nil { | ||||||
| 		return fmt.Errorf("error checking if seeding is needed: %w", err) | 		return fmt.Errorf("error checking if seeding is needed: %w", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -58,7 +57,7 @@ func seedDatabase(ctx context.Context) error { | |||||||
| 	log.Println("Seeding database with initial data...") | 	log.Println("Seeding database with initial data...") | ||||||
| 
 | 
 | ||||||
| 	// Start transaction | 	// Start transaction | ||||||
| 	return db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | 	return models.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | ||||||
| 		// Create default company | 		// Create default company | ||||||
| 		defaultCompany := models.Company{ | 		defaultCompany := models.Company{ | ||||||
| 			Name: "Default Company", | 			Name: "Default Company", | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ type ProjectDto struct { | |||||||
| 	UpdatedAt    time.Time `json:"updatedAt" example:"2024-01-01T00:00:00Z"` | 	UpdatedAt    time.Time `json:"updatedAt" example:"2024-01-01T00:00:00Z"` | ||||||
| 	LastEditorID string    `json:"lastEditorID" example:"01HGW2BBG0000000000000000"` | 	LastEditorID string    `json:"lastEditorID" example:"01HGW2BBG0000000000000000"` | ||||||
| 	Name         string    `json:"name" example:"Time Tracking App"` | 	Name         string    `json:"name" example:"Time Tracking App"` | ||||||
| 	CustomerID   *string   `json:"customerId,omitempty" example:"01HGW2BBG0000000000000000"` | 	CustomerID   *string   `json:"customerId" example:"01HGW2BBG0000000000000000"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ProjectCreateDto struct { | type ProjectCreateDto struct { | ||||||
|  | |||||||
| @ -148,17 +148,13 @@ func (h *ProjectHandler) DeleteProject(c *gin.Context) { | |||||||
| // Helper functions for DTO conversion | // Helper functions for DTO conversion | ||||||
| 
 | 
 | ||||||
| func convertProjectToDTO(project *models.Project) dto.ProjectDto { | func convertProjectToDTO(project *models.Project) dto.ProjectDto { | ||||||
| 	var customerIdPtr *string | 	customerId := project.CustomerID.String() | ||||||
| 	if project.CustomerID != nil { |  | ||||||
| 		customerIdStr := project.CustomerID.String() |  | ||||||
| 		customerIdPtr = &customerIdStr |  | ||||||
| 	} |  | ||||||
| 	return dto.ProjectDto{ | 	return dto.ProjectDto{ | ||||||
| 		ID:         project.ID.String(), | 		ID:         project.ID.String(), | ||||||
| 		CreatedAt:  project.CreatedAt, | 		CreatedAt:  project.CreatedAt, | ||||||
| 		UpdatedAt:  project.UpdatedAt, | 		UpdatedAt:  project.UpdatedAt, | ||||||
| 		Name:       project.Name, | 		Name:       project.Name, | ||||||
| 		CustomerID: customerIdPtr, | 		CustomerID: &customerId, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -186,13 +182,13 @@ func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto, id types.ULID) (mo | |||||||
| 
 | 
 | ||||||
| 	if dto.CustomerID.Valid { | 	if dto.CustomerID.Valid { | ||||||
| 		if dto.CustomerID.Value == nil { | 		if dto.CustomerID.Value == nil { | ||||||
| 			update.CustomerID = types.Null[types.ULID]() | 			update.CustomerID = nil | ||||||
| 		} else { | 		} else { | ||||||
| 			customerID, err := types.ULIDFromString(*dto.CustomerID.Value) | 			customerID, err := types.ULIDFromString(*dto.CustomerID.Value) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err) | 				return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err) | ||||||
| 			} | 			} | ||||||
| 			update.CustomerID = types.NewNullable(customerID) | 			update.CustomerID = &customerID | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -73,7 +73,7 @@ func fileExists(path string) bool { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // generateRSAKeys generates RSA keys and saves them to disk | // generateRSAKeys generates RSA keys and saves them to disk | ||||||
| func generateRSAKeys(cfg config.JWTConfig) error { | func generateRSAKeys(cfg models.JWTConfig) error { | ||||||
| 	// Create key directory if it doesn't exist | 	// Create key directory if it doesn't exist | ||||||
| 	if err := os.MkdirAll(cfg.KeyDir, 0700); err != nil { | 	if err := os.MkdirAll(cfg.KeyDir, 0700); err != nil { | ||||||
| 		return fmt.Errorf("failed to create key directory: %w", err) | 		return fmt.Errorf("failed to create key directory: %w", err) | ||||||
|  | |||||||
| @ -9,54 +9,14 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/joho/godotenv" | 	"github.com/joho/godotenv" | ||||||
|  | 	"github.com/timetracker/backend/internal/models" | ||||||
| 	"gorm.io/gorm/logger" | 	"gorm.io/gorm/logger" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| // DatabaseConfig contains the configuration data for the database connection |  | ||||||
| type DatabaseConfig struct { |  | ||||||
| 	Host         string |  | ||||||
| 	Port         int |  | ||||||
| 	User         string |  | ||||||
| 	Password     string |  | ||||||
| 	DBName       string |  | ||||||
| 	SSLMode      string |  | ||||||
| 	MaxIdleConns int           // Maximum number of idle connections |  | ||||||
| 	MaxOpenConns int           // Maximum number of open connections |  | ||||||
| 	MaxLifetime  time.Duration // Maximum lifetime of a connection |  | ||||||
| 	LogLevel     logger.LogLevel |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // DefaultDatabaseConfig returns a default configuration with sensible values |  | ||||||
| func DefaultDatabaseConfig() DatabaseConfig { |  | ||||||
| 	return DatabaseConfig{ |  | ||||||
| 		Host:         "localhost", |  | ||||||
| 		Port:         5432, |  | ||||||
| 		User:         "timetracker", |  | ||||||
| 		Password:     "password", |  | ||||||
| 		DBName:       "timetracker", |  | ||||||
| 		SSLMode:      "disable", |  | ||||||
| 		MaxIdleConns: 10, |  | ||||||
| 		MaxOpenConns: 100, |  | ||||||
| 		MaxLifetime:  time.Hour, |  | ||||||
| 		LogLevel:     logger.Info, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // JWTConfig represents the configuration for JWT authentication |  | ||||||
| type JWTConfig struct { |  | ||||||
| 	Secret        string |  | ||||||
| 	TokenDuration time.Duration |  | ||||||
| 	KeyGenerate   bool |  | ||||||
| 	KeyDir        string |  | ||||||
| 	PrivKeyFile   string |  | ||||||
| 	PubKeyFile    string |  | ||||||
| 	KeyBits       int |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Config represents the application configuration | // Config represents the application configuration | ||||||
| type Config struct { | type Config struct { | ||||||
| 	Database  DatabaseConfig | 	Database  models.DatabaseConfig | ||||||
| 	JWTConfig JWTConfig | 	JWTConfig models.JWTConfig | ||||||
| 	APIKey    string | 	APIKey    string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -66,8 +26,8 @@ func LoadConfig() (*Config, error) { | |||||||
| 	_ = godotenv.Load() | 	_ = godotenv.Load() | ||||||
| 
 | 
 | ||||||
| 	cfg := &Config{ | 	cfg := &Config{ | ||||||
| 		Database:  DefaultDatabaseConfig(), | 		Database:  models.DefaultDatabaseConfig(), | ||||||
| 		JWTConfig: JWTConfig{}, | 		JWTConfig: models.JWTConfig{}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Load database configuration | 	// Load database configuration | ||||||
|  | |||||||
| @ -1,126 +0,0 @@ | |||||||
| package db |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 	"log" |  | ||||||
| 	"time" |  | ||||||
| 
 |  | ||||||
| 	"github.com/timetracker/backend/internal/config" |  | ||||||
| 	"gorm.io/driver/postgres" |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"gorm.io/gorm/logger" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Global variable for the DB connection |  | ||||||
| var db *gorm.DB |  | ||||||
| 
 |  | ||||||
| // ErrDBNotInitialized is returned when a database operation is attempted before initialization |  | ||||||
| var ErrDBNotInitialized = errors.New("database not initialized") |  | ||||||
| 
 |  | ||||||
| // InitDB initializes the database connection (once at startup) |  | ||||||
| // with the provided configuration |  | ||||||
| func InitDB(config config.DatabaseConfig) error { |  | ||||||
| 	// Create connection using the default database name |  | ||||||
| 	gormDB, err := createConnection(config, config.DBName) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Set the global db instance |  | ||||||
| 	db = gormDB |  | ||||||
| 
 |  | ||||||
| 	// Configure connection pool |  | ||||||
| 	return configureConnectionPool(db, config) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetEngine returns the DB instance with context |  | ||||||
| func GetEngine(ctx context.Context) *gorm.DB { |  | ||||||
| 	if db == nil { |  | ||||||
| 		panic(ErrDBNotInitialized) |  | ||||||
| 	} |  | ||||||
| 	return db.WithContext(ctx) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // CloseDB closes the database connection |  | ||||||
| func CloseDB() error { |  | ||||||
| 	if db == nil { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	sqlDB, err := db.DB() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("error getting database connection: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if err := sqlDB.Close(); err != nil { |  | ||||||
| 		return fmt.Errorf("error closing database connection: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetGormDB is used for special cases like database creation |  | ||||||
| func GetGormDB(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) { |  | ||||||
| 	return createConnection(dbConfig, dbName) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // MigrateDB performs database migrations for all models |  | ||||||
| // This is a placeholder that will be called by models.MigrateDB |  | ||||||
| func MigrateDB() error { |  | ||||||
| 	if db == nil { |  | ||||||
| 		return ErrDBNotInitialized |  | ||||||
| 	} |  | ||||||
| 	// The actual migration is implemented in models.MigrateDB |  | ||||||
| 	// This is just a placeholder to make the migrate/main.go file work |  | ||||||
| 	return errors.New("MigrateDB should be called from models package") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // createConnection creates a new database connection with the given configuration |  | ||||||
| func createConnection(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) { |  | ||||||
| 	// Create DSN (Data Source Name) |  | ||||||
| 	dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", |  | ||||||
| 		dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode) |  | ||||||
| 
 |  | ||||||
| 	// Configure GORM logger |  | ||||||
| 	gormLogger := createGormLogger(dbConfig) |  | ||||||
| 
 |  | ||||||
| 	// Establish database connection with custom logger |  | ||||||
| 	gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ |  | ||||||
| 		Logger: gormLogger, |  | ||||||
| 	}) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("error connecting to the database: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return gormDB, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // createGormLogger creates a configured GORM logger instance |  | ||||||
| func createGormLogger(dbConfig config.DatabaseConfig) logger.Interface { |  | ||||||
| 	return logger.New( |  | ||||||
| 		log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer |  | ||||||
| 		logger.Config{ |  | ||||||
| 			SlowThreshold:             200 * time.Millisecond, // Slow SQL threshold |  | ||||||
| 			LogLevel:                  dbConfig.LogLevel,      // Log level |  | ||||||
| 			IgnoreRecordNotFoundError: true,                   // Ignore ErrRecordNotFound error for logger |  | ||||||
| 			Colorful:                  true,                   // Enable color |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // configureConnectionPool sets up the connection pool parameters |  | ||||||
| func configureConnectionPool(db *gorm.DB, config config.DatabaseConfig) error { |  | ||||||
| 	sqlDB, err := db.DB() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return fmt.Errorf("error getting database connection: %w", err) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Set connection pool parameters |  | ||||||
| 	sqlDB.SetMaxIdleConns(config.MaxIdleConns) |  | ||||||
| 	sqlDB.SetMaxOpenConns(config.MaxOpenConns) |  | ||||||
| 	sqlDB.SetConnMaxLifetime(config.MaxLifetime) |  | ||||||
| 
 |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| @ -4,7 +4,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -37,7 +36,7 @@ type ActivityCreate struct { | |||||||
| // GetActivityByID finds an Activity by its ID | // GetActivityByID finds an Activity by its ID | ||||||
| func GetActivityByID(ctx context.Context, id types.ULID) (*Activity, error) { | func GetActivityByID(ctx context.Context, id types.ULID) (*Activity, error) { | ||||||
| 	var activity Activity | 	var activity Activity | ||||||
| 	result := db.GetEngine(ctx).Where("id = ?", id).First(&activity) | 	result := GetEngine(ctx).Where("id = ?", id).First(&activity) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -50,7 +49,7 @@ func GetActivityByID(ctx context.Context, id types.ULID) (*Activity, error) { | |||||||
| // GetAllActivities returns all Activities | // GetAllActivities returns all Activities | ||||||
| func GetAllActivities(ctx context.Context) ([]Activity, error) { | func GetAllActivities(ctx context.Context) ([]Activity, error) { | ||||||
| 	var activities []Activity | 	var activities []Activity | ||||||
| 	result := db.GetEngine(ctx).Find(&activities) | 	result := GetEngine(ctx).Find(&activities) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -64,7 +63,7 @@ func CreateActivity(ctx context.Context, create ActivityCreate) (*Activity, erro | |||||||
| 		BillingRate: create.BillingRate, | 		BillingRate: create.BillingRate, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	result := db.GetEngine(ctx).Create(&activity) | 	result := GetEngine(ctx).Create(&activity) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -92,6 +91,6 @@ func UpdateActivity(ctx context.Context, update ActivityUpdate) (*Activity, erro | |||||||
| 
 | 
 | ||||||
| // DeleteActivity deletes an Activity by its ID | // DeleteActivity deletes an Activity by its ID | ||||||
| func DeleteActivity(ctx context.Context, id types.ULID) error { | func DeleteActivity(ctx context.Context, id types.ULID) error { | ||||||
| 	result := db.GetEngine(ctx).Delete(&Activity{}, id) | 	result := GetEngine(ctx).Delete(&Activity{}, id) | ||||||
| 	return result.Error | 	return result.Error | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -34,7 +33,7 @@ type CompanyUpdate struct { | |||||||
| // GetCompanyByID finds a company by its ID | // GetCompanyByID finds a company by its ID | ||||||
| func GetCompanyByID(ctx context.Context, id types.ULID) (*Company, error) { | func GetCompanyByID(ctx context.Context, id types.ULID) (*Company, error) { | ||||||
| 	var company Company | 	var company Company | ||||||
| 	result := db.GetEngine(ctx).Where("id = ?", id).First(&company) | 	result := GetEngine(ctx).Where("id = ?", id).First(&company) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -47,7 +46,7 @@ func GetCompanyByID(ctx context.Context, id types.ULID) (*Company, error) { | |||||||
| // GetAllCompanies returns all companies | // GetAllCompanies returns all companies | ||||||
| func GetAllCompanies(ctx context.Context) ([]Company, error) { | func GetAllCompanies(ctx context.Context) ([]Company, error) { | ||||||
| 	var companies []Company | 	var companies []Company | ||||||
| 	result := db.GetEngine(ctx).Find(&companies) | 	result := GetEngine(ctx).Find(&companies) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -56,7 +55,7 @@ func GetAllCompanies(ctx context.Context) ([]Company, error) { | |||||||
| 
 | 
 | ||||||
| func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, error) { | func GetCustomersByCompanyID(ctx context.Context, companyID int) ([]Customer, error) { | ||||||
| 	var customers []Customer | 	var customers []Customer | ||||||
| 	result := db.GetEngine(ctx).Where("company_id = ?", companyID).Find(&customers) | 	result := GetEngine(ctx).Where("company_id = ?", companyID).Find(&customers) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -69,7 +68,7 @@ func CreateCompany(ctx context.Context, create CompanyCreate) (*Company, error) | |||||||
| 		Name: create.Name, | 		Name: create.Name, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	result := db.GetEngine(ctx).Create(&company) | 	result := GetEngine(ctx).Create(&company) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -97,6 +96,6 @@ func UpdateCompany(ctx context.Context, update CompanyUpdate) (*Company, error) | |||||||
| 
 | 
 | ||||||
| // DeleteCompany deletes a company by its ID | // DeleteCompany deletes a company by its ID | ||||||
| func DeleteCompany(ctx context.Context, id types.ULID) error { | func DeleteCompany(ctx context.Context, id types.ULID) error { | ||||||
| 	result := db.GetEngine(ctx).Delete(&Company{}, id) | 	result := GetEngine(ctx).Delete(&Company{}, id) | ||||||
| 	return result.Error | 	return result.Error | ||||||
| } | } | ||||||
|  | |||||||
| @ -4,7 +4,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -40,7 +39,7 @@ type CustomerUpdate struct { | |||||||
| // GetCustomerByID finds a customer by its ID | // GetCustomerByID finds a customer by its ID | ||||||
| func GetCustomerByID(ctx context.Context, id types.ULID) (*Customer, error) { | func GetCustomerByID(ctx context.Context, id types.ULID) (*Customer, error) { | ||||||
| 	var customer Customer | 	var customer Customer | ||||||
| 	result := db.GetEngine(ctx).Where("id = ?", id).First(&customer) | 	result := GetEngine(ctx).Where("id = ?", id).First(&customer) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -53,7 +52,7 @@ func GetCustomerByID(ctx context.Context, id types.ULID) (*Customer, error) { | |||||||
| // GetAllCustomers returns all customers | // GetAllCustomers returns all customers | ||||||
| func GetAllCustomers(ctx context.Context) ([]Customer, error) { | func GetAllCustomers(ctx context.Context) ([]Customer, error) { | ||||||
| 	var customers []Customer | 	var customers []Customer | ||||||
| 	result := db.GetEngine(ctx).Find(&customers) | 	result := GetEngine(ctx).Find(&customers) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -67,7 +66,7 @@ func CreateCustomer(ctx context.Context, create CustomerCreate) (*Customer, erro | |||||||
| 		CompanyID: create.CompanyID, | 		CompanyID: create.CompanyID, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	result := db.GetEngine(ctx).Create(&customer) | 	result := GetEngine(ctx).Create(&customer) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -95,6 +94,6 @@ func UpdateCustomer(ctx context.Context, update CustomerUpdate) (*Customer, erro | |||||||
| 
 | 
 | ||||||
| // DeleteCustomer deletes a customer by its ID | // DeleteCustomer deletes a customer by its ID | ||||||
| func DeleteCustomer(ctx context.Context, id types.ULID) error { | func DeleteCustomer(ctx context.Context, id types.ULID) error { | ||||||
| 	result := db.GetEngine(ctx).Delete(&Customer{}, id) | 	result := GetEngine(ctx).Delete(&Customer{}, id) | ||||||
| 	return result.Error | 	return result.Error | ||||||
| } | } | ||||||
|  | |||||||
| @ -9,33 +9,101 @@ import ( | |||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/config" | 	"gorm.io/driver/postgres" // For PostgreSQL | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/permissions" |  | ||||||
| 	"gorm.io/driver/postgres" |  | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| 	"gorm.io/gorm/logger" | 	"gorm.io/gorm/logger" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Global variable for the DB connection | ||||||
|  | var defaultDB *gorm.DB | ||||||
|  | 
 | ||||||
|  | // DatabaseConfig contains the configuration data for the database connection | ||||||
|  | type DatabaseConfig struct { | ||||||
|  | 	Host         string | ||||||
|  | 	Port         int | ||||||
|  | 	User         string | ||||||
|  | 	Password     string | ||||||
|  | 	DBName       string | ||||||
|  | 	SSLMode      string | ||||||
|  | 	MaxIdleConns int           // Maximum number of idle connections | ||||||
|  | 	MaxOpenConns int           // Maximum number of open connections | ||||||
|  | 	MaxLifetime  time.Duration // Maximum lifetime of a connection | ||||||
|  | 	LogLevel     logger.LogLevel | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // DefaultDatabaseConfig returns a default configuration with sensible values | ||||||
|  | func DefaultDatabaseConfig() DatabaseConfig { | ||||||
|  | 	return DatabaseConfig{ | ||||||
|  | 		Host:         "localhost", | ||||||
|  | 		Port:         5432, | ||||||
|  | 		User:         "timetracker", | ||||||
|  | 		Password:     "password", | ||||||
|  | 		DBName:       "timetracker", | ||||||
|  | 		SSLMode:      "disable", | ||||||
|  | 		MaxIdleConns: 10, | ||||||
|  | 		MaxOpenConns: 100, | ||||||
|  | 		MaxLifetime:  time.Hour, | ||||||
|  | 		LogLevel:     logger.Info, | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // InitDB initializes the database connection (once at startup) | ||||||
|  | // with the provided configuration | ||||||
|  | func InitDB(config DatabaseConfig) error { | ||||||
|  | 	// Create DSN (Data Source Name) | ||||||
|  | 	dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", | ||||||
|  | 		config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode) | ||||||
|  | 
 | ||||||
|  | 	// Configure GORM logger | ||||||
|  | 	gormLogger := logger.New( | ||||||
|  | 		log.New(log.Writer(), "\r\n", log.LstdFlags), // io writer | ||||||
|  | 		logger.Config{ | ||||||
|  | 			SlowThreshold:             200 * time.Millisecond, // Slow SQL threshold | ||||||
|  | 			LogLevel:                  config.LogLevel,        // Log level | ||||||
|  | 			IgnoreRecordNotFoundError: true,                   // Ignore ErrRecordNotFound error for logger | ||||||
|  | 			Colorful:                  true,                   // Enable color | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	// Establish database connection with custom logger | ||||||
|  | 	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ | ||||||
|  | 		Logger: gormLogger, | ||||||
|  | 	}) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error connecting to the database: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Configure connection pool | ||||||
|  | 	sqlDB, err := db.DB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error getting database connection: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Set connection pool parameters | ||||||
|  | 	sqlDB.SetMaxIdleConns(config.MaxIdleConns) | ||||||
|  | 	sqlDB.SetMaxOpenConns(config.MaxOpenConns) | ||||||
|  | 	sqlDB.SetConnMaxLifetime(config.MaxLifetime) | ||||||
|  | 
 | ||||||
|  | 	defaultDB = db | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // MigrateDB performs database migrations for all models | // MigrateDB performs database migrations for all models | ||||||
| func MigrateDB() error { | func MigrateDB() error { | ||||||
| 	gormDB := db.GetEngine(context.Background()) | 	if defaultDB == nil { | ||||||
| 	if gormDB == nil { |  | ||||||
| 		return errors.New("database not initialized") | 		return errors.New("database not initialized") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	log.Println("Starting database migration...") | 	log.Println("Starting database migration...") | ||||||
| 
 | 
 | ||||||
| 	// Add all models that should be migrated here | 	// Add all models that should be migrated here | ||||||
| 	err := gormDB.AutoMigrate( | 	err := defaultDB.AutoMigrate( | ||||||
| 		&Company{}, | 		&Company{}, | ||||||
| 		&User{}, | 		&User{}, | ||||||
| 		&Customer{}, | 		&Customer{}, | ||||||
| 		&Project{}, | 		&Project{}, | ||||||
| 		&Activity{}, | 		&Activity{}, | ||||||
| 		&TimeEntry{}, | 		&TimeEntry{}, | ||||||
| 		&permissions.Role{}, |  | ||||||
| 		&permissions.Policy{}, |  | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| @ -46,8 +114,34 @@ func MigrateDB() error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetGormDB is used for special cases like database creation | // GetEngine returns the DB instance, possibly with context | ||||||
| func GetGormDB(dbConfig config.DatabaseConfig, dbName string) (*gorm.DB, error) { | func GetEngine(ctx context.Context) *gorm.DB { | ||||||
|  | 	if defaultDB == nil { | ||||||
|  | 		panic("database not initialized") | ||||||
|  | 	} | ||||||
|  | 	// If a special transaction is in ctx, you could check it here | ||||||
|  | 	return defaultDB.WithContext(ctx) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CloseDB closes the database connection | ||||||
|  | func CloseDB() error { | ||||||
|  | 	if defaultDB == nil { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	sqlDB, err := defaultDB.DB() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("error getting database connection: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := sqlDB.Close(); err != nil { | ||||||
|  | 		return fmt.Errorf("error closing database connection: %w", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func GetGormDB(dbConfig DatabaseConfig, dbName string) (*gorm.DB, error) { | ||||||
| 	dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", | 	dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", | ||||||
| 		dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode) | 		dbConfig.Host, dbConfig.Port, dbConfig.User, dbConfig.Password, dbName, dbConfig.SSLMode) | ||||||
| 
 | 
 | ||||||
| @ -90,7 +184,7 @@ func UpdateModel(ctx context.Context, model any, updates any) error { | |||||||
| 	updateMap := make(map[string]any) | 	updateMap := make(map[string]any) | ||||||
| 
 | 
 | ||||||
| 	// Iterate through all fields | 	// Iterate through all fields | ||||||
| 	for i := range updateValue.NumField() { | 	for i := 0; i < updateValue.NumField(); i++ { | ||||||
| 		field := updateValue.Field(i) | 		field := updateValue.Field(i) | ||||||
| 		fieldType := updateType.Field(i) | 		fieldType := updateType.Field(i) | ||||||
| 
 | 
 | ||||||
| @ -129,14 +223,5 @@ func UpdateModel(ctx context.Context, model any, updates any) error { | |||||||
| 		return nil // Nothing to update | 		return nil // Nothing to update | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return db.GetEngine(ctx).Model(model).Updates(updateMap).Error | 	return GetEngine(ctx).Model(model).Updates(updateMap).Error | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // InitDB and CloseDB are forwarded to the db package for backward compatibility |  | ||||||
| func InitDB(config config.DatabaseConfig) error { |  | ||||||
| 	return db.InitDB(config) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func CloseDB() error { |  | ||||||
| 	return db.CloseDB() |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,4 +1,13 @@ | |||||||
| package models | package models | ||||||
| 
 | 
 | ||||||
| // This file is intentionally left empty. | import "time" | ||||||
| // The JWTConfig struct has been moved to the config package. | 
 | ||||||
|  | type JWTConfig struct { | ||||||
|  | 	Secret        string        `env:"JWT_SECRET" default:""` | ||||||
|  | 	TokenDuration time.Duration `env:"JWT_TOKEN_DURATION" default:"24h"` | ||||||
|  | 	KeyGenerate   bool          `env:"JWT_KEY_GENERATE" default:"true"` | ||||||
|  | 	KeyDir        string        `env:"JWT_KEY_DIR" default:"./keys"` | ||||||
|  | 	PrivKeyFile   string        `env:"JWT_PRIV_KEY_FILE" default:"jwt.key"` | ||||||
|  | 	PubKeyFile    string        `env:"JWT_PUB_KEY_FILE" default:"jwt.key.pub"` | ||||||
|  | 	KeyBits       int           `env:"JWT_KEY_BITS" default:"2048"` | ||||||
|  | } | ||||||
|  | |||||||
| @ -5,7 +5,6 @@ import ( | |||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -14,7 +13,7 @@ import ( | |||||||
| type Project struct { | type Project struct { | ||||||
| 	EntityBase | 	EntityBase | ||||||
| 	Name       string      `gorm:"column:name;not null"` | 	Name       string      `gorm:"column:name;not null"` | ||||||
| 	CustomerID *types.ULID `gorm:"column:customer_id;type:bytea;index"` | 	CustomerID *types.ULID `gorm:"column:customer_id;type:bytea;not null"` | ||||||
| 
 | 
 | ||||||
| 	// Relationships (for Eager Loading) | 	// Relationships (for Eager Loading) | ||||||
| 	Customer *Customer `gorm:"foreignKey:CustomerID"` | 	Customer *Customer `gorm:"foreignKey:CustomerID"` | ||||||
| @ -33,9 +32,9 @@ type ProjectCreate struct { | |||||||
| 
 | 
 | ||||||
| // ProjectUpdate contains the updatable fields of a project | // ProjectUpdate contains the updatable fields of a project | ||||||
| type ProjectUpdate struct { | type ProjectUpdate struct { | ||||||
| 	ID         types.ULID                 `gorm:"-"` // Exclude from updates | 	ID         types.ULID  `gorm:"-"` // Exclude from updates | ||||||
| 	Name       *string                    `gorm:"column:name"` | 	Name       *string     `gorm:"column:name"` | ||||||
| 	CustomerID types.Nullable[types.ULID] `gorm:"column:customer_id"` | 	CustomerID *types.ULID `gorm:"column:customer_id"` | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Validate checks if the Create struct contains valid data | // Validate checks if the Create struct contains valid data | ||||||
| @ -44,7 +43,7 @@ func (pc *ProjectCreate) Validate() error { | |||||||
| 		return errors.New("project name cannot be empty") | 		return errors.New("project name cannot be empty") | ||||||
| 	} | 	} | ||||||
| 	// Check for valid CustomerID | 	// Check for valid CustomerID | ||||||
| 	if pc.CustomerID != nil && pc.CustomerID.Compare(types.ULID{}) == 0 { | 	if pc.CustomerID.Compare(types.ULID{}) == 0 { | ||||||
| 		return errors.New("customerID cannot be empty") | 		return errors.New("customerID cannot be empty") | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| @ -61,7 +60,7 @@ func (pu *ProjectUpdate) Validate() error { | |||||||
| // GetProjectByID finds a project by its ID | // GetProjectByID finds a project by its ID | ||||||
| func GetProjectByID(ctx context.Context, id types.ULID) (*Project, error) { | func GetProjectByID(ctx context.Context, id types.ULID) (*Project, error) { | ||||||
| 	var project Project | 	var project Project | ||||||
| 	result := db.GetEngine(ctx).Where("id = ?", id).First(&project) | 	result := GetEngine(ctx).Where("id = ?", id).First(&project) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -74,7 +73,7 @@ func GetProjectByID(ctx context.Context, id types.ULID) (*Project, error) { | |||||||
| // GetProjectWithCustomer loads a project with the associated customer information | // GetProjectWithCustomer loads a project with the associated customer information | ||||||
| func GetProjectWithCustomer(ctx context.Context, id types.ULID) (*Project, error) { | func GetProjectWithCustomer(ctx context.Context, id types.ULID) (*Project, error) { | ||||||
| 	var project Project | 	var project Project | ||||||
| 	result := db.GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&project) | 	result := GetEngine(ctx).Preload("Customer").Where("id = ?", id).First(&project) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -87,7 +86,7 @@ func GetProjectWithCustomer(ctx context.Context, id types.ULID) (*Project, error | |||||||
| // GetAllProjects returns all projects | // GetAllProjects returns all projects | ||||||
| func GetAllProjects(ctx context.Context) ([]Project, error) { | func GetAllProjects(ctx context.Context) ([]Project, error) { | ||||||
| 	var projects []Project | 	var projects []Project | ||||||
| 	result := db.GetEngine(ctx).Find(&projects) | 	result := GetEngine(ctx).Find(&projects) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -97,7 +96,7 @@ func GetAllProjects(ctx context.Context) ([]Project, error) { | |||||||
| // GetAllProjectsWithCustomers returns all projects with customer information | // GetAllProjectsWithCustomers returns all projects with customer information | ||||||
| func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) { | func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) { | ||||||
| 	var projects []Project | 	var projects []Project | ||||||
| 	result := db.GetEngine(ctx).Preload("Customer").Find(&projects) | 	result := GetEngine(ctx).Preload("Customer").Find(&projects) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -107,7 +106,7 @@ func GetAllProjectsWithCustomers(ctx context.Context) ([]Project, error) { | |||||||
| // GetProjectsByCustomerID returns all projects of a specific customer | // GetProjectsByCustomerID returns all projects of a specific customer | ||||||
| func GetProjectsByCustomerID(ctx context.Context, customerId types.ULID) ([]Project, error) { | func GetProjectsByCustomerID(ctx context.Context, customerId types.ULID) ([]Project, error) { | ||||||
| 	var projects []Project | 	var projects []Project | ||||||
| 	result := db.GetEngine(ctx).Where("customer_id = ?", customerId.ULID).Find(&projects) | 	result := GetEngine(ctx).Where("customer_id = ?", customerId.ULID).Find(&projects) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -122,7 +121,7 @@ func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Check if the customer exists | 	// Check if the customer exists | ||||||
| 	if create.CustomerID != nil { | 	if create.CustomerID == nil { | ||||||
| 		customer, err := GetCustomerByID(ctx, *create.CustomerID) | 		customer, err := GetCustomerByID(ctx, *create.CustomerID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("error checking the customer: %w", err) | 			return nil, fmt.Errorf("error checking the customer: %w", err) | ||||||
| @ -137,7 +136,7 @@ func CreateProject(ctx context.Context, create ProjectCreate) (*Project, error) | |||||||
| 		CustomerID: create.CustomerID, | 		CustomerID: create.CustomerID, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	result := db.GetEngine(ctx).Create(&project) | 	result := GetEngine(ctx).Create(&project) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, fmt.Errorf("error creating the project: %w", result.Error) | 		return nil, fmt.Errorf("error creating the project: %w", result.Error) | ||||||
| 	} | 	} | ||||||
| @ -160,18 +159,13 @@ func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// If CustomerID is updated, check if the customer exists | 	// If CustomerID is updated, check if the customer exists | ||||||
| 	if update.CustomerID.Valid { | 	if update.CustomerID != nil { | ||||||
| 		if update.CustomerID.Value != nil { | 		customer, err := GetCustomerByID(ctx, *update.CustomerID) | ||||||
| 			customer, err := GetCustomerByID(ctx, *update.CustomerID.Value) | 		if err != nil { | ||||||
| 			if err != nil { | 			return nil, fmt.Errorf("error checking the customer: %w", err) | ||||||
| 				return nil, fmt.Errorf("error checking the customer: %w", err) | 		} | ||||||
| 			} | 		if customer == nil { | ||||||
| 			if customer == nil { | 			return nil, errors.New("the specified customer does not exist") | ||||||
| 				return nil, errors.New("the specified customer does not exist") |  | ||||||
| 			} |  | ||||||
| 		} else { |  | ||||||
| 			// If CustomerID is nil, set it to nil in the project |  | ||||||
| 			project.CustomerID = nil |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -187,7 +181,7 @@ func UpdateProject(ctx context.Context, update ProjectUpdate) (*Project, error) | |||||||
| // DeleteProject deletes a project by its ID | // DeleteProject deletes a project by its ID | ||||||
| func DeleteProject(ctx context.Context, id types.ULID) error { | func DeleteProject(ctx context.Context, id types.ULID) error { | ||||||
| 	// Here you could check if dependent entities exist | 	// Here you could check if dependent entities exist | ||||||
| 	result := db.GetEngine(ctx).Delete(&Project{}, id) | 	result := GetEngine(ctx).Delete(&Project{}, id) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return fmt.Errorf("error deleting the project: %w", result.Error) | 		return fmt.Errorf("error deleting the project: %w", result.Error) | ||||||
| 	} | 	} | ||||||
| @ -204,7 +198,7 @@ func CreateProjectWithTransaction(ctx context.Context, create ProjectCreate) (*P | |||||||
| 	var project *Project | 	var project *Project | ||||||
| 
 | 
 | ||||||
| 	// Start transaction | 	// Start transaction | ||||||
| 	err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | 	err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | ||||||
| 		// Customer check within the transaction | 		// Customer check within the transaction | ||||||
| 		var customer Customer | 		var customer Customer | ||||||
| 		if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil { | 		if err := tx.Where("id = ?", create.CustomerID).First(&customer).Error; err != nil { | ||||||
|  | |||||||
| @ -6,7 +6,6 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| @ -106,7 +105,7 @@ func (tu *TimeEntryUpdate) Validate() error { | |||||||
| // GetTimeEntryByID finds a time entry by its ID | // GetTimeEntryByID finds a time entry by its ID | ||||||
| func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) { | func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) { | ||||||
| 	var timeEntry TimeEntry | 	var timeEntry TimeEntry | ||||||
| 	result := db.GetEngine(ctx).Where("id = ?", id).First(&timeEntry) | 	result := GetEngine(ctx).Where("id = ?", id).First(&timeEntry) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -119,7 +118,7 @@ func GetTimeEntryByID(ctx context.Context, id types.ULID) (*TimeEntry, error) { | |||||||
| // GetTimeEntryWithRelations loads a time entry with all associated data | // GetTimeEntryWithRelations loads a time entry with all associated data | ||||||
| func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry, error) { | func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry, error) { | ||||||
| 	var timeEntry TimeEntry | 	var timeEntry TimeEntry | ||||||
| 	result := db.GetEngine(ctx). | 	result := GetEngine(ctx). | ||||||
| 		Preload("User"). | 		Preload("User"). | ||||||
| 		Preload("Project"). | 		Preload("Project"). | ||||||
| 		Preload("Project.Customer"). // Nested relationship | 		Preload("Project.Customer"). // Nested relationship | ||||||
| @ -139,7 +138,7 @@ func GetTimeEntryWithRelations(ctx context.Context, id types.ULID) (*TimeEntry, | |||||||
| // GetAllTimeEntries returns all time entries | // GetAllTimeEntries returns all time entries | ||||||
| func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) { | func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) { | ||||||
| 	var timeEntries []TimeEntry | 	var timeEntries []TimeEntry | ||||||
| 	result := db.GetEngine(ctx).Find(&timeEntries) | 	result := GetEngine(ctx).Find(&timeEntries) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -149,7 +148,7 @@ func GetAllTimeEntries(ctx context.Context) ([]TimeEntry, error) { | |||||||
| // GetTimeEntriesByUserID returns all time entries of a user | // GetTimeEntriesByUserID returns all time entries of a user | ||||||
| func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry, error) { | func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry, error) { | ||||||
| 	var timeEntries []TimeEntry | 	var timeEntries []TimeEntry | ||||||
| 	result := db.GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries) | 	result := GetEngine(ctx).Where("user_id = ?", userID).Find(&timeEntries) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -159,7 +158,7 @@ func GetTimeEntriesByUserID(ctx context.Context, userID types.ULID) ([]TimeEntry | |||||||
| // GetTimeEntriesByProjectID returns all time entries of a project | // GetTimeEntriesByProjectID returns all time entries of a project | ||||||
| func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]TimeEntry, error) { | func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]TimeEntry, error) { | ||||||
| 	var timeEntries []TimeEntry | 	var timeEntries []TimeEntry | ||||||
| 	result := db.GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries) | 	result := GetEngine(ctx).Where("project_id = ?", projectID).Find(&timeEntries) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -170,7 +169,7 @@ func GetTimeEntriesByProjectID(ctx context.Context, projectID types.ULID) ([]Tim | |||||||
| func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) { | func GetTimeEntriesByDateRange(ctx context.Context, start, end time.Time) ([]TimeEntry, error) { | ||||||
| 	var timeEntries []TimeEntry | 	var timeEntries []TimeEntry | ||||||
| 	// Search for overlaps in the time range | 	// Search for overlaps in the time range | ||||||
| 	result := db.GetEngine(ctx). | 	result := GetEngine(ctx). | ||||||
| 		Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)", | 		Where("(start BETWEEN ? AND ?) OR (end BETWEEN ? AND ?)", | ||||||
| 			start, end, start, end). | 			start, end, start, end). | ||||||
| 		Find(&timeEntries) | 		Find(&timeEntries) | ||||||
| @ -190,7 +189,7 @@ func SumBillableHoursByProject(ctx context.Context, projectID types.ULID) (float | |||||||
| 	var result Result | 	var result Result | ||||||
| 
 | 
 | ||||||
| 	// SQL calculation of weighted hours | 	// SQL calculation of weighted hours | ||||||
| 	err := db.GetEngine(ctx).Raw(` | 	err := GetEngine(ctx).Raw(` | ||||||
|         SELECT SUM( |         SELECT SUM( | ||||||
|             EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0) |             EXTRACT(EPOCH FROM (end - start)) / 3600 * (billable / 100.0) | ||||||
|         ) as total_hours  |         ) as total_hours  | ||||||
| @ -215,7 +214,7 @@ func CreateTimeEntry(ctx context.Context, create TimeEntryCreate) (*TimeEntry, e | |||||||
| 	// Start a transaction | 	// Start a transaction | ||||||
| 	var timeEntry *TimeEntry | 	var timeEntry *TimeEntry | ||||||
| 
 | 
 | ||||||
| 	err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | 	err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | ||||||
| 		// Check references | 		// Check references | ||||||
| 		if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil { | 		if err := validateReferences(tx, create.UserID, create.ProjectID, create.ActivityID); err != nil { | ||||||
| 			return err | 			return err | ||||||
| @ -296,7 +295,7 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Start a transaction for the update | 	// Start a transaction for the update | ||||||
| 	err = db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | 	err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | ||||||
| 		// Check references if they are updated | 		// Check references if they are updated | ||||||
| 		if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil { | 		if update.UserID != nil || update.ProjectID != nil || update.ActivityID != nil { | ||||||
| 			// Use current values if not updated | 			// Use current values if not updated | ||||||
| @ -353,7 +352,7 @@ func UpdateTimeEntry(ctx context.Context, update TimeEntryUpdate) (*TimeEntry, e | |||||||
| 
 | 
 | ||||||
| // DeleteTimeEntry deletes a time entry by its ID | // DeleteTimeEntry deletes a time entry by its ID | ||||||
| func DeleteTimeEntry(ctx context.Context, id types.ULID) error { | func DeleteTimeEntry(ctx context.Context, id types.ULID) error { | ||||||
| 	result := db.GetEngine(ctx).Delete(&TimeEntry{}, id) | 	result := GetEngine(ctx).Delete(&TimeEntry{}, id) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return fmt.Errorf("error deleting the time entry: %w", result.Error) | 		return fmt.Errorf("error deleting the time entry: %w", result.Error) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -11,12 +11,28 @@ import ( | |||||||
| 
 | 
 | ||||||
| 	"slices" | 	"slices" | ||||||
| 
 | 
 | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"github.com/timetracker/backend/internal/types" | 	"github.com/timetracker/backend/internal/types" | ||||||
| 	"golang.org/x/crypto/argon2" | 	"golang.org/x/crypto/argon2" | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Argon2 Parameters | ||||||
|  | const ( | ||||||
|  | 	// Recommended values for Argon2id | ||||||
|  | 	ArgonTime    = 1 | ||||||
|  | 	ArgonMemory  = 64 * 1024 // 64MB | ||||||
|  | 	ArgonThreads = 4 | ||||||
|  | 	ArgonKeyLen  = 32 | ||||||
|  | 	SaltLength   = 16 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Role Constants | ||||||
|  | const ( | ||||||
|  | 	RoleAdmin  = "admin" | ||||||
|  | 	RoleUser   = "user" | ||||||
|  | 	RoleViewer = "viewer" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| // User represents a user in the system | // User represents a user in the system | ||||||
| type User struct { | type User struct { | ||||||
| 	EntityBase | 	EntityBase | ||||||
| @ -26,7 +42,6 @@ type User struct { | |||||||
| 	Role       string      `gorm:"column:role;not null;default:'user'"` | 	Role       string      `gorm:"column:role;not null;default:'user'"` | ||||||
| 	CompanyID  *types.ULID `gorm:"column:company_id;type:bytea;index"` | 	CompanyID  *types.ULID `gorm:"column:company_id;type:bytea;index"` | ||||||
| 	HourlyRate float64     `gorm:"column:hourly_rate;not null;default:0"` | 	HourlyRate float64     `gorm:"column:hourly_rate;not null;default:0"` | ||||||
| 	Companies  []string    `gorm:"type:text[]"` |  | ||||||
| 
 | 
 | ||||||
| 	// Relationship for Eager Loading | 	// Relationship for Eager Loading | ||||||
| 	Company *Company `gorm:"foreignKey:CompanyID"` | 	Company *Company `gorm:"foreignKey:CompanyID"` | ||||||
| @ -187,7 +202,7 @@ func (uc *UserCreate) Validate() error { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if uc.CompanyID != nil && uc.CompanyID.Compare(types.ULID{}) == 0 { | 	if uc.CompanyID.Compare(types.ULID{}) == 0 { | ||||||
| 		return errors.New("companyID cannot be empty") | 		return errors.New("companyID cannot be empty") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| @ -275,7 +290,7 @@ func (uu *UserUpdate) Validate() error { | |||||||
| // GetUserByID finds a user by their ID | // GetUserByID finds a user by their ID | ||||||
| func GetUserByID(ctx context.Context, id types.ULID) (*User, error) { | func GetUserByID(ctx context.Context, id types.ULID) (*User, error) { | ||||||
| 	var user User | 	var user User | ||||||
| 	result := db.GetEngine(ctx).Where("id = ?", id).First(&user) | 	result := GetEngine(ctx).Where("id = ?", id).First(&user) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -288,7 +303,7 @@ func GetUserByID(ctx context.Context, id types.ULID) (*User, error) { | |||||||
| // GetUserByEmail finds a user by their email | // GetUserByEmail finds a user by their email | ||||||
| func GetUserByEmail(ctx context.Context, email string) (*User, error) { | func GetUserByEmail(ctx context.Context, email string) (*User, error) { | ||||||
| 	var user User | 	var user User | ||||||
| 	result := db.GetEngine(ctx).Where("email = ?", email).First(&user) | 	result := GetEngine(ctx).Where("email = ?", email).First(&user) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -301,7 +316,7 @@ func GetUserByEmail(ctx context.Context, email string) (*User, error) { | |||||||
| // GetUserWithCompany loads a user with their company | // GetUserWithCompany loads a user with their company | ||||||
| func GetUserWithCompany(ctx context.Context, id types.ULID) (*User, error) { | func GetUserWithCompany(ctx context.Context, id types.ULID) (*User, error) { | ||||||
| 	var user User | 	var user User | ||||||
| 	result := db.GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&user) | 	result := GetEngine(ctx).Preload("Company").Where("id = ?", id).First(&user) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | 		if errors.Is(result.Error, gorm.ErrRecordNotFound) { | ||||||
| 			return nil, nil | 			return nil, nil | ||||||
| @ -314,7 +329,7 @@ func GetUserWithCompany(ctx context.Context, id types.ULID) (*User, error) { | |||||||
| // GetAllUsers returns all users | // GetAllUsers returns all users | ||||||
| func GetAllUsers(ctx context.Context) ([]User, error) { | func GetAllUsers(ctx context.Context) ([]User, error) { | ||||||
| 	var users []User | 	var users []User | ||||||
| 	result := db.GetEngine(ctx).Find(&users) | 	result := GetEngine(ctx).Find(&users) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -336,7 +351,7 @@ func GetUsersByCompanyID(ctx context.Context, companyID types.ULID) ([]User, err | |||||||
| 	var users []User | 	var users []User | ||||||
| 	// Apply the dynamic company condition | 	// Apply the dynamic company condition | ||||||
| 	condition := getCompanyCondition(&companyID) | 	condition := getCompanyCondition(&companyID) | ||||||
| 	result := db.GetEngine(ctx).Scopes(condition).Find(&users) | 	result := GetEngine(ctx).Scopes(condition).Find(&users) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return nil, result.Error | 		return nil, result.Error | ||||||
| 	} | 	} | ||||||
| @ -353,7 +368,7 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) { | |||||||
| 	// Start a transaction | 	// Start a transaction | ||||||
| 	var user *User | 	var user *User | ||||||
| 
 | 
 | ||||||
| 	err := db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | 	err := GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | ||||||
| 		// Check if email already exists | 		// Check if email already exists | ||||||
| 		var count int64 | 		var count int64 | ||||||
| 		if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil { | 		if err := tx.Model(&User{}).Where("email = ?", create.Email).Count(&count).Error; err != nil { | ||||||
| @ -363,15 +378,13 @@ func CreateUser(ctx context.Context, create UserCreate) (*User, error) { | |||||||
| 			return errors.New("email is already in use") | 			return errors.New("email is already in use") | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if create.CompanyID != nil { | 		// Check if company exists | ||||||
| 			// Check if company exists | 		var companyCount int64 | ||||||
| 			var companyCount int64 | 		if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil { | ||||||
| 			if err := tx.Model(&Company{}).Where("id = ?", create.CompanyID).Count(&companyCount).Error; err != nil { | 			return fmt.Errorf("error checking company: %w", err) | ||||||
| 				return fmt.Errorf("error checking company: %w", err) | 		} | ||||||
| 			} | 		if companyCount == 0 { | ||||||
| 			if companyCount == 0 { | 			return errors.New("the specified company does not exist") | ||||||
| 				return errors.New("the specified company does not exist") |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Hash password with unique salt | 		// Hash password with unique salt | ||||||
| @ -422,7 +435,7 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) { | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Start a transaction for the update | 	// Start a transaction for the update | ||||||
| 	err = db.GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | 	err = GetEngine(ctx).Transaction(func(tx *gorm.DB) error { | ||||||
| 		// If email is updated, check if it's already in use | 		// If email is updated, check if it's already in use | ||||||
| 		if update.Email != nil && *update.Email != user.Email { | 		if update.Email != nil && *update.Email != user.Email { | ||||||
| 			var count int64 | 			var count int64 | ||||||
| @ -479,6 +492,7 @@ func UpdateUser(ctx context.Context, update UserUpdate) (*User, error) { | |||||||
| 			} else { | 			} else { | ||||||
| 				updates["company_id"] = *update.CompanyID.Value | 				updates["company_id"] = *update.CompanyID.Value | ||||||
| 			} | 			} | ||||||
|  | 
 | ||||||
| 		} | 		} | ||||||
| 		if update.HourlyRate != nil { | 		if update.HourlyRate != nil { | ||||||
| 			updates["hourly_rate"] = *update.HourlyRate | 			updates["hourly_rate"] = *update.HourlyRate | ||||||
| @ -507,7 +521,7 @@ func DeleteUser(ctx context.Context, id types.ULID) error { | |||||||
| 	// Here one could check if dependent entities exist | 	// Here one could check if dependent entities exist | ||||||
| 	// e.g., don't delete if time entries still exist | 	// e.g., don't delete if time entries still exist | ||||||
| 
 | 
 | ||||||
| 	result := db.GetEngine(ctx).Delete(&User{}, id) | 	result := GetEngine(ctx).Delete(&User{}, id) | ||||||
| 	if result.Error != nil { | 	if result.Error != nil { | ||||||
| 		return fmt.Errorf("error deleting user: %w", result.Error) | 		return fmt.Errorf("error deleting user: %w", result.Error) | ||||||
| 	} | 	} | ||||||
| @ -537,20 +551,3 @@ func AuthenticateUser(ctx context.Context, email, password string) (*User, error | |||||||
| 
 | 
 | ||||||
| 	return user, nil | 	return user, nil | ||||||
| } | } | ||||||
| 
 |  | ||||||
| // Argon2 Parameters |  | ||||||
| const ( |  | ||||||
| 	// Recommended values for Argon2id |  | ||||||
| 	ArgonTime    = 1 |  | ||||||
| 	ArgonMemory  = 64 * 1024 // 64MB |  | ||||||
| 	ArgonThreads = 4 |  | ||||||
| 	ArgonKeyLen  = 32 |  | ||||||
| 	SaltLength   = 16 |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // Role Constants |  | ||||||
| const ( |  | ||||||
| 	RoleAdmin  = "admin" |  | ||||||
| 	RoleUser   = "user" |  | ||||||
| 	RoleViewer = "viewer" |  | ||||||
| ) |  | ||||||
|  | |||||||
| @ -1,35 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| func (u *User) EffectivePermissions(ctx context.Context, scope string) (Permission, error) { |  | ||||||
| 	if u.ActiveRole == nil { |  | ||||||
| 		return 0, nil |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// Load the role and its associated policies using the helper function. |  | ||||||
| 	role, err := LoadRoleWithPolicies(ctx, u.ActiveRole.ID) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return 0, err |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var perm Permission |  | ||||||
| 	for _, policy := range role.Policies { |  | ||||||
| 		for pat, p := range policy.Scopes { |  | ||||||
| 			if MatchScope(pat, scope) { |  | ||||||
| 				perm |= p |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return perm, nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) HasPermission(ctx context.Context, scope string, requiredPerm Permission) (bool, error) { |  | ||||||
| 	effective, err := u.EffectivePermissions(ctx, scope) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return false, err |  | ||||||
| 	} |  | ||||||
| 	return (effective & requiredPerm) == requiredPerm, nil |  | ||||||
| } |  | ||||||
| @ -1,23 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"context" |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	"github.com/oklog/ulid/v2" |  | ||||||
| 	"github.com/timetracker/backend/internal/db" |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| // LoadRoleWithPolicies loads a role with its associated policies from the database. |  | ||||||
| func LoadRoleWithPolicies(ctx context.Context, roleID ulid.ULID) (*Role, error) { |  | ||||||
| 	var role Role |  | ||||||
| 	err := db.GetEngine(ctx).Preload("Policies").First(&role, "id = ?", roleID).Error |  | ||||||
| 	if err != nil { |  | ||||||
| 		if err == gorm.ErrRecordNotFound { |  | ||||||
| 			return nil, fmt.Errorf("role with ID %s not found", roleID) |  | ||||||
| 		} |  | ||||||
| 		return nil, fmt.Errorf("failed to load role: %w", err) |  | ||||||
| 	} |  | ||||||
| 	return &role, nil |  | ||||||
| } |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import "strings" |  | ||||||
| 
 |  | ||||||
| func MatchScope(pattern, scope string) bool { |  | ||||||
| 	if strings.HasSuffix(pattern, "/*") { |  | ||||||
| 		prefix := strings.TrimSuffix(pattern, "/*") |  | ||||||
| 		return strings.HasPrefix(scope, prefix) |  | ||||||
| 	} |  | ||||||
| 	return pattern == scope |  | ||||||
| } |  | ||||||
| @ -1,13 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| type Permission uint64 |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
| 	PermRead Permission = 1 << iota // 1 |  | ||||||
| 	PermWrite                       // 2 |  | ||||||
| 	PermCreate                      // 4 |  | ||||||
| 	PermList                        // 8 |  | ||||||
| 	PermDelete                      // 16 |  | ||||||
| 	PermModerate                    // 32 |  | ||||||
| 	PermSuperadmin                  // 64 |  | ||||||
| ) |  | ||||||
| @ -1,40 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"database/sql/driver" |  | ||||||
| 	"encoding/json" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" |  | ||||||
| 
 |  | ||||||
| 	"github.com/oklog/ulid/v2" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Policy struct { |  | ||||||
| 	ID     ulid.ULID `gorm:"primaryKey;type:bytea"` |  | ||||||
| 	Name   string    `gorm:"not null"` |  | ||||||
| 	RoleID ulid.ULID `gorm:"type:bytea"`          //Fremdschlüssel |  | ||||||
| 	Scopes Scopes    `gorm:"type:jsonb;not null"` // JSONB-Spalte |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Scopes type to handle JSON marshalling |  | ||||||
| type Scopes map[string]Permission |  | ||||||
| 
 |  | ||||||
| // Scan scan value into Jsonb, implements sql.Scanner interface |  | ||||||
| func (j *Scopes) Scan(value interface{}) error { |  | ||||||
| 	bytes, ok := value.([]byte) |  | ||||||
| 	if !ok { |  | ||||||
| 		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var scopes map[string]Permission |  | ||||||
| 	if err := json.Unmarshal(bytes, &scopes); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	*j = scopes |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Value return json value, implement driver.Valuer interface |  | ||||||
| func (j Scopes) Value() (driver.Value, error) { |  | ||||||
| 	return json.Marshal(j) |  | ||||||
| } |  | ||||||
| @ -1,11 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/oklog/ulid/v2" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Role struct { |  | ||||||
| 	ID       ulid.ULID `gorm:"primaryKey;type:bytea"` |  | ||||||
| 	Name     string    `gorm:"unique;not null"` |  | ||||||
| 	Policies []Policy  `gorm:"foreignKey:RoleID"` |  | ||||||
| } |  | ||||||
| @ -1,10 +0,0 @@ | |||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/oklog/ulid/v2" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type User struct { |  | ||||||
| 	ActiveRole *Role     `gorm:"foreignKey:UserID"` // Beziehung zur aktiven Rolle |  | ||||||
| 	UserID     ulid.ULID `gorm:"type:bytea"`        //Fremdschlüssel |  | ||||||
| } |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								backend/migrate
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								backend/migrate
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							| @ -1,168 +0,0 @@ | |||||||
| { |  | ||||||
|     "info": { |  | ||||||
|         "_postman_id": "YOUR_POSTMAN_ID", |  | ||||||
|         "name": "Activity API", |  | ||||||
|         "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" |  | ||||||
|     }, |  | ||||||
|     "item": [ |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/activities", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/activities", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "activities" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/activities/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/activities/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "activities", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "POST /api/activities", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "POST", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\",\n\t\"billingRate\": 0\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/activities", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "activities" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "PUT /api/activities/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "PUT", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\",\n\t\"billingRate\": 0\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/activities/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "activities", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "DELETE /api/activities/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "DELETE", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/activities/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "activities", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,168 +0,0 @@ | |||||||
| { |  | ||||||
|     "info": { |  | ||||||
|         "_postman_id": "YOUR_POSTMAN_ID", |  | ||||||
|         "name": "Company API", |  | ||||||
|         "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" |  | ||||||
|     }, |  | ||||||
|     "item": [ |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/companies", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/companies", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "companies" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/companies/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/companies/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "companies", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "POST /api/companies", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "POST", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\"\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/companies", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "companies" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "PUT /api/companies/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "PUT", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\"\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/companies/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "companies", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "DELETE /api/companies/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "DELETE", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/companies/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "companies", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,200 +0,0 @@ | |||||||
| { |  | ||||||
|     "info": { |  | ||||||
|         "_postman_id": "YOUR_POSTMAN_ID", |  | ||||||
|         "name": "Customer API", |  | ||||||
|         "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" |  | ||||||
|     }, |  | ||||||
|     "item": [ |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/customers", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/customers", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "customers" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/customers/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/customers/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "customers", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/customers/company/:companyId", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/customers/company/:companyId", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "customers", |  | ||||||
|                         "company", |  | ||||||
|                         ":companyId" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "companyId", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "POST /api/customers", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "POST", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\",\n\t\"companyId\": \"\",\n\t\"ownerUserID\": \"\"\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/customers", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "customers" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "PUT /api/customers/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "PUT", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\",\n\t\"companyId\": \"\",\n\t\"ownerUserID\": \"\"\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/customers/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "customers", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "DELETE /api/customers/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "DELETE", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/customers/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "customers", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,225 +0,0 @@ | |||||||
| { |  | ||||||
|     "info": { |  | ||||||
|         "_postman_id": "YOUR_POSTMAN_ID", |  | ||||||
|         "name": "Project API", |  | ||||||
|         "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" |  | ||||||
|     }, |  | ||||||
|     "item": [ |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/projects", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/projects/with-customers", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects/with-customers", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects", |  | ||||||
|                         "with-customers" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/projects/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/projects/customer/:customerId", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects/customer/:customerId", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects", |  | ||||||
|                         "customer", |  | ||||||
|                         ":customerId" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "customerId", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "POST /api/projects", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "POST", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\",\n\t\"customerId\": \"\"\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "PUT /api/projects/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "PUT", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n\t\"name\": \"\",\n\t\"customerId\": \"\"\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "DELETE /api/projects/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "DELETE", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/projects/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "projects", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,292 +0,0 @@ | |||||||
| { |  | ||||||
|     "info": { |  | ||||||
|         "_postman_id": "YOUR_POSTMAN_ID", |  | ||||||
|         "name": "TimeEntry API", |  | ||||||
|         "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" |  | ||||||
|     }, |  | ||||||
|     "item": [ |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/time-entries", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/time-entries/me", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/me", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         "me" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/time-entries/range", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/range?start=2023-01-01T00:00:00Z&end=2023-01-02T00:00:00Z", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         "range" |  | ||||||
|                     ], |  | ||||||
|                     "query": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "start", |  | ||||||
|                             "value": "2023-01-01T00:00:00Z" |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             "key": "end", |  | ||||||
|                             "value": "2023-01-02T00:00:00Z" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/time-entries/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/time-entries/user/:userId", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/user/:userId", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         "user", |  | ||||||
|                         ":userId" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "userId", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "GET /api/time-entries/project/:projectId", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "GET", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/project/:projectId", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         "project", |  | ||||||
|                         ":projectId" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "projectId", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "POST /api/time-entries", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "POST", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n    \"userID\": \"\",\n    \"projectID\": \"\",\n    \"activityID\": \"\",\n    \"start\": \"2023-01-01T00:00:00Z\",\n    \"end\": \"2023-01-01T01:00:00Z\",\n    \"description\": \"\",\n    \"billable\": true\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries" |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "PUT /api/time-entries/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "PUT", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "body": { |  | ||||||
|                     "mode": "raw", |  | ||||||
|                     "raw": "{\n    \"userID\": \"\",\n    \"projectID\": \"\",\n    \"activityID\": \"\",\n    \"start\": \"2023-01-01T00:00:00Z\",\n    \"end\": \"2023-01-01T01:00:00Z\",\n    \"description\": \"\",\n    \"billable\": true\n}", |  | ||||||
|                     "options": { |  | ||||||
|                         "raw": { |  | ||||||
|                             "language": "json" |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "DELETE /api/time-entries/:id", |  | ||||||
|             "request": { |  | ||||||
|                 "method": "DELETE", |  | ||||||
|                 "header": [ |  | ||||||
|                     { |  | ||||||
|                         "key": "Authorization", |  | ||||||
|                         "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                         "type": "text" |  | ||||||
|                     } |  | ||||||
|                 ], |  | ||||||
|                 "url": { |  | ||||||
|                     "raw": "{{API_URL}}/api/time-entries/:id", |  | ||||||
|                     "host": [ |  | ||||||
|                         "{{API_URL}}" |  | ||||||
|                     ], |  | ||||||
|                     "path": [ |  | ||||||
|                         "api", |  | ||||||
|                         "time-entries", |  | ||||||
|                         ":id" |  | ||||||
|                     ], |  | ||||||
|                     "variable": [ |  | ||||||
|                         { |  | ||||||
|                             "key": "id", |  | ||||||
|                             "value": "" |  | ||||||
|                         } |  | ||||||
|                     ] |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             "response": [] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,241 +0,0 @@ | |||||||
| { |  | ||||||
|     "info": { |  | ||||||
|         "_postman_id": "YOUR_POSTMAN_ID", |  | ||||||
|         "name": "User API", |  | ||||||
|         "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" |  | ||||||
|     }, |  | ||||||
|     "item": [ |  | ||||||
|         { |  | ||||||
|             "name": "Auth", |  | ||||||
|             "item": [ |  | ||||||
|                 { |  | ||||||
|                     "name": "POST /api/auth/login", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "POST", |  | ||||||
|                         "header": [], |  | ||||||
|                         "body": { |  | ||||||
|                             "mode": "raw", |  | ||||||
|                             "raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\"\n}", |  | ||||||
|                             "options": { |  | ||||||
|                                 "raw": { |  | ||||||
|                                     "language": "json" |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/auth/login", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "auth", |  | ||||||
|                                 "login" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "name": "POST /api/auth/register", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "POST", |  | ||||||
|                         "header": [], |  | ||||||
|                         "body": { |  | ||||||
|                             "mode": "raw", |  | ||||||
|                             "raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\",\n\t\"role\": \"user\",\n\t\"companyID\": \"\",\n\t\"hourlyRate\": 0\n}", |  | ||||||
|                             "options": { |  | ||||||
|                                 "raw": { |  | ||||||
|                                     "language": "json" |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/auth/register", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "auth", |  | ||||||
|                                 "register" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "name": "GET /api/auth/me", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "GET", |  | ||||||
|                         "header": [ |  | ||||||
|                             { |  | ||||||
|                                 "key": "Authorization", |  | ||||||
|                                 "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                                 "type": "text" |  | ||||||
|                             } |  | ||||||
|                         ], |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/auth/me", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "auth", |  | ||||||
|                                 "me" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         }, |  | ||||||
|         { |  | ||||||
|             "name": "Users", |  | ||||||
|             "item": [ |  | ||||||
|                 { |  | ||||||
|                     "name": "GET /api/users", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "GET", |  | ||||||
|                         "header": [ |  | ||||||
|                             { |  | ||||||
|                                 "key": "Authorization", |  | ||||||
|                                 "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                                 "type": "text" |  | ||||||
|                             } |  | ||||||
|                         ], |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/users", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "users" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "name": "GET /api/users/:id", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "GET", |  | ||||||
|                         "header": [ |  | ||||||
|                             { |  | ||||||
|                                 "key": "Authorization", |  | ||||||
|                                 "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                                 "type": "text" |  | ||||||
|                             } |  | ||||||
|                         ], |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/users/:id", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "users", |  | ||||||
|                                 ":id" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "name": "POST /api/users", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "POST", |  | ||||||
|                         "header": [ |  | ||||||
|                             { |  | ||||||
|                                 "key": "Authorization", |  | ||||||
|                                 "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                                 "type": "text" |  | ||||||
|                             } |  | ||||||
|                         ], |  | ||||||
|                         "body": { |  | ||||||
|                             "mode": "raw", |  | ||||||
|                             "raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\",\n\t\"role\": \"user\",\n\t\"companyID\": \"\",\n\t\"hourlyRate\": 0\n}", |  | ||||||
|                             "options": { |  | ||||||
|                                 "raw": { |  | ||||||
|                                     "language": "json" |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/users", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "users" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "name": "PUT /api/users/:id", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "PUT", |  | ||||||
|                         "header": [ |  | ||||||
|                             { |  | ||||||
|                                 "key": "Authorization", |  | ||||||
|                                 "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                                 "type": "text" |  | ||||||
|                             } |  | ||||||
|                         ], |  | ||||||
|                         "body": { |  | ||||||
|                             "mode": "raw", |  | ||||||
|                             "raw": "{\n\t\"email\": \"\",\n\t\"password\": \"\",\n\t\"role\": \"user\",\n\t\"companyID\": \"\",\n\t\"hourlyRate\": 0\n}", |  | ||||||
|                             "options": { |  | ||||||
|                                 "raw": { |  | ||||||
|                                     "language": "json" |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/users/:id", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "users", |  | ||||||
|                                 ":id" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     "name": "DELETE /api/users/:id", |  | ||||||
|                     "request": { |  | ||||||
|                         "method": "DELETE", |  | ||||||
|                         "header": [ |  | ||||||
|                             { |  | ||||||
|                                 "key": "Authorization", |  | ||||||
|                                 "value": "Bearer {{JWT_TOKEN}}", |  | ||||||
|                                 "type": "text" |  | ||||||
|                             } |  | ||||||
|                         ], |  | ||||||
|                         "url": { |  | ||||||
|                             "raw": "{{API_URL}}/api/users/:id", |  | ||||||
|                             "host": [ |  | ||||||
|                                 "{{API_URL}}" |  | ||||||
|                             ], |  | ||||||
|                             "path": [ |  | ||||||
|                                 "api", |  | ||||||
|                                 "users", |  | ||||||
|                                 ":id" |  | ||||||
|                             ] |  | ||||||
|                         } |  | ||||||
|                     }, |  | ||||||
|                     "response": [] |  | ||||||
|                 } |  | ||||||
|             ] |  | ||||||
|         } |  | ||||||
|     ] |  | ||||||
| } |  | ||||||
| @ -1,250 +0,0 @@ | |||||||
| # Berechtigungssystem Plan |  | ||||||
| 
 |  | ||||||
| Dieser Plan beschreibt die Implementierung eines scope-basierten Berechtigungssystems für das TimeTracker-Projekt. |  | ||||||
| 
 |  | ||||||
| ## Grundkonzept |  | ||||||
| 
 |  | ||||||
| - Ein **Benutzer** kann eine **Rolle** annehmen, aber immer nur eine ist aktiv. |  | ||||||
| - Eine **Rolle** besteht aus mehreren **Policies**. |  | ||||||
| - Eine **Policy** hat einen Namen und eine Map, die **Scopes** (z. B. `items/books`) einem **Berechtigungsschlüssel** (Bitflag) zuordnet. |  | ||||||
| - Berechtigungsschlüssel sind Bitflags, die Permissions wie `read`, `write`, `create`, `list`, `delete`, `moderate`, `superadmin` usw. repräsentieren. |  | ||||||
| - Scopes können **Wildcards** enthalten, z. B. `items/*`, das auf `items/books` vererbt wird. |  | ||||||
| - Ziel: Berechtigungen sowohl im Go-Backend (für API-Sicherheit) als auch im TypeScript-Frontend (für UI-Anpassung) evaluieren. |  | ||||||
| 
 |  | ||||||
| ## Implementierung im Go-Backend |  | ||||||
| 
 |  | ||||||
| ### 1. Ordnerstruktur |  | ||||||
| 
 |  | ||||||
| - Neuer Ordner: `backend/internal/permissions` |  | ||||||
| - Dateien: |  | ||||||
|     - `permissions.go`: `Permission`-Konstanten (Bitflags). |  | ||||||
|     - `policy.go`: `Policy`-Struktur. |  | ||||||
|     - `role.go`: `Role`-Struktur. |  | ||||||
|     - `user.go`: Erweiterung der `User`-Struktur. |  | ||||||
|     - `matching.go`: `matchScope`-Funktion. |  | ||||||
|     - `evaluator.go`: `EffectivePermissions`- und `HasPermission`-Funktionen. |  | ||||||
| 
 |  | ||||||
| ### 2. Go-Strukturen |  | ||||||
| 
 |  | ||||||
| - `permissions.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| type Permission uint64 |  | ||||||
| 
 |  | ||||||
| const ( |  | ||||||
|     PermRead Permission = 1 << iota // 1 |  | ||||||
|     PermWrite                       // 2 |  | ||||||
|     PermCreate                      // 4 |  | ||||||
|     PermList                        // 8 |  | ||||||
|     PermDelete                      // 16 |  | ||||||
|     PermModerate                    // 32 |  | ||||||
|     PermSuperadmin                  // 64 |  | ||||||
| ) |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| - `policy.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| type Policy struct { |  | ||||||
|     Name   string |  | ||||||
|     Scopes map[string]Permission |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| - `role.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| type Role struct { |  | ||||||
|     Name     string |  | ||||||
|     Policies []Policy |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| - `user.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import "github.com/your-org/your-project/backend/internal/models" // Pfad anpassen |  | ||||||
| 
 |  | ||||||
| type User struct { |  | ||||||
|     models.User // Einbettung |  | ||||||
|     ActiveRole *Role |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### 3. Funktionen |  | ||||||
| 
 |  | ||||||
| - `matching.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import "strings" |  | ||||||
| 
 |  | ||||||
| func MatchScope(pattern, scope string) bool { |  | ||||||
|     if strings.HasSuffix(pattern, "/*") { |  | ||||||
|         prefix := strings.TrimSuffix(pattern, "/*") |  | ||||||
|         return strings.HasPrefix(scope, prefix) |  | ||||||
|     } |  | ||||||
|     return pattern == scope |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| - `evaluator.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| func (u *User) EffectivePermissions(scope string) Permission { |  | ||||||
|     if u.ActiveRole == nil { |  | ||||||
|         return 0 |  | ||||||
|     } |  | ||||||
|     var perm Permission |  | ||||||
|     for _, policy := range u.ActiveRole.Policies { |  | ||||||
|         for pat, p := range policy.Scopes { |  | ||||||
|             if MatchScope(pat, scope) { |  | ||||||
|                 perm |= p |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|     return perm |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (u *User) HasPermission(scope string, requiredPerm Permission) bool { |  | ||||||
|     effective := u.EffectivePermissions(scope) |  | ||||||
|     return (effective & requiredPerm) == requiredPerm |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| ### 4. Integration in die API-Handler |  | ||||||
| 
 |  | ||||||
| - Anpassung der `jwt_auth.go` Middleware. |  | ||||||
| - Verwendung von `HasPermission` in den API-Handlern. |  | ||||||
| 
 |  | ||||||
| ## Persistierung (Datenbank) |  | ||||||
| 
 |  | ||||||
| ### 1. Datenbankmodell |  | ||||||
| 
 |  | ||||||
| - Zwei neue Tabellen: `roles` und `policies`. |  | ||||||
| - `roles`: |  | ||||||
|     - `id` (ULID, Primärschlüssel) |  | ||||||
|     - `name` (VARCHAR, eindeutig) |  | ||||||
| - `policies`: |  | ||||||
|     - `id` (ULID, Primärschlüssel) |  | ||||||
|     - `name` (VARCHAR, eindeutig) |  | ||||||
|     - `role_id` (ULID, Fremdschlüssel, der auf `roles.id` verweist) |  | ||||||
|     - `scopes` (JSONB oder TEXT, speichert die `map[string]Permission` als JSON) |  | ||||||
| - Beziehung: 1:n zwischen `roles` und `policies`. |  | ||||||
| 
 |  | ||||||
| ### 2. Go-Strukturen (Anpassungen) |  | ||||||
| 
 |  | ||||||
| - `role.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"github.com/your-org/your-project/backend/internal/types" // Pfad anpassen |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Role struct { |  | ||||||
|     ID       types.ULID `gorm:"primaryKey;type:bytea"` |  | ||||||
|     Name     string     `gorm:"unique;not null"` |  | ||||||
|     Policies []Policy   `gorm:"foreignKey:RoleID"` |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| 
 |  | ||||||
| - `policy.go`: |  | ||||||
| 
 |  | ||||||
| ```go |  | ||||||
| package permissions |  | ||||||
| 
 |  | ||||||
| import ( |  | ||||||
| 	"encoding/json" |  | ||||||
|     "database/sql/driver" |  | ||||||
|     "errors" |  | ||||||
|     "fmt" |  | ||||||
| 
 |  | ||||||
| 	"github.com/your-org/your-project/backend/internal/types" // Pfad anpassen |  | ||||||
| 	"gorm.io/gorm" |  | ||||||
| 	"gorm.io/gorm/clause" |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| type Policy struct { |  | ||||||
|     ID      types.ULID      `gorm:"primaryKey;type:bytea"` |  | ||||||
|     Name    string          `gorm:"not null"` |  | ||||||
|     RoleID  types.ULID      `gorm:"type:bytea"` //Fremdschlüssel |  | ||||||
|     Scopes  Scopes          `gorm:"type:jsonb;not null"` // JSONB-Spalte |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| //Scopes type to handle JSON marshalling |  | ||||||
| type Scopes map[string]Permission |  | ||||||
| 
 |  | ||||||
| // Scan scan value into Jsonb, implements sql.Scanner interface |  | ||||||
| func (j *Scopes) Scan(value interface{}) error { |  | ||||||
| 	bytes, ok := value.([]byte) |  | ||||||
| 	if !ok { |  | ||||||
| 		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	var scopes map[string]Permission |  | ||||||
| 	if err := json.Unmarshal(bytes, &scopes); err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
| 	*j = scopes |  | ||||||
| 	return nil |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // Value return json value, implement driver.Valuer interface |  | ||||||
| func (j Scopes) Value() (driver.Value, error) { |  | ||||||
| 	return json.Marshal(j) |  | ||||||
| } |  | ||||||
| ``` |  | ||||||
| ### 3. Migration |  | ||||||
| 
 |  | ||||||
| - Verwendung des vorhandenen Migrations-Frameworks (`backend/cmd/migrate/main.go`). |  | ||||||
| 
 |  | ||||||
| ### 4. Seed-Daten |  | ||||||
| 
 |  | ||||||
| - Optionale Seed-Daten (`backend/cmd/seed/main.go`). |  | ||||||
| 
 |  | ||||||
| ### 5. Anpassung der Funktionen |  | ||||||
| 
 |  | ||||||
| - Anpassung von `EffectivePermissions` und `HasPermission` in `evaluator.go` für Datenbankzugriff. |  | ||||||
| 
 |  | ||||||
| ## Mermaid Diagramm |  | ||||||
| ```mermaid |  | ||||||
| graph LR |  | ||||||
|     subgraph Benutzer |  | ||||||
|         U[User] --> AR(ActiveRole) |  | ||||||
|     end |  | ||||||
|     subgraph Rolle |  | ||||||
|         AR --> R(Role) |  | ||||||
|         R --> P1(Policy 1) |  | ||||||
|         R --> P2(Policy 2) |  | ||||||
|         R --> Pn(Policy n) |  | ||||||
|     end |  | ||||||
|     subgraph Policy |  | ||||||
|         P1 --> S1(Scope 1: Permissions) |  | ||||||
|         P1 --> S2(Scope 2: Permissions) |  | ||||||
|         P2 --> S3(Scope 3: Permissions) |  | ||||||
|         Pn --> Sm(Scope m: Permissions) |  | ||||||
|     end |  | ||||||
| 
 |  | ||||||
|     S1 -- Permissions --> PR(PermRead) |  | ||||||
|     S1 -- Permissions --> PW(PermWrite) |  | ||||||
|     S2 -- Permissions --> PL(PermList) |  | ||||||
|     Sm -- Permissions --> PD(PermDelete) |  | ||||||
|      |  | ||||||
|     style U fill:#f9f,stroke:#333,stroke-width:2px |  | ||||||
|     style R fill:#ccf,stroke:#333,stroke-width:2px |  | ||||||
|     style P1,P2,Pn fill:#ddf,stroke:#333,stroke-width:2px |  | ||||||
|     style S1,S2,S3,Sm fill:#eef,stroke:#333,stroke-width:1px |  | ||||||
|     style PR,PW,PL,PD fill:#ff9,stroke:#333,stroke-width:1px |  | ||||||
							
								
								
									
										113
									
								
								flake.nix
									
									
									
									
									
								
							
							
						
						
									
										113
									
								
								flake.nix
									
									
									
									
									
								
							| @ -1,113 +0,0 @@ | |||||||
| { |  | ||||||
|   description = "Development environment for Go and Next.js (TypeScript)"; |  | ||||||
| 
 |  | ||||||
|   inputs = { |  | ||||||
|     nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; |  | ||||||
|     flake-utils.url = "github:numtide/flake-utils"; |  | ||||||
|   }; |  | ||||||
| 
 |  | ||||||
|   outputs = { self, nixpkgs, flake-utils }: |  | ||||||
|     flake-utils.lib.eachDefaultSystem (system: |  | ||||||
|       let |  | ||||||
|         pkgs = import nixpkgs { |  | ||||||
|           inherit system; |  | ||||||
|           config = { |  | ||||||
|             allowUnfree = true; |  | ||||||
|           }; |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         # Go development tools |  | ||||||
|         goPackages = with pkgs; [ |  | ||||||
|           go |  | ||||||
|           gopls |  | ||||||
|           golangci-lint |  | ||||||
|           delve |  | ||||||
|           go-outline |  | ||||||
|           gotools |  | ||||||
|           go-mockgen |  | ||||||
|           gomodifytags |  | ||||||
|           impl |  | ||||||
|           gotests |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         # TypeScript/Next.js development tools |  | ||||||
|         nodePackages = with pkgs; [ |  | ||||||
|           nodejs_20 |  | ||||||
|           nodePackages.typescript |  | ||||||
|           nodePackages.typescript-language-server |  | ||||||
|           nodePackages.yarn |  | ||||||
|           nodePackages.pnpm |  | ||||||
|           nodePackages.npm |  | ||||||
|           nodePackages.prettier |  | ||||||
|           nodePackages.eslint |  | ||||||
|           nodePackages.next |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         # General development tools |  | ||||||
|         commonPackages = with pkgs; [ |  | ||||||
|           git |  | ||||||
|           gh |  | ||||||
|           nixpkgs-fmt |  | ||||||
|           pre-commit |  | ||||||
|           ripgrep |  | ||||||
|           jq |  | ||||||
|           curl |  | ||||||
|           coreutils |  | ||||||
|           gnumake |  | ||||||
|         ]; |  | ||||||
| 
 |  | ||||||
|         # VSCode with extensions |  | ||||||
|         vscodeWithExtensions = pkgs.vscode-with-extensions.override { |  | ||||||
|           vscodeExtensions = with pkgs.vscode-extensions; [ |  | ||||||
|             golang.go                   # Go support |  | ||||||
|             esbenp.prettier-vscode      # Prettier |  | ||||||
|             dbaeumer.vscode-eslint      # ESLint |  | ||||||
|             ms-vscode.vscode-typescript-tslint-plugin # TypeScript |  | ||||||
|             bradlc.vscode-tailwindcss   # Tailwind CSS support |  | ||||||
|             jnoortheen.nix-ide          # Nix support |  | ||||||
|           ] ++ pkgs.vscode-utils.extensionsFromVscodeMarketplace [ |  | ||||||
|             { |  | ||||||
|               name = "nextjs"; |  | ||||||
|               publisher = "pulkitgangwar"; |  | ||||||
|               version = "1.0.6"; |  | ||||||
|               sha256 = "sha256-L6ZgqNkM0qzSiTKiGfgQB9m3U0HmwLA3NZ9nrslQjeg="; |  | ||||||
|             } |  | ||||||
|           ]; |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|       in |  | ||||||
|       { |  | ||||||
|         devShells.default = pkgs.mkShell { |  | ||||||
|           buildInputs = goPackages ++ nodePackages ++ commonPackages ++ [ vscodeWithExtensions ]; |  | ||||||
| 
 |  | ||||||
|           shellHook = '' |  | ||||||
|             echo "🚀 Welcome to the Go and Next.js (TypeScript) development environment!" |  | ||||||
|             echo "📦 Available tools:" |  | ||||||
|             echo "   Go: $(go version)" |  | ||||||
|             echo "   Node: $(node --version)" |  | ||||||
|             echo "   TypeScript: $(tsc --version)" |  | ||||||
|             echo "   Next.js: $(npx next --version)" |  | ||||||
|             echo "" |  | ||||||
|             echo "🔧 Use 'code .' to open VSCode with the appropriate extensions" |  | ||||||
|             echo "🔄 Run 'nix flake update' to update dependencies" |  | ||||||
|           ''; |  | ||||||
| 
 |  | ||||||
|           # Environment variables |  | ||||||
|           GOROOT = "${pkgs.go}/share/go"; |  | ||||||
|           GOPATH = "$(pwd)/.go"; |  | ||||||
|           GO111MODULE = "on"; |  | ||||||
|            |  | ||||||
|           # NodeJS setup |  | ||||||
|           NODE_OPTIONS = "--max-old-space-size=4096"; |  | ||||||
|         }; |  | ||||||
| 
 |  | ||||||
|         # Optional: Add custom packages if needed |  | ||||||
|         packages = { |  | ||||||
|           # Example of a custom package or script if needed |  | ||||||
|           # my-tool = ... |  | ||||||
|         }; |  | ||||||
|          |  | ||||||
|         # Default package if someone runs `nix build` |  | ||||||
|         defaultPackage = self.devShells.${system}.default; |  | ||||||
|       }); |  | ||||||
| } |  | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user