lblt 5 mesiacov pred
rodič
commit
9e42f802df

+ 8 - 0
.dockerignore

@@ -0,0 +1,8 @@
+.git
+.gitignore
+.env
+*.md
+data/
+docs/
+docker-compose.yml
+Dockerfile 

+ 2 - 0
.gitignore

@@ -31,3 +31,5 @@ dist/
 # Local test
 .trash/
 config.yaml
+.data/
+data/

+ 31 - 26
app/routes.go

@@ -17,7 +17,6 @@ func addRoutes(
 	workspaceHandler *handlers.WorkspaceHandler,
 	profileHandler *handlers.ProfileHandler,
 ) {
-
 	//group routes behind /api/v1/core
 	coreRtr := rtr.Group("/api/v1/core")
 
@@ -27,39 +26,45 @@ func addRoutes(
 	})
 
 	// Auth routes
-	coreRtr.POST("/auth/login", userHandler.Login)
+	authRoutes := coreRtr.Group("/auth")
+	authRoutes.POST("/login", userHandler.Login)
+
+	// Workspace onboarding routes
+	workspaceOnboarding := coreRtr.Group("/workspaces")
+	workspaceOnboarding.POST("/owners/init", userHandler.InitOwner)
+	workspaceOnboarding.PUT("/owners", userHandler.CreateOwner)
 
-	// User init routes
-	coreRtr.POST("/workspaces/owner", userHandler.InitWorkspaceOwner)
-	coreRtr.PUT("/workspaces/owner", userHandler.CreateWorkspaceOwner)
-	coreRtr.POST("/workspaces/invite", userHandler.CreateInvitedUser)
-	coreRtr.GET("/workspaces/invite/validate", userHandler.ValidateInvitedUser)
+	// Invitation management (public)
+	invitationRoutes := coreRtr.Group("/invitations")
+	invitationRoutes.POST("/accept", userHandler.AcceptInvitation)
+	invitationRoutes.GET("/validate", userHandler.ValidateInvitation)
 
-	// Logged in user routes
+	// Authenticated routes
 	auth := coreRtr.Group("/")
 	auth.Use(authMiddleware(jwtSvc))
 
-	// User management
-	auth.POST("/users/invite", userHandler.InviteUser)
-	//add user to workspace
-	auth.POST("/users/workspaces", userHandler.AddUserToWorkspace)
-	// auth.GET("/users/invitations", userHandler.ListInvitations)
-	// auth.DELETE("/users/invitations/:id", userHandler.CancelInvitation)
-
 	// Workspace management
-	auth.POST("/workspaces", workspaceHandler.CreateWorkspace)
+	workspaces := auth.Group("/workspaces")
+	workspaces.POST("", workspaceHandler.Create)
+	workspaces.POST("/:id/members", userHandler.AddMember)
+	workspaces.GET("/:id/profiles", profileHandler.ListByWorkspace)
 
 	// Profile management
-	auth.GET("/profiles", profileHandler.GetProfiles)
-	auth.POST("/profiles", profileHandler.CreateProfile)
-	auth.GET("/profiles/:id", profileHandler.GetProfile)
-	auth.PUT("/profiles/:id", profileHandler.UpdateProfile)
-	auth.DELETE("/profiles/:id", profileHandler.DeleteProfile)
-	auth.GET("/profiles/workspaces", profileHandler.GetProfilesByWorkspace)
-
-	// User profile
-	auth.GET("/users/me", userHandler.GetCurrentUser)
-	auth.PUT("/users/me", userHandler.UpdateCurrentUser)
+	profiles := auth.Group("/profiles")
+	profiles.GET("", profileHandler.List)
+	profiles.POST("", profileHandler.Create)
+	profiles.GET("/:id", profileHandler.Get)
+	profiles.PUT("/:id", profileHandler.Update)
+	profiles.DELETE("/:id", profileHandler.Delete)
+
+	// User management
+	users := auth.Group("/users")
+	users.GET("/current", userHandler.GetCurrent)
+	users.PUT("/current", userHandler.UpdateCurrent)
+
+	// Invitation management (authenticated)
+	authInvitations := auth.Group("/invitations")
+	authInvitations.POST("", userHandler.CreateInvitation)
 }
 
 func authMiddleware(jwtSvc *jwtutils.Service) gin.HandlerFunc {

+ 102 - 54
app/server.go

@@ -15,9 +15,10 @@ import (
 	"git.linuxforward.com/byom/byom-core/handlers"
 	"git.linuxforward.com/byom/byom-core/hook"
 	"git.linuxforward.com/byom/byom-core/jwtutils"
+	"git.linuxforward.com/byom/byom-core/logger"
+	"git.linuxforward.com/byom/byom-core/middleware"
 	"git.linuxforward.com/byom/byom-core/smtp"
 	"git.linuxforward.com/byom/byom-core/store"
-	"github.com/gin-contrib/cors"
 	"github.com/gin-gonic/gin"
 	"github.com/sirupsen/logrus"
 	"gorm.io/driver/sqlite"
@@ -44,62 +45,97 @@ type ServiceHandler interface {
 	Close() error
 }
 
+// NewApp creates a new application instance with the given options
 func NewApp(cnf *config.Config) (*App, error) {
 	ctx, cancel := context.WithCancel(context.Background())
 
-	rtr := gin.New()
-	rtr.Use(gin.Recovery())
-	rtr.Use(gin.Logger())
+	// Create logger
+	entry := logger.NewLogger("app")
 
-	config := cors.DefaultConfig()
-	config.AllowOrigins = []string{"*"}
-	config.AllowMethods = []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}
-	config.AllowHeaders = []string{"Origin", "Content-Type", "Accept", "Authorization"}
-	config.AllowCredentials = true
-	config.ExposeHeaders = []string{"Content-Length"}
+	// Create app instance
+	app := &App{
+		entry:  entry,
+		cnf:    cnf,
+		ctx:    ctx,
+		cancel: cancel,
+	}
 
-	rtr.Use(cors.New(config))
+	// Initialize router and middleware
+	if err := app.initRouter(); err != nil {
+		cancel()
+		return nil, fmt.Errorf("init router: %w", err)
+	}
 
-	//init GORM
-	dbConn, err := initDBConn(cnf.Database)
-	if err != nil {
+	// Initialize services
+	if err := app.initServices(); err != nil {
 		cancel()
-		return nil, fmt.Errorf("init db: %w", err)
+		return nil, fmt.Errorf("failed to initialize services: %w", err)
 	}
 
-	//init email service
-	emailSvc := smtp.NewService(cnf.Smtp)
+	return app, nil
+}
 
-	//init jwt service
-	jwtSvc := jwtutils.NewService(cnf.Jwt.JwtSecret)
+// initRouter initializes the router and middleware
+func (a *App) initRouter() error {
+	rtr := gin.New()
 
-	//init hook service
-	hookSvc := hook.NewHookClient(cnf.Hook.BaseURL, cnf.Hook.Domain, cnf.Hook.SecretKey)
+	// Add core middleware
+	rtr.Use(middleware.Recovery(a.entry.Logger))
+	rtr.Use(middleware.RequestID())
+	rtr.Use(middleware.RequestLogger(a.entry.Logger))
+	rtr.Use(middleware.ErrorHandler())
 
-	//init core functions
-	store := store.NewDataStore(dbConn)
-	userHandler := handlers.NewUserHandler(store, emailSvc, jwtSvc, hookSvc)
-	workspaceHandler := handlers.NewWorkspaceHandler(store)
-	profileHandler := handlers.NewProfileHandler(store)
+	// Add security middleware
+	rtr.Use(middleware.SecurityHeaders())
+	rtr.Use(middleware.RequestSanitizer())
 
-	//add routes
-	addRoutes(rtr, jwtSvc, userHandler, workspaceHandler, profileHandler)
+	// Configure request timeout
+	timeout := 30 * time.Second
+	if a.cnf.Server.RequestTimeout > 0 {
+		timeout = time.Duration(a.cnf.Server.RequestTimeout) * time.Second
+	}
+	rtr.Use(middleware.TimeoutMiddleware(timeout))
+
+	// Configure CORS
+	corsConfig := middleware.DefaultCORSConfig()
+	if len(a.cnf.Server.CorsOrigins) > 0 {
+		a.entry.WithField("origins", a.cnf.Server.CorsOrigins).Info("Configuring CORS with specific origins")
+		corsConfig.AllowOrigins = a.cnf.Server.CorsOrigins
+	} else {
+		a.entry.Warn("No CORS origins specified, using development defaults (localhost only)")
+	}
+	rtr.Use(middleware.CORS(corsConfig))
 
-	app := &App{
-		entry:            logrus.WithField("component", "app"),
-		cnf:              cnf,
-		ctx:              ctx,
-		cancel:           cancel,
-		router:           rtr,
-		dataStore:        store,
-		emailSvc:         emailSvc,
-		jwtSvc:           jwtSvc,
-		userHandler:      userHandler,
-		workspaceHandler: workspaceHandler,
-		profileHandler:   profileHandler,
+	// Add health check endpoint
+	addRoutes(rtr, a.jwtSvc, a.userHandler, a.workspaceHandler, a.profileHandler)
+
+	a.router = rtr
+	return nil
+}
+
+// initServices initializes all required services
+func (a *App) initServices() error {
+	// Initialize database
+	dbConn, err := initDBConn(a.cnf.Database)
+	if err != nil {
+		return fmt.Errorf("init database: %w", err)
 	}
 
-	return app, nil
+	// Initialize services if not already set through options
+	if a.emailSvc == nil {
+		a.emailSvc = smtp.NewService(a.cnf.Smtp)
+	}
+
+	a.jwtSvc = jwtutils.NewService(a.cnf.Jwt.JwtSecret)
+	a.hookSvc = hook.NewHookClient(a.cnf.Hook.BaseURL, a.cnf.Hook.Domain, a.cnf.Hook.SecretKey)
+	a.dataStore = store.NewDataStore(dbConn)
+
+	// Initialize handlers
+	a.userHandler = handlers.NewUserHandler(a.dataStore, a.emailSvc, a.jwtSvc, a.hookSvc)
+	a.workspaceHandler = handlers.NewWorkspaceHandler(a.dataStore)
+	a.profileHandler = handlers.NewProfileHandler(a.dataStore)
+
+	return nil
 }
 
 func (a *App) Run() error {
@@ -117,23 +153,24 @@ func (a *App) Run() error {
 		})
 	}
 
-	// Configure server
+	// Configure server with timeouts
 	srv := &http.Server{
-		Addr:    fmt.Sprintf(":%s", strconv.Itoa(a.cnf.Server.ListeningPort)),
-		Handler: a.router,
+		Addr:         fmt.Sprintf(":%s", strconv.Itoa(a.cnf.Server.ListeningPort)),
+		Handler:      a.router,
+		ReadTimeout:  15 * time.Second,
+		WriteTimeout: 15 * time.Second,
+		IdleTimeout:  60 * time.Second,
 	}
 
 	// Start server
 	go func() {
 		var err error
-
 		if a.cnf.Server.TlsConfig.Enabled {
 			a.entry.Infof("Starting server on port %d with TLS", a.cnf.Server.ListeningPort)
 			err = srv.ListenAndServeTLS(
 				a.cnf.Server.TlsConfig.CertFile,
 				a.cnf.Server.TlsConfig.KeyFile,
 			)
-
 		} else {
 			a.entry.Infof("Starting server on port %d without TLS", a.cnf.Server.ListeningPort)
 			err = srv.ListenAndServe()
@@ -141,28 +178,39 @@ func (a *App) Run() error {
 
 		if err != nil && err != http.ErrServerClosed {
 			a.entry.WithError(err).Error("Server error")
+			// Signal shutdown on critical error
+			a.cancel()
 		}
 	}()
 
 	// Graceful shutdown
 	quit := make(chan os.Signal, 1)
 	signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
-	<-quit
 
-	a.entry.Info("Stopping server...")
-	ctxTimeout, cancelFunc := context.WithTimeout(context.Background(), 30*time.Second)
-	defer cancelFunc()
-	err := srv.Shutdown(ctxTimeout)
-	if err != nil {
-		return fmt.Errorf("shutdown server: %w", err)
+	select {
+	case <-quit:
+		a.entry.Info("Shutdown signal received...")
+	case <-a.ctx.Done():
+		a.entry.Info("Server error occurred, initiating shutdown...")
+	}
+
+	// Create shutdown context with timeout
+	ctxShutdown, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
+
+	// Shutdown server
+	if err := srv.Shutdown(ctxShutdown); err != nil {
+		a.entry.WithError(err).Error("Server forced to shutdown")
+		return fmt.Errorf("server forced to shutdown: %w", err)
 	}
 
 	// Close all services
 	if err := a.Close(); err != nil {
 		a.entry.WithError(err).Error("Error during service cleanup")
+		return fmt.Errorf("service cleanup error: %w", err)
 	}
 
-	a.entry.Info("Server stopped successfully")
+	a.entry.Info("Server stopped gracefully")
 	return nil
 }
 

+ 162 - 0
common/requests.go

@@ -0,0 +1,162 @@
+package common
+
+import "github.com/google/uuid"
+
+// Common Response Types
+type ErrorResponse struct {
+	Error string `json:"error"`
+}
+
+type MessageResponse struct {
+	Message string `json:"message"`
+}
+
+// Auth Requests/Responses
+type AuthLoginRequest struct {
+	Email    string `json:"email" binding:"required,email"`
+	Password string `json:"password" binding:"required,password"`
+}
+
+type AuthLoginResponse struct {
+	Token string `json:"token"`
+	User  User   `json:"user"`
+}
+
+// User Requests/Responses
+type UserCreateRequest struct {
+	Email       string `json:"email" binding:"required,email"`
+	Name        string `json:"name" binding:"required,min=2,max=100"`
+	PhoneNumber string `json:"phone_number" binding:"omitempty,phone"`
+	Password    string `json:"password" binding:"required,password"`
+}
+
+type UserCreateResponse struct {
+	User User `json:"user"`
+}
+
+type UserUpdateRequest struct {
+	Name        string `json:"name" binding:"required,min=2,max=100"`
+	PhoneNumber string `json:"phone_number" binding:"omitempty,phone"`
+}
+
+type UserUpdateResponse struct {
+	User User `json:"user"`
+}
+
+type UserResponse struct {
+	User       User        `json:"user"`
+	Workspaces []Workspace `json:"workspaces"`
+}
+
+// Workspace Requests/Responses
+type WorkspaceCreateRequest struct {
+	Name string `json:"name" binding:"required,min=2,max=100"`
+}
+
+type WorkspaceCreateResponse struct {
+	ID   uuid.UUID `json:"id"`
+	Name string    `json:"name"`
+}
+
+type WorkspaceResponse struct {
+	Workspace Workspace `json:"workspace"`
+}
+
+type WorkspaceListResponse struct {
+	Workspaces []Workspace `json:"workspaces"`
+}
+
+type WorkspaceOwnerInitRequest struct {
+	Email       string `json:"email" binding:"required,email"`
+	Name        string `json:"name" binding:"required,min=2,max=100"`
+	PhoneNumber string `json:"phone_number" binding:"omitempty,phone"`
+}
+
+type WorkspaceOwnerInitResponse struct {
+	User User `json:"user"`
+}
+
+type WorkspaceOwnerCreateRequest struct {
+	Email       string `json:"email" binding:"required,email"`
+	Name        string `json:"name" binding:"required,min=2,max=100"`
+	PhoneNumber string `json:"phone_number" binding:"omitempty,phone"`
+	Password    string `json:"password" binding:"required,password"`
+}
+
+type WorkspaceOwnerCreateResponse struct {
+	User User `json:"user"`
+}
+
+type WorkspaceMemberAddRequest struct {
+	WorkspaceID uuid.UUID `json:"workspace_id" binding:"required,uuid"`
+	Role        string    `json:"role" binding:"required,oneof=admin member"`
+}
+
+type WorkspaceMemberAddResponse struct {
+	Message string `json:"message"`
+}
+
+// Profile Requests/Responses
+type ProfileCreateRequest struct {
+	Name        string    `json:"name" binding:"required,min=2,max=100"`
+	WorkspaceID uuid.UUID `json:"workspace_id" binding:"required,uuid"`
+}
+
+type ProfileCreateResponse struct {
+	Profile Profile `json:"profile"`
+}
+
+type ProfileUpdateRequest struct {
+	Name string `json:"name" binding:"required,min=2,max=100"`
+}
+
+type ProfileUpdateResponse struct {
+	Profile Profile `json:"profile"`
+}
+
+type ProfileResponse struct {
+	Profile Profile `json:"profile"`
+}
+
+type ProfileListResponse struct {
+	Profiles []Profile `json:"profiles"`
+}
+
+// Invitation Requests/Responses
+type InvitationCreateRequest struct {
+	Email       string    `json:"email" binding:"required,email"`
+	WorkspaceID uuid.UUID `json:"workspace_id" binding:"required,uuid"`
+	Role        string    `json:"role" binding:"required,oneof=admin member"`
+}
+
+type InvitationCreateResponse struct {
+	ID          uuid.UUID `json:"id"`
+	Email       string    `json:"email"`
+	Status      string    `json:"status"`
+	ExpiresAt   string    `json:"expires_at"`
+	WorkspaceID uuid.UUID `json:"workspace_id"`
+}
+
+type InvitationAcceptRequest struct {
+	Email       string `json:"email" binding:"required,email"`
+	Name        string `json:"name" binding:"required,min=2,max=100"`
+	PhoneNumber string `json:"phone_number" binding:"omitempty,phone"`
+	Password    string `json:"password" binding:"required,password"`
+	Token       string `json:"token" binding:"required"`
+}
+
+type InvitationAcceptResponse struct {
+	User        User      `json:"user"`
+	WorkspaceID uuid.UUID `json:"workspace_id"`
+}
+
+type InvitationValidateResponse struct {
+	Valid       bool      `json:"valid"`
+	WorkspaceID uuid.UUID `json:"workspace_id"`
+	Email       string    `json:"email"`
+	Error       string    `json:"error,omitempty"`
+}
+
+type InvitationListResponse struct {
+	Invitations []Invite `json:"invitations"`
+}

+ 9 - 9
common/user_model.go

@@ -29,15 +29,15 @@ type UserMe struct {
 
 // Invite represents an invitation to join the system.
 type Invite struct {
-	ID        uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
-	Email     string    `gorm:"size:255" json:"email"`
-	Workspace string    `gorm:"size:255" json:"workspace_id"`
-	Role      string    `gorm:"size:50;default:member" json:"role"`
-	Token     string    `gorm:"unique;size:255" json:"token"`
-	Status    string    `gorm:"size:50;default:pending" json:"status"`
-	ExpiresAt time.Time `json:"expires_at"`
-	CreatedAt time.Time `json:"created_at"`
-	UpdatedAt time.Time `json:"updated_at"`
+	ID          uuid.UUID `gorm:"type:uuid;primary_key" json:"id"`
+	Email       string    `gorm:"size:255" json:"email"`
+	WorkspaceID uuid.UUID `gorm:"size:255" json:"workspace_id"`
+	Role        string    `gorm:"size:50;default:member" json:"role"`
+	Token       string    `gorm:"unique;size:255" json:"token"`
+	Status      string    `gorm:"size:50;default:pending" json:"status"`
+	ExpiresAt   time.Time `json:"expires_at"`
+	CreatedAt   time.Time `json:"created_at"`
+	UpdatedAt   time.Time `json:"updated_at"`
 }
 
 type UserWorkspaceRole struct {

+ 10 - 20
config/config.go

@@ -4,7 +4,7 @@ import (
 	"fmt"
 	"os"
 
-	log "github.com/sirupsen/logrus"
+	"git.linuxforward.com/byom/byom-core/logger"
 	"gopkg.in/yaml.v3"
 )
 
@@ -31,9 +31,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
 }
 
 type Server struct {
-	ListeningPort int        `yaml:"listening_port"`
-	TlsConfig     *TlsConfig `yaml:"tls"`
-	Opts          []string   `yaml:"opts"`
+	ListeningPort  int        `yaml:"listening_port"`
+	TlsConfig      *TlsConfig `yaml:"tls"`
+	Opts           []string   `yaml:"opts"`
+	CorsOrigins    []string   `yaml:"cors_origins"`
+	RequestTimeout int        `yaml:"request_timeout"` // in seconds
 }
 
 func (c *Server) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -48,6 +50,9 @@ func (c *Server) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	if c.TlsConfig == nil {
 		return fmt.Errorf("tls is required")
 	}
+	if c.TlsConfig.Enabled && (c.CorsOrigins == nil || len(c.CorsOrigins) == 0) {
+		return fmt.Errorf("cors_origins must be set when TLS is enabled")
+	}
 	return nil
 }
 
@@ -196,23 +201,8 @@ func (c *Log) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	if err != nil {
 		return err
 	}
-	log.SetFormatter(&log.TextFormatter{
-		ForceColors:   c.ForceColors,
-		DisableColors: c.NoColor,
-		FullTimestamp: true,
-	})
-	if c.Level != "" {
-		lvl, err := log.ParseLevel(c.Level)
-		if err != nil {
-			return err
-		}
-		log.SetLevel(lvl)
-	}
-	if c.InJson {
-		log.SetFormatter(&log.JSONFormatter{})
-	}
 
-	return nil
+	return logger.Configure(c.Level, c.NoColor, c.ForceColors, c.InJson)
 }
 
 type Hook struct {

+ 288 - 299
docs/api.md

@@ -1,330 +1,319 @@
+# BYOM Core API Documentation
 
+## Overview
 
-# API Documentation
+This document describes the BYOM Core API endpoints, including request/response formats and authentication requirements.
 
-## Database Schema
+## Base URL
 
-```mermaid
-erDiagram
-    Vitrine {
-        string hostname PK
-        boolean used
-    }
-
-    VPS {
-        string id PK
-        string size
-        string ip
-        string version
-        string plan
-        string state
-        string hostname FK
-    }
-
-    Workspace {
-        string id PK
-        string name
-        string plan
-        timestamp created_at
-    }
-
-    User {
-        string id PK
-        string username
-        string email
-        string password_hash
-        boolean is_active
-        timestamp created_at
-    }
-
-    Profile {
-        string id PK
-        string profile_name
-        string workspace_id FK
-        timestamp created_at
-    }
-
-    Account {
-        string id PK
-        string type
-        string identifier
-        string profile_id FK
-        timestamp last_sync
-    }
-
-    AccountTrend {
-        string id PK
-        string account_id FK
-        string trend_name
-        float engagement_rate
-        int followers_growth
-        timestamp detected_at
-        timestamp expires_at
-    }
-
-    Suggestion {
-        string id PK
-        string profile_id FK
-        string type
-        string title
-        string description
-        float relevance_score
-        string status
-        timestamp created_at
-        timestamp updated_at
-    }
-
-    GeneratedImage {
-        string id PK
-        string suggestion_id FK
-        string image_url
-        string prompt_used
-        timestamp generated_at
-        boolean is_selected
-    }
-
-    WorkspaceUser {
-        string workspace_id FK
-        string user_id FK
-        string role
-        timestamp joined_at
-    }
-
-    VitrineUsers {
-        string hostname FK
-        string user_id FK
-    }
-
-    Vitrine ||--o{ VitrineUsers : "has"
-    Vitrine ||--o{ VPS : "hosts"
-    Workspace ||--o{ WorkspaceUser : "has"
-    User ||--o{ WorkspaceUser : "belongs_to"
-    Workspace ||--o{ Profile : "contains"
-    Profile ||--o{ Account : "has"
-    Account ||--o{ AccountTrend : "trends"
-    Profile ||--o{ Suggestion : "receives"
-    Suggestion ||--o{ GeneratedImage : "has"
-```
+All API endpoints are prefixed with: `/api/v1/core`
 
 ## Authentication
-All protected routes require a Bearer token in the Authorization header:
-```
-Authorization: Bearer <token>
-```
-
-## Base URLs
-- Main platform: `https://localhost:8443/`
-- Workspace specific: `https://{workspace-name}.yourdomain.com/api`
 
-## Routes
-
-### Create Workspace Owner
-Creates the first user (owner) for a new workspace.
-
-```http
-POST /workspace/owner
-```
-
-**Request Body:**
-```json
-{
-    "email": "string",
-    "name": "string",
-    "password": "string",
-    "phone_number": "string"
-}
-```
-
-**Response:** `201 Created`
-```json
-{
-    "id": "uuid",
-    "email": "string",
-    "name": "string",
-    "phone_number": "string",
-    "role": "owner",
-    "status": "active"
-}
+Most endpoints require authentication using a JWT token. Include the token in the Authorization header:
 ```
-
-### Accept Invitation
-Creates a new user account from an invitation.
-
-```http
-POST /workspace/invite
-```
-
-**Request Body:**
-```json
-{
-    "email": "string",
-    "name": "string",
-    "password": "string",
-    "phone_number": "string",
-    "token": "string"
-}
-```
-
-**Response:** `201 Created`
-```json
-{
-    "id": "uuid",
-    "email": "string",
-    "name": "string",
-    "phone_number": "string",
-    "role": "string",
-    "status": "active"
-}
+Authorization: Bearer <token>
 ```
 
-### Invite User
-Protected route to invite a new user to the workspace.
-
-```http
-POST /users/invite
-```
+## Common Response Types
 
-**Request Body:**
+### Error Response
 ```json
 {
-    "email": "string",
-    "role": "string"
+  "error": "Error message description"
 }
 ```
 
-**Response:** `201 Created`
+### Success Message Response
 ```json
 {
-    "id": "uuid",
-    "email": "string",
-    "role": "string",
-    "token": "string",
-    "expires_at": "timestamp"
+  "message": "Success message description"
 }
 ```
 
-### List Invitations
-Protected route to get all pending invitations.
-
-```http
-GET /users/invitations
-```
-
-**Response:** `200 OK`
-```json
-{
-    "invitations": [
-        {
-            "id": "uuid",
-            "email": "string",
-            "role": "string",
-            "status": "string",
-            "expires_at": "timestamp",
-            "created_at": "timestamp"
-        }
+## Endpoints
+
+### Authentication
+
+#### Login
+- **POST** `/auth/login`
+- **Description**: Authenticate user and get access token
+- **Request**:
+  ```json
+  {
+    "email": "user@example.com",
+    "password": "userpassword"
+  }
+  ```
+- **Response** (200 OK):
+  ```json
+  {
+    "token": "jwt.token.here",
+    "user": {
+      "id": "uuid",
+      "email": "user@example.com",
+      "name": "User Name",
+      "role": "user"
+    }
+  }
+  ```
+
+### User Management
+
+#### Get Current User
+- **GET** `/users/current`
+- **Auth Required**: Yes
+- **Response** (200 OK):
+  ```json
+  {
+    "user": {
+      "id": "uuid",
+      "email": "user@example.com",
+      "name": "User Name",
+      "phone_number": "1234567890",
+      "role": "user",
+      "status": "active"
+    },
+    "workspaces": [
+      {
+        "id": "uuid",
+        "name": "Workspace Name"
+      }
     ]
-}
-```
-
-### Cancel Invitation
-Protected route to cancel a pending invitation.
-
-```http
-DELETE /users/invitations/:id
-```
-
-**Response:** `200 OK`
-```json
-{
-    "success": true
-}
-```
-
-### Get Current User Profile
-Protected route to get the current user's profile.
-
-```http
-GET /users/me
-```
-
-**Response:** `200 OK`
-```json
-{
+  }
+  ```
+
+#### Update Current User
+- **PUT** `/users/current`
+- **Auth Required**: Yes
+- **Request**:
+  ```json
+  {
+    "name": "Updated Name",
+    "phone_number": "1234567890"
+  }
+  ```
+- **Response** (200 OK):
+  ```json
+  {
+    "user": {
+      "id": "uuid",
+      "name": "Updated Name",
+      "phone_number": "1234567890"
+    }
+  }
+  ```
+
+### Workspace Management
+
+#### Create Workspace
+- **POST** `/workspaces`
+- **Auth Required**: Yes
+- **Request**:
+  ```json
+  {
+    "name": "New Workspace"
+  }
+  ```
+- **Response** (201 Created):
+  ```json
+  {
     "id": "uuid",
-    "email": "string",
-    "name": "string",
-    "phone_number": "string",
-    "role": "string",
-    "status": "string"
-}
-```
-
-### Update Current User Profile
-Protected route to update the current user's profile.
-
-```http
-PUT /users/me
-```
-
-**Request Body:**
-```json
-{
-    "name": "string",
-    "phone_number": "string"
-}
-```
-
-**Response:** `200 OK`
-```json
-{
+    "name": "New Workspace"
+  }
+  ```
+
+#### Initialize Workspace Owner
+- **POST** `/workspaces/owners/init`
+- **Request**:
+  ```json
+  {
+    "email": "owner@example.com",
+    "name": "Owner Name",
+    "phone_number": "1234567890"
+  }
+  ```
+- **Response** (201 Created):
+  ```json
+  {
+    "user": {
+      "id": "uuid",
+      "email": "owner@example.com",
+      "name": "Owner Name",
+      "role": "owner",
+      "status": "pending"
+    }
+  }
+  ```
+
+#### Add Workspace Member
+- **POST** `/workspaces/:id/members`
+- **Auth Required**: Yes
+- **Request**:
+  ```json
+  {
+    "workspace_id": "uuid",
+    "role": "member"
+  }
+  ```
+- **Response** (200 OK):
+  ```json
+  {
+    "message": "User added to workspace successfully"
+  }
+  ```
+
+### Profile Management
+
+#### List Profiles
+- **GET** `/profiles`
+- **Auth Required**: Yes
+- **Response** (200 OK):
+  ```json
+  {
+    "profiles": [
+      {
+        "id": "uuid",
+        "name": "Profile Name",
+        "workspace_id": "uuid"
+      }
+    ]
+  }
+  ```
+
+#### Create Profile
+- **POST** `/profiles`
+- **Auth Required**: Yes
+- **Request**:
+  ```json
+  {
+    "name": "New Profile",
+    "workspace_id": "uuid"
+  }
+  ```
+- **Response** (201 Created):
+  ```json
+  {
+    "profile": {
+      "id": "uuid",
+      "name": "New Profile",
+      "workspace_id": "uuid"
+    }
+  }
+  ```
+
+#### Get Profile
+- **GET** `/profiles/:id`
+- **Auth Required**: Yes
+- **Response** (200 OK):
+  ```json
+  {
+    "profile": {
+      "id": "uuid",
+      "name": "Profile Name",
+      "workspace_id": "uuid"
+    }
+  }
+  ```
+
+#### Update Profile
+- **PUT** `/profiles/:id`
+- **Auth Required**: Yes
+- **Request**:
+  ```json
+  {
+    "name": "Updated Profile Name"
+  }
+  ```
+- **Response** (200 OK):
+  ```json
+  {
+    "profile": {
+      "id": "uuid",
+      "name": "Updated Profile Name",
+      "workspace_id": "uuid"
+    }
+  }
+  ```
+
+### Invitation Management
+
+#### Create Invitation
+- **POST** `/invitations`
+- **Auth Required**: Yes
+- **Request**:
+  ```json
+  {
+    "email": "newuser@example.com",
+    "workspace_id": "uuid",
+    "role": "member"
+  }
+  ```
+- **Response** (201 Created):
+  ```json
+  {
     "id": "uuid",
-    "email": "string",
-    "name": "string",
-    "phone_number": "string",
-    "role": "string",
-    "status": "string"
-}
-```
+    "email": "newuser@example.com",
+    "status": "pending",
+    "expires_at": "2024-03-21T12:00:00Z",
+    "workspace_id": "uuid"
+  }
+  ```
+
+#### Accept Invitation
+- **POST** `/invitations/accept`
+- **Request**:
+  ```json
+  {
+    "email": "newuser@example.com",
+    "name": "New User",
+    "phone_number": "1234567890",
+    "password": "userpassword",
+    "token": "invitation-token"
+  }
+  ```
+- **Response** (201 Created):
+  ```json
+  {
+    "user": {
+      "id": "uuid",
+      "email": "newuser@example.com",
+      "name": "New User"
+    },
+    "workspace_id": "uuid"
+  }
+  ```
+
+#### Validate Invitation
+- **GET** `/invitations/validate?token=<token>`
+- **Response** (200 OK):
+  ```json
+  {
+    "valid": true,
+    "workspace_id": "uuid",
+    "email": "newuser@example.com"
+  }
+  ```
+
+## HTTP Status Codes
+
+- `200 OK`: Successful request
+- `201 Created`: Resource created successfully
+- `400 Bad Request`: Invalid request parameters
+- `401 Unauthorized`: Authentication required or failed
+- `403 Forbidden`: Permission denied
+- `404 Not Found`: Resource not found
+- `500 Internal Server Error`: Server error
 
 ## Data Types
 
-### Role Values
-- `owner`: Workspace owner with full permissions
-- `admin`: Administrative user with elevated permissions
+### User Roles
+- `owner`: Workspace owner
+- `admin`: Workspace administrator
 - `member`: Regular workspace member
 
-### Status Values
-- `active`: User account is active
-- `inactive`: User account is temporarily inactive
-- `suspended`: User account has been suspended
-
-### Invitation Status Values
-- `pending`: Invitation is waiting to be accepted
+### Invitation Status
+- `pending`: Invitation awaiting acceptance
 - `accepted`: Invitation has been accepted
 - `expired`: Invitation has expired
-- `cancelled`: Invitation was cancelled
-
-### Notes
-- All timestamps are in ISO 8601 format
-- All IDs are UUIDs
-- Workspace is identified by subdomain
-- Password requirements:
-  - Minimum 8 characters
-  - Must contain at least one number
-  - Must contain at least one uppercase letter
-  - Must contain at least one special character
-
-## Error Responses
-All error responses follow this format:
-```json
-{
-    "error": "string"
-}
-```
-
-Common HTTP status codes:
-- `400`: Bad Request - Invalid input data
-- `401`: Unauthorized - Missing or invalid authentication
-- `403`: Forbidden - Insufficient permissions
-- `404`: Not Found - Resource doesn't exist
-- `500`: Internal Server Error - Server-side error
+- `cancelled`: Invitation was cancelled

+ 571 - 0
docs/openapi.yaml

@@ -0,0 +1,571 @@
+openapi: 3.0.0
+info:
+  title: BYOM Core API
+  version: '1.0'
+  description: API for managing BYOM Core services
+  contact:
+    name: BYOM Support
+servers:
+  - url: /api/v1/core
+    description: BYOM Core API
+
+components:
+  securitySchemes:
+    BearerAuth:
+      type: http
+      scheme: bearer
+      bearerFormat: JWT
+
+  schemas:
+    Error:
+      type: object
+      properties:
+        error:
+          type: string
+          example: "Error message description"
+
+    Message:
+      type: object
+      properties:
+        message:
+          type: string
+          example: "Success message description"
+
+    User:
+      type: object
+      properties:
+        id:
+          type: string
+          format: uuid
+        email:
+          type: string
+          format: email
+        name:
+          type: string
+        phone_number:
+          type: string
+        role:
+          type: string
+          enum: [owner, admin, member]
+        status:
+          type: string
+          enum: [active, pending, inactive]
+        created_at:
+          type: string
+          format: date-time
+        updated_at:
+          type: string
+          format: date-time
+
+    Workspace:
+      type: object
+      properties:
+        id:
+          type: string
+          format: uuid
+        name:
+          type: string
+        created_at:
+          type: string
+          format: date-time
+        updated_at:
+          type: string
+          format: date-time
+
+    Profile:
+      type: object
+      properties:
+        id:
+          type: string
+          format: uuid
+        name:
+          type: string
+        workspace_id:
+          type: string
+          format: uuid
+
+    Invite:
+      type: object
+      properties:
+        id:
+          type: string
+          format: uuid
+        email:
+          type: string
+          format: email
+        workspace_id:
+          type: string
+          format: uuid
+        role:
+          type: string
+          enum: [owner, admin, member]
+        status:
+          type: string
+          enum: [pending, accepted, expired, cancelled]
+        expires_at:
+          type: string
+          format: date-time
+        created_at:
+          type: string
+          format: date-time
+
+paths:
+  /health:
+    get:
+      summary: Health check endpoint
+      responses:
+        '200':
+          description: Service is healthy
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  status:
+                    type: string
+                    example: "ok"
+
+  /auth/login:
+    post:
+      summary: Authenticate user
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - email
+                - password
+              properties:
+                email:
+                  type: string
+                  format: email
+                password:
+                  type: string
+                  format: password
+      responses:
+        '200':
+          description: Login successful
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  token:
+                    type: string
+                  user:
+                    $ref: '#/components/schemas/User'
+        '400':
+          description: Invalid credentials
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /users/current:
+    get:
+      summary: Get current user information
+      security:
+        - BearerAuth: []
+      responses:
+        '200':
+          description: Current user information
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  user:
+                    $ref: '#/components/schemas/User'
+                  workspaces:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/Workspace'
+    put:
+      summary: Update current user
+      security:
+        - BearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - name
+              properties:
+                name:
+                  type: string
+                phone_number:
+                  type: string
+      responses:
+        '200':
+          description: User updated successfully
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  user:
+                    $ref: '#/components/schemas/User'
+
+  /workspaces:
+    post:
+      summary: Create new workspace
+      security:
+        - BearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - name
+              properties:
+                name:
+                  type: string
+      responses:
+        '201':
+          description: Workspace created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Workspace'
+
+  /workspaces/owners/init:
+    post:
+      summary: Initialize workspace owner
+      description: First step of workspace owner creation - creates a pending owner account
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - email
+                - name
+              properties:
+                email:
+                  type: string
+                  format: email
+                name:
+                  type: string
+                phone_number:
+                  type: string
+      responses:
+        '201':
+          description: Workspace owner initialized
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  user:
+                    $ref: '#/components/schemas/User'
+        '400':
+          description: Invalid request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '409':
+          description: User already exists
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /workspaces/owners:
+    put:
+      summary: Complete workspace owner creation
+      description: Second step of workspace owner creation - sets password and activates the account
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - email
+                - name
+                - password
+              properties:
+                email:
+                  type: string
+                  format: email
+                name:
+                  type: string
+                phone_number:
+                  type: string
+                password:
+                  type: string
+                  format: password
+      responses:
+        '201':
+          description: Workspace owner created
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  user:
+                    $ref: '#/components/schemas/User'
+        '400':
+          description: Invalid request
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '404':
+          description: Pending owner not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+        '409':
+          description: Email does not match pending owner
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Error'
+
+  /workspaces/{id}/members:
+    post:
+      summary: Add member to workspace
+      security:
+        - BearerAuth: []
+      parameters:
+        - name: id
+          in: path
+          required: true
+          schema:
+            type: string
+            format: uuid
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - role
+              properties:
+                role:
+                  type: string
+                  enum: [admin, member]
+      responses:
+        '200':
+          description: Member added successfully
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Message'
+
+  /profiles:
+    get:
+      summary: List profiles
+      security:
+        - BearerAuth: []
+      responses:
+        '200':
+          description: List of profiles
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  profiles:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/Profile'
+    post:
+      summary: Create new profile
+      security:
+        - BearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - name
+                - workspace_id
+              properties:
+                name:
+                  type: string
+                workspace_id:
+                  type: string
+                  format: uuid
+      responses:
+        '201':
+          description: Profile created
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  profile:
+                    $ref: '#/components/schemas/Profile'
+
+  /profiles/{id}:
+    parameters:
+      - name: id
+        in: path
+        required: true
+        schema:
+          type: string
+          format: uuid
+    get:
+      summary: Get profile by ID
+      security:
+        - BearerAuth: []
+      responses:
+        '200':
+          description: Profile details
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  profile:
+                    $ref: '#/components/schemas/Profile'
+    put:
+      summary: Update profile
+      security:
+        - BearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - name
+              properties:
+                name:
+                  type: string
+      responses:
+        '200':
+          description: Profile updated
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  profile:
+                    $ref: '#/components/schemas/Profile'
+    delete:
+      summary: Delete profile
+      security:
+        - BearerAuth: []
+      responses:
+        '200':
+          description: Profile deleted
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Message'
+
+  /invitations:
+    post:
+      summary: Create invitation
+      security:
+        - BearerAuth: []
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - email
+                - workspace_id
+                - role
+              properties:
+                email:
+                  type: string
+                  format: email
+                workspace_id:
+                  type: string
+                  format: uuid
+                role:
+                  type: string
+                  enum: [admin, member]
+      responses:
+        '201':
+          description: Invitation created
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Invite'
+
+  /invitations/accept:
+    post:
+      summary: Accept invitation
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              type: object
+              required:
+                - email
+                - name
+                - password
+                - token
+              properties:
+                email:
+                  type: string
+                  format: email
+                name:
+                  type: string
+                phone_number:
+                  type: string
+                password:
+                  type: string
+                  format: password
+                token:
+                  type: string
+      responses:
+        '201':
+          description: Invitation accepted
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  user:
+                    $ref: '#/components/schemas/User'
+                  workspace_id:
+                    type: string
+                    format: uuid
+
+  /invitations/validate:
+    get:
+      summary: Validate invitation token
+      parameters:
+        - name: token
+          in: query
+          required: true
+          schema:
+            type: string
+      responses:
+        '200':
+          description: Token validation result
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  valid:
+                    type: boolean
+                  workspace_id:
+                    type: string
+                    format: uuid
+                  email:
+                    type: string
+                    format: email 

+ 143 - 0
errors/errors.go

@@ -0,0 +1,143 @@
+package errors
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// ErrorType represents the type of error
+type ErrorType string
+
+const (
+	ErrorTypeValidation     ErrorType = "VALIDATION_ERROR"
+	ErrorTypeAuthorization  ErrorType = "AUTHORIZATION_ERROR"
+	ErrorTypeAuthentication ErrorType = "AUTHENTICATION_ERROR"
+	ErrorTypeNotFound       ErrorType = "NOT_FOUND"
+	ErrorTypeConflict       ErrorType = "CONFLICT"
+	ErrorTypeInternal       ErrorType = "INTERNAL_ERROR"
+	ErrorTypeBadRequest     ErrorType = "BAD_REQUEST"
+)
+
+// AppError represents an application error
+type AppError struct {
+	Type    ErrorType `json:"type"`
+	Message string    `json:"message"`
+	Detail  string    `json:"detail,omitempty"`
+	Field   string    `json:"field,omitempty"`
+	Code    int       `json:"-"`
+	Err     error     `json:"-"`
+}
+
+func (e *AppError) Error() string {
+	if e.Detail != "" {
+		return fmt.Sprintf("%s: %s (%s)", e.Type, e.Message, e.Detail)
+	}
+	return fmt.Sprintf("%s: %s", e.Type, e.Message)
+}
+
+func (e *AppError) Unwrap() error {
+	return e.Err
+}
+
+// Error constructors
+func NewValidationError(message, field string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeValidation,
+		Message: message,
+		Field:   field,
+		Code:    http.StatusBadRequest,
+	}
+}
+
+func NewAuthorizationError(message string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeAuthorization,
+		Message: message,
+		Code:    http.StatusForbidden,
+	}
+}
+
+func NewAuthenticationError(message string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeAuthentication,
+		Message: message,
+		Code:    http.StatusUnauthorized,
+	}
+}
+
+func NewNotFoundError(resource, id string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeNotFound,
+		Message: fmt.Sprintf("%s not found", resource),
+		Detail:  fmt.Sprintf("ID: %s", id),
+		Code:    http.StatusNotFound,
+	}
+}
+
+func NewConflictError(message, detail string) *AppError {
+	return &AppError{
+		Type:    ErrorTypeConflict,
+		Message: message,
+		Detail:  detail,
+		Code:    http.StatusConflict,
+	}
+}
+
+func NewInternalError(message string, err error) *AppError {
+	return &AppError{
+		Type:    ErrorTypeInternal,
+		Message: message,
+		Detail:  err.Error(),
+		Code:    http.StatusInternalServerError,
+		Err:     err,
+	}
+}
+
+// Error type checks
+func IsNotFound(err error) bool {
+	if err == nil {
+		return false
+	}
+	if e, ok := err.(*AppError); ok {
+		return e.Type == ErrorTypeNotFound
+	}
+	return false
+}
+
+func IsValidation(err error) bool {
+	if err == nil {
+		return false
+	}
+	if e, ok := err.(*AppError); ok {
+		return e.Type == ErrorTypeValidation
+	}
+	return false
+}
+
+func IsAuthorization(err error) bool {
+	if err == nil {
+		return false
+	}
+	if e, ok := err.(*AppError); ok {
+		return e.Type == ErrorTypeAuthorization || e.Type == ErrorTypeAuthentication
+	}
+	return false
+}
+
+// WithDetail adds detail to an existing error
+func WithDetail(err *AppError, detail string) *AppError {
+	if err == nil {
+		return nil
+	}
+	err.Detail = detail
+	return err
+}
+
+// WithField adds a field name to an existing error
+func WithField(err *AppError, field string) *AppError {
+	if err == nil {
+		return nil
+	}
+	err.Field = field
+	return err
+}

+ 3 - 0
go.mod

@@ -12,6 +12,7 @@ require (
 	github.com/bytedance/sonic/loader v0.2.1 // indirect
 	github.com/cloudwego/base64x v0.1.4 // indirect
 	github.com/cloudwego/iasm v0.2.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/gabriel-vasile/mimetype v1.4.7 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
@@ -28,6 +29,8 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/stretchr/testify v1.10.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
 	golang.org/x/arch v0.12.0 // indirect

+ 4 - 0
go.sum

@@ -13,6 +13,7 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ
 github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
 github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
 github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
@@ -68,6 +69,7 @@ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6
 github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
 github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
 github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
 github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
@@ -82,6 +84,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=

+ 75 - 44
handlers/profile.go

@@ -1,7 +1,12 @@
 package handlers
 
 import (
+	"net/http"
+
 	"git.linuxforward.com/byom/byom-core/common"
+	"git.linuxforward.com/byom/byom-core/errors"
+	"git.linuxforward.com/byom/byom-core/logger"
+	"git.linuxforward.com/byom/byom-core/middleware"
 	"git.linuxforward.com/byom/byom-core/store"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
@@ -14,123 +19,149 @@ type ProfileHandler struct {
 }
 
 func NewProfileHandler(db *store.DataStore) *ProfileHandler {
-	logger := logrus.WithField("core", "profile-handler")
 	return &ProfileHandler{
-		logger: logger,
+		logger: logger.NewLogger("profile-handler"),
 		store:  db,
 	}
 }
 
-func (h *ProfileHandler) GetProfiles(c *gin.Context) {
+// Profile Management
+func (h *ProfileHandler) List(c *gin.Context) {
 	profiles, err := h.store.GetProfiles(c)
 	if err != nil {
-		c.JSON(500, gin.H{"error": "Failed to get profiles"})
+		h.logger.WithError(err).Error("failed to get profiles")
+		_ = c.Error(errors.NewInternalError("Failed to get profiles", err))
 		return
 	}
 
-	c.JSON(200, profiles)
+	c.JSON(http.StatusOK, common.ProfileListResponse{Profiles: profiles})
 }
 
-func (h *ProfileHandler) CreateProfile(c *gin.Context) {
-	var req common.CreateProfileRequest
-	if err := c.ShouldBindJSON(&req); err != nil {
-		c.JSON(400, gin.H{"error": "Invalid request"})
+func (h *ProfileHandler) Create(c *gin.Context) {
+	req, ok := middleware.ValidateRequest[common.ProfileCreateRequest](c)
+	if !ok {
 		return
 	}
 
-	h.logger.Info("Creating profile")
-	h.logger.Info(req)
-
 	profile := &common.Profile{
 		ID:          uuid.New(),
 		Name:        req.Name,
-		WorkspaceID: uuid.MustParse(req.WorkspaceID),
+		WorkspaceID: req.WorkspaceID,
 	}
 
 	if err := h.store.CreateProfile(c, profile); err != nil {
-		c.JSON(500, gin.H{"error": "Failed to create profile"})
+		h.logger.WithError(err).Error("failed to create profile")
+		_ = c.Error(errors.NewInternalError("Failed to create profile", err))
 		return
 	}
 
-	c.JSON(201, profile)
-
+	c.JSON(http.StatusCreated, common.ProfileResponse{Profile: *profile})
 }
 
-func (h *ProfileHandler) GetProfile(c *gin.Context) {
+func (h *ProfileHandler) Get(c *gin.Context) {
 	id := c.Param("id")
 	if id == "" {
-		c.JSON(400, gin.H{"error": "profile id is required"})
+		_ = c.Error(errors.NewValidationError("Profile ID is required", "id"))
+		return
+	}
+
+	// Validate UUID format
+	if _, err := uuid.Parse(id); err != nil {
+		_ = c.Error(errors.NewValidationError("Profile ID is required", "id"))
 		return
 	}
 
 	profile, err := h.store.GetProfile(c, id)
 	if err != nil {
-		c.JSON(500, gin.H{"error": "Failed to get profile"})
+		if errors.IsNotFound(err) {
+			_ = c.Error(errors.NewNotFoundError("Profile", id))
+			return
+		}
+		h.logger.WithError(err).Error("failed to get profile")
+		_ = c.Error(errors.NewInternalError("Failed to get profile", err))
 		return
 	}
 
-	c.JSON(200, profile)
-
+	c.JSON(http.StatusOK, common.ProfileResponse{Profile: *profile})
 }
 
-func (h *ProfileHandler) UpdateProfile(c *gin.Context) {
-	var req common.UpdateProfileRequest
-
-	if err := c.ShouldBindJSON(&req); err != nil {
-		c.JSON(400, gin.H{"error": "Invalid request"})
+func (h *ProfileHandler) Update(c *gin.Context) {
+	id := c.Param("id")
+	if id == "" {
+		_ = c.Error(errors.NewValidationError("Profile ID is required", "id"))
 		return
 	}
 
-	id := c.Param("id")
-	if id == "" {
-		c.JSON(400, gin.H{"error": "profile id is required"})
+	req, ok := middleware.ValidateRequest[common.ProfileUpdateRequest](c)
+	if !ok {
 		return
 	}
 
 	profile, err := h.store.GetProfile(c, id)
 	if err != nil {
-		c.JSON(500, gin.H{"error": "Failed to get profile"})
+		if errors.IsNotFound(err) {
+			_ = c.Error(errors.NewNotFoundError("Profile", id))
+			return
+		}
+		h.logger.WithError(err).Error("failed to get profile")
+		_ = c.Error(errors.NewInternalError("Failed to get profile", err))
 		return
 	}
 
 	profile.Name = req.Name
 
 	if err := h.store.UpdateProfile(c, profile); err != nil {
-		c.JSON(500, gin.H{"error": "Failed to update profile"})
+		h.logger.WithError(err).Error("failed to update profile")
+		_ = c.Error(errors.NewInternalError("Failed to update profile", err))
 		return
 	}
 
-	c.JSON(200, profile)
+	c.JSON(http.StatusOK, common.ProfileResponse{Profile: *profile})
 }
 
-func (h *ProfileHandler) DeleteProfile(c *gin.Context) {
+func (h *ProfileHandler) Delete(c *gin.Context) {
 	id := c.Param("id")
 	if id == "" {
-		c.JSON(400, gin.H{"error": "profile id is required"})
+		_ = c.Error(errors.NewValidationError("Profile ID is required", "id"))
 		return
 	}
 
 	if err := h.store.DeleteProfile(c, id); err != nil {
-		c.JSON(500, gin.H{"error": "Failed to delete profile"})
+		if errors.IsNotFound(err) {
+			_ = c.Error(errors.NewNotFoundError("Profile", id))
+			return
+		}
+		h.logger.WithError(err).Error("failed to delete profile")
+		_ = c.Error(errors.NewInternalError("Failed to delete profile", err))
 		return
 	}
 
-	c.JSON(200, gin.H{"message": "Profile deleted"})
-
+	c.JSON(http.StatusOK, common.MessageResponse{Message: "Profile deleted successfully"})
 }
 
-func (h *ProfileHandler) GetProfilesByWorkspace(c *gin.Context) {
-	wId := c.Query("workspaceID")
-	if wId == "" {
-		c.JSON(400, gin.H{"error": "workspaceID is required"})
+func (h *ProfileHandler) ListByWorkspace(c *gin.Context) {
+	workspaceID := c.Param("id")
+	if workspaceID == "" {
+		_ = c.Error(errors.NewValidationError("Workspace ID is required", "id"))
+		return
+	}
+
+	// Validate workspace exists
+	if _, err := h.store.GetWorkspace(c, workspaceID); err != nil {
+		if errors.IsNotFound(err) {
+			_ = c.Error(errors.NewNotFoundError("Workspace", workspaceID))
+			return
+		}
+		_ = c.Error(errors.NewValidationError("Invalid workspace ID", "id"))
 		return
 	}
 
-	profiles, err := h.store.GetProfilesByWorkspaceID(c, wId)
+	profiles, err := h.store.GetProfilesByWorkspaceID(c, workspaceID)
 	if err != nil {
-		c.JSON(500, gin.H{"error": "Failed to get profiles"})
+		h.logger.WithError(err).Error("failed to get profiles by workspace")
+		_ = c.Error(errors.NewInternalError("Failed to get profiles", err))
 		return
 	}
 
-	c.JSON(200, profiles)
+	c.JSON(http.StatusOK, common.ProfileListResponse{Profiles: profiles})
 }

+ 165 - 0
handlers/profile_test.go

@@ -0,0 +1,165 @@
+package handlers
+
+import (
+	"bytes"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+
+	"git.linuxforward.com/byom/byom-core/common"
+	"git.linuxforward.com/byom/byom-core/errors"
+	"git.linuxforward.com/byom/byom-core/middleware"
+	testhelpers "git.linuxforward.com/byom/byom-core/testing"
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestProfileHandler_Create(t *testing.T) {
+	// Setup test database and handler
+	store := testhelpers.TestDB(t)
+	handler := NewProfileHandler(store)
+
+	// Setup gin test router with error handler
+	gin.SetMode(gin.TestMode)
+	router := gin.New()
+	router.Use(middleware.ErrorHandler())
+	router.POST("/api/v1/core/profiles", handler.Create)
+
+	tests := []struct {
+		name          string
+		request       *common.ProfileCreateRequest
+		expectedCode  int
+		expectedError string
+	}{
+		{
+			name: "valid_request",
+			request: &common.ProfileCreateRequest{
+				Name:        "Test Profile",
+				WorkspaceID: uuid.New(),
+			},
+			expectedCode: http.StatusCreated,
+		},
+		{
+			name: "missing_name",
+			request: &common.ProfileCreateRequest{
+				WorkspaceID: uuid.New(),
+			},
+			expectedCode:  http.StatusBadRequest,
+			expectedError: "Invalid request format",
+		},
+		{
+			name: "missing_workspace_ID",
+			request: &common.ProfileCreateRequest{
+				Name: "Test Profile",
+			},
+			expectedCode:  http.StatusBadRequest,
+			expectedError: "Invalid request format",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			body, err := json.Marshal(tt.request)
+			require.NoError(t, err)
+
+			w := httptest.NewRecorder()
+			req := httptest.NewRequest(http.MethodPost, "/api/v1/core/profiles", bytes.NewReader(body))
+			req.Header.Set("Content-Type", "application/json")
+			router.ServeHTTP(w, req)
+
+			assert.Equal(t, tt.expectedCode, w.Code)
+			if tt.expectedError != "" {
+				var resp gin.H
+				err := json.NewDecoder(w.Body).Decode(&resp)
+				require.NoError(t, err)
+				assert.Equal(t, tt.expectedError, resp["error"])
+			}
+		})
+	}
+}
+
+func TestProfileHandler_Get(t *testing.T) {
+	// Setup test database and handler
+	store := testhelpers.TestDB(t)
+	handler := NewProfileHandler(store)
+
+	// Create a test profile
+	testProfile := &common.Profile{
+		ID:          uuid.New(),
+		Name:        "Test Profile",
+		WorkspaceID: uuid.New(),
+	}
+	ctx, cancel := testhelpers.TestContext()
+	defer cancel()
+	err := store.CreateProfile(ctx, testProfile)
+	require.NoError(t, err)
+
+	// Setup gin test router with error handler
+	gin.SetMode(gin.TestMode)
+	router := gin.New()
+	router.Use(middleware.ErrorHandler())
+	router.GET("/api/v1/core/profiles/:id", handler.Get)
+
+	tests := []struct {
+		name          string
+		profileID     string
+		expectedCode  int
+		expectedError *errors.AppError
+	}{
+		{
+			name:         "existing_profile",
+			profileID:    testProfile.ID.String(),
+			expectedCode: http.StatusOK,
+		},
+		{
+			name:         "non-existent_profile",
+			profileID:    uuid.New().String(),
+			expectedCode: http.StatusNotFound,
+			expectedError: &errors.AppError{
+				Type:    errors.ErrorTypeNotFound,
+				Message: "Profile not found",
+			},
+		},
+		{
+			name:         "invalid_UUID",
+			profileID:    "invalid-uuid",
+			expectedCode: http.StatusBadRequest,
+			expectedError: &errors.AppError{
+				Type:    errors.ErrorTypeValidation,
+				Message: "Profile ID is required",
+				Field:   "id",
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			w := httptest.NewRecorder()
+			req := httptest.NewRequest(http.MethodGet, "/api/v1/core/profiles/"+tt.profileID, nil)
+			router.ServeHTTP(w, req)
+
+			assert.Equal(t, tt.expectedCode, w.Code)
+
+			if tt.expectedError != nil {
+				var resp errors.AppError
+				err := json.NewDecoder(w.Body).Decode(&resp)
+				require.NoError(t, err)
+				assert.Equal(t, tt.expectedError.Type, resp.Type)
+				assert.Equal(t, tt.expectedError.Message, resp.Message)
+				if tt.expectedError.Field != "" {
+					assert.Equal(t, tt.expectedError.Field, resp.Field)
+				}
+			} else {
+				var resp common.ProfileResponse
+				err := json.NewDecoder(w.Body).Decode(&resp)
+				require.NoError(t, err)
+				assert.Equal(t, testProfile.ID, resp.Profile.ID)
+				assert.Equal(t, testProfile.Name, resp.Profile.Name)
+				assert.Equal(t, testProfile.WorkspaceID, resp.Profile.WorkspaceID)
+			}
+		})
+	}
+}

+ 122 - 123
handlers/user.go

@@ -6,6 +6,7 @@ import (
 	"git.linuxforward.com/byom/byom-core/common"
 	"git.linuxforward.com/byom/byom-core/hook"
 	"git.linuxforward.com/byom/byom-core/jwtutils"
+	"git.linuxforward.com/byom/byom-core/logger"
 	"git.linuxforward.com/byom/byom-core/smtp"
 	"git.linuxforward.com/byom/byom-core/store"
 	"github.com/gin-gonic/gin"
@@ -17,27 +18,61 @@ import (
 
 type UserHandler struct {
 	store        *store.DataStore
-	emailService *smtp.Service
+	emailService smtp.EmailService
 	jwtService   *jwtutils.Service
 	logger       *logrus.Entry
 	hook         *hook.HookClient
 }
 
-func NewUserHandler(db *store.DataStore, emailSvc *smtp.Service, jwtSvc *jwtutils.Service, hook *hook.HookClient) *UserHandler {
-
-	logger := logrus.WithField("core", "user-handler")
-
+func NewUserHandler(db *store.DataStore, emailSvc smtp.EmailService, jwtSvc *jwtutils.Service, hook *hook.HookClient) *UserHandler {
 	return &UserHandler{
 		store:        db,
 		emailService: emailSvc,
 		jwtService:   jwtSvc,
-		logger:       logger,
+		logger:       logger.NewLogger("user-handler"),
 		hook:         hook,
 	}
 }
 
-func (h *UserHandler) InitWorkspaceOwner(c *gin.Context) {
-	var req common.InitWorkspaceOwnerRequest
+// Auth
+func (h *UserHandler) Login(c *gin.Context) {
+	var req common.AuthLoginRequest
+	var resp common.AuthLoginResponse
+
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": "Invalid request"})
+		return
+	}
+
+	user, err := h.store.GetUserByEmail(c, req.Email)
+	if err != nil {
+		c.JSON(400, gin.H{"error": "Invalid email or password"})
+		return
+	}
+
+	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
+	if err != nil {
+		c.JSON(400, gin.H{"error": "Invalid email or password"})
+		return
+	}
+
+	token, err := h.jwtService.GenerateToken(
+		user.Email,
+		user.Role,
+	)
+	if err != nil {
+		c.JSON(500, gin.H{"error": "Failed to generate token"})
+		return
+	}
+
+	resp.Token = token
+
+	c.JSON(200, resp)
+}
+
+// Workspace Owner Management
+func (h *UserHandler) InitOwner(c *gin.Context) {
+	var req common.WorkspaceOwnerInitRequest
 
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(400, gin.H{"error": "Invalid request"})
@@ -62,8 +97,8 @@ func (h *UserHandler) InitWorkspaceOwner(c *gin.Context) {
 	c.JSON(201, user)
 }
 
-func (h *UserHandler) CreateWorkspaceOwner(c *gin.Context) {
-	var req common.CreateWorkspaceOwnerRequest
+func (h *UserHandler) CreateOwner(c *gin.Context) {
+	var req common.WorkspaceOwnerCreateRequest
 
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(400, gin.H{"error": "Invalid request"})
@@ -104,8 +139,9 @@ func (h *UserHandler) CreateWorkspaceOwner(c *gin.Context) {
 	c.JSON(201, user)
 }
 
-func (h *UserHandler) CreateInvitedUser(c *gin.Context) {
-	var req common.CreateInvitedUserRequest
+// Invitation Management
+func (h *UserHandler) AcceptInvitation(c *gin.Context) {
+	var req common.InvitationAcceptRequest
 
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(400, gin.H{"error": "Invalid request"})
@@ -141,13 +177,7 @@ func (h *UserHandler) CreateInvitedUser(c *gin.Context) {
 	}
 
 	//add user to workspace
-	workspaceID, err := uuid.Parse(invite.Workspace)
-	if err != nil {
-		tx.Rollback()
-		c.JSON(400, gin.H{"error": "Invalid workspace ID"})
-		return
-	}
-	if err := h.store.AddUserToWorkspaceTx(c, tx, invite.Role, user.ID, workspaceID); err != nil {
+	if err := h.store.AddUserToWorkspaceTx(c, tx, invite.Role, user.ID, invite.WorkspaceID); err != nil {
 		tx.Rollback()
 		c.JSON(500, gin.H{"error": "Failed to add user to workspace"})
 		return
@@ -169,68 +199,14 @@ func (h *UserHandler) CreateInvitedUser(c *gin.Context) {
 
 	user.Password = ""
 
-	resp := common.CreateInvitedUserResponse{
-		User:     *user,
-		Worspace: invite.Workspace,
+	resp := common.InvitationAcceptResponse{
+		User:        *user,
+		WorkspaceID: invite.WorkspaceID,
 	}
 	c.JSON(201, resp)
 }
 
-// Invite handlers
-func (h *UserHandler) InviteUser(c *gin.Context) {
-	var req common.InviteUserRequest
-
-	if err := c.ShouldBindJSON(&req); err != nil {
-		c.JSON(400, gin.H{"error": "Invalid request"})
-		return
-	}
-
-	//check if user exists
-	_, err := h.store.GetUserByEmail(c, req.Email)
-	if err == nil {
-		c.JSON(400, gin.H{"error": "User already exists"})
-		return
-	}
-
-	//generate jwt token
-	token, err := h.jwtService.GenerateToken(
-		req.Email,
-		req.Role,
-	)
-	if err != nil {
-		c.JSON(500, gin.H{"error": "Failed to generate token"})
-		return
-	}
-
-	//store token in db
-	invite := &common.Invite{
-		ID:        uuid.New(),
-		Email:     req.Email,
-		Role:      req.Role,
-		Workspace: req.Workspace,
-		Token:     token,
-		Status:    "pending",
-		ExpiresAt: time.Now().Add(24 * time.Hour),
-		CreatedAt: time.Now(),
-		UpdatedAt: time.Now(),
-	}
-
-	//return invite
-	if err := h.store.CreateInvite(c, invite); err != nil {
-		c.JSON(500, gin.H{"error": "Failed to create invitation"})
-		return
-	}
-
-	//send email with token
-	if err := h.emailService.SendInviteEmail(req.Email, token, req.Workspace); err != nil {
-		c.JSON(500, gin.H{"error": "Failed to send email"})
-		return
-	}
-
-	c.JSON(201, invite)
-}
-
-func (h *UserHandler) ValidateInvitedUser(c *gin.Context) {
+func (h *UserHandler) ValidateInvitation(c *gin.Context) {
 	//request token from query
 	token := c.Query("token")
 
@@ -257,66 +233,75 @@ func (h *UserHandler) ValidateInvitedUser(c *gin.Context) {
 		}
 	}
 
-	response := common.ValidateInvitedUserRequest{
+	response := common.InvitationValidateResponse{
 		Valid:       true,
-		WorkspaceID: inv.Workspace,
+		WorkspaceID: inv.WorkspaceID,
 		Email:       inv.Email,
 	}
 
 	c.JSON(200, response)
 }
 
-func (h *UserHandler) Login(c *gin.Context) {
-
-	var req common.LoginRequest
-	var resp common.LoginResponse
+func (h *UserHandler) CreateInvitation(c *gin.Context) {
+	var req common.InvitationCreateRequest
 
 	if err := c.ShouldBindJSON(&req); err != nil {
 		c.JSON(400, gin.H{"error": "Invalid request"})
 		return
 	}
 
-	user, err := h.store.GetUserByEmail(c, req.Email)
-	if err != nil {
-		c.JSON(400, gin.H{"error": "Invalid email or password"})
-		return
-	}
-
-	err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
-	if err != nil {
-		c.JSON(400, gin.H{"error": "Invalid email or password"})
+	//check if user exists
+	_, err := h.store.GetUserByEmail(c, req.Email)
+	if err == nil {
+		c.JSON(400, gin.H{"error": "User already exists"})
 		return
 	}
 
+	//generate jwt token
 	token, err := h.jwtService.GenerateToken(
-		user.Email,
-		user.Role,
+		req.Email,
+		req.Role,
 	)
 	if err != nil {
 		c.JSON(500, gin.H{"error": "Failed to generate token"})
 		return
 	}
 
-	resp.Token = token
-
-	c.JSON(200, resp)
-}
-
-func (h *UserHandler) AddUserToWorkspace(c *gin.Context) {
-	var req common.AddUserToWorkspaceRequest
+	//store token in db
+	invite := &common.Invite{
+		ID:          uuid.New(),
+		Email:       req.Email,
+		Role:        req.Role,
+		WorkspaceID: req.WorkspaceID,
+		Token:       token,
+		Status:      "pending",
+		ExpiresAt:   time.Now().Add(24 * time.Hour),
+		CreatedAt:   time.Now(),
+		UpdatedAt:   time.Now(),
+	}
 
-	if err := c.ShouldBindJSON(&req); err != nil {
-		c.JSON(400, gin.H{"error": "Invalid request"})
+	//return invite
+	if err := h.store.CreateInvite(c, invite); err != nil {
+		c.JSON(500, gin.H{"error": "Failed to create invitation"})
+		return
+	}
+	//send email with token
+	if err := h.emailService.SendInviteEmail(req.Email, token, req.WorkspaceID.String()); err != nil {
+		c.JSON(500, gin.H{"error": "Failed to send email"})
 		return
 	}
 
-	//retrieve user from token
+	c.JSON(201, invite)
+}
+
+// User Management
+func (h *UserHandler) GetCurrent(c *gin.Context) {
+	// Get claims from context that were set by middleware
 	claimsValue, exists := c.Get("claims")
 	if !exists {
 		c.JSON(500, gin.H{"error": "Failed to get user claims"})
 		return
 	}
-
 	claims := claimsValue.(jwt.MapClaims)
 	if claims == nil {
 		c.JSON(500, gin.H{"error": "Failed to get user claims"})
@@ -335,26 +320,46 @@ func (h *UserHandler) AddUserToWorkspace(c *gin.Context) {
 		return
 	}
 
-	//add user to workspace
-	if err := h.store.AddUserToWorkspace(c, user.ID, req.WorkspaceID, req.Role); err != nil {
-		c.JSON(500, gin.H{"error": "Failed to add user to workspace"})
+	workspaces, err := h.store.GetWorkspacesByUserID(c, user.ID)
+	if err != nil {
+		c.JSON(500, gin.H{"error": "Failed to get user workspace role"})
 		return
 	}
 
-	c.JSON(200, gin.H{"message": "User added to workspace"})
+	resp := common.UserResponse{
+		User:       *user,
+		Workspaces: workspaces,
+	}
+
+	c.JSON(200, resp)
 }
 
-func (h *UserHandler) CancelInvitation(c *gin.Context) {
+func (h *UserHandler) UpdateCurrent(c *gin.Context) {
+	var req common.UserUpdateRequest
+
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": "Invalid request"})
+		return
+	}
 	// Implementation
 }
 
-func (h *UserHandler) GetCurrentUser(c *gin.Context) {
-	// Get claims from context that were set by middleware
+// Workspace Member Management
+func (h *UserHandler) AddMember(c *gin.Context) {
+	var req common.WorkspaceMemberAddRequest
+
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.JSON(400, gin.H{"error": "Invalid request"})
+		return
+	}
+
+	//retrieve user from token
 	claimsValue, exists := c.Get("claims")
 	if !exists {
 		c.JSON(500, gin.H{"error": "Failed to get user claims"})
 		return
 	}
+
 	claims := claimsValue.(jwt.MapClaims)
 	if claims == nil {
 		c.JSON(500, gin.H{"error": "Failed to get user claims"})
@@ -373,21 +378,15 @@ func (h *UserHandler) GetCurrentUser(c *gin.Context) {
 		return
 	}
 
-	workspaces, err := h.store.GetWorkspacesByUserID(c, user.ID)
-	if err != nil {
-		c.JSON(500, gin.H{"error": "Failed to get user workspace role"})
+	//add user to workspace
+	if err := h.store.AddUserToWorkspace(c, user.ID, req.WorkspaceID, req.Role); err != nil {
+		c.JSON(500, gin.H{"error": "Failed to add user to workspace"})
 		return
 	}
 
-	resp := common.UserMe{
-		User:       *user,
-		Workspaces: workspaces,
-	}
-
-	c.JSON(200, resp)
-
+	c.JSON(200, gin.H{"message": "User added to workspace"})
 }
 
-func (h *UserHandler) UpdateCurrentUser(c *gin.Context) {
+func (h *UserHandler) CancelInvitation(c *gin.Context) {
 	// Implementation
 }

+ 33 - 20
handlers/workspace.go

@@ -4,6 +4,7 @@ import (
 	"net/http"
 
 	"git.linuxforward.com/byom/byom-core/common"
+	"git.linuxforward.com/byom/byom-core/logger"
 	"git.linuxforward.com/byom/byom-core/store"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
@@ -16,27 +17,17 @@ type WorkspaceHandler struct {
 }
 
 func NewWorkspaceHandler(db *store.DataStore) *WorkspaceHandler {
-	logger := logrus.WithField("core", "workspace-handler")
-
 	return &WorkspaceHandler{
 		store:  db,
-		logger: logger,
+		logger: logger.NewLogger("workspace-handler"),
 	}
 }
 
-type CreateWorkspaceRequest struct {
-	Name string `json:"name" binding:"required"`
-}
-
-type WorkspaceResponse struct {
-	ID   string `json:"id"`
-	Name string `json:"name"`
-}
-
-func (h *WorkspaceHandler) CreateWorkspace(c *gin.Context) {
-	var req CreateWorkspaceRequest
+// Workspace Management
+func (h *WorkspaceHandler) Create(c *gin.Context) {
+	var req common.WorkspaceCreateRequest
 	if err := c.ShouldBindJSON(&req); err != nil {
-		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
 		return
 	}
 
@@ -46,14 +37,36 @@ func (h *WorkspaceHandler) CreateWorkspace(c *gin.Context) {
 	}
 
 	if err := h.store.CreateWorkspace(c, workspace); err != nil {
-		h.logger.WithError(err).Error("failed to create user")
-		c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
+		h.logger.WithError(err).Error("failed to create workspace")
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create workspace"})
 		return
 	}
 
-	c.JSON(http.StatusCreated, workspace)
+	c.JSON(http.StatusCreated, common.WorkspaceCreateResponse{
+		ID:   workspace.ID,
+		Name: workspace.Name,
+	})
 }
 
-func (h *WorkspaceHandler) GetWorkspace(c *gin.Context) {
-	// Implementation
+func (h *WorkspaceHandler) Get(c *gin.Context) {
+	id := c.Param("id")
+	if id == "" {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Workspace ID is required"})
+		return
+	}
+
+	workspaceID, err := uuid.Parse(id)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid workspace ID format"})
+		return
+	}
+
+	workspace, err := h.store.GetWorkspace(c, workspaceID.String())
+	if err != nil {
+		h.logger.WithError(err).Error("failed to get workspace")
+		c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get workspace"})
+		return
+	}
+
+	c.JSON(http.StatusOK, workspace)
 }

+ 2 - 2
jwtutils/service.go

@@ -3,6 +3,7 @@ package jwtutils
 import (
 	"time"
 
+	"git.linuxforward.com/byom/byom-core/logger"
 	"github.com/golang-jwt/jwt/v5"
 	"github.com/sirupsen/logrus"
 )
@@ -16,10 +17,9 @@ type Service struct {
 }
 
 func NewService(secret string) *Service {
-	logger := logrus.WithField("core", "jwt")
 	return &Service{
 		secret: secret,
-		logger: logger,
+		logger: logger.NewLogger("jwt"),
 	}
 }
 

+ 64 - 0
logger/logger.go

@@ -0,0 +1,64 @@
+package logger
+
+import (
+	"os"
+
+	"github.com/sirupsen/logrus"
+)
+
+// Fields type for structured logging
+type Fields logrus.Fields
+
+var (
+	// defaultLogger is the default logger instance
+	defaultLogger = logrus.New()
+)
+
+func init() {
+	// Set default configuration
+	defaultLogger.SetOutput(os.Stdout)
+	defaultLogger.SetLevel(logrus.InfoLevel)
+	defaultLogger.SetFormatter(&logrus.TextFormatter{
+		FullTimestamp: true,
+	})
+}
+
+// Configure sets up the global logger configuration
+func Configure(level string, noColor, forceColors, inJSON bool) error {
+	// Set log level
+	if level != "" {
+		lvl, err := logrus.ParseLevel(level)
+		if err != nil {
+			return err
+		}
+		defaultLogger.SetLevel(lvl)
+	}
+
+	// Configure formatter
+	if inJSON {
+		defaultLogger.SetFormatter(&logrus.JSONFormatter{})
+	} else {
+		defaultLogger.SetFormatter(&logrus.TextFormatter{
+			ForceColors:   forceColors,
+			DisableColors: noColor,
+			FullTimestamp: true,
+		})
+	}
+
+	return nil
+}
+
+// NewLogger creates a new logger entry with component field
+func NewLogger(component string) *logrus.Entry {
+	return defaultLogger.WithField("component", component)
+}
+
+// WithRequestID adds request ID to the logger entry
+func WithRequestID(logger *logrus.Entry, requestID string) *logrus.Entry {
+	return logger.WithField("request_id", requestID)
+}
+
+// GetLogger returns the default logger instance
+func GetLogger() *logrus.Logger {
+	return defaultLogger
+}

+ 0 - 1
main.go

@@ -49,5 +49,4 @@ func main() {
 	ctx := kong.Parse(&cli)
 	err := ctx.Run()
 	ctx.FatalIfErrorf(err)
-
 }

+ 50 - 0
middleware/cors.go

@@ -0,0 +1,50 @@
+package middleware
+
+import (
+	"time"
+
+	"github.com/gin-contrib/cors"
+	"github.com/gin-gonic/gin"
+)
+
+// CORSConfig represents the configuration for CORS
+type CORSConfig struct {
+	AllowOrigins     []string
+	AllowCredentials bool
+	MaxAge           time.Duration
+	TrustedProxies   []string
+}
+
+// DefaultCORSConfig returns the default CORS configuration
+func DefaultCORSConfig() *CORSConfig {
+	return &CORSConfig{
+		AllowOrigins:     []string{"*"},
+		AllowCredentials: true,
+		MaxAge:           12 * time.Hour,
+		TrustedProxies:   []string{"localhost", "127.0.0.1"},
+	}
+}
+
+// CORS returns a middleware for handling CORS with secure defaults
+func CORS(cfg *CORSConfig) gin.HandlerFunc {
+	if cfg == nil {
+		cfg = DefaultCORSConfig()
+	}
+
+	config := cors.Config{
+		AllowOrigins:     cfg.AllowOrigins,
+		AllowMethods:     []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
+		AllowHeaders:     []string{"Origin", "Content-Type", "Accept", "Authorization", "X-Request-ID"},
+		ExposeHeaders:    []string{"Content-Length", "Content-Type", "X-Request-ID"},
+		AllowCredentials: cfg.AllowCredentials,
+		MaxAge:           cfg.MaxAge,
+	}
+
+	// Add security-focused CORS settings
+	config.AllowWildcard = true
+	config.AllowBrowserExtensions = false
+	config.AllowWebSockets = false
+	config.AllowFiles = false
+
+	return cors.New(config)
+}

+ 37 - 0
middleware/error_handler.go

@@ -0,0 +1,37 @@
+package middleware
+
+import (
+	"net/http"
+
+	"git.linuxforward.com/byom/byom-core/errors"
+	"github.com/gin-gonic/gin"
+)
+
+// ErrorHandler handles errors and returns appropriate responses
+func ErrorHandler() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		c.Next()
+
+		if len(c.Errors) > 0 {
+			err := c.Errors.Last().Err
+			if appErr, ok := err.(*errors.AppError); ok {
+				switch appErr.Type {
+				case errors.ErrorTypeValidation:
+					c.AbortWithStatusJSON(http.StatusBadRequest, appErr)
+				case errors.ErrorTypeNotFound:
+					c.AbortWithStatusJSON(http.StatusNotFound, appErr)
+				case errors.ErrorTypeAuthorization:
+					c.AbortWithStatusJSON(http.StatusUnauthorized, appErr)
+				case errors.ErrorTypeConflict:
+					c.AbortWithStatusJSON(http.StatusConflict, appErr)
+				default:
+					c.AbortWithStatusJSON(http.StatusInternalServerError, appErr)
+				}
+				return
+			}
+			c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+				"error": err.Error(),
+			})
+		}
+	}
+}

+ 50 - 0
middleware/recovery.go

@@ -0,0 +1,50 @@
+package middleware
+
+import (
+	"fmt"
+	"net/http"
+	"runtime/debug"
+
+	"git.linuxforward.com/byom/byom-core/logger"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+// Recovery returns a middleware that recovers from panics and logs the stack trace
+func Recovery(log *logrus.Logger) gin.HandlerFunc {
+	recoveryLogger := logger.NewLogger("recovery")
+	return func(c *gin.Context) {
+		defer func() {
+			if err := recover(); err != nil {
+				// Get request ID if available
+				requestID := c.GetString(RequestIDKey)
+				if requestID == "" {
+					requestID = "no-request-id"
+				}
+
+				// Get stack trace
+				stack := string(debug.Stack())
+
+				// Add request ID to logger
+				reqLogger := logger.WithRequestID(recoveryLogger, requestID)
+
+				// Log the panic with context
+				reqLogger.WithFields(logrus.Fields{
+					"error":     fmt.Sprintf("%v", err),
+					"stack":     stack,
+					"method":    c.Request.Method,
+					"path":      c.Request.URL.Path,
+					"client_ip": c.ClientIP(),
+				}).Error("Panic recovered")
+
+				// Return a 500 error to the client
+				c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
+					"error": "Internal server error",
+					"code":  "INTERNAL_SERVER_ERROR",
+				})
+			}
+		}()
+
+		c.Next()
+	}
+}

+ 31 - 0
middleware/request_id.go

@@ -0,0 +1,31 @@
+package middleware
+
+import (
+	"context"
+
+	"github.com/gin-gonic/gin"
+	"github.com/google/uuid"
+)
+
+const RequestIDKey = "X-Request-ID"
+
+// RequestID middleware adds a unique request ID to each request
+func RequestID() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Check if request already has an ID
+		requestID := c.GetHeader(RequestIDKey)
+		if requestID == "" {
+			requestID = uuid.New().String()
+		}
+
+		// Set request ID in header and context
+		c.Header(RequestIDKey, requestID)
+		c.Set(RequestIDKey, requestID)
+
+		// Add request ID to context for logging
+		ctx := context.WithValue(c.Request.Context(), RequestIDKey, requestID)
+		c.Request = c.Request.WithContext(ctx)
+
+		c.Next()
+	}
+}

+ 55 - 0
middleware/request_logger.go

@@ -0,0 +1,55 @@
+package middleware
+
+import (
+	"fmt"
+	"time"
+
+	"git.linuxforward.com/byom/byom-core/logger"
+	"github.com/gin-gonic/gin"
+	"github.com/sirupsen/logrus"
+)
+
+// RequestLogger middleware logs details about each request
+func RequestLogger(log *logrus.Logger) gin.HandlerFunc {
+	requestLogger := logger.NewLogger("request")
+	return func(c *gin.Context) {
+		// Start timer
+		start := time.Now()
+
+		// Get request ID
+		requestID := c.GetString(RequestIDKey)
+		if requestID == "" {
+			requestID = "no-request-id"
+		}
+
+		// Add request ID to logger
+		reqLogger := logger.WithRequestID(requestLogger, requestID)
+
+		// Process request
+		c.Next()
+
+		// Calculate latency
+		latency := time.Since(start)
+
+		// Get path and status
+		path := c.Request.URL.Path
+		if raw := c.Request.URL.RawQuery; raw != "" {
+			path = fmt.Sprintf("%s?%s", path, raw)
+		}
+
+		// Log request details
+		reqLogger.WithFields(logrus.Fields{
+			"method":     c.Request.Method,
+			"path":       path,
+			"status":     c.Writer.Status(),
+			"latency_ms": float64(latency.Nanoseconds()) / 1e6,
+			"client_ip":  c.ClientIP(),
+			"user_agent": c.Request.UserAgent(),
+		}).Info("Request processed")
+
+		// If there was an error, log it with the error field
+		if len(c.Errors) > 0 {
+			reqLogger.WithField("errors", c.Errors.String()).Error("Request errors")
+		}
+	}
+}

+ 98 - 0
middleware/security.go

@@ -0,0 +1,98 @@
+package middleware
+
+import (
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+// SecurityHeaders adds security-related headers to all responses
+func SecurityHeaders() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Security headers
+		c.Header("X-Content-Type-Options", "nosniff")
+		c.Header("X-Frame-Options", "DENY")
+		c.Header("X-XSS-Protection", "1; mode=block")
+		c.Header("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
+		c.Header("Content-Security-Policy", "default-src 'self'; frame-ancestors 'none'")
+		c.Header("Referrer-Policy", "strict-origin-when-cross-origin")
+		c.Header("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
+
+		c.Next()
+	}
+}
+
+// RequestSanitizer sanitizes incoming requests
+func RequestSanitizer() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Sanitize headers
+		sanitizeHeaders(c)
+
+		// Block potentially dangerous file extensions
+		if containsDangerousExtension(c.Request.URL.Path) {
+			c.AbortWithStatus(http.StatusForbidden)
+			return
+		}
+
+		c.Next()
+	}
+}
+
+// TimeoutMiddleware adds a timeout to the request context
+func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Wrap the request in a timeout
+		ch := make(chan struct{})
+		go func() {
+			c.Next()
+			ch <- struct{}{}
+		}()
+
+		select {
+		case <-ch:
+			return
+		case <-time.After(timeout):
+			c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{
+				"code":  "REQUEST_TIMEOUT",
+				"error": "Request timeout exceeded",
+			})
+			return
+		}
+	}
+}
+
+// Helper functions
+func sanitizeHeaders(c *gin.Context) {
+	// Remove potentially dangerous headers
+	c.Request.Header.Del("X-Forwarded-For")
+	c.Request.Header.Del("X-Real-IP")
+	c.Request.Header.Del("X-Forwarded-Proto")
+
+	// Sanitize User-Agent
+	if ua := c.Request.Header.Get("User-Agent"); ua != "" {
+		c.Request.Header.Set("User-Agent", sanitizeString(ua))
+	}
+}
+
+func containsDangerousExtension(path string) bool {
+	dangerous := []string{".php", ".asp", ".aspx", ".jsp", ".cgi", ".exe", ".bat", ".cmd", ".sh", ".pl"}
+	path = strings.ToLower(path)
+	for _, ext := range dangerous {
+		if strings.HasSuffix(path, ext) {
+			return true
+		}
+	}
+	return false
+}
+
+func sanitizeString(s string) string {
+	// Remove any control characters
+	return strings.Map(func(r rune) rune {
+		if r < 32 || r == 127 {
+			return -1
+		}
+		return r
+	}, s)
+}

+ 32 - 0
middleware/validation.go

@@ -0,0 +1,32 @@
+package middleware
+
+import (
+	"net/http"
+
+	"git.linuxforward.com/byom/byom-core/validation"
+	"github.com/gin-gonic/gin"
+)
+
+// ValidateRequest validates the request body against the given struct and returns validation errors
+func ValidateRequest[T any](c *gin.Context) (*T, bool) {
+	var req T
+	if err := c.ShouldBindJSON(&req); err != nil {
+		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
+			"code":   "INVALID_REQUEST",
+			"error":  "Invalid request format",
+			"errors": []validation.ValidationError{{Field: "body", Message: err.Error()}},
+		})
+		return nil, false
+	}
+
+	if errors := validation.Validate(req); len(errors) > 0 {
+		c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
+			"code":   "VALIDATION_ERROR",
+			"error":  "Validation failed",
+			"errors": errors,
+		})
+		return nil, false
+	}
+
+	return &req, true
+}

+ 25 - 5
smtp/service.go

@@ -11,6 +11,12 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
+// EmailService defines the interface for email operations
+type EmailService interface {
+	SendInviteEmail(to, token, workspace string) error
+	Close() error
+}
+
 type Service struct {
 	client   *smtp.Client
 	template *template.Template
@@ -27,25 +33,39 @@ type EmailData struct {
 }
 
 func NewService(cnf *config.Smtp) *Service {
-
 	logger := logrus.WithField("core", "email")
 
-	//mock the smtp client
-	client := &smtp.Client{}
+	// If SMTP is not configured, return service with nil client
+	if cnf == nil || cnf.Host == "" {
+		logger.Info("SMTP not configured, email service will be disabled")
+		return &Service{
+			logger: logger,
+		}
+	}
 
-	//parse the email template
+	// Parse the email template
 	tmpl, err := template.ParseFS(inviteTpl, "templates/invite.html")
 	if err != nil {
 		logger.WithError(err).Error("failed to parse email template")
 		panic(err)
 	}
 
+	// Create SMTP client
+	addr := fmt.Sprintf("%s:%d", cnf.Host, cnf.Port)
+	client, err := smtp.Dial(addr)
+	if err != nil {
+		logger.WithError(err).Error("failed to connect to SMTP server")
+		return &Service{
+			logger:   logger,
+			template: tmpl,
+		}
+	}
+
 	return &Service{
 		logger:   logger,
 		client:   client,
 		template: tmpl,
 	}
-
 }
 
 func (s *Service) SendEmail(to, subject, body string) error {

+ 2 - 4
store/interfaces.go

@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"git.linuxforward.com/byom/byom-core/common"
+	"git.linuxforward.com/byom/byom-core/logger"
 	"github.com/sirupsen/logrus"
 	"gorm.io/gorm"
 )
@@ -14,12 +15,9 @@ type DataStore struct {
 }
 
 func NewDataStore(db *gorm.DB) *DataStore {
-	logger := logrus.WithField("core", "user-store")
-
-	InitTables(db)
 	return &DataStore{
 		db:     db,
-		logger: logger,
+		logger: logger.NewLogger("data-store"),
 	}
 }
 

+ 6 - 1
store/profile.go

@@ -5,7 +5,9 @@ import (
 	"fmt"
 
 	"git.linuxforward.com/byom/byom-core/common"
+	"git.linuxforward.com/byom/byom-core/errors"
 	"github.com/google/uuid"
+	"gorm.io/gorm"
 )
 
 //CRUD methods for the profile
@@ -23,8 +25,11 @@ func (s *DataStore) CreateProfile(ctx context.Context, p *common.Profile) error
 
 func (s *DataStore) GetProfile(ctx context.Context, id string) (*common.Profile, error) {
 	var profile common.Profile
-	result := s.db.First(&profile, id)
+	result := s.db.Where("id = ?", id).First(&profile)
 	if result.Error != nil {
+		if result.Error == gorm.ErrRecordNotFound {
+			return nil, errors.NewNotFoundError("Profile", id)
+		}
 		return nil, fmt.Errorf("get profile: %w", result.Error)
 	}
 	return &profile, nil

+ 115 - 0
testing/helpers.go

@@ -0,0 +1,115 @@
+package testing
+
+import (
+	"context"
+	"os"
+	"testing"
+	"time"
+
+	"git.linuxforward.com/byom/byom-core/config"
+	"git.linuxforward.com/byom/byom-core/store"
+	"github.com/sirupsen/logrus"
+	"github.com/stretchr/testify/require"
+	"gorm.io/driver/sqlite"
+	"gorm.io/gorm"
+)
+
+// TestDB creates a temporary test database
+func TestDB(t *testing.T) *store.DataStore {
+	t.Helper()
+
+	// Create temp directory for test database
+	tmpDir, err := os.MkdirTemp("", "byom-test-*")
+	require.NoError(t, err)
+
+	// Clean up after test
+	t.Cleanup(func() {
+		os.RemoveAll(tmpDir)
+	})
+
+	// Create test database
+	//dbPath := filepath.Join(tmpDir, "test.db")
+	dbPath := "../.data/app.db"
+	db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
+	require.NoError(t, err)
+
+	return store.NewDataStore(db)
+}
+
+// TestConfig creates a test configuration
+func TestConfig() *config.Config {
+	return &config.Config{
+		Server: &config.Server{
+			ListeningPort: 8080,
+			TlsConfig: &config.TlsConfig{
+				Enabled: false,
+			},
+			CorsOrigins: []string{"http://localhost:3000"},
+		},
+		Database: &config.Database{
+			Path:            "./data/app.db",
+			MaxOpenConns:    1,
+			MaxIdleConns:    1,
+			ConnMaxLifetime: 300,
+		},
+		Jwt: &config.Jwt{
+			JwtSecret: "test-secret",
+		},
+		Smtp: &config.Smtp{
+			Host: "localhost",
+			Port: 1025,
+			User: "test",
+			Pass: "test",
+		},
+		Hook: &config.Hook{
+			BaseURL:   "http://localhost:8081",
+			Domain:    "test",
+			SecretKey: "test-secret",
+		},
+	}
+}
+
+// TestLogger creates a test logger
+func TestLogger() *logrus.Logger {
+	logger := logrus.New()
+	logger.SetOutput(os.Stdout)
+	logger.SetLevel(logrus.DebugLevel)
+	return logger
+}
+
+// TestContext creates a context for testing
+func TestContext() (context.Context, context.CancelFunc) {
+	return context.WithTimeout(context.Background(), 5*time.Second)
+}
+
+// MockEmailService is a mock implementation of the email service
+type MockEmailService struct {
+	SendInviteEmailFunc func(email, token, workspaceID string) error
+}
+
+func (m *MockEmailService) SendInviteEmail(email, token, workspaceID string) error {
+	if m.SendInviteEmailFunc != nil {
+		return m.SendInviteEmailFunc(email, token, workspaceID)
+	}
+	return nil
+}
+
+func (m *MockEmailService) Close() error {
+	return nil
+}
+
+// MockHookClient is a mock implementation of the hook client
+type MockHookClient struct {
+	SendHookFunc func(event string, payload interface{}) error
+}
+
+func (m *MockHookClient) SendHook(event string, payload interface{}) error {
+	if m.SendHookFunc != nil {
+		return m.SendHookFunc(event, payload)
+	}
+	return nil
+}
+
+func (m *MockHookClient) Close() error {
+	return nil
+}

+ 91 - 0
validation/validator.go

@@ -0,0 +1,91 @@
+package validation
+
+import (
+	"fmt"
+	"reflect"
+	"regexp"
+	"strings"
+
+	"github.com/go-playground/validator/v10"
+)
+
+var (
+	validate *validator.Validate
+	// Common validation patterns
+	passwordRegex = regexp.MustCompile(`^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};:'",.<>/?]{8,}$`)
+	phoneRegex    = regexp.MustCompile(`^\+?[1-9]\d{1,14}$`)
+)
+
+func init() {
+	validate = validator.New()
+
+	// Register custom validation tags
+	validate.RegisterValidation("password", validatePassword)
+	validate.RegisterValidation("phone", validatePhone)
+
+	// Register custom error messages
+	validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
+		name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
+		if name == "-" {
+			return fld.Name
+		}
+		return name
+	})
+}
+
+// Validate performs validation on the given struct and returns formatted error messages
+func Validate(i interface{}) []ValidationError {
+	err := validate.Struct(i)
+	if err == nil {
+		return nil
+	}
+
+	var validationErrors []ValidationError
+	for _, err := range err.(validator.ValidationErrors) {
+		validationErrors = append(validationErrors, ValidationError{
+			Field:   err.Field(),
+			Message: formatErrorMessage(err),
+		})
+	}
+	return validationErrors
+}
+
+// ValidationError represents a single validation error
+type ValidationError struct {
+	Field   string `json:"field"`
+	Message string `json:"message"`
+}
+
+// Custom validators
+func validatePassword(fl validator.FieldLevel) bool {
+	return passwordRegex.MatchString(fl.Field().String())
+}
+
+func validatePhone(fl validator.FieldLevel) bool {
+	if fl.Field().String() == "" {
+		return true // Phone is optional
+	}
+	return phoneRegex.MatchString(fl.Field().String())
+}
+
+// formatErrorMessage returns a user-friendly error message for validation errors
+func formatErrorMessage(err validator.FieldError) string {
+	switch err.Tag() {
+	case "required":
+		return fmt.Sprintf("%s is required", err.Field())
+	case "email":
+		return fmt.Sprintf("%s must be a valid email address", err.Field())
+	case "min":
+		return fmt.Sprintf("%s must be at least %s characters long", err.Field(), err.Param())
+	case "max":
+		return fmt.Sprintf("%s must not exceed %s characters", err.Field(), err.Param())
+	case "password":
+		return fmt.Sprintf("%s must be at least 8 characters long and contain only letters, numbers, and special characters", err.Field())
+	case "phone":
+		return fmt.Sprintf("%s must be a valid phone number in E.164 format", err.Field())
+	case "uuid":
+		return fmt.Sprintf("%s must be a valid UUID", err.Field())
+	default:
+		return fmt.Sprintf("%s failed validation: %s", err.Field(), err.Tag())
+	}
+}