Initial commit: Plataforma de Economía
Features: - React 18 + TypeScript frontend with Vite - Go + Gin backend API - PostgreSQL database - JWT authentication with refresh tokens - User management (admin panel) - Docker containerization - Progress tracking system - 4 economic modules structure Fixed: - Login with username or email - User creation without required email - Database nullable timestamps - API response field naming
This commit is contained in:
259
backend/cmd/server/main.go
Normal file
259
backend/cmd/server/main.go
Normal file
@@ -0,0 +1,259 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"github.com/ren/econ/backend/internal/config"
|
||||
"github.com/ren/econ/backend/internal/handlers"
|
||||
"github.com/ren/econ/backend/internal/middleware"
|
||||
"github.com/ren/econ/backend/internal/repository"
|
||||
"github.com/ren/econ/backend/internal/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load .env file if exists
|
||||
_ = godotenv.Load()
|
||||
|
||||
cfg := config.Load()
|
||||
|
||||
// Connect to PostgreSQL
|
||||
connStr := fmt.Sprintf(
|
||||
"postgres://%s:%s@%s:%d/%s?sslmode=disable",
|
||||
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName,
|
||||
)
|
||||
|
||||
ctx := context.Background()
|
||||
dbPool, err := pgxpool.New(ctx, connStr)
|
||||
if err != nil {
|
||||
log.Fatalf("Error connecting to database: %v", err)
|
||||
}
|
||||
defer dbPool.Close()
|
||||
|
||||
// Test connection
|
||||
if err := dbPool.Ping(ctx); err != nil {
|
||||
log.Fatalf("Error pinging database: %v", err)
|
||||
}
|
||||
log.Println("Connected to PostgreSQL")
|
||||
|
||||
// Run migrations
|
||||
runMigrations(ctx, dbPool)
|
||||
|
||||
// Initialize repositories
|
||||
userRepo := repository.NewUserRepository(dbPool)
|
||||
progresoRepo := repository.NewProgresoRepository(dbPool)
|
||||
contenidoRepo := repository.NewContenidoRepository(dbPool)
|
||||
|
||||
// Initialize services
|
||||
authService := services.NewAuthService(cfg, userRepo)
|
||||
|
||||
// Initialize handlers
|
||||
authHandler := handlers.NewAuthHandler(authService)
|
||||
usersHandler := handlers.NewUsersHandler(userRepo, progresoRepo, authService)
|
||||
progresoHandler := handlers.NewProgresoHandler(progresoRepo)
|
||||
contenidoHandler := handlers.NewContenidoHandler(contenidoRepo)
|
||||
|
||||
// Setup Gin router
|
||||
router := gin.Default()
|
||||
|
||||
// CORS middleware
|
||||
router.Use(func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// Health check
|
||||
router.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// API routes
|
||||
api := router.Group("/api")
|
||||
{
|
||||
// Auth routes (public)
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", authHandler.Login)
|
||||
auth.POST("/refresh", authHandler.RefreshToken)
|
||||
}
|
||||
|
||||
// Protected routes
|
||||
protected := api.Group("")
|
||||
protected.Use(middleware.AuthMiddleware(authService))
|
||||
{
|
||||
// Auth logout
|
||||
auth.POST("/auth/logout", authHandler.Logout)
|
||||
|
||||
// Progreso routes
|
||||
progreso := protected.Group("/progreso")
|
||||
{
|
||||
progreso.GET("", progresoHandler.GetProgreso)
|
||||
progreso.GET("/modulo/:numero", progresoHandler.GetProgresoModulo)
|
||||
progreso.PUT("/:ejercicioId", progresoHandler.UpdateProgreso)
|
||||
progreso.GET("/resumen", progresoHandler.GetResumen)
|
||||
}
|
||||
|
||||
// Contenido routes
|
||||
contenido := protected.Group("/contenido")
|
||||
{
|
||||
contenido.GET("/modulos", contenidoHandler.GetModulos)
|
||||
contenido.GET("/modulos/:numero", contenidoHandler.GetModulo)
|
||||
}
|
||||
|
||||
// Admin routes (requires admin role)
|
||||
admin := protected.Group("/admin")
|
||||
admin.Use(middleware.AdminMiddleware())
|
||||
{
|
||||
admin.GET("/usuarios", usersHandler.ListUsers)
|
||||
admin.POST("/usuarios", usersHandler.CreateUser)
|
||||
admin.GET("/usuarios/:id", usersHandler.GetUser)
|
||||
admin.PUT("/usuarios/:id", usersHandler.UpdateUser)
|
||||
admin.DELETE("/usuarios/:id", usersHandler.DeleteUser)
|
||||
admin.GET("/usuarios/:id/progreso", usersHandler.GetUserProgreso)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start server
|
||||
port := getEnv("PORT", "8080")
|
||||
log.Printf("Server starting on port %s", port)
|
||||
if err := router.Run(":" + port); err != nil {
|
||||
log.Fatalf("Error starting server: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runMigrations(ctx context.Context, dbPool *pgxpool.Pool) {
|
||||
// Inline migrations
|
||||
migrations := []string{
|
||||
`CREATE TABLE IF NOT EXISTS usuarios (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
nombre VARCHAR(100) NOT NULL,
|
||||
rol VARCHAR(20) DEFAULT 'estudiante' CHECK (rol IN ('admin', 'estudiante')),
|
||||
creado_en TIMESTAMP DEFAULT NOW(),
|
||||
ultimo_login TIMESTAMP,
|
||||
activo BOOLEAN DEFAULT TRUE
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS progreso_usuario (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||
modulo_numero INTEGER NOT NULL CHECK (modulo_numero BETWEEN 1 AND 4),
|
||||
ejercicio_id VARCHAR(50) NOT NULL,
|
||||
completado BOOLEAN DEFAULT FALSE,
|
||||
puntuacion INTEGER DEFAULT 0,
|
||||
intentos INTEGER DEFAULT 0,
|
||||
ultima_vez TIMESTAMP DEFAULT NOW(),
|
||||
respuesta_json JSONB,
|
||||
UNIQUE(usuario_id, modulo_numero, ejercicio_id)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS ejercicios (
|
||||
id VARCHAR(50) PRIMARY KEY,
|
||||
modulo_numero INTEGER NOT NULL,
|
||||
titulo VARCHAR(200) NOT NULL,
|
||||
tipo VARCHAR(50) NOT NULL,
|
||||
contenido JSONB NOT NULL,
|
||||
orden INTEGER DEFAULT 0
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS refresh_tokens (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
usuario_id UUID REFERENCES usuarios(id) ON DELETE CASCADE,
|
||||
token VARCHAR(255) UNIQUE NOT NULL,
|
||||
expira TIMESTAMP NOT NULL,
|
||||
creado_en TIMESTAMP DEFAULT NOW()
|
||||
)`,
|
||||
}
|
||||
|
||||
for _, m := range migrations {
|
||||
if _, err := dbPool.Exec(ctx, m); err != nil {
|
||||
log.Printf("Migration warning: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Seed ejercicios if empty
|
||||
var count int
|
||||
dbPool.QueryRow(ctx, "SELECT COUNT(*) FROM ejercicios").Scan(&count)
|
||||
if count == 0 {
|
||||
seedEjercicios(ctx, dbPool)
|
||||
}
|
||||
|
||||
// Create admin user if not exists
|
||||
var adminCount int
|
||||
dbPool.QueryRow(ctx, "SELECT COUNT(*) FROM usuarios WHERE rol = 'admin'").Scan(&adminCount)
|
||||
if adminCount == 0 {
|
||||
// Default admin: renato97 / wlillidan1 (bcrypt hash)
|
||||
_, err := dbPool.Exec(ctx, `
|
||||
INSERT INTO usuarios (email, username, password_hash, nombre, rol)
|
||||
VALUES ('renato97@econ.local', 'renato97', '$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZRGdjGj/n3.rsW4WzOFbMB3dHI.Bu', 'Renato', 'admin')
|
||||
`)
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not create admin user: %v", err)
|
||||
} else {
|
||||
log.Println("Admin user created: renato97 / wlillidan1")
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Migrations completed successfully")
|
||||
}
|
||||
|
||||
func seedEjercicios(ctx context.Context, pool *pgxpool.Pool) {
|
||||
ejercicios := []struct {
|
||||
ID string
|
||||
ModuloNumero int
|
||||
Titulo string
|
||||
Tipo string
|
||||
Contenido string
|
||||
Orden int
|
||||
}{
|
||||
// Módulo 1
|
||||
{"m1e1", 1, "Simulador de Disyuntivas", "interactivo", `{"tipo":"slider","descripcion":"Elige cuanto producir de cada bien"}`, 1},
|
||||
{"m1e2", 1, "Clasificación de Bienes", "quiz", `{"preguntas":[{"id":"q1","pregunta":"La leche es un bien...","opciones":["normal","inferior","de lujo"],"respuesta":"normal"}]}`, 2},
|
||||
{"m1e3", 1, "Flujo Circular", "dragdrop", `{"agentes":["familias","empresas"]}`, 3},
|
||||
// Módulo 2
|
||||
{"m2e1", 2, "Constructor de Curvas", "interactivo", `{"curvas":["oferta","demanda"]}`, 1},
|
||||
{"m2e2", 2, "Precios Máximos y Mínimos", "simulador", `{"tipo_precio":"seleccionar"}`, 2},
|
||||
{"m2e3", 2, "Shocks de Mercado", "quiz", `{"escenarios":[{"id":"s1","evento":"Sequía","pregunta":"¿Qué curva se ve afectada?","respuesta":"oferta"}]}`, 3},
|
||||
// Módulo 3
|
||||
{"m3e1", 3, "Calculadora de Elasticidad", "calculadora", `{"formula":"elasticidad"}`, 1},
|
||||
{"m3e2", 3, "Clasificador de Bienes", "quiz", `{"bienes":[{"nombre":"Pan","elasticidad":"inelastico"}]}`, 2},
|
||||
{"m3e3", 3, "Ejercicios tipo Examen", "quiz", `{"preguntas":[]}`, 3},
|
||||
// Módulo 4
|
||||
{"m4e1", 4, "Simulador de Producción", "simulador", `{"max_produccion":100}`, 1},
|
||||
{"m4e2", 4, "Calculadora de Costos", "calculadora", `{"tipos":["fijo","variable"]}`, 2},
|
||||
{"m4e3", 4, "Excedentes", "interactivo", `{"excedentes":["productor","consumidor"]}`, 3},
|
||||
}
|
||||
|
||||
for _, e := range ejercicios {
|
||||
_, err := pool.Exec(ctx, `
|
||||
INSERT INTO ejercicios (id, modulo_numero, titulo, tipo, contenido, orden)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, e.ID, e.ModuloNumero, e.Titulo, e.Tipo, e.Contenido, e.Orden)
|
||||
if err != nil {
|
||||
log.Printf("Warning: could not seed ejercicio %s: %v", e.ID, err)
|
||||
}
|
||||
}
|
||||
log.Println("Ejercicios seeded")
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value, exists := os.LookupEnv(key); exists {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
Reference in New Issue
Block a user