Compare commits
No commits in common. "78be762430f515d0a4ef2d94a2d14b919f8c73fe" and "8785b86bfc1723d49571c5d4fadb01cbb8da708d" have entirely different histories.
78be762430
...
8785b86bfc
@ -1,77 +0,0 @@
|
|||||||
# Time Tracker Backend Makefile
|
|
||||||
|
|
||||||
.PHONY: db-start db-stop db-test model-test run build clean migrate seed help
|
|
||||||
|
|
||||||
# Default target
|
|
||||||
.DEFAULT_GOAL := help
|
|
||||||
|
|
||||||
# Variables
|
|
||||||
BINARY_NAME=timetracker
|
|
||||||
DB_CONTAINER=timetracker_db
|
|
||||||
|
|
||||||
# Help target
|
|
||||||
help:
|
|
||||||
@echo "Time Tracker Backend Makefile"
|
|
||||||
@echo ""
|
|
||||||
@echo "Usage:"
|
|
||||||
@echo " make db-start - Start the PostgreSQL database container"
|
|
||||||
@echo " make db-stop - Stop the PostgreSQL database container"
|
|
||||||
@echo " make db-test - Test the database connection"
|
|
||||||
@echo " make model-test - Test the database models"
|
|
||||||
@echo " make run - Run the application"
|
|
||||||
@echo " make build - Build the application"
|
|
||||||
@echo " make clean - Remove build artifacts"
|
|
||||||
@echo " make migrate - Run database migrations"
|
|
||||||
@echo " make seed - Seed the database with initial data"
|
|
||||||
@echo " make help - Show this help message"
|
|
||||||
|
|
||||||
# Start the database
|
|
||||||
db-start:
|
|
||||||
@echo "Starting PostgreSQL database container..."
|
|
||||||
@cd .. && docker-compose up -d db
|
|
||||||
@echo "Database container started"
|
|
||||||
|
|
||||||
# Stop the database
|
|
||||||
db-stop:
|
|
||||||
@echo "Stopping PostgreSQL database container..."
|
|
||||||
@cd .. && docker-compose stop db
|
|
||||||
@echo "Database container stopped"
|
|
||||||
|
|
||||||
# Test the database connection
|
|
||||||
db-test:
|
|
||||||
@echo "Testing database connection..."
|
|
||||||
@go run cmd/dbtest/main.go
|
|
||||||
|
|
||||||
# Test the database models
|
|
||||||
model-test:
|
|
||||||
@echo "Testing database models..."
|
|
||||||
@go run cmd/modeltest/main.go
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
run:
|
|
||||||
@echo "Running the application..."
|
|
||||||
@go run cmd/api/main.go
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
build:
|
|
||||||
@echo "Building the application..."
|
|
||||||
@go build -o $(BINARY_NAME) cmd/api/main.go
|
|
||||||
@echo "Build complete: $(BINARY_NAME)"
|
|
||||||
|
|
||||||
# Clean build artifacts
|
|
||||||
clean:
|
|
||||||
@echo "Cleaning build artifacts..."
|
|
||||||
@rm -f $(BINARY_NAME)
|
|
||||||
@echo "Clean complete"
|
|
||||||
|
|
||||||
# Run database migrations
|
|
||||||
migrate:
|
|
||||||
@echo "Running database migrations..."
|
|
||||||
@go run -mod=mod cmd/migrate/main.go
|
|
||||||
@echo "Migrations complete"
|
|
||||||
|
|
||||||
# Seed the database with initial data
|
|
||||||
seed:
|
|
||||||
@echo "Seeding the database..."
|
|
||||||
@go run -mod=mod cmd/seed/main.go
|
|
||||||
@echo "Seeding complete"
|
|
@ -1,147 +0,0 @@
|
|||||||
# Time Tracker Backend
|
|
||||||
|
|
||||||
This is the backend service for the Time Tracker application, built with Go, Gin, and GORM.
|
|
||||||
|
|
||||||
## Database Setup
|
|
||||||
|
|
||||||
The application uses PostgreSQL as its database. The database connection is configured using GORM, a popular Go ORM library.
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
Database configuration is handled through the `models.DatabaseConfig` struct in `internal/models/db.go`. The application uses sensible defaults that can be overridden with environment variables:
|
|
||||||
|
|
||||||
- `DB_HOST`: Database host (default: "localhost")
|
|
||||||
- `DB_PORT`: Database port (default: 5432)
|
|
||||||
- `DB_USER`: Database user (default: "timetracker")
|
|
||||||
- `DB_PASSWORD`: Database password (default: "password")
|
|
||||||
- `DB_NAME`: Database name (default: "timetracker")
|
|
||||||
- `DB_SSLMODE`: SSL mode (default: "disable")
|
|
||||||
|
|
||||||
### Running with Docker
|
|
||||||
|
|
||||||
The easiest way to run the database is using Docker Compose:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start the database
|
|
||||||
docker-compose up -d db
|
|
||||||
|
|
||||||
# Check if the database is running
|
|
||||||
docker-compose ps
|
|
||||||
```
|
|
||||||
|
|
||||||
### Database Migrations
|
|
||||||
|
|
||||||
The application automatically migrates the database schema on startup using GORM's AutoMigrate feature. This creates all necessary tables based on the model definitions.
|
|
||||||
|
|
||||||
### Initial Data Seeding
|
|
||||||
|
|
||||||
The application seeds the database with initial data if it's empty. This includes:
|
|
||||||
|
|
||||||
- A default company
|
|
||||||
- An admin user with email "admin@example.com" and password "Admin@123456"
|
|
||||||
|
|
||||||
## Running the Application
|
|
||||||
|
|
||||||
### Using Make Commands
|
|
||||||
|
|
||||||
The project includes a Makefile with common commands:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start the database
|
|
||||||
make db-start
|
|
||||||
|
|
||||||
# Test the database connection
|
|
||||||
make db-test
|
|
||||||
|
|
||||||
# Run database migrations
|
|
||||||
make migrate
|
|
||||||
|
|
||||||
# Seed the database with initial data
|
|
||||||
make seed
|
|
||||||
|
|
||||||
# Test the database models
|
|
||||||
make model-test
|
|
||||||
|
|
||||||
# Run the application
|
|
||||||
make run
|
|
||||||
|
|
||||||
# Build the application
|
|
||||||
make build
|
|
||||||
|
|
||||||
# Show all available commands
|
|
||||||
make help
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Commands
|
|
||||||
|
|
||||||
If you prefer not to use Make, you can run the commands directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start the database
|
|
||||||
cd /path/to/timetracker
|
|
||||||
docker-compose up -d db
|
|
||||||
|
|
||||||
# Test the database connection
|
|
||||||
cd backend
|
|
||||||
go run cmd/dbtest/main.go
|
|
||||||
|
|
||||||
# Run database migrations
|
|
||||||
cd backend
|
|
||||||
go run cmd/migrate/main.go
|
|
||||||
|
|
||||||
# Seed the database with initial data
|
|
||||||
cd backend
|
|
||||||
go run cmd/seed/main.go
|
|
||||||
|
|
||||||
# Run the backend application
|
|
||||||
cd backend
|
|
||||||
go run cmd/api/main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
The API will be available at http://localhost:8080/api and the Swagger documentation at http://localhost:8080/swagger/index.html.
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
You can configure the database connection using environment variables:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Example: Connect to a different database
|
|
||||||
DB_HOST=my-postgres-server DB_PORT=5432 DB_USER=myuser DB_PASSWORD=mypassword DB_NAME=mydb go run cmd/api/main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
## Database Models
|
|
||||||
|
|
||||||
The application uses the following models:
|
|
||||||
|
|
||||||
- `User`: Represents a user in the system
|
|
||||||
- `Company`: Represents a company
|
|
||||||
- `Customer`: Represents a customer
|
|
||||||
- `Project`: Represents a project
|
|
||||||
- `Activity`: Represents an activity
|
|
||||||
- `TimeEntry`: Represents a time entry
|
|
||||||
|
|
||||||
Each model has corresponding CRUD operations and relationships defined in the `internal/models` directory.
|
|
||||||
|
|
||||||
## GORM Best Practices
|
|
||||||
|
|
||||||
The application follows these GORM best practices:
|
|
||||||
|
|
||||||
1. **Connection Pooling**: Configured with sensible defaults for maximum idle connections, maximum open connections, and connection lifetime.
|
|
||||||
|
|
||||||
2. **Migrations**: Uses GORM's AutoMigrate to automatically create and update database tables.
|
|
||||||
|
|
||||||
3. **Transactions**: Uses transactions for operations that require multiple database changes to ensure data consistency.
|
|
||||||
|
|
||||||
4. **Soft Deletes**: Uses GORM's soft delete feature to mark records as deleted without actually removing them from the database.
|
|
||||||
|
|
||||||
5. **Relationships**: Properly defines relationships between models using GORM's relationship features.
|
|
||||||
|
|
||||||
6. **Error Handling**: Properly handles database errors and returns appropriate error messages.
|
|
||||||
|
|
||||||
7. **Context Support**: Uses context for database operations to support timeouts and cancellation.
|
|
||||||
|
|
||||||
8. **Logging**: Configures GORM's logger for appropriate logging based on the environment.
|
|
||||||
|
|
||||||
9. **Graceful Shutdown**: Properly closes database connections when the application shuts down.
|
|
||||||
|
|
||||||
10. **Validation**: Implements validation for model fields before saving to the database.
|
|
@ -1,62 +0,0 @@
|
|||||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
|
||||||
package docs
|
|
||||||
|
|
||||||
import "github.com/swaggo/swag"
|
|
||||||
|
|
||||||
const docTemplate = `{
|
|
||||||
"schemes": {{ marshal .Schemes }},
|
|
||||||
"swagger": "2.0",
|
|
||||||
"info": {
|
|
||||||
"description": "{{escape .Description}}",
|
|
||||||
"title": "{{.Title}}",
|
|
||||||
"contact": {},
|
|
||||||
"version": "{{.Version}}"
|
|
||||||
},
|
|
||||||
"host": "{{.Host}}",
|
|
||||||
"basePath": "{{.BasePath}}",
|
|
||||||
"paths": {
|
|
||||||
"/": {
|
|
||||||
"get": {
|
|
||||||
"description": "Get a hello message",
|
|
||||||
"produces": [
|
|
||||||
"text/plain"
|
|
||||||
],
|
|
||||||
"summary": "Say hello",
|
|
||||||
"operationId": "hello",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Hello from the Time Tracker Backend!",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securityDefinitions": {
|
|
||||||
"BearerAuth": {
|
|
||||||
"type": "apiKey",
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
|
|
||||||
// SwaggerInfo holds exported Swagger Info so clients can modify it
|
|
||||||
var SwaggerInfo = &swag.Spec{
|
|
||||||
Version: "1.0",
|
|
||||||
Host: "localhost:8080",
|
|
||||||
BasePath: "/api",
|
|
||||||
Schemes: []string{},
|
|
||||||
Title: "Time Tracker API",
|
|
||||||
Description: "This is a simple time tracker API.",
|
|
||||||
InfoInstanceName: "swagger",
|
|
||||||
SwaggerTemplate: docTemplate,
|
|
||||||
LeftDelim: "{{",
|
|
||||||
RightDelim: "}}",
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
{
|
|
||||||
"swagger": "2.0",
|
|
||||||
"info": {
|
|
||||||
"description": "This is a simple time tracker API.",
|
|
||||||
"title": "Time Tracker API",
|
|
||||||
"contact": {},
|
|
||||||
"version": "1.0"
|
|
||||||
},
|
|
||||||
"host": "localhost:8080",
|
|
||||||
"basePath": "/api",
|
|
||||||
"paths": {
|
|
||||||
"/": {
|
|
||||||
"get": {
|
|
||||||
"description": "Get a hello message",
|
|
||||||
"produces": [
|
|
||||||
"text/plain"
|
|
||||||
],
|
|
||||||
"summary": "Say hello",
|
|
||||||
"operationId": "hello",
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Hello from the Time Tracker Backend!",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"securityDefinitions": {
|
|
||||||
"BearerAuth": {
|
|
||||||
"type": "apiKey",
|
|
||||||
"name": "Authorization",
|
|
||||||
"in": "header"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
basePath: /api
|
|
||||||
host: localhost:8080
|
|
||||||
info:
|
|
||||||
contact: {}
|
|
||||||
description: This is a simple time tracker API.
|
|
||||||
title: Time Tracker API
|
|
||||||
version: "1.0"
|
|
||||||
paths:
|
|
||||||
/:
|
|
||||||
get:
|
|
||||||
description: Get a hello message
|
|
||||||
operationId: hello
|
|
||||||
produces:
|
|
||||||
- text/plain
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Hello from the Time Tracker Backend!
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
summary: Say hello
|
|
||||||
securityDefinitions:
|
|
||||||
BearerAuth:
|
|
||||||
in: header
|
|
||||||
name: Authorization
|
|
||||||
type: apiKey
|
|
||||||
swagger: "2.0"
|
|
@ -1,14 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
@ -17,7 +12,6 @@ import (
|
|||||||
"github.com/timetracker/backend/internal/api/routes"
|
"github.com/timetracker/backend/internal/api/routes"
|
||||||
"github.com/timetracker/backend/internal/models"
|
"github.com/timetracker/backend/internal/models"
|
||||||
_ "gorm.io/driver/postgres"
|
_ "gorm.io/driver/postgres"
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
// GORM IMPORTS MARKER
|
// GORM IMPORTS MARKER
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -30,76 +24,39 @@ import (
|
|||||||
// @in header
|
// @in header
|
||||||
// @name Authorization
|
// @name Authorization
|
||||||
|
|
||||||
// @Summary Say hello
|
// @x-extension ulid.ULID string
|
||||||
// @Description Get a hello message
|
|
||||||
// @ID hello
|
// @Summary Say hello
|
||||||
// @Produce plain
|
// @Description Get a hello message
|
||||||
// @Success 200 {string} string "Hello from the Time Tracker Backend!"
|
// @ID hello
|
||||||
// @Router / [get]
|
// @Produce plain
|
||||||
|
// @Success 200 {string} string "Hello from the Time Tracker Backend!"
|
||||||
|
// @Router / [get]
|
||||||
func helloHandler(c *gin.Context) {
|
func helloHandler(c *gin.Context) {
|
||||||
c.String(http.StatusOK, "Hello from the Time Tracker Backend!")
|
c.String(http.StatusOK, "Hello from the Time Tracker Backend!")
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Get database configuration with sensible defaults
|
// Configure database
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
dbConfig := models.DatabaseConfig{
|
||||||
|
Host: "localhost",
|
||||||
// Override with environment variables if provided
|
Port: 5432,
|
||||||
if host := os.Getenv("DB_HOST"); host != "" {
|
User: "postgres",
|
||||||
dbConfig.Host = host
|
Password: "password",
|
||||||
}
|
DBName: "mydatabase",
|
||||||
if port := os.Getenv("DB_PORT"); port != "" {
|
SSLMode: "disable", // For development environment
|
||||||
var portInt int
|
|
||||||
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
|
|
||||||
dbConfig.Port = portInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if user := os.Getenv("DB_USER"); user != "" {
|
|
||||||
dbConfig.User = user
|
|
||||||
}
|
|
||||||
if password := os.Getenv("DB_PASSWORD"); password != "" {
|
|
||||||
dbConfig.Password = password
|
|
||||||
}
|
|
||||||
if dbName := os.Getenv("DB_NAME"); dbName != "" {
|
|
||||||
dbConfig.DBName = dbName
|
|
||||||
}
|
|
||||||
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
|
|
||||||
dbConfig.SSLMode = sslMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set log level based on environment
|
|
||||||
if gin.Mode() == gin.ReleaseMode {
|
|
||||||
dbConfig.LogLevel = logger.Error // Only log errors in production
|
|
||||||
} else {
|
|
||||||
dbConfig.LogLevel = logger.Info // Log more in development
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize database
|
// Initialize database
|
||||||
if err := models.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() {
|
|
||||||
if err := models.CloseDB(); err != nil {
|
|
||||||
log.Printf("Error closing database connection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Migrate database schema
|
|
||||||
if err := models.MigrateDB(); err != nil {
|
|
||||||
log.Fatalf("Error migrating database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed database with initial data if needed
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := models.SeedDB(ctx); err != nil {
|
|
||||||
log.Fatalf("Error seeding database: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Gin router
|
// Create Gin router
|
||||||
r := gin.Default()
|
r := gin.Default()
|
||||||
|
|
||||||
// Basic route for health check
|
// Basic route for health check
|
||||||
r.GET("/api", helloHandler)
|
r.GET("/", helloHandler)
|
||||||
|
|
||||||
// Setup API routes
|
// Setup API routes
|
||||||
routes.SetupRouter(r)
|
routes.SetupRouter(r)
|
||||||
@ -107,34 +64,7 @@ func main() {
|
|||||||
// Swagger documentation
|
// Swagger documentation
|
||||||
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
|
|
||||||
// Create a server with graceful shutdown
|
// Start server
|
||||||
srv := &http.Server{
|
fmt.Println("Server listening on port 8080")
|
||||||
Addr: ":8080",
|
r.Run(":8080")
|
||||||
Handler: r,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start server in a goroutine
|
|
||||||
go func() {
|
|
||||||
fmt.Println("Server listening on port 8080")
|
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
log.Fatalf("Error starting server: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for interrupt signal to gracefully shut down the server
|
|
||||||
quit := make(chan os.Signal, 1)
|
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
<-quit
|
|
||||||
log.Println("Shutting down server...")
|
|
||||||
|
|
||||||
// Create a deadline for server shutdown
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Shutdown the server
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
|
||||||
log.Fatalf("Server forced to shutdown: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Server exited properly")
|
|
||||||
}
|
}
|
||||||
|
@ -1,84 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Get database configuration with sensible defaults
|
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
|
||||||
|
|
||||||
// Initialize database
|
|
||||||
fmt.Println("Connecting to database...")
|
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := models.CloseDB(); err != nil {
|
|
||||||
log.Printf("Error closing database connection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fmt.Println("✓ Database connection successful")
|
|
||||||
|
|
||||||
// Test a simple query
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Get the database engine
|
|
||||||
db := models.GetEngine(ctx)
|
|
||||||
|
|
||||||
// Test database connection with a simple query
|
|
||||||
var result int
|
|
||||||
err := db.Raw("SELECT 1").Scan(&result).Error
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error executing test query: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("✓ Test query executed successfully")
|
|
||||||
|
|
||||||
// Check if tables exist
|
|
||||||
fmt.Println("Checking database tables...")
|
|
||||||
var tables []string
|
|
||||||
err = db.Raw("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'").Scan(&tables).Error
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error checking tables: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(tables) == 0 {
|
|
||||||
fmt.Println("No tables found. You may need to run migrations.")
|
|
||||||
fmt.Println("Attempting to run migrations...")
|
|
||||||
|
|
||||||
// Run migrations
|
|
||||||
if err := models.MigrateDB(); err != nil {
|
|
||||||
log.Fatalf("Error migrating database: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("✓ Migrations completed successfully")
|
|
||||||
} else {
|
|
||||||
fmt.Println("Found tables:")
|
|
||||||
for _, table := range tables {
|
|
||||||
fmt.Printf(" - %s\n", table)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count users
|
|
||||||
var userCount int64
|
|
||||||
err = db.Model(&models.User{}).Count(&userCount).Error
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error counting users: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ User count: %d\n", userCount)
|
|
||||||
|
|
||||||
// Count companies
|
|
||||||
var companyCount int64
|
|
||||||
err = db.Model(&models.Company{}).Count(&companyCount).Error
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error counting companies: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Company count: %d\n", companyCount)
|
|
||||||
|
|
||||||
fmt.Println("\nDatabase test completed successfully!")
|
|
||||||
}
|
|
@ -1,72 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Parse command line flags
|
|
||||||
verbose := false
|
|
||||||
for _, arg := range os.Args[1:] {
|
|
||||||
if arg == "--verbose" || arg == "-v" {
|
|
||||||
verbose = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
fmt.Println("Running in verbose mode")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get database configuration with sensible defaults
|
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
|
||||||
|
|
||||||
// Override with environment variables if provided
|
|
||||||
if host := os.Getenv("DB_HOST"); host != "" {
|
|
||||||
dbConfig.Host = host
|
|
||||||
}
|
|
||||||
if port := os.Getenv("DB_PORT"); port != "" {
|
|
||||||
var portInt int
|
|
||||||
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
|
|
||||||
dbConfig.Port = portInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if user := os.Getenv("DB_USER"); user != "" {
|
|
||||||
dbConfig.User = user
|
|
||||||
}
|
|
||||||
if password := os.Getenv("DB_PASSWORD"); password != "" {
|
|
||||||
dbConfig.Password = password
|
|
||||||
}
|
|
||||||
if dbName := os.Getenv("DB_NAME"); dbName != "" {
|
|
||||||
dbConfig.DBName = dbName
|
|
||||||
}
|
|
||||||
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
|
|
||||||
dbConfig.SSLMode = sslMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set log level
|
|
||||||
dbConfig.LogLevel = logger.Info
|
|
||||||
|
|
||||||
// Initialize database
|
|
||||||
fmt.Println("Connecting to database...")
|
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := models.CloseDB(); err != nil {
|
|
||||||
log.Printf("Error closing database connection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fmt.Println("✓ Database connection successful")
|
|
||||||
|
|
||||||
// Run migrations
|
|
||||||
fmt.Println("Running database migrations...")
|
|
||||||
if err := models.MigrateDB(); err != nil {
|
|
||||||
log.Fatalf("Error migrating database: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("✓ Database migrations completed successfully")
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/oklog/ulid/v2"
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Get database configuration with sensible defaults
|
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
|
||||||
|
|
||||||
// Initialize database
|
|
||||||
fmt.Println("Connecting to database...")
|
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := models.CloseDB(); err != nil {
|
|
||||||
log.Printf("Error closing database connection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fmt.Println("✓ Database connection successful")
|
|
||||||
|
|
||||||
// Create context with timeout
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Test Company model
|
|
||||||
fmt.Println("\n=== Testing Company Model ===")
|
|
||||||
testCompanyModel(ctx)
|
|
||||||
|
|
||||||
// Test User model
|
|
||||||
fmt.Println("\n=== Testing User Model ===")
|
|
||||||
testUserModel(ctx)
|
|
||||||
|
|
||||||
// Test relationships
|
|
||||||
fmt.Println("\n=== Testing Relationships ===")
|
|
||||||
testRelationships(ctx)
|
|
||||||
|
|
||||||
fmt.Println("\nModel tests completed successfully!")
|
|
||||||
}
|
|
||||||
|
|
||||||
func testCompanyModel(ctx context.Context) {
|
|
||||||
// Create a new company
|
|
||||||
companyCreate := models.CompanyCreate{
|
|
||||||
Name: "Test Company",
|
|
||||||
}
|
|
||||||
|
|
||||||
company, err := models.CreateCompany(ctx, companyCreate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error creating company: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Created company: %s (ID: %s)\n", company.Name, company.ID)
|
|
||||||
|
|
||||||
// Get the company by ID
|
|
||||||
retrievedCompany, err := models.GetCompanyByID(ctx, company.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting company: %v", err)
|
|
||||||
}
|
|
||||||
if retrievedCompany == nil {
|
|
||||||
log.Fatalf("Company not found")
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved company: %s\n", retrievedCompany.Name)
|
|
||||||
|
|
||||||
// Update the company
|
|
||||||
newName := "Updated Test Company"
|
|
||||||
companyUpdate := models.CompanyUpdate{
|
|
||||||
ID: company.ID,
|
|
||||||
Name: &newName,
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedCompany, err := models.UpdateCompany(ctx, companyUpdate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error updating company: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Updated company name to: %s\n", updatedCompany.Name)
|
|
||||||
|
|
||||||
// Get all companies
|
|
||||||
companies, err := models.GetAllCompanies(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting all companies: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved %d companies\n", len(companies))
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUserModel(ctx context.Context) {
|
|
||||||
// Get a company to associate with the user
|
|
||||||
companies, err := models.GetAllCompanies(ctx)
|
|
||||||
if err != nil || len(companies) == 0 {
|
|
||||||
log.Fatalf("Error getting companies or no companies found: %v", err)
|
|
||||||
}
|
|
||||||
companyID := companies[0].ID
|
|
||||||
|
|
||||||
// Create a new user
|
|
||||||
userCreate := models.UserCreate{
|
|
||||||
Email: "test@example.com",
|
|
||||||
Password: "Test@123456",
|
|
||||||
Role: models.RoleUser,
|
|
||||||
CompanyID: companyID,
|
|
||||||
HourlyRate: 50.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := models.CreateUser(ctx, userCreate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error creating user: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Created user: %s (ID: %s)\n", user.Email, user.ID)
|
|
||||||
|
|
||||||
// Get the user by ID
|
|
||||||
retrievedUser, err := models.GetUserByID(ctx, user.ID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting user: %v", err)
|
|
||||||
}
|
|
||||||
if retrievedUser == nil {
|
|
||||||
log.Fatalf("User not found")
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved user: %s\n", retrievedUser.Email)
|
|
||||||
|
|
||||||
// Get the user by email
|
|
||||||
emailUser, err := models.GetUserByEmail(ctx, user.Email)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting user by email: %v", err)
|
|
||||||
}
|
|
||||||
if emailUser == nil {
|
|
||||||
log.Fatalf("User not found by email")
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved user by email: %s\n", emailUser.Email)
|
|
||||||
|
|
||||||
// Update the user
|
|
||||||
newEmail := "updated@example.com"
|
|
||||||
newRole := models.RoleAdmin
|
|
||||||
newHourlyRate := 75.0
|
|
||||||
userUpdate := models.UserUpdate{
|
|
||||||
ID: user.ID,
|
|
||||||
Email: &newEmail,
|
|
||||||
Role: &newRole,
|
|
||||||
HourlyRate: &newHourlyRate,
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedUser, err := models.UpdateUser(ctx, userUpdate)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error updating user: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Updated user email to: %s, role to: %s\n", updatedUser.Email, updatedUser.Role)
|
|
||||||
|
|
||||||
// Test authentication
|
|
||||||
authUser, err := models.AuthenticateUser(ctx, updatedUser.Email, "Test@123456")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error authenticating user: %v", err)
|
|
||||||
}
|
|
||||||
if authUser == nil {
|
|
||||||
log.Fatalf("Authentication failed")
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ User authentication successful\n")
|
|
||||||
|
|
||||||
// Get all users
|
|
||||||
users, err := models.GetAllUsers(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting all users: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved %d users\n", len(users))
|
|
||||||
|
|
||||||
// Get users by company ID
|
|
||||||
companyUsers, err := models.GetUsersByCompanyID(ctx, companyID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting users by company ID: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved %d users for company ID: %s\n", len(companyUsers), companyID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testRelationships(ctx context.Context) {
|
|
||||||
// Get a user with company
|
|
||||||
users, err := models.GetAllUsers(ctx)
|
|
||||||
if err != nil || len(users) == 0 {
|
|
||||||
log.Fatalf("Error getting users or no users found: %v", err)
|
|
||||||
}
|
|
||||||
userID := users[0].ID
|
|
||||||
|
|
||||||
// Get user with company
|
|
||||||
user, err := models.GetUserWithCompany(ctx, userID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting user with company: %v", err)
|
|
||||||
}
|
|
||||||
if user == nil {
|
|
||||||
log.Fatalf("User not found")
|
|
||||||
}
|
|
||||||
if user.Company == nil {
|
|
||||||
log.Fatalf("User's company not loaded")
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Retrieved user %s with company %s\n", user.Email, user.Company.Name)
|
|
||||||
|
|
||||||
// Test invalid ID
|
|
||||||
invalidID := ulid.MustNew(ulid.Timestamp(time.Now()), ulid.DefaultEntropy())
|
|
||||||
invalidUser, err := models.GetUserByID(ctx, invalidID)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalf("Error getting user with invalid ID: %v", err)
|
|
||||||
}
|
|
||||||
if invalidUser != nil {
|
|
||||||
log.Fatalf("User found with invalid ID")
|
|
||||||
}
|
|
||||||
fmt.Printf("✓ Correctly handled invalid user ID\n")
|
|
||||||
}
|
|
@ -1,89 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/timetracker/backend/internal/models"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Parse command line flags
|
|
||||||
force := false
|
|
||||||
for _, arg := range os.Args[1:] {
|
|
||||||
if arg == "--force" || arg == "-f" {
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get database configuration with sensible defaults
|
|
||||||
dbConfig := models.DefaultDatabaseConfig()
|
|
||||||
|
|
||||||
// Override with environment variables if provided
|
|
||||||
if host := os.Getenv("DB_HOST"); host != "" {
|
|
||||||
dbConfig.Host = host
|
|
||||||
}
|
|
||||||
if port := os.Getenv("DB_PORT"); port != "" {
|
|
||||||
var portInt int
|
|
||||||
if _, err := fmt.Sscanf(port, "%d", &portInt); err == nil && portInt > 0 {
|
|
||||||
dbConfig.Port = portInt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if user := os.Getenv("DB_USER"); user != "" {
|
|
||||||
dbConfig.User = user
|
|
||||||
}
|
|
||||||
if password := os.Getenv("DB_PASSWORD"); password != "" {
|
|
||||||
dbConfig.Password = password
|
|
||||||
}
|
|
||||||
if dbName := os.Getenv("DB_NAME"); dbName != "" {
|
|
||||||
dbConfig.DBName = dbName
|
|
||||||
}
|
|
||||||
if sslMode := os.Getenv("DB_SSLMODE"); sslMode != "" {
|
|
||||||
dbConfig.SSLMode = sslMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set log level
|
|
||||||
dbConfig.LogLevel = logger.Info
|
|
||||||
|
|
||||||
// Initialize database
|
|
||||||
fmt.Println("Connecting to database...")
|
|
||||||
if err := models.InitDB(dbConfig); err != nil {
|
|
||||||
log.Fatalf("Error initializing database: %v", err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := models.CloseDB(); err != nil {
|
|
||||||
log.Printf("Error closing database connection: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fmt.Println("✓ Database connection successful")
|
|
||||||
|
|
||||||
// Create context with timeout
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Check if we need to seed (e.g., no companies exist)
|
|
||||||
if !force {
|
|
||||||
var count int64
|
|
||||||
db := models.GetEngine(ctx)
|
|
||||||
if err := db.Model(&models.Company{}).Count(&count).Error; err != nil {
|
|
||||||
log.Fatalf("Error checking if seeding is needed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If data already exists, skip seeding
|
|
||||||
if count > 0 {
|
|
||||||
fmt.Println("Database already contains data. Use --force to override.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed the database
|
|
||||||
fmt.Println("Seeding database with initial data...")
|
|
||||||
if err := models.SeedDB(ctx); err != nil {
|
|
||||||
log.Fatalf("Error seeding database: %v", err)
|
|
||||||
}
|
|
||||||
fmt.Println("✓ Database seeding completed successfully")
|
|
||||||
}
|
|
4534
backend/docs/docs.go
4534
backend/docs/docs.go
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -26,7 +26,7 @@ func NewActivityHandler() *ActivityHandler {
|
|||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Success 200 {object} utils.Response{data=[]dto.ActivityDto}
|
// @Success 200 {object} utils.Response{data=[]utils.ActivityResponse}
|
||||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Router /activities [get]
|
// @Router /activities [get]
|
||||||
@ -56,7 +56,7 @@ func (h *ActivityHandler) GetActivities(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Security BearerAuth
|
// @Security BearerAuth
|
||||||
// @Param id path string true "Activity ID"
|
// @Param id path string true "Activity ID"
|
||||||
// @Success 200 {object} utils.Response{data=dto.ActivityDto}
|
// @Success 200 {object} utils.Response{data=utils.ActivityResponse}
|
||||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
// @Failure 404 {object} utils.Response{error=utils.ErrorInfo}
|
||||||
|
@ -261,52 +261,6 @@ func (h *UserHandler) Login(c *gin.Context) {
|
|||||||
utils.SuccessResponse(c, http.StatusOK, tokenDTO)
|
utils.SuccessResponse(c, http.StatusOK, tokenDTO)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register handles POST /auth/register
|
|
||||||
//
|
|
||||||
// @Summary Register
|
|
||||||
// @Description Register a new user and get a JWT token
|
|
||||||
// @Tags auth
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Param user body dto.UserCreateDto true "User data"
|
|
||||||
// @Success 201 {object} utils.Response{data=dto.TokenDto}
|
|
||||||
// @Failure 400 {object} utils.Response{error=utils.ErrorInfo}
|
|
||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
|
||||||
// @Router /auth/register [post]
|
|
||||||
func (h *UserHandler) Register(c *gin.Context) {
|
|
||||||
// Parse request body
|
|
||||||
var userCreateDTO dto.UserCreateDto
|
|
||||||
if err := c.ShouldBindJSON(&userCreateDTO); err != nil {
|
|
||||||
utils.BadRequestResponse(c, "Invalid request body: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert DTO to model
|
|
||||||
userCreate := convertCreateDTOToModel(userCreateDTO)
|
|
||||||
|
|
||||||
// Create user in the database
|
|
||||||
user, err := models.CreateUser(c.Request.Context(), userCreate)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error creating user: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate JWT token
|
|
||||||
token, err := middleware.GenerateToken(user)
|
|
||||||
if err != nil {
|
|
||||||
utils.InternalErrorResponse(c, "Error generating token: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return token
|
|
||||||
tokenDTO := dto.TokenDto{
|
|
||||||
Token: token,
|
|
||||||
User: convertUserToDTO(user),
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.SuccessResponse(c, http.StatusCreated, tokenDTO)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentUser handles GET /auth/me
|
// GetCurrentUser handles GET /auth/me
|
||||||
//
|
//
|
||||||
// @Summary Get current user
|
// @Summary Get current user
|
||||||
|
@ -16,92 +16,84 @@ func SetupRouter(r *gin.Engine) {
|
|||||||
projectHandler := handlers.NewProjectHandler()
|
projectHandler := handlers.NewProjectHandler()
|
||||||
timeEntryHandler := handlers.NewTimeEntryHandler()
|
timeEntryHandler := handlers.NewTimeEntryHandler()
|
||||||
|
|
||||||
// API routes
|
// Public routes
|
||||||
|
r.POST("/auth/login", userHandler.Login)
|
||||||
|
|
||||||
|
// API routes (protected)
|
||||||
api := r.Group("/api")
|
api := r.Group("/api")
|
||||||
|
api.Use(middleware.AuthMiddleware())
|
||||||
{
|
{
|
||||||
// Auth routes (public)
|
// Auth routes
|
||||||
auth := api.Group("/auth")
|
auth := api.Group("/auth")
|
||||||
{
|
{
|
||||||
auth.POST("/login", userHandler.Login)
|
auth.GET("/me", userHandler.GetCurrentUser)
|
||||||
auth.POST("/register", userHandler.Register)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Protected routes
|
// User routes
|
||||||
protected := api.Group("")
|
users := api.Group("/users")
|
||||||
protected.Use(middleware.AuthMiddleware())
|
|
||||||
{
|
{
|
||||||
// Auth routes (protected)
|
users.GET("", userHandler.GetUsers)
|
||||||
protectedAuth := protected.Group("/auth")
|
users.GET("/:id", userHandler.GetUserByID)
|
||||||
{
|
users.POST("", middleware.RoleMiddleware("admin"), userHandler.CreateUser)
|
||||||
protectedAuth.GET("/me", userHandler.GetCurrentUser)
|
users.PUT("/:id", middleware.RoleMiddleware("admin"), userHandler.UpdateUser)
|
||||||
}
|
users.DELETE("/:id", middleware.RoleMiddleware("admin"), userHandler.DeleteUser)
|
||||||
|
}
|
||||||
|
|
||||||
// User routes
|
// Activity routes
|
||||||
users := protected.Group("/users")
|
activities := api.Group("/activities")
|
||||||
{
|
{
|
||||||
users.GET("", userHandler.GetUsers)
|
activities.GET("", activityHandler.GetActivities)
|
||||||
users.GET("/:id", userHandler.GetUserByID)
|
activities.GET("/:id", activityHandler.GetActivityByID)
|
||||||
users.POST("", middleware.RoleMiddleware("admin"), userHandler.CreateUser)
|
activities.POST("", middleware.RoleMiddleware("admin"), activityHandler.CreateActivity)
|
||||||
users.PUT("/:id", middleware.RoleMiddleware("admin"), userHandler.UpdateUser)
|
activities.PUT("/:id", middleware.RoleMiddleware("admin"), activityHandler.UpdateActivity)
|
||||||
users.DELETE("/:id", middleware.RoleMiddleware("admin"), userHandler.DeleteUser)
|
activities.DELETE("/:id", middleware.RoleMiddleware("admin"), activityHandler.DeleteActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activity routes
|
// Company routes
|
||||||
activities := protected.Group("/activities")
|
companies := api.Group("/companies")
|
||||||
{
|
{
|
||||||
activities.GET("", activityHandler.GetActivities)
|
companies.GET("", companyHandler.GetCompanies)
|
||||||
activities.GET("/:id", activityHandler.GetActivityByID)
|
companies.GET("/:id", companyHandler.GetCompanyByID)
|
||||||
activities.POST("", middleware.RoleMiddleware("admin"), activityHandler.CreateActivity)
|
companies.POST("", middleware.RoleMiddleware("admin"), companyHandler.CreateCompany)
|
||||||
activities.PUT("/:id", middleware.RoleMiddleware("admin"), activityHandler.UpdateActivity)
|
companies.PUT("/:id", middleware.RoleMiddleware("admin"), companyHandler.UpdateCompany)
|
||||||
activities.DELETE("/:id", middleware.RoleMiddleware("admin"), activityHandler.DeleteActivity)
|
companies.DELETE("/:id", middleware.RoleMiddleware("admin"), companyHandler.DeleteCompany)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Company routes
|
// Customer routes
|
||||||
companies := protected.Group("/companies")
|
customers := api.Group("/customers")
|
||||||
{
|
{
|
||||||
companies.GET("", companyHandler.GetCompanies)
|
customers.GET("", customerHandler.GetCustomers)
|
||||||
companies.GET("/:id", companyHandler.GetCompanyByID)
|
customers.GET("/:id", customerHandler.GetCustomerByID)
|
||||||
companies.POST("", middleware.RoleMiddleware("admin"), companyHandler.CreateCompany)
|
customers.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID)
|
||||||
companies.PUT("/:id", middleware.RoleMiddleware("admin"), companyHandler.UpdateCompany)
|
customers.POST("", middleware.RoleMiddleware("admin"), customerHandler.CreateCustomer)
|
||||||
companies.DELETE("/:id", middleware.RoleMiddleware("admin"), companyHandler.DeleteCompany)
|
customers.PUT("/:id", middleware.RoleMiddleware("admin"), customerHandler.UpdateCustomer)
|
||||||
}
|
customers.DELETE("/:id", middleware.RoleMiddleware("admin"), customerHandler.DeleteCustomer)
|
||||||
|
}
|
||||||
|
|
||||||
// Customer routes
|
// Project routes
|
||||||
customers := protected.Group("/customers")
|
projects := api.Group("/projects")
|
||||||
{
|
{
|
||||||
customers.GET("", customerHandler.GetCustomers)
|
projects.GET("", projectHandler.GetProjects)
|
||||||
customers.GET("/:id", customerHandler.GetCustomerByID)
|
projects.GET("/with-customers", projectHandler.GetProjectsWithCustomers)
|
||||||
customers.GET("/company/:companyId", customerHandler.GetCustomersByCompanyID)
|
projects.GET("/:id", projectHandler.GetProjectByID)
|
||||||
customers.POST("", middleware.RoleMiddleware("admin"), customerHandler.CreateCustomer)
|
projects.GET("/customer/:customerId", projectHandler.GetProjectsByCustomerID)
|
||||||
customers.PUT("/:id", middleware.RoleMiddleware("admin"), customerHandler.UpdateCustomer)
|
projects.POST("", middleware.RoleMiddleware("admin"), projectHandler.CreateProject)
|
||||||
customers.DELETE("/:id", middleware.RoleMiddleware("admin"), customerHandler.DeleteCustomer)
|
projects.PUT("/:id", middleware.RoleMiddleware("admin"), projectHandler.UpdateProject)
|
||||||
}
|
projects.DELETE("/:id", middleware.RoleMiddleware("admin"), projectHandler.DeleteProject)
|
||||||
|
}
|
||||||
|
|
||||||
// Project routes
|
// Time Entry routes
|
||||||
projects := protected.Group("/projects")
|
timeEntries := api.Group("/time-entries")
|
||||||
{
|
{
|
||||||
projects.GET("", projectHandler.GetProjects)
|
timeEntries.GET("", timeEntryHandler.GetTimeEntries)
|
||||||
projects.GET("/with-customers", projectHandler.GetProjectsWithCustomers)
|
timeEntries.GET("/me", timeEntryHandler.GetMyTimeEntries)
|
||||||
projects.GET("/:id", projectHandler.GetProjectByID)
|
timeEntries.GET("/range", timeEntryHandler.GetTimeEntriesByDateRange)
|
||||||
projects.GET("/customer/:customerId", projectHandler.GetProjectsByCustomerID)
|
timeEntries.GET("/:id", timeEntryHandler.GetTimeEntryByID)
|
||||||
projects.POST("", middleware.RoleMiddleware("admin"), projectHandler.CreateProject)
|
timeEntries.GET("/user/:userId", timeEntryHandler.GetTimeEntriesByUserID)
|
||||||
projects.PUT("/:id", middleware.RoleMiddleware("admin"), projectHandler.UpdateProject)
|
timeEntries.GET("/project/:projectId", timeEntryHandler.GetTimeEntriesByProjectID)
|
||||||
projects.DELETE("/:id", middleware.RoleMiddleware("admin"), projectHandler.DeleteProject)
|
timeEntries.POST("", timeEntryHandler.CreateTimeEntry)
|
||||||
}
|
timeEntries.PUT("/:id", timeEntryHandler.UpdateTimeEntry)
|
||||||
|
timeEntries.DELETE("/:id", timeEntryHandler.DeleteTimeEntry)
|
||||||
// Time Entry routes
|
|
||||||
timeEntries := protected.Group("/time-entries")
|
|
||||||
{
|
|
||||||
timeEntries.GET("", timeEntryHandler.GetTimeEntries)
|
|
||||||
timeEntries.GET("/me", timeEntryHandler.GetMyTimeEntries)
|
|
||||||
timeEntries.GET("/range", timeEntryHandler.GetTimeEntriesByDateRange)
|
|
||||||
timeEntries.GET("/:id", timeEntryHandler.GetTimeEntryByID)
|
|
||||||
timeEntries.GET("/user/:userId", timeEntryHandler.GetTimeEntriesByUserID)
|
|
||||||
timeEntries.GET("/project/:projectId", timeEntryHandler.GetTimeEntriesByProjectID)
|
|
||||||
timeEntries.POST("", timeEntryHandler.CreateTimeEntry)
|
|
||||||
timeEntries.PUT("/:id", timeEntryHandler.UpdateTimeEntry)
|
|
||||||
timeEntries.DELETE("/:id", timeEntryHandler.DeleteTimeEntry)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,14 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"gorm.io/driver/postgres" // For PostgreSQL
|
"gorm.io/driver/postgres" // For PostgreSQL
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Global variable for the DB connection
|
// Global variable for the DB connection
|
||||||
@ -19,32 +16,12 @@ var defaultDB *gorm.DB
|
|||||||
|
|
||||||
// DatabaseConfig contains the configuration data for the database connection
|
// DatabaseConfig contains the configuration data for the database connection
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Host string
|
Host string
|
||||||
Port int
|
Port int
|
||||||
User string
|
User string
|
||||||
Password string
|
Password string
|
||||||
DBName string
|
DBName string
|
||||||
SSLMode 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)
|
// InitDB initializes the database connection (once at startup)
|
||||||
@ -54,151 +31,22 @@ func InitDB(config DatabaseConfig) 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",
|
||||||
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
config.Host, config.Port, config.User, config.Password, config.DBName, config.SSLMode)
|
||||||
|
|
||||||
// Configure GORM logger
|
// Establish database connection
|
||||||
gormLogger := logger.New(
|
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
|
||||||
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("error connecting to the database: %w", err)
|
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
|
defaultDB = db
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MigrateDB performs database migrations for all models
|
|
||||||
func MigrateDB() error {
|
|
||||||
if defaultDB == nil {
|
|
||||||
return errors.New("database not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Starting database migration...")
|
|
||||||
|
|
||||||
// Add all models that should be migrated here
|
|
||||||
err := defaultDB.AutoMigrate(
|
|
||||||
&Company{},
|
|
||||||
&User{},
|
|
||||||
&Customer{},
|
|
||||||
&Project{},
|
|
||||||
&Activity{},
|
|
||||||
&TimeEntry{},
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error migrating database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Database migration completed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeedDB seeds the database with initial data if needed
|
|
||||||
func SeedDB(ctx context.Context) error {
|
|
||||||
if defaultDB == nil {
|
|
||||||
return errors.New("database not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Checking if database seeding is needed...")
|
|
||||||
|
|
||||||
// Check if we need to seed (e.g., no companies exist)
|
|
||||||
var count int64
|
|
||||||
if err := defaultDB.Model(&Company{}).Count(&count).Error; err != nil {
|
|
||||||
return fmt.Errorf("error checking if seeding is needed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If data already exists, skip seeding
|
|
||||||
if count > 0 {
|
|
||||||
log.Println("Database already contains data, skipping seeding")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Seeding database with initial data...")
|
|
||||||
|
|
||||||
// Start a transaction for all seed operations
|
|
||||||
return defaultDB.Transaction(func(tx *gorm.DB) error {
|
|
||||||
// Create a default company
|
|
||||||
defaultCompany := Company{
|
|
||||||
Name: "Default Company",
|
|
||||||
}
|
|
||||||
if err := tx.Create(&defaultCompany).Error; err != nil {
|
|
||||||
return fmt.Errorf("error creating default company: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an admin user
|
|
||||||
adminUser := User{
|
|
||||||
Email: "admin@example.com",
|
|
||||||
Role: RoleAdmin,
|
|
||||||
CompanyID: defaultCompany.ID,
|
|
||||||
HourlyRate: 100.0,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hash a default password
|
|
||||||
pwData, err := HashPassword("Admin@123456")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("error hashing password: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
adminUser.Salt = pwData.Salt
|
|
||||||
adminUser.Hash = pwData.Hash
|
|
||||||
|
|
||||||
if err := tx.Create(&adminUser).Error; err != nil {
|
|
||||||
return fmt.Errorf("error creating admin user: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Database seeding completed successfully")
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEngine returns the DB instance, possibly with context
|
// GetEngine returns the DB instance, possibly with context
|
||||||
func GetEngine(ctx context.Context) *gorm.DB {
|
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
|
// If a special transaction is in ctx, you could check it here
|
||||||
return defaultDB.WithContext(ctx)
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateModel updates a model based on the set pointer fields
|
// UpdateModel updates a model based on the set pointer fields
|
||||||
func UpdateModel(ctx context.Context, model any, updates any) error {
|
func UpdateModel(ctx context.Context, model any, updates any) error {
|
||||||
updateValue := reflect.ValueOf(updates)
|
updateValue := reflect.ValueOf(updates)
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:14
|
|
||||||
container_name: timetracker_db
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: timetracker
|
|
||||||
POSTGRES_PASSWORD: password
|
|
||||||
POSTGRES_DB: timetracker
|
|
||||||
volumes:
|
|
||||||
- db_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
db_data:
|
|
@ -1,84 +0,0 @@
|
|||||||
# Swagger Documentation
|
|
||||||
|
|
||||||
This document explains how to access and update the Swagger documentation for the Time Tracker API.
|
|
||||||
|
|
||||||
## Accessing Swagger UI
|
|
||||||
|
|
||||||
After starting the backend server, access the Swagger UI at:
|
|
||||||
```
|
|
||||||
http://localhost:8080/swagger/index.html
|
|
||||||
```
|
|
||||||
|
|
||||||
This interactive interface allows you to:
|
|
||||||
- Browse all available API endpoints
|
|
||||||
- See request parameters and response formats
|
|
||||||
- Test API calls directly from the browser
|
|
||||||
|
|
||||||
## Updating Swagger Documentation
|
|
||||||
|
|
||||||
To update the Swagger documentation for the Time Tracker API, follow these steps:
|
|
||||||
|
|
||||||
1. **Add or update Swagger annotations in your code**
|
|
||||||
- Annotations should be added as comments above handler functions
|
|
||||||
- Use the correct types in annotations (e.g., `dto.ActivityDto` instead of `utils.ActivityResponse`)
|
|
||||||
- Make sure all parameters, responses, and types are properly documented
|
|
||||||
|
|
||||||
2. **Run the Swagger generation command**
|
|
||||||
```bash
|
|
||||||
cd backend && swag init -g cmd/api/main.go --output docs
|
|
||||||
```
|
|
||||||
|
|
||||||
This command:
|
|
||||||
- Uses `swag` CLI tool to parse your code
|
|
||||||
- Looks for the main entry point in `cmd/api/main.go`
|
|
||||||
- Outputs the generated files to the `docs` directory
|
|
||||||
|
|
||||||
3. **Verify the generated files**
|
|
||||||
The command will generate or update three files:
|
|
||||||
- `docs/docs.go` - Go code for the Swagger documentation
|
|
||||||
- `docs/swagger.json` - JSON representation of the API
|
|
||||||
- `docs/swagger.yaml` - YAML representation of the API
|
|
||||||
|
|
||||||
4. **Common issues and solutions**
|
|
||||||
- If you encounter "cannot find type definition" errors, check that you're using the correct type names in your annotations
|
|
||||||
- If endpoints are missing, ensure they have proper Swagger annotations
|
|
||||||
- If you change the base path or other global settings, update them in the `main.go` file annotations
|
|
||||||
|
|
||||||
## Swagger Annotation Examples
|
|
||||||
|
|
||||||
### Main API Information
|
|
||||||
|
|
||||||
In `main.go`:
|
|
||||||
```go
|
|
||||||
// @title Time Tracker API
|
|
||||||
// @version 1.0
|
|
||||||
// @description This is a simple time tracker API.
|
|
||||||
// @host localhost:8080
|
|
||||||
// @BasePath /api
|
|
||||||
// @securityDefinitions.apikey BearerAuth
|
|
||||||
// @in header
|
|
||||||
// @name Authorization
|
|
||||||
```
|
|
||||||
|
|
||||||
### Endpoint Documentation
|
|
||||||
|
|
||||||
Example from a handler function:
|
|
||||||
```go
|
|
||||||
// GetActivities handles GET /activities
|
|
||||||
//
|
|
||||||
// @Summary Get all activities
|
|
||||||
// @Description Get a list of all activities
|
|
||||||
// @Tags activities
|
|
||||||
// @Accept json
|
|
||||||
// @Produce json
|
|
||||||
// @Security BearerAuth
|
|
||||||
// @Success 200 {object} utils.Response{data=[]dto.ActivityDto}
|
|
||||||
// @Failure 401 {object} utils.Response{error=utils.ErrorInfo}
|
|
||||||
// @Failure 500 {object} utils.Response{error=utils.ErrorInfo}
|
|
||||||
// @Router /activities [get]
|
|
||||||
func (h *ActivityHandler) GetActivities(c *gin.Context) {
|
|
||||||
// Function implementation
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Remember that the Swagger documentation is generated from the annotations in your code, so keeping these annotations up-to-date is essential for accurate API documentation.
|
|
Loading…
x
Reference in New Issue
Block a user