Framework Architecture
Understanding EZ-Console's architecture is essential for building robust applications. This guide explains the framework's design, components, and how they interact.
High-Level Architectureβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Frontend Layer β
β React + TypeScript + Ant Design + React Router β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β HTTP/HTTPS (JSON)
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β API Gateway Layer β
β Gin Router + Middleware (Auth, CORS, Log) β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β Controller Layer β
β HTTP Request Handling + Input Validation β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β Service Layer β
β Business Logic + Transaction Management β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β Data Access Layer β
β GORM ORM + Database Operations β
ββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
ββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββ
β Database β
β SQLite / MySQL / PostgreSQL β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Backend Architectureβ
Layered Architectureβ
EZ-Console backend follows a clean, layered architecture:
1. Controller Layerβ
Responsibility: HTTP request handling
type UserController struct {
svc server.Service
}
func (c *UserController) GetUser(ctx *gin.Context) {
userID := ctx.Param("id")
// Input validation
if userID == "" {
util.RespondWithError(ctx, util.NewErrorMessage("E4001", "User ID is required"))
return
}
// Call service layer
user, err := c.svc.User().GetUserByID(ctx.Request.Context(), userID)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E5001", "Failed to get user", err))
return
}
// Return response
util.RespondWithSuccess(ctx, http.StatusOK, user)
}
Key Points:
- Handles HTTP-specific concerns
- Validates input parameters
- Calls service layer for business logic
- Formats and returns responses
- Does NOT contain business logic
2. Service Layerβ
Responsibility: Business logic and orchestration
type UserService struct {
db *gorm.DB
}
func (s *UserService) GetUserByID(ctx context.Context, resourceID string) (*model.User, error) {
logger := log.GetContextLogger(ctx)
var user model.User
err := s.db.Where("resource_id = ?", resourceID).
Preload("Roles").
First(&user).Error
if err != nil {
logger.Log("msg", "failed to get user", "err", err, "resource_id", resourceID)
return nil, err
}
return &user, nil
}
Key Points:
- Contains pure business logic
- Performs database operations
- Manages transactions
- Can call other services
- Returns domain models
3. Model Layerβ
Responsibility: Data structure definition
type User struct {
Base
Username string `gorm:"uniqueIndex;size:64" json:"username"`
Email string `gorm:"uniqueIndex;size:128" json:"email"`
FullName string `gorm:"size:128" json:"full_name"`
PasswordHash string `gorm:"size:128" json:"-"`
Status UserStatus `gorm:"default:active" json:"status"`
Roles []Role `gorm:"many2many:user_roles;" json:"roles"`
LastLoginAt *time.Time `json:"last_login_at"`
}
Key Points:
- Defines database schema
- Uses GORM tags for ORM mapping
- Includes validation rules
- Defines relationships
4. Middleware Layerβ
Responsibility: Cross-cutting concerns
func RequireAuth() gin.HandlerFunc {
return func(ctx *gin.Context) {
token := extractToken(ctx)
claims, err := jwt.ValidateToken(token)
if err != nil {
util.RespondWithError(ctx, util.NewErrorMessage("E4012", "Invalid auth token"))
ctx.Abort()
return
}
// Store user info in context
ctx.Set("user_id", claims.UserID)
ctx.Next()
}
}
Built-in Middleware:
- Authentication: JWT token validation
- Permission: RBAC permission checking
- Logging: Structured request/response logging
- CORS: Cross-origin resource sharing
- Recovery: Panic recovery
- Metrics: Request metrics collection
Request Flowβ
Here's how a typical request flows through the backend:
1. Client Request
β
2. Gin Router (route matching)
β
3. Middleware Chain
- Logging (start)
- CORS headers
- Authentication (JWT validation)
- Permission check
β
4. Controller
- Parse request parameters
- Validate input
- Call service
β
5. Service
- Execute business logic
- Database operations
- Call other services if needed
β
6. Model/Repository
- GORM query execution
- Data persistence
β
7. Controller (continued)
- Format response
- Return HTTP response
β
8. Middleware Chain (reversed)
- Logging (end)
- Metrics collection
β
9. Client Response
Frontend Architectureβ
Component Hierarchyβ
βββββββββββββββββββββββββββββββββββββββββββ
β Application Shell β
β (Layout, Navigation, Authentication) β
ββββββββββββββ¬βββββββββββββββββββββββββββββ
β
ββββββββββ΄βββββββββ
β β
βββββΌβββββ ββββββββΌβββββββ
β Public β β Private β
β Routes β β Routes β
β β β (Protected) β
ββββββββββ ββββββββ¬βββββββ
β
βββββββββββββΌββββββββββββ
β β β
ββββββΌββββ βββββΌβββββ βββββΌβββββ
β Page β β Page β β Page β
βComponentsβ βComponentsβ βComponentsβ
ββββββ¬ββββ βββββ¬βββββ βββββ¬βββββ
β β β
ββββββΌβββββββββββΌβββββββββββΌβββββ
β Shared Components β
β (Tables, Forms, Modals) β
ββββββ¬βββββββββββββββββββββββββββ
β
ββββββΌβββββββββββββββββββββββββ
β API Service Layer β
β (HTTP Client + Interceptors)β
ββββββ¬βββββββββββββββββββββββββ
β
ββββββΌβββββββββββββββββββββββββ
β State Management β
β (Context API + React Query) β
βββββββββββββββββββββββββββββββ
Frontend Layersβ
1. Application Shellβ
The EZApp component provides:
- Layout (header, sidebar, content)
- Navigation menu
- Authentication flow
- Route protection
- Global state providers
<EZApp
basePath='/'
extraPrivateRoutes={[
{
path: '/products',
element: withSuspense(ProductPage),
name: 'products',
}
]}
/>
2. Page Componentsβ
Pages are lazy-loaded route components:
const ProductPage = lazy(() => import('@/pages/ProductPage'));
3. Shared Componentsβ
Reusable UI components:
DataTable- Table with pagination, sorting, filteringFormModal- Modal with form handlingFileUpload- File upload with progressPermissionGuard- Component-level permission check
4. API Service Layerβ
Axios-based HTTP client with interceptors:
// Request interceptor - add auth token
axios.interceptors.request.use((config) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Response interceptor - handle errors
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Token expired, redirect to login
window.location.href = '/login';
}
return Promise.reject(error);
}
);
5. State Managementβ
Two types of state:
Server State (React Query):
const { data, loading } = useRequest(() => apiGet('/users'));
Client State (React Context):
const { user, setUser } = useAuth();
const { settings } = useSettings();
Core Conceptsβ
1. Controller-Service Patternβ
Controllers handle HTTP concerns:
- Parse request parameters
- Validate input
- Format responses
- Handle HTTP errors
Services handle business logic:
- Data processing
- Business rules
- Database operations
- Integration with external systems
Benefits:
- Clear separation of concerns
- Easier testing (mock services)
- Reusable business logic
- Better maintainability
2. Resource-Based IDsβ
All resources use UUID-based resource_id:
type Base struct {
ID uint `gorm:"primarykey" json:"-"`
ResourceID string `gorm:"uniqueIndex;size:36" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
Why?
- Prevents enumeration attacks
- No information leakage about system size
- Can be used across distributed systems
- More secure than sequential IDs
Usage:
- Internal: Use
IDfor database joins - External: Use
ResourceIDfor APIs (exposed asid)
3. Soft Deletesβ
All models support soft deletes via DeletedAt:
// Soft delete - sets DeletedAt
db.Delete(&user)
// Queries automatically exclude soft-deleted
db.Find(&users) // Won't include deleted users
// Include soft-deleted in query
db.Unscoped().Find(&users)
// Permanent delete
db.Unscoped().Delete(&user)
Benefits:
- Data recovery possible
- Audit trail maintained
- Compliance with data retention policies
4. Audit Loggingβ
Automatic tracking of important operations:
err := svc.StartAudit(ctx, resourceID, func(auditLog *model.AuditLog) error {
// Perform operation
user, err := createUser(ctx, req)
// Configure audit log
auditLog.ResourceType = "user"
auditLog.ResourceID = user.ResourceID
auditLog.Action = "create"
auditLog.Details = map[string]interface{}{
"username": user.Username,
}
return err
})
Automatically Captured:
- User ID and username
- IP address and geolocation
- User agent
- Timestamp
- Action and resource
5. RBAC with Policiesβ
Flexible authorization system:
User ββ¬β Role 1 ββ¬β Permission Group 1 ββ¬β Permission 1
β β ββ Permission 2
β ββ Permission Group 2 βββ Permission 3
ββ Role 2 βββ Permission Group 3 βββ Permission 4
Policy Documents (JSON):
{
"effect": "allow",
"actions": ["users:read", "users:write"],
"resources": ["users/*"],
"conditions": {
"ip_range": ["10.0.0.0/8"]
}
}
Permission Check:
// In controller
router.GET("/users",
middleware.RequirePermission("users:read"),
controller.ListUsers,
)
Built-in Modulesβ
EZ-Console provides several built-in modules:
1. User Management Moduleβ
- User CRUD operations
- Password management
- MFA configuration
- Status management
- Last login tracking
2. Authorization Moduleβ
- Role management
- Permission definitions
- Permission groups
- Service accounts
- Session management
- OAuth2/OIDC integration
- LDAP/AD integration
3. System Settings Moduleβ
- Password policies
- MFA settings
- Session timeout
- Email/SMTP configuration
- OAuth provider configuration
4. File Management Moduleβ
- File upload/download
- Storage abstraction
- Access control
- File metadata
5. Audit Log Moduleβ
- Action logging
- IP tracking and geolocation
- User agent tracking
- Searchable logs
6. Statistics Moduleβ
- User activity metrics
- Login statistics
- API usage tracking
- System health
Extensibilityβ
Controllers and routesβ
Register HTTP controllers from your application module (typically in init()):
func init() {
server.RegisterControllers(func(ctx context.Context, svc server.Service) server.Controller {
return NewProductController(svc)
})
}
Gin middlewareβ
The stock server applies Gin middleware inside the framework. To add your own, pass WithEngineOptions when building the command (same package as NewCommandServer):
import (
consoleserver "github.com/sven-victor/ez-console/server"
"github.com/gin-gonic/gin"
)
root := consoleserver.NewCommandServer(
"my-app", version, "My app",
consoleserver.WithEngineOptions(func(engine *gin.Engine) {
engine.Use(MyMiddleware())
}),
)
User and role change hooksβ
The underlying authorization layer supports RegisterUserChangeHook and RegisterUserRoleChangeHook on server.Service. The default Run path constructs the service inside the framework, so registering these hooks from a plain main + NewCommandServer app may require following the patterns in the source or a custom bootstrap. See Hooks & Events.
Next Stepsβ
Now that you understand the architecture:
- Install EZ-Console on your machine
- Follow the Quick Start to build your first app
- Learn about Core Concepts in detail
- Explore Backend Development
Questions about the architecture? Ask on GitHub Discussions.