feat: Update database models and DTOs to use bytea for ULIDWrapper and add JWT configuration to environment

This commit is contained in:
2025-03-11 23:11:49 +00:00
parent c08da6fc92
commit 9057adebdd
19 changed files with 315 additions and 327 deletions
@@ -152,7 +152,11 @@ func (h *CustomerHandler) CreateCustomer(c *gin.Context) {
}
// Convert DTO to model
customerCreate := convertCreateCustomerDTOToModel(customerCreateDTO)
customerCreate, err := convertCreateCustomerDTOToModel(customerCreateDTO)
if err != nil {
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
return
}
// Create customer in the database
customer, err := models.CreateCustomer(c.Request.Context(), customerCreate)
@@ -203,7 +207,11 @@ func (h *CustomerHandler) UpdateCustomer(c *gin.Context) {
customerUpdateDTO.ID = id.String()
// Convert DTO to model
customerUpdate := convertUpdateCustomerDTOToModel(customerUpdateDTO)
customerUpdate, err := convertUpdateCustomerDTOToModel(customerUpdateDTO)
if err != nil {
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
return
}
// Update customer in the database
customer, err := models.UpdateCustomer(c.Request.Context(), customerUpdate)
@@ -264,21 +272,32 @@ func convertCustomerToDTO(customer *models.Customer) dto.CustomerDto {
CreatedAt: customer.CreatedAt,
UpdatedAt: customer.UpdatedAt,
Name: customer.Name,
CompanyID: customer.CompanyID,
CompanyID: customer.CompanyID.String(),
}
}
func convertCreateCustomerDTOToModel(dto dto.CustomerCreateDto) models.CustomerCreate {
return models.CustomerCreate{
func convertCreateCustomerDTOToModel(dto dto.CustomerCreateDto) (models.CustomerCreate, error) {
companyID, err := models.ULIDWrapperFromString(dto.CompanyID)
if err != nil {
return models.CustomerCreate{}, fmt.Errorf("invalid company ID: %w", err)
}
create := models.CustomerCreate{
Name: dto.Name,
CompanyID: dto.CompanyID,
CompanyID: companyID,
}
return create, nil
}
func convertUpdateCustomerDTOToModel(dto dto.CustomerUpdateDto) models.CustomerUpdate {
id, _ := ulid.Parse(dto.ID)
func convertUpdateCustomerDTOToModel(dto dto.CustomerUpdateDto) (models.CustomerUpdate, error) {
id, err := models.ULIDWrapperFromString(dto.ID)
if err != nil {
return models.CustomerUpdate{}, fmt.Errorf("invalid customer ID: %w", err)
}
update := models.CustomerUpdate{
ID: models.FromULID(id),
ID: id,
}
if dto.Name != nil {
@@ -286,10 +305,14 @@ func convertUpdateCustomerDTOToModel(dto dto.CustomerUpdateDto) models.CustomerU
}
if dto.CompanyID != nil {
update.CompanyID = dto.CompanyID
companyID, err := models.ULIDWrapperFromString(*dto.CompanyID)
if err != nil {
return models.CustomerUpdate{}, fmt.Errorf("invalid company ID: %w", err)
}
update.CompanyID = &companyID
}
return update
return update, nil
}
// Helper function to parse company ID from string
@@ -296,36 +296,34 @@ func (h *ProjectHandler) DeleteProject(c *gin.Context) {
// Helper functions for DTO conversion
func convertProjectToDTO(project *models.Project) dto.ProjectDto {
customerID := 0
if project.CustomerID.Compare(models.ULIDWrapper{}) != 0 {
// This is a simplification, adjust as needed
customerID = int(project.CustomerID.Time())
}
return dto.ProjectDto{
ID: project.ID.String(),
CreatedAt: project.CreatedAt,
UpdatedAt: project.UpdatedAt,
Name: project.Name,
CustomerID: customerID,
CustomerID: project.CustomerID.String(),
}
}
func convertCreateProjectDTOToModel(dto dto.ProjectCreateDto) (models.ProjectCreate, error) {
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
customerID, err := customerIDToULID(dto.CustomerID)
customerID, err := models.ULIDWrapperFromString(dto.CustomerID)
if err != nil {
return models.ProjectCreate{}, fmt.Errorf("invalid customer ID: %w", err)
}
return models.ProjectCreate{
Name: dto.Name,
CustomerID: models.FromULID(customerID),
CustomerID: customerID,
}, nil
}
func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto) (models.ProjectUpdate, error) {
id, _ := ulid.Parse(dto.ID)
id, err := ulid.Parse(dto.ID)
if err != nil {
return models.ProjectUpdate{}, fmt.Errorf("invalid project ID: %w", err)
}
update := models.ProjectUpdate{
ID: models.FromULID(id),
}
@@ -336,25 +334,12 @@ func convertUpdateProjectDTOToModel(dto dto.ProjectUpdateDto) (models.ProjectUpd
if dto.CustomerID != nil {
// Convert CustomerID from int to ULID (this is a simplification, adjust as needed)
customerID, err := customerIDToULID(*dto.CustomerID)
customerID, err := models.ULIDWrapperFromString(*dto.CustomerID)
if err != nil {
return models.ProjectUpdate{}, fmt.Errorf("invalid customer ID: %w", err)
}
wrappedID := models.FromULID(customerID)
update.CustomerID = &wrappedID
update.CustomerID = &customerID
}
return update, nil
}
// Helper function to convert customer ID from int to ULID
func customerIDToULID(id int) (ulid.ULID, error) {
// This is a simplification, in a real application you would need to
// fetch the actual ULID from the database or use a proper conversion method
// For now, we'll create a deterministic ULID based on the int value
entropy := ulid.Monotonic(nil, 0)
timestamp := uint64(id)
// Create a new ULID with the timestamp and entropy
return ulid.MustNew(timestamp, entropy), nil
}
@@ -406,9 +406,9 @@ func convertTimeEntryToDTO(timeEntry *models.TimeEntry) dto.TimeEntryDto {
ID: timeEntry.ID.String(),
CreatedAt: timeEntry.CreatedAt,
UpdatedAt: timeEntry.UpdatedAt,
UserID: int(timeEntry.UserID.Time()), // Simplified conversion
ProjectID: int(timeEntry.ProjectID.Time()), // Simplified conversion
ActivityID: int(timeEntry.ActivityID.Time()), // Simplified conversion
UserID: timeEntry.UserID.String(), // Simplified conversion
ProjectID: timeEntry.ProjectID.String(), // Simplified conversion
ActivityID: timeEntry.ActivityID.String(), // Simplified conversion
Start: timeEntry.Start,
End: timeEntry.End,
Description: timeEntry.Description,
@@ -418,25 +418,25 @@ func convertTimeEntryToDTO(timeEntry *models.TimeEntry) dto.TimeEntryDto {
func convertCreateTimeEntryDTOToModel(dto dto.TimeEntryCreateDto) (models.TimeEntryCreate, error) {
// Convert IDs from int to ULID (this is a simplification, adjust as needed)
userID, err := idToULID(dto.UserID)
userID, err := models.ULIDWrapperFromString(dto.UserID)
if err != nil {
return models.TimeEntryCreate{}, fmt.Errorf("invalid user ID: %w", err)
}
projectID, err := idToULID(dto.ProjectID)
projectID, err := models.ULIDWrapperFromString(dto.ProjectID)
if err != nil {
return models.TimeEntryCreate{}, fmt.Errorf("invalid project ID: %w", err)
}
activityID, err := idToULID(dto.ActivityID)
activityID, err := models.ULIDWrapperFromString(dto.ActivityID)
if err != nil {
return models.TimeEntryCreate{}, fmt.Errorf("invalid activity ID: %w", err)
}
return models.TimeEntryCreate{
UserID: models.FromULID(userID),
ProjectID: models.FromULID(projectID),
ActivityID: models.FromULID(activityID),
UserID: userID,
ProjectID: projectID,
ActivityID: activityID,
Start: dto.Start,
End: dto.End,
Description: dto.Description,
@@ -445,36 +445,36 @@ func convertCreateTimeEntryDTOToModel(dto dto.TimeEntryCreateDto) (models.TimeEn
}
func convertUpdateTimeEntryDTOToModel(dto dto.TimeEntryUpdateDto) (models.TimeEntryUpdate, error) {
id, _ := ulid.Parse(dto.ID)
id, err := ulid.Parse(dto.ID)
if err != nil {
return models.TimeEntryUpdate{}, fmt.Errorf("invalid time entry ID: %w", err)
}
update := models.TimeEntryUpdate{
ID: models.FromULID(id),
}
if dto.UserID != nil {
userID, err := idToULID(*dto.UserID)
userID, err := models.ULIDWrapperFromString(*dto.UserID)
if err != nil {
return models.TimeEntryUpdate{}, fmt.Errorf("invalid user ID: %w", err)
}
wrappedID := models.FromULID(userID)
update.UserID = &wrappedID
update.UserID = &userID
}
if dto.ProjectID != nil {
projectID, err := idToULID(*dto.ProjectID)
projectID, err := models.ULIDWrapperFromString(*dto.ProjectID)
if err != nil {
return models.TimeEntryUpdate{}, fmt.Errorf("invalid project ID: %w", err)
}
wrappedProjectID := models.FromULID(projectID)
update.ProjectID = &wrappedProjectID
update.ProjectID = &projectID
}
if dto.ActivityID != nil {
activityID, err := idToULID(*dto.ActivityID)
activityID, err := models.ULIDWrapperFromString(*dto.ActivityID)
if err != nil {
return models.TimeEntryUpdate{}, fmt.Errorf("invalid activity ID: %w", err)
}
wrappedActivityID := models.FromULID(activityID)
update.ActivityID = &wrappedActivityID
update.ActivityID = &activityID
}
if dto.Start != nil {
@@ -495,13 +495,3 @@ func convertUpdateTimeEntryDTOToModel(dto dto.TimeEntryUpdateDto) (models.TimeEn
return update, nil
}
// Helper function to convert ID from int to ULID
func idToULID(id int) (ulid.ULID, error) {
// This is a simplification, in a real application you would need to
// fetch the actual ULID from the database or use a proper conversion method
// For now, we'll create a deterministic ULID based on the int value
entropy := ulid.Monotonic(nil, 0)
timestamp := uint64(id)
return ulid.MustNew(timestamp, entropy), nil
}
@@ -1,23 +1,97 @@
package middleware
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"os"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/joho/godotenv"
"github.com/oklog/ulid/v2"
"github.com/timetracker/backend/internal/api/utils"
"github.com/timetracker/backend/internal/models"
)
// JWT configuration
const (
// This should be moved to environment variables in production
jwtSecret = "your-secret-key-change-in-production"
var (
jwtSecret string
tokenDuration = 24 * time.Hour
)
func init() {
// Load .env file
_ = godotenv.Load()
// Get JWT secret from environment
jwtSecret = os.Getenv("JWT_SECRET")
// Generate a random secret if none is provided
if jwtSecret == "" {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
panic("failed to generate JWT secret: " + err.Error())
}
jwtSecret = string(randomBytes)
}
// Generate and store RSA keys if configured
if os.Getenv("JWT_KEY_GENERATE") == "true" {
keyDir := os.Getenv("JWT_KEY_DIR")
if keyDir == "" {
keyDir = "./keys"
}
// Create directory if it doesn't exist
if err := os.MkdirAll(keyDir, 0755); err != nil {
panic("failed to create key directory: " + err.Error())
}
// Generate RSA key pair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic("failed to generate RSA key pair: " + err.Error())
}
// Save private key
privateKeyFile, err := os.Create(fmt.Sprintf("%s/private.pem", keyDir))
if err != nil {
panic("failed to create private key file: " + err.Error())
}
defer privateKeyFile.Close()
privateKeyPEM := &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(privateKey),
}
if err := pem.Encode(privateKeyFile, privateKeyPEM); err != nil {
panic("failed to encode private key: " + err.Error())
}
// Save public key
publicKeyFile, err := os.Create(fmt.Sprintf("%s/public.pem", keyDir))
if err != nil {
panic("failed to create public key file: " + err.Error())
}
defer publicKeyFile.Close()
publicKeyPEM := &pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: x509.MarshalPKCS1PublicKey(&privateKey.PublicKey),
}
if err := pem.Encode(publicKeyFile, publicKeyPEM); err != nil {
panic("failed to encode public key: " + err.Error())
}
}
}
// Claims represents the JWT claims
type Claims struct {
UserID string `json:"userId"`