diff --git a/.cbmigrations.json b/.cbmigrations.json deleted file mode 100755 index 69da4f9..0000000 --- a/.cbmigrations.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "default": { - "manager": "cfmigrations.models.QBMigrationManager", - "migrationsDirectory": "resources/database/migrations/", - "seedsDirectory": "resources/database/seeds/", - "properties": { - "defaultGrammar": "AutoDiscover@qb", - "schema": "${DB_SCHEMA}", - "migrationsTable": "cfmigrations", - "connectionInfo": { - "type": "${DB_DRIVER}", - "database": "${DB_DATABASE}", - "host": "${DB_HOST}", - "port": "${DB_PORT}", - "username": "${DB_USER}", - "password": "${DB_PASSWORD}" - } - } - } -} diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c7109f5 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,515 @@ +# BoxLang ColdBox Template - AI Coding Instructions + +This is a BoxLang application template using the ColdBox HMVC framework. These instructions are designed to help AI assistants understand the project structure and provide better coding assistance. + +## 🎯 Project Overview + +**Language**: BoxLang (modern JVM language, successor to CFML) +**Framework**: ColdBox HMVC Framework v8+ +**Template Type**: Full-stack web application template +**Architecture**: Model-View-Controller with Hierarchical MVC support + +## βš™οΈ Requirements + +This project requires the following to be installed on your operating system: + +### Required Software + +1. **CommandBox** - CLI toolchain, package manager, and server runtime + - Installation: https://commandbox.ortusbooks.com/getting-started/installation + - Minimum Version: 6.0+ + - Used for: dependency management, server starting, testing, and task automation + +2. **BoxLang** - Modern JVM language runtime + - Installation: https://boxlang.ortusbooks.com/getting-started/installation + - Minimum Version: 1.0+ + - Can be installed via CommandBox: `box install commandbox-boxlang` + - Used for: running BoxLang applications and scripts + +### Verification + +Verify installations: +```bash +# Check CommandBox is installed +box version + +# Check BoxLang is available +box boxlang version + +# Verify project setup (run from project root) +boxlang Setup.bx +``` + +### Optional but Recommended + +- **Java JDK** - Required for BoxLang runtime (JDK 11+ recommended) +- **Docker** - For containerized development (if using Docker setup) +- **Git** - For version control + +## πŸ“ Project Structure + +``` +/app/ # Application source code +β”œβ”€β”€ Application.bx # Application entry point +β”œβ”€β”€ config/ # Framework configuration +β”‚ β”œβ”€β”€ CacheBox.bx # Caching configuration +β”‚ β”œβ”€β”€ ColdBox.bx # Main ColdBox settings +β”‚ β”œβ”€β”€ Router.bx # Route definitions +β”‚ β”œβ”€β”€ Scheduler.bx # Scheduled tasks +β”‚ └── WireBox.bx # Dependency injection configuration +β”œβ”€β”€ handlers/ # Controllers (request handlers) +β”œβ”€β”€ helpers/ # View helper functions +β”œβ”€β”€ interceptors/ # Event interceptors/listeners +β”œβ”€β”€ layouts/ # Page layouts/templates +β”œβ”€β”€ logs/ # Application logs +β”œβ”€β”€ models/ # Business logic and data models +β”œβ”€β”€ modules/ # Application-specific modules (HMVC) +└── views/ # View templates + +/public/ # Web-accessible files (document root) +β”œβ”€β”€ Application.bx # Public application bootstrap +β”œβ”€β”€ index.bxm # Application entry point (markup) +β”œβ”€β”€ favicon.ico # Site favicon +β”œβ”€β”€ robots.txt # Search engine directives +└── includes/ # Static assets + β”œβ”€β”€ i18n/ # Client-side translations + └── images/ # Image assets (CSS, JS in subfolders) + +/resources/ # Non-web resources +β”œβ”€β”€ database/ # Database management +β”‚ └── migrations/ # Database migration files +└── copilot-instructions.md # This file (AI coding instructions) + +/tests/ # Test suites +β”œβ”€β”€ Application.bx # Test application setup +β”œβ”€β”€ index.bxm # Test runner entry point +β”œβ”€β”€ runner.bxm # TestBox runner +β”œβ”€β”€ specs/ # BDD test specifications +└── resources/ # Test resources and fixtures + +/docker/ # Docker configuration +β”œβ”€β”€ Dockerfile # Container image definition +└── docker-compose.yml # Multi-container setup + +/runtime/ # BoxLang runtime files (auto-generated) +β”œβ”€β”€ boxlang.json # Runtime configuration +└── global/ # Global runtime cache + +/.devcontainer/ # VS Code dev container configuration +/.github/ # GitHub Actions workflows and settings +/.vscode/ # VS Code workspace settings + +# Root Configuration Files +β”œβ”€β”€ Build.bx # Build automation script +β”œβ”€β”€ box.json # CommandBox package descriptor +β”œβ”€β”€ server.json # CommandBox server configuration +β”œβ”€β”€ .env.example # Environment variable template +β”œβ”€β”€ .cfformat.json # Code formatting rules +β”œβ”€β”€ .cflintrc # Linting configuration +β”œβ”€β”€ .editorconfig # Editor configuration +└── pom.xml # Maven build configuration (optional) +``` + +## πŸ”§ Key Technologies + +### Core Stack + +- **BoxLang**: Modern JVM language with CFML compatibility +- **ColdBox**: HMVC framework for enterprise applications +- **WireBox**: Dependency injection and AOP container +- **CacheBox**: Enterprise caching engine +- **LogBox**: Logging and debugging framework + +### Development Tools + +- **CommandBox**: CLI toolchain and package manager +- **TestBox**: BDD/TDD testing framework +- **CFFormat**: Code formatting and linting + +## πŸ“ BoxLang Syntax Guidelines + +### File Extensions + +- `.bx` - BoxLang classes and components +- `.bxm` - BoxLang markup (templates/views) +- `.bxs` - BoxLang scripts + +### Class Declaration + +```js +// BoxLang uses 'class' keyword instead of 'component' +class extends="BaseHandler" { + + function index( event, rc, prc ){ + return "Hello from BoxLang!"; + } + +} +``` + +### Function Syntax + +```js +// Modern function syntax +function getUserById( required numeric id ){ + return userService.findById( arguments.id ); +} + +// Arrow functions supported as closures +variables.transform = ( item ) => item.toUpperCase(); + +// Thin Arrow function supported as pure lambdas +// BoxLang lambdas only have acess to arguments and function local scope +variables.filter = ( item ) -> item.isActive(); +``` + +### Dependency Injection + +```js +class UserService { + // Long Form + @inject( "UserDAO" ) + property userDAO; + + // Short Form if the property name matches the class name + @inject + property userDAO; + + @inject + property wirebox; + + function getUsers(){ + return userDAO.list(); + } +} +``` + +## πŸŽ›οΈ ColdBox Patterns + +### Handler Actions + +```js +class extends="BaseHandler" { + + function index( event, rc, prc ){ + prc.users = userService.list(); + event.setView( "users/index" ); + } + + function show( event, rc, prc ){ + prc.user = userService.get( rc.id ); + event.setView( "users/show" ); + } +} +``` + +### Models with Dependency Injection + +```js +class extends="BaseService" { + + @inject( "UserGateway" ) + property userGateway; + + @inject + property cachebox; + + function list(){ + return cachebox.get( "users" ) ?: userGateway.getAll(); + } +} +``` + +### Event-Driven Architecture + +```js +// Interceptors for cross-cutting concerns +class SecurityInterceptor { + + function preProcess( event, data ){ + if( !security.isLoggedIn() ){ + relocate( "auth.login" ); + } + } +} +``` + +## πŸ§ͺ Testing Patterns + +### BDD Test Structure + +```js +class extends="BaseTestCase" { + + function beforeAll(){ + super.beforeAll(); + userService = getInstance( "UserService" ); + } + + function run(){ + describe( "UserService", () => { + + it( "should return all users", () => { + var users = userService.list() + expect( users ).toBeArray() + }) + + }) + } +} +``` + +## πŸ”— Common Integrations + +### Database Operations + +- Use ColdBox ORM or Query Builder or Quick ORM or native queries +- Migration files in `/resources/database/migrations/` +- Seeders in `/resources/database/seeds/` + +### API Development + +- RESTful handlers in `/handlers/api/` +- **Use RESTHandler base class** for REST API endpoints +- Use `event.renderData()` for JSON responses +- JWT authentication via ColdBox Security module + +#### RESTHandler Pattern + +For REST API development, extend `coldbox.system.RestHandler` instead of `BaseHandler`: + +```js +class extends="coldbox.system.RestHandler" { + + // RESTHandler provides automatic: + // - Content negotiation (JSON, XML, HTML) + // - HTTP status code handling + // - Error response formatting + // - CORS support + + function index( event, rc, prc ){ + // Automatically serializes to requested format + return userService.list(); + } + + function show( event, rc, prc ){ + prc.response = userService.get( rc.id ); + } + + function create( event, rc, prc ){ + var result = userService.create( rc ); + prc.statusCode = 201; + return result; + } + + function update( event, rc, prc ){ + return userService.update( rc.id, rc ); + } + + function delete( event, rc, prc ){ + userService.delete( rc.id ); + prc.statusCode = 204; + } + + // Handle errors with proper HTTP status codes + function onError( event, rc, prc, faultAction, exception ){ + prc.statusCode = 500; + return { + "error": true, + "messages": [ exception.message ] + }; + } +} +``` + +**RESTHandler Benefits**: + +- Automatic content type detection and rendering +- Built-in HTTP verb routing (GET, POST, PUT, DELETE, PATCH) +- Standardized error handling with `onError()` and `onInvalidHTTPMethod()` +- Easy status code management via `prc.statusCode` +- Return data directly from actions (auto-serialization) + +**Documentation**: https://coldbox.ortusbooks.com/digging-deeper/rest-handler + +#### Resourceful Routes (RECOMMENDED for ALL Resource-Based Handlers) + +**Always use resourceful routes** for both web handlers and REST APIs. ColdBox provides automatic route generation following RESTful conventions, reducing boilerplate and ensuring consistency. + +```js +// In /app/config/Router.bx +function configure(){ + + // Standard resourceful routes (web handlers with views) + resources( "photos" ); + // Generates: index, new, create, show, edit, update, delete + // Perfect for traditional CRUD web interfaces + + // API resourceful routes (REST APIs, JSON responses) + apiResources( "users" ); + // Generates: index, show, create, update, delete (no 'new' or 'edit' forms) + // Use with RestHandler for REST APIs + + // Nested resources for hierarchical data + resources( "photos", function(){ + resources( "comments" ); + }); + // Generates: /photos/:photoId/comments + + // Restrict to specific actions + resources( resource="articles", only="index,show" ); + resources( resource="profiles", except="delete" ); + + // Customize parameter name + resources( resource="users", parameterName="userId" ); +} +``` + +**Generated Routes for `resources( "photos" )` (Web)**: + +| HTTP Verb | Route | Action | Purpose | +|-----------|------------------|----------|----------------------------| +| GET | /photos | index | List all photos | +| GET | /photos/new | new | Show create form | +| POST | /photos | create | Create new photo | +| GET | /photos/:id | show | Show specific photo | +| GET | /photos/:id/edit | edit | Show edit form | +| PUT/PATCH | /photos/:id | update | Update specific photo | +| DELETE | /photos/:id | delete | Delete specific photo | + +**Generated Routes for `apiResources( "users" )` (API)**: + +| HTTP Verb | Route | Action | Purpose | +|-----------|------------------|----------|----------------------------| +| GET | /users | index | List all users | +| GET | /users/:id | show | Get specific user | +| POST | /users | create | Create new user | +| PUT/PATCH | /users/:id | update | Update specific user | +| DELETE | /users/:id | delete | Delete specific user | + +**Benefits**: +- **Use `resources()` for web handlers** (with forms and views) and `apiResources()` for REST APIs +- Automatic RESTful route generation following industry standards +- Consistent URL patterns across your entire application +- Reduced boilerplate route definitions +- Easy to understand and maintain +- Works seamlessly with BaseHandler (web) and RestHandler (API) base classes +- Supports nested resources for hierarchical data +- Self-documenting route structure + +**When to Use**: +- βœ… Any CRUD-based resource (users, posts, photos, products, etc.) +- βœ… Traditional web interfaces with forms (`resources()`) +- βœ… REST APIs returning JSON/XML (`apiResources()`) +- βœ… Nested/hierarchical resources (posts with comments) +- ❌ Non-resource actions (search, reports, utilities) - use custom routes + +**Documentation**: https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes + +### Frontend Integration + +- Assets managed via `/public/assets/` +- View helpers for asset compilation +- Modern JavaScript/CSS build tools supported + +## πŸš€ Development Workflow + +### Local Development +```bash +# Install dependencies +box install + +# Start development server +box server start + +# Run tests +box testbox run + +# Format code +box run-script format +``` + +### Code Quality + +- Follow Ortus coding standards +- Use CFFormat for consistent formatting +- Write tests for all business logic +- Document public APIs with JavaDoc-style comments + +## πŸ” AI Assistant Guidelines + +### When Generating Code + +1. **Use BoxLang syntax** (class, not component) +2. **Follow ColdBox conventions** for handlers, models, views +3. **Include proper dependency injection** when creating services +4. **Write accompanying tests** for new functionality +5. **Use modern BoxLang features** (arrow functions, proper typing) + +### File Creation Patterns + +- **Handlers**: Extend `BaseHandler`, use dependency injection +- **Models**: Extend appropriate base classes, implement interfaces +- **Tests**: Follow BDD patterns with `describe()` and `it()` +- **Views**: Use `.bxm` extension, leverage ColdBox view helpers + +### Security Considerations + +- Always validate input parameters +- Use ColdBox Security module for authentication +- Implement CSRF protection for forms +- Sanitize output in views + +## πŸ“š Documentation References + +- [BoxLang Documentation](https://boxlang.ortusbooks.com/) +- [ColdBox Documentation](https://coldbox.ortusbooks.com/) +- [TestBox Documentation](https://testbox.ortusbooks.com/) +- [WireBox Documentation](https://wirebox.ortusbooks.com/) +- [CacheBox Documentation](https://cachebox.ortusbooks.com/) +- [LogBox Documentation](https://logbox.ortusbooks.com/) +- [Ortus Coding Standards](https://github.com/Ortus-Solutions/coding-standards) + +## πŸ€– MCP (Model Context Protocol) Servers + +For AI assistants that support MCP, access comprehensive documentation through these servers: + +### Framework Documentation +- **ColdBox MCP Server**: `https://coldbox.ortusbooks.com/~gitbook/mcp` +- **WireBox MCP Server**: `https://wirebox.ortusbooks.com/~gitbook/mcp` +- **CacheBox MCP Server**: `https://cachebox.ortusbooks.com/~gitbook/mcp` +- **LogBox MCP Server**: `https://logbox.ortusbooks.com/~gitbook/mcp` + +### Language & Testing Documentation +- **BoxLang MCP Server**: `https://boxlang.ortusbooks.com/~gitbook/mcp` +- **TestBox MCP Server**: `https://testbox.ortusbooks.com/~gitbook/mcp` + +These MCP servers provide real-time access to official documentation, examples, and API references. AI assistants can query these servers for: +- Framework APIs and configuration options +- Code examples and patterns +- Best practices and conventions +- Troubleshooting guides +- Version-specific documentation + +## 🎯 Best Practices + +### Performance + +- Use CacheBox for expensive operations +- Implement proper database indexing +- Lazy load dependencies when appropriate +- Use async patterns for I/O operations + +### Maintainability + +- Keep handlers thin, move logic to services +- Use events for decoupled communication +- Implement proper error handling +- Write comprehensive tests + +### Security + +- Validate all inputs +- Use parameterized queries +- Implement proper session management +- Regular security audits of dependencies \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2687a86..fa42297 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store settings.xml WEB-INF +build/** # Engines + Database + CBFS + Secrets .tmp/** @@ -10,6 +11,9 @@ WEB-INF .cbfs/** docker/.db/** +# Vite +/public/includes/build/** + # logs + tests app/logs/** runtime/logs/** diff --git a/Build.bx b/Build.bx new file mode 100644 index 0000000..828b5d3 --- /dev/null +++ b/Build.bx @@ -0,0 +1,303 @@ +/** + * Build automation script for ColdBox BoxLang Template + * + * This component handles the build processes including: + * - Compilation and packaging + * - Distribution preparation + * - Checksum generation + * + * Usage: boxlang Build.bx + */ +class { + + /** + * Constructor - Initialize build environment + */ + function init(){ + // Setup Pathing + variables.cwd = server.cli.executionPath & "/"; + variables.buildDir = variables.cwd & "build"; + variables.packageDir = variables.buildDir & "/package"; + variables.distDir = variables.buildDir & "/distributions"; + + // Load box.json for project metadata + variables.boxJSON = jsonDeserialize( fileRead( variables.cwd & "box.json" ) ); + variables.projectName = variables.boxJSON.slug ?: "coldbox-app"; + variables.projectVersion = variables.boxJSON.version ?: "1.0.0"; + + // Source directories to package + variables.sources = [ + ".cbmigrations.json", + "box.json", + "app", + "modules", + "public", + "runtime" + ]; + + // Files and folders to exclude from the build (regex patterns) + variables.excludes = [ + "logs/", // Log directories + "\.DS_Store$", // macOS system files + "Thumbs\.db$" // Windows system files + ]; + + return this; + } + + /** + * Main entry point for the build process + */ + function main(){ + printHeader( "Starting Build Process for #variables.projectName# v#variables.projectVersion#" ); + + // Clean and prepare build directory + prepareBuildDirectory(); + + // Copy source files + copySources(); + + // Create build ID file + createBuildID(); + + // Compile sources + compileSources(); + + // Create distribution zip + createDistribution(); + + // Generate checksums + generateChecksums(); + + printHeader( "✨ Build Complete! Distribution ready at: #variables.distDir#" ); + } + + /** + * Prepare the build directory - delete if exists and recreate + */ + private function prepareBuildDirectory(){ + printStep( "🧹 Preparing build directory..." ); + + // Wipe build directory if it exists + if ( directoryExists( variables.buildDir ) ) { + printInfo( "Removing existing build directory..." ); + directoryDelete( variables.buildDir, true ); + } + + // Create directory structure + directoryCreate( variables.packageDir, true, true ); + directoryCreate( variables.distDir, true, true ); + + printSuccess( "Build directory prepared" ); + } + + /** + * Copy source files to package directory + */ + private function copySources(){ + printStep( "πŸ“ Copying source files..." ); + + variables.sources.each( ( source ) => { + var sourcePath = variables.cwd & source; + var targetPath = variables.packageDir & "/" & source; + + if ( directoryExists( sourcePath ) ) { + printInfo( "Copying #source#/ ..." ); + copyDirectoryWithExclusions( sourcePath, targetPath ); + } else if ( fileExists( sourcePath ) ) { + printInfo( "Copying #source# ..." ); + fileCopy( sourcePath, targetPath ); + } else { + printWarning( "Source not found: #source#" ); + } + } ); + + printSuccess( "Sources copied successfully" ); + } + + /** + * Copy directory recursively with exclusion patterns + * + * @source The source directory path + * @target The target directory path + */ + private function copyDirectoryWithExclusions( required string source, required string target ){ + // Create target directory if it doesn't exist + if ( !directoryExists( arguments.target ) ) { + directoryCreate( arguments.target, true, true ); + } + + // Get all items in the source directory as an array of paths + var items = directoryList( arguments.source, false, "array" ); + + items.each( ( itemPath ) => { + var itemName = listLast( itemPath, "/\" ); + var targetPath = target & "/" & itemName; + var relativePath = itemPath.replace( variables.cwd, "" ); + + // Check if item should be excluded + var isExcluded = isPathExcluded( relativePath ); + + if ( isExcluded ) { + printInfo( "⊘ Excluding: #relativePath#" ); + return; + } + + // Copy files or directories recursively + if ( directoryExists( itemPath ) ) { + copyDirectoryWithExclusions( itemPath, targetPath ); + } else { + fileCopy( itemPath, targetPath ); + } + } ); + } + + /** + * Check if a path matches any exclusion pattern + * + * @path The path to check (relative to project root) + * @return True if path should be excluded + */ + private function isPathExcluded( required string path ){ + var excluded = false; + + variables.excludes.each( ( pattern ) => { + if ( path.reFindNoCase( pattern ) ) { + excluded = true; + } + } ); + + return excluded; + } + + /** + * Create build ID file + */ + private function createBuildID(){ + printStep( "🏷️ Creating build ID file..." ); + var buildIDFileName = "#variables.projectName#-#variables.projectVersion#.md"; + var buildIDPath = variables.packageDir & "/" & buildIDFileName; + var buildContent = "## Build Information + + **Project**: #variables.projectName# + **Version**: #variables.projectVersion# + **Built on**: #dateTimeFormat( now(), "full" )# + "; + + fileWrite( buildIDPath, buildContent ); + printSuccess( "Build ID file created: #buildIDFileName#" ); + } + + /** + * Compile BoxLang sources + */ + private function compileSources(){ + printStep( "πŸ”¨ Compiling BoxLang sources..." ); + + var compilePaths = [ + variables.packageDir & "/app/", + variables.packageDir & "/public/" + ]; + + compilePaths.each( ( path ) => { + if ( directoryExists( path ) ) { + printInfo( "Compiling: [#path#]" ); + try { + var result = systemExecute( "boxlang", "compile --source #path# --target #path#" ); + println( result.output ) + } catch( any e ) { + printWarning( "Compilation error for #path#: #e.message#" ); + } + } + } ); + + printSuccess( "Source compilation completed" ); + } + + /** + * Create distribution zip file + */ + private function createDistribution(){ + printStep( "πŸ“¦ Creating distribution package..." ); + + var zipFileName = "#variables.projectName#-#variables.projectVersion#.zip"; + var zipPath = variables.distDir & "/" & zipFileName; + + printInfo( "Zipping package to: #zipFileName#" ); + + bx:zip + file = zipPath + source = variables.packageDir + overwrite = true + recurse = true; + + fileCopy( variables.packageDir & "/box.json", variables.distDir & "/box.json" ); + + printSuccess( "Distribution created: #zipFileName#" ); + } + + /** + * Generate checksums for the distribution + */ + private function generateChecksums(){ + printStep( "πŸ” Generating checksums..." ); + + var zipFileName = "#variables.projectName#-#variables.projectVersion#.zip"; + var zipPath = variables.distDir & "/" & zipFileName; + + if ( !fileExists( zipPath ) ) { + printWarning( "Zip file not found for checksum generation" ); + return; + } + + // Generate MD5 + var md5Hash = hash( fileReadBinary( zipPath ), "MD5" ); + fileWrite( zipPath & ".md5", md5Hash ); + printSuccess( "MD5: #md5Hash#" ); + + // Generate SHA-256 + var sha256Hash = hash( fileReadBinary( zipPath ), "SHA-256" ); + fileWrite( zipPath & ".sha256", sha256Hash ); + printSuccess( "SHA-256: #sha256Hash#" ); + + // Generate SHA-512 + var sha512Hash = hash( fileReadBinary( zipPath ), "SHA-512" ); + fileWrite( zipPath & ".sha512", sha512Hash ); + printSuccess( "SHA-512: #sha512Hash#" ); + } + + // ============================================================================ + // Print Helpers + // ============================================================================ + + private function printHeader( required string message ){ + var separator = repeatString( "=", 70 ); + println( "", true ); + println( separator, true ); + println( " πŸš€ " & arguments.message, true ); + println( separator, true ); + println( "", true ); + } + + private function printStep( required string message ){ + println( "", true ); + println( "πŸ“¦ " & arguments.message, true ); + } + + private function printInfo( required string message ){ + println( " πŸ“„ " & arguments.message, true ); + } + + private function printSuccess( required string message ){ + println( " βœ… " & arguments.message, true ); + } + + private function printWarning( required string message ){ + println( " ⚠️ " & arguments.message, true ); + } + + private function printError( required string message ){ + println( " ❌ " & arguments.message, true ); + } + +} \ No newline at end of file diff --git a/app/config/Coldbox.bx b/app/config/Coldbox.bx index 5a87a28..63c2176 100644 --- a/app/config/Coldbox.bx +++ b/app/config/Coldbox.bx @@ -23,7 +23,7 @@ class { reinitKey : "fwreinit", handlersIndexAutoReload : true, // Implicit Events - defaultEvent : "", + defaultEvent : "Main.index", requestStartHandler : "Main.onRequestStart", requestEndHandler : "", applicationStartHandler : "Main.onAppInit", @@ -97,6 +97,10 @@ class { variables.logBox = { // Define Appenders appenders : { coldboxTracer : { class : "coldbox.system.logging.appenders.ConsoleAppender" } }, + filelog : { + class : "coldbox.system.logging.appenders.RollingFileAppender", + properties : { filename : "app", filePath : "/app/logs" } + } // Root Logger root : { levelmax : "INFO", appenders : "*" }, // Implicit Level Categories diff --git a/app/modules_app/.gitkeep b/app/interceptors/.gitkeep similarity index 100% rename from app/modules_app/.gitkeep rename to app/interceptors/.gitkeep diff --git a/runtime/modules/.gitkeep b/app/modules/.gitkeep similarity index 100% rename from runtime/modules/.gitkeep rename to app/modules/.gitkeep diff --git a/box.json b/box.json index 7c6c9a9..bf233b0 100644 --- a/box.json +++ b/box.json @@ -1,6 +1,6 @@ { "name":"Default ColdBox App Template For BoxLang", - "version":"1.0.0", + "version":"8.0.0", "author":"You", "location":"forgeboxStorage", "slug":"cbtemplate-boxlang", @@ -9,26 +9,33 @@ "language":"boxlang", "keywords":"boxlang", "shortDescription":"", - "ignore":[], + "ignore":[ + "changelog.md", + ".github/ISSUE_TEMPLATE/*", + ".github/workflows/*", + ".github/CONTRIBUTING.md", + ".github/FUNDING.YML", + ".github/PULL_REQUEST_TEMPLATE.md" + ], "dependencies":{ - "coldbox":"be", - "route-visualizer":"^2.2.0+2" + "coldbox":"be" }, "devDependencies":{ + "route-visualizer":"^2.2.0+2", "testbox":"*", "commandbox-boxlang":"*", - "commandbox-cfformat":"*" + "commandbox-cfformat":"*", + "coldbox-cli":"*", + "testbox-cli":"*" }, "installPaths":{ "coldbox":"runtime/lib/coldbox/", - "testbox":"runtime/lib/testbox/", - "route-visualizer":"modules/route-visualizer/" + "testbox":"runtime/lib/testbox/" }, "scripts":{ - "postInstall":"pathExists .env || cp .env.example .env && package set ignore=[]", - "format":"cfformat run app/**/*.cfc,tests/specs/,*.cfc --overwrite", - "format:check":"cfformat check app/**/*.cfc,tests/specs/,*.cfc ./.cfformat.json", - "format:watch":"cfformat watch path='app/**/*.cfc,tests/specs/,*.cfc' settingsPath='.cfformat.json'", + "format":"cfformat run app/**/*.bx,tests/specs/,*.bx --overwrite", + "format:check":"cfformat check app/**/*.bx,tests/specs/,*.bx ./.cfformat.json", + "format:watch":"cfformat watch path='app/**/*.bx,tests/specs/,*.bx' settingsPath='.cfformat.json'", "docker:build":"!docker build --no-cache -t my-coldbox-app -f ./docker/Dockerfile ./", "docker:run":"!docker run -it -p 8080:8080 my-coldbox-app", "docker:bash":"!docker run -it my-coldbox-app /bin/bash", diff --git a/changelog.md b/changelog.md index 7b8423b..e06c550 100644 --- a/changelog.md +++ b/changelog.md @@ -9,7 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [1.0.0] - 2025-10-08 +### Added + +- ColdBox 8 readyness ## [1.1.0] - 2025-06-07 diff --git a/pom.xml b/pom.xml index 34ca2ac..aff843d 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ diff --git a/readme.md b/readme.md index 8d2569d..c91220b 100644 --- a/readme.md +++ b/readme.md @@ -26,7 +26,307 @@ Welcome to the modern ColdBox 8 BoxLang application template! πŸŽ‰ This template provides a solid foundation for building enterprise-grade HMVC (Hierarchical Model-View-Controller) web applications using the BoxLang runtime. Perfect for developers looking to leverage the power of ColdBox with the performance and modern features of BoxLang. -## πŸ“ Application Structure +## βš™οΈ Requirements + +Before getting started, ensure you have the following installed on your operating system: + +1. **BoxLang OS** - Operating System Binary + - πŸ“₯ Installation: + - πŸ“Œ Minimum Version: 1.0+ + - 🎯 Used for: running BoxLang applications and scripts at the operating system level +2. **CommandBox** - CLI toolchain, package manager, and server runtime + - πŸ“₯ Installation: + - πŸ“Œ Minimum Version: 6.0+ + - 🎯 Used for: dependency management, server starting, testing, and task automation +3. **Maven** - Java dependency manager (Optional, only if you need Java dependencies) + - πŸ“₯ Installation: + - πŸ“Œ Minimum Version: 3.6+ + - 🎯 Used for: managing Java dependencies if your project requires them + + +## ⚑ Quick Installation + +In order to work with this template, you need to have [CommandBox](https://www.ortussolutions.com/products/commandbox) and the [BoxLang](https://boxlang.ortusbooks.com/) operating system runtime installed on your machine. CommandBox is the application server of choice for BoxLang applications. Please note that running BoxLang web applications is different than the BoxLang OS runtime. The BoxLang OS runtime is used to run BoxLang scripts and command line applications, while CommandBox is used to run web applications. + +```bash +# Create a new ColdBox application using this BoxLang template +box coldbox create app --boxlang +# Start up the web server +box server start +``` + +Your application will be available at `http://localhost:8080` 🌐 + +Code to your liking and enjoy! 🎊 + +## ⚑ Vite Frontend Build System + +If you chose to use **Vite** during setup, this template includes a modern frontend build system with Vue 3 and Tailwind CSS support. Vite provides lightning-fast hot module replacement (HMR) and optimized production builds. + +### πŸ“‚ Asset Structure: + +```text +resources/ +└── assets/ + β”œβ”€β”€ css/ + β”‚ └── app.css # Main stylesheet (Tailwind) + └── js/ + └── app.js # Main JavaScript (Vue 3 app) + +public/ +└── includes/ # Compiled assets (generated by Vite) + β”œβ”€β”€ manifest.json # Asset manifest for production + └── assets/ + β”œβ”€β”€ app-[hash].css # Compiled & hashed CSS + └── app-[hash].js # Compiled & hashed JS +``` + +### πŸš€ Using Vite + +#### Development Mode (Hot Module Replacement) + +```bash +npm run dev +``` + +This starts the Vite development server with HMR at `http://localhost:5173`. The ColdBox application automatically detects the dev server and loads assets from it. + +#### Production Build + +```bash +npm run build +``` + +Compiles and optimizes assets for production, outputting them to `public/includes/`. The generated files include content hashes for cache busting. + +### πŸ“ Including Assets in Views + +The `vite()` helper function automatically loads assets based on the environment: + +```xml + + + + + #vite([ + "resources/assets/css/app.css", + "resources/assets/js/app.js" + ])# + +``` + +**Development**: Loads from Vite dev server (`http://localhost:5173`) +**Production**: Loads compiled assets from `public/includes/assets/` + +### 🎨 Customizing Vite + +#### Change Output Directory + +```javascript +coldbox({ + input: [ "resources/assets/css/app.css", "resources/assets/js/app.js" ], + refresh: true, + publicDirectory: "public", + buildDirectory: "dist" // Assets output to public/dist/ +}) +``` + +#### Add More Entry Points: + +```javascript +coldbox({ + input: [ + "resources/assets/css/app.css", + "resources/assets/js/app.js", + "resources/assets/js/admin.js", // Additional JS entry + "resources/assets/css/admin.css" // Additional CSS entry + ], + refresh: true +}) +``` + +#### Configure Refresh Paths: + +```javascript +coldbox({ + input: [ "resources/assets/css/app.css", "resources/assets/js/app.js" ], + refresh: [ + "app/layouts/**", + "app/views/**", + "app/config/Router.bx", + "app/handlers/**/*.bx" // Also refresh on handler changes + ] +}) +``` + +> **πŸ“š Learn More**: Check out the [coldbox-vite-plugin documentation](https://github.com/coldbox/coldbox-vite-plugin) and [Vite documentation](https://vitejs.dev) for advanced configuration options. + +## πŸ“¦ Build Script (`Build.bx`) + +The **Build.bx** script compiles and packages your application for distribution. It creates optimized, production-ready builds that can be deployed to any environment. + +```bash +boxlang Build.bx +``` + +### What Build.bx Does: + +The build process performs the following steps: + +1. **🧹 Clean Build Directory**: Removes any existing `build/` folder and creates a fresh structure +2. **πŸ“ Copy Sources**: Copies application files (`app/`, `modules/`, `public/`, `runtime/`) to the build package +3. **⊘ Smart Exclusions**: Automatically excludes: + - Log files and directories (`logs/`, `*.log`) + - System files (`.DS_Store`, `Thumbs.db`) + - Hidden files and folders (`.git`, `.gitignore`, etc.) +4. **🏷️ Build ID**: Creates a build information file with project name, version, and timestamp +5. **πŸ”¨ Compilation**: Compiles BoxLang sources in `app/` and `public/` to optimized bytecode +6. **πŸ“¦ Distribution Package**: Creates a ZIP file: `build/distributions/{projectName}-{projectVersion}.zip` +7. **πŸ” Checksums**: Generates security checksums (MD5, SHA-256, SHA-512) for integrity verification + +### Build Output Structure: + +```text +build/ +β”œβ”€β”€ package/ # Staged files ready for distribution +β”‚ β”œβ”€β”€ app/ # Compiled application code +β”‚ β”œβ”€β”€ modules/ # Application modules +β”‚ β”œβ”€β”€ public/ # Compiled public assets +β”‚ β”œβ”€β”€ runtime/ # Runtime configuration (without logs) +β”‚ └── {projectName}-{version}.md # Build information +└── distributions/ # Final distribution files + β”œβ”€β”€ {projectName}-{version}.zip + β”œβ”€β”€ {projectName}-{version}.zip.md5 + β”œβ”€β”€ {projectName}-{version}.zip.sha256 + └── {projectName}-{version}.zip.sha512 +``` + +### Customizing the Build: + +You can customize what gets included or excluded by editing the `Build.bx` file's initialization section. The build script uses two configurable arrays: + +#### πŸ“¦ **Sources Array** - What to Include + +Controls which directories and files get packaged in your distribution: + +```boxlang +// Source directories to package +variables.sources = [ + ".cbmigrations.json", // Database migrations state + "box.json", // Project metadata + "app", // Your ColdBox application + "modules", // Installed modules + "public", // Web root with assets + "runtime" // BoxLang runtime config +]; +``` + +**To add more sources**, simply append to the array: + +```boxlang +variables.sources = [ + ".cbmigrations.json", + "box.json", + "app", + "modules", + "public", + "runtime", + "config", // Add custom config directory + "resources/database" // Include database resources +]; +``` + +#### 🚫 **Excludes Array** - What to Skip + +Uses **regex patterns** to exclude files/directories from the build: + +```boxlang +// Files and folders to exclude from the build (regex patterns) +variables.excludes = [ + "logs/", // Log directories + "\.DS_Store$", // macOS system files + "Thumbs\.db$" // Windows system files +]; +``` + +**Common exclusion patterns**: + +```boxlang +variables.excludes = [ + "logs/", // Exclude all log directories + "\.log$", // Exclude .log files + "\.tmp$", // Exclude .tmp files + "test-results/", // Exclude test output + "node_modules/", // Exclude npm dependencies + "\.git", // Exclude git repository + "\.env", // Exclude environment files + "\.bak$", // Exclude backup files + "resources/vite/", // Exclude vite resources after setup + "resources/rest/" // Exclude rest resources after setup +]; +``` + +**Regex Pattern Tips**: +- Use `$` to match end of string: `\.log$` matches files ending in `.log` +- Use `/` to match directories: `logs/` matches any `logs` directory +- Use `\.` to escape dots: `\.DS_Store$` matches `.DS_Store` files +- Use `.*` for wildcards: `temp.*` matches anything starting with `temp` + +#### πŸ”§ **Example: Custom Build Configuration** + +```boxlang +function init(){ + // ... existing code ... + + // Custom sources for your project + variables.sources = [ + ".cbmigrations.json", + "box.json", + "app", + "modules", + "public", + "runtime", + "custom-config", // Add your custom directory + "static-files" // Add static file directory + ]; + + // Comprehensive exclusions + variables.excludes = [ + "logs/", // No log files + "\.DS_Store$", // No macOS files + "Thumbs\.db$", // No Windows files + "test-results/", // No test outputs + "\.env\..*", // No environment files + "resources/vite/", // No vite setup resources + "resources/rest/", // No rest setup resources + "resources/docker/", // No docker setup resources + "Setup\.bx$" // No setup script + ]; + + return this; +} +``` + +> **πŸ’‘ Pro Tip**: Review your `variables.excludes` after running `Setup.bx` to ensure you're not packaging unnecessary setup resources! + +### Deploying Your Build: + +Once the build completes, you can: + +1. **Upload the ZIP**: Deploy `{projectName}-{version}.zip` to your server +2. **Verify Integrity**: Use the checksum files to verify the package wasn't corrupted during transfer +3. **Extract & Run**: Unzip on your server and start with CommandBox + +```bash +# On your server +unzip cbtemplate-boxlang-1.1.0.zip +cd cbtemplate-boxlang-1.1.0 +box server start +``` + +> **πŸš€ Pro Tip**: Integrate `Build.bx` into your CI/CD pipeline to automatically build and deploy your application on every release! + +## πŸ“Application Structure This ColdBox 8 application follows a clean, modern architecture with the following structure: @@ -82,7 +382,6 @@ This folder contains configuration files, dependencies, Docker setup, and runtim β”‚ β”‚ └── components/ # Global BoxLang components β”‚ β”œβ”€β”€ lib/ # Runtime libraries (Managed by Maven/CommandBox) β”‚ β”œβ”€β”€ logs/ # BoxLang logs -β”‚ β”œβ”€β”€ modules/ # BoxLang runtime modules └── πŸ“š resources/ # ColdBox/CommandBox module resources β”œβ”€β”€ migrations/ # Database migrations (cbmigrations) β”œβ”€β”€ seeders/ # Database seeders @@ -90,29 +389,6 @@ This folder contains configuration files, dependencies, Docker setup, and runtim └── other module assets/ # Various module-specific resources ``` -## ⚑ Quick Installation - -In order to work with this template, you need to have [CommandBox](https://www.ortussolutions.com/products/commandbox) installed on your machine. CommandBox is the application server of choice for BoxLang applications. - -```bash -# Go into the CommandBox shell -box -# Create a new directory and go into it -mkdir MyApp --cd -# Create a new ColdBox application using the BoxLang template -coldbox create app --boxlang -``` - -This will create a new ColdBox application using this BoxLang template and install all the needed dependencies. You can then startup your BoxLang server using the following command: - -```bash -box server start -``` - -Your application will be available at `http://localhost:8080` 🌐 - -Code to your liking and enjoy! 🎊 - ## πŸ—ΊοΈ BoxLang Mappings This template comes pre-configured with essential BoxLang mappings in the `runtime/config/boxlang.json` file to make development seamless. These mappings provide convenient shortcuts to access different parts of your application: diff --git a/resources/assets/css/app.css b/resources/assets/css/app.css new file mode 100644 index 0000000..a461c50 --- /dev/null +++ b/resources/assets/css/app.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js new file mode 100644 index 0000000..a3ac4a4 --- /dev/null +++ b/resources/assets/js/app.js @@ -0,0 +1,4 @@ +import { createApp } from "vue"; +import Hello from "./components/Hello.vue"; + +createApp( Hello ).mount( "#app" ); diff --git a/resources/assets/js/components/Hello.vue b/resources/assets/js/components/Hello.vue new file mode 100644 index 0000000..aa9ec8c --- /dev/null +++ b/resources/assets/js/components/Hello.vue @@ -0,0 +1,7 @@ + diff --git a/resources/copilot-instructions.md b/resources/copilot-instructions.md new file mode 100644 index 0000000..c7109f5 --- /dev/null +++ b/resources/copilot-instructions.md @@ -0,0 +1,515 @@ +# BoxLang ColdBox Template - AI Coding Instructions + +This is a BoxLang application template using the ColdBox HMVC framework. These instructions are designed to help AI assistants understand the project structure and provide better coding assistance. + +## 🎯 Project Overview + +**Language**: BoxLang (modern JVM language, successor to CFML) +**Framework**: ColdBox HMVC Framework v8+ +**Template Type**: Full-stack web application template +**Architecture**: Model-View-Controller with Hierarchical MVC support + +## βš™οΈ Requirements + +This project requires the following to be installed on your operating system: + +### Required Software + +1. **CommandBox** - CLI toolchain, package manager, and server runtime + - Installation: https://commandbox.ortusbooks.com/getting-started/installation + - Minimum Version: 6.0+ + - Used for: dependency management, server starting, testing, and task automation + +2. **BoxLang** - Modern JVM language runtime + - Installation: https://boxlang.ortusbooks.com/getting-started/installation + - Minimum Version: 1.0+ + - Can be installed via CommandBox: `box install commandbox-boxlang` + - Used for: running BoxLang applications and scripts + +### Verification + +Verify installations: +```bash +# Check CommandBox is installed +box version + +# Check BoxLang is available +box boxlang version + +# Verify project setup (run from project root) +boxlang Setup.bx +``` + +### Optional but Recommended + +- **Java JDK** - Required for BoxLang runtime (JDK 11+ recommended) +- **Docker** - For containerized development (if using Docker setup) +- **Git** - For version control + +## πŸ“ Project Structure + +``` +/app/ # Application source code +β”œβ”€β”€ Application.bx # Application entry point +β”œβ”€β”€ config/ # Framework configuration +β”‚ β”œβ”€β”€ CacheBox.bx # Caching configuration +β”‚ β”œβ”€β”€ ColdBox.bx # Main ColdBox settings +β”‚ β”œβ”€β”€ Router.bx # Route definitions +β”‚ β”œβ”€β”€ Scheduler.bx # Scheduled tasks +β”‚ └── WireBox.bx # Dependency injection configuration +β”œβ”€β”€ handlers/ # Controllers (request handlers) +β”œβ”€β”€ helpers/ # View helper functions +β”œβ”€β”€ interceptors/ # Event interceptors/listeners +β”œβ”€β”€ layouts/ # Page layouts/templates +β”œβ”€β”€ logs/ # Application logs +β”œβ”€β”€ models/ # Business logic and data models +β”œβ”€β”€ modules/ # Application-specific modules (HMVC) +└── views/ # View templates + +/public/ # Web-accessible files (document root) +β”œβ”€β”€ Application.bx # Public application bootstrap +β”œβ”€β”€ index.bxm # Application entry point (markup) +β”œβ”€β”€ favicon.ico # Site favicon +β”œβ”€β”€ robots.txt # Search engine directives +└── includes/ # Static assets + β”œβ”€β”€ i18n/ # Client-side translations + └── images/ # Image assets (CSS, JS in subfolders) + +/resources/ # Non-web resources +β”œβ”€β”€ database/ # Database management +β”‚ └── migrations/ # Database migration files +└── copilot-instructions.md # This file (AI coding instructions) + +/tests/ # Test suites +β”œβ”€β”€ Application.bx # Test application setup +β”œβ”€β”€ index.bxm # Test runner entry point +β”œβ”€β”€ runner.bxm # TestBox runner +β”œβ”€β”€ specs/ # BDD test specifications +└── resources/ # Test resources and fixtures + +/docker/ # Docker configuration +β”œβ”€β”€ Dockerfile # Container image definition +└── docker-compose.yml # Multi-container setup + +/runtime/ # BoxLang runtime files (auto-generated) +β”œβ”€β”€ boxlang.json # Runtime configuration +└── global/ # Global runtime cache + +/.devcontainer/ # VS Code dev container configuration +/.github/ # GitHub Actions workflows and settings +/.vscode/ # VS Code workspace settings + +# Root Configuration Files +β”œβ”€β”€ Build.bx # Build automation script +β”œβ”€β”€ box.json # CommandBox package descriptor +β”œβ”€β”€ server.json # CommandBox server configuration +β”œβ”€β”€ .env.example # Environment variable template +β”œβ”€β”€ .cfformat.json # Code formatting rules +β”œβ”€β”€ .cflintrc # Linting configuration +β”œβ”€β”€ .editorconfig # Editor configuration +└── pom.xml # Maven build configuration (optional) +``` + +## πŸ”§ Key Technologies + +### Core Stack + +- **BoxLang**: Modern JVM language with CFML compatibility +- **ColdBox**: HMVC framework for enterprise applications +- **WireBox**: Dependency injection and AOP container +- **CacheBox**: Enterprise caching engine +- **LogBox**: Logging and debugging framework + +### Development Tools + +- **CommandBox**: CLI toolchain and package manager +- **TestBox**: BDD/TDD testing framework +- **CFFormat**: Code formatting and linting + +## πŸ“ BoxLang Syntax Guidelines + +### File Extensions + +- `.bx` - BoxLang classes and components +- `.bxm` - BoxLang markup (templates/views) +- `.bxs` - BoxLang scripts + +### Class Declaration + +```js +// BoxLang uses 'class' keyword instead of 'component' +class extends="BaseHandler" { + + function index( event, rc, prc ){ + return "Hello from BoxLang!"; + } + +} +``` + +### Function Syntax + +```js +// Modern function syntax +function getUserById( required numeric id ){ + return userService.findById( arguments.id ); +} + +// Arrow functions supported as closures +variables.transform = ( item ) => item.toUpperCase(); + +// Thin Arrow function supported as pure lambdas +// BoxLang lambdas only have acess to arguments and function local scope +variables.filter = ( item ) -> item.isActive(); +``` + +### Dependency Injection + +```js +class UserService { + // Long Form + @inject( "UserDAO" ) + property userDAO; + + // Short Form if the property name matches the class name + @inject + property userDAO; + + @inject + property wirebox; + + function getUsers(){ + return userDAO.list(); + } +} +``` + +## πŸŽ›οΈ ColdBox Patterns + +### Handler Actions + +```js +class extends="BaseHandler" { + + function index( event, rc, prc ){ + prc.users = userService.list(); + event.setView( "users/index" ); + } + + function show( event, rc, prc ){ + prc.user = userService.get( rc.id ); + event.setView( "users/show" ); + } +} +``` + +### Models with Dependency Injection + +```js +class extends="BaseService" { + + @inject( "UserGateway" ) + property userGateway; + + @inject + property cachebox; + + function list(){ + return cachebox.get( "users" ) ?: userGateway.getAll(); + } +} +``` + +### Event-Driven Architecture + +```js +// Interceptors for cross-cutting concerns +class SecurityInterceptor { + + function preProcess( event, data ){ + if( !security.isLoggedIn() ){ + relocate( "auth.login" ); + } + } +} +``` + +## πŸ§ͺ Testing Patterns + +### BDD Test Structure + +```js +class extends="BaseTestCase" { + + function beforeAll(){ + super.beforeAll(); + userService = getInstance( "UserService" ); + } + + function run(){ + describe( "UserService", () => { + + it( "should return all users", () => { + var users = userService.list() + expect( users ).toBeArray() + }) + + }) + } +} +``` + +## πŸ”— Common Integrations + +### Database Operations + +- Use ColdBox ORM or Query Builder or Quick ORM or native queries +- Migration files in `/resources/database/migrations/` +- Seeders in `/resources/database/seeds/` + +### API Development + +- RESTful handlers in `/handlers/api/` +- **Use RESTHandler base class** for REST API endpoints +- Use `event.renderData()` for JSON responses +- JWT authentication via ColdBox Security module + +#### RESTHandler Pattern + +For REST API development, extend `coldbox.system.RestHandler` instead of `BaseHandler`: + +```js +class extends="coldbox.system.RestHandler" { + + // RESTHandler provides automatic: + // - Content negotiation (JSON, XML, HTML) + // - HTTP status code handling + // - Error response formatting + // - CORS support + + function index( event, rc, prc ){ + // Automatically serializes to requested format + return userService.list(); + } + + function show( event, rc, prc ){ + prc.response = userService.get( rc.id ); + } + + function create( event, rc, prc ){ + var result = userService.create( rc ); + prc.statusCode = 201; + return result; + } + + function update( event, rc, prc ){ + return userService.update( rc.id, rc ); + } + + function delete( event, rc, prc ){ + userService.delete( rc.id ); + prc.statusCode = 204; + } + + // Handle errors with proper HTTP status codes + function onError( event, rc, prc, faultAction, exception ){ + prc.statusCode = 500; + return { + "error": true, + "messages": [ exception.message ] + }; + } +} +``` + +**RESTHandler Benefits**: + +- Automatic content type detection and rendering +- Built-in HTTP verb routing (GET, POST, PUT, DELETE, PATCH) +- Standardized error handling with `onError()` and `onInvalidHTTPMethod()` +- Easy status code management via `prc.statusCode` +- Return data directly from actions (auto-serialization) + +**Documentation**: https://coldbox.ortusbooks.com/digging-deeper/rest-handler + +#### Resourceful Routes (RECOMMENDED for ALL Resource-Based Handlers) + +**Always use resourceful routes** for both web handlers and REST APIs. ColdBox provides automatic route generation following RESTful conventions, reducing boilerplate and ensuring consistency. + +```js +// In /app/config/Router.bx +function configure(){ + + // Standard resourceful routes (web handlers with views) + resources( "photos" ); + // Generates: index, new, create, show, edit, update, delete + // Perfect for traditional CRUD web interfaces + + // API resourceful routes (REST APIs, JSON responses) + apiResources( "users" ); + // Generates: index, show, create, update, delete (no 'new' or 'edit' forms) + // Use with RestHandler for REST APIs + + // Nested resources for hierarchical data + resources( "photos", function(){ + resources( "comments" ); + }); + // Generates: /photos/:photoId/comments + + // Restrict to specific actions + resources( resource="articles", only="index,show" ); + resources( resource="profiles", except="delete" ); + + // Customize parameter name + resources( resource="users", parameterName="userId" ); +} +``` + +**Generated Routes for `resources( "photos" )` (Web)**: + +| HTTP Verb | Route | Action | Purpose | +|-----------|------------------|----------|----------------------------| +| GET | /photos | index | List all photos | +| GET | /photos/new | new | Show create form | +| POST | /photos | create | Create new photo | +| GET | /photos/:id | show | Show specific photo | +| GET | /photos/:id/edit | edit | Show edit form | +| PUT/PATCH | /photos/:id | update | Update specific photo | +| DELETE | /photos/:id | delete | Delete specific photo | + +**Generated Routes for `apiResources( "users" )` (API)**: + +| HTTP Verb | Route | Action | Purpose | +|-----------|------------------|----------|----------------------------| +| GET | /users | index | List all users | +| GET | /users/:id | show | Get specific user | +| POST | /users | create | Create new user | +| PUT/PATCH | /users/:id | update | Update specific user | +| DELETE | /users/:id | delete | Delete specific user | + +**Benefits**: +- **Use `resources()` for web handlers** (with forms and views) and `apiResources()` for REST APIs +- Automatic RESTful route generation following industry standards +- Consistent URL patterns across your entire application +- Reduced boilerplate route definitions +- Easy to understand and maintain +- Works seamlessly with BaseHandler (web) and RestHandler (API) base classes +- Supports nested resources for hierarchical data +- Self-documenting route structure + +**When to Use**: +- βœ… Any CRUD-based resource (users, posts, photos, products, etc.) +- βœ… Traditional web interfaces with forms (`resources()`) +- βœ… REST APIs returning JSON/XML (`apiResources()`) +- βœ… Nested/hierarchical resources (posts with comments) +- ❌ Non-resource actions (search, reports, utilities) - use custom routes + +**Documentation**: https://coldbox.ortusbooks.com/the-basics/routing/routing-dsl/resourceful-routes + +### Frontend Integration + +- Assets managed via `/public/assets/` +- View helpers for asset compilation +- Modern JavaScript/CSS build tools supported + +## πŸš€ Development Workflow + +### Local Development +```bash +# Install dependencies +box install + +# Start development server +box server start + +# Run tests +box testbox run + +# Format code +box run-script format +``` + +### Code Quality + +- Follow Ortus coding standards +- Use CFFormat for consistent formatting +- Write tests for all business logic +- Document public APIs with JavaDoc-style comments + +## πŸ” AI Assistant Guidelines + +### When Generating Code + +1. **Use BoxLang syntax** (class, not component) +2. **Follow ColdBox conventions** for handlers, models, views +3. **Include proper dependency injection** when creating services +4. **Write accompanying tests** for new functionality +5. **Use modern BoxLang features** (arrow functions, proper typing) + +### File Creation Patterns + +- **Handlers**: Extend `BaseHandler`, use dependency injection +- **Models**: Extend appropriate base classes, implement interfaces +- **Tests**: Follow BDD patterns with `describe()` and `it()` +- **Views**: Use `.bxm` extension, leverage ColdBox view helpers + +### Security Considerations + +- Always validate input parameters +- Use ColdBox Security module for authentication +- Implement CSRF protection for forms +- Sanitize output in views + +## πŸ“š Documentation References + +- [BoxLang Documentation](https://boxlang.ortusbooks.com/) +- [ColdBox Documentation](https://coldbox.ortusbooks.com/) +- [TestBox Documentation](https://testbox.ortusbooks.com/) +- [WireBox Documentation](https://wirebox.ortusbooks.com/) +- [CacheBox Documentation](https://cachebox.ortusbooks.com/) +- [LogBox Documentation](https://logbox.ortusbooks.com/) +- [Ortus Coding Standards](https://github.com/Ortus-Solutions/coding-standards) + +## πŸ€– MCP (Model Context Protocol) Servers + +For AI assistants that support MCP, access comprehensive documentation through these servers: + +### Framework Documentation +- **ColdBox MCP Server**: `https://coldbox.ortusbooks.com/~gitbook/mcp` +- **WireBox MCP Server**: `https://wirebox.ortusbooks.com/~gitbook/mcp` +- **CacheBox MCP Server**: `https://cachebox.ortusbooks.com/~gitbook/mcp` +- **LogBox MCP Server**: `https://logbox.ortusbooks.com/~gitbook/mcp` + +### Language & Testing Documentation +- **BoxLang MCP Server**: `https://boxlang.ortusbooks.com/~gitbook/mcp` +- **TestBox MCP Server**: `https://testbox.ortusbooks.com/~gitbook/mcp` + +These MCP servers provide real-time access to official documentation, examples, and API references. AI assistants can query these servers for: +- Framework APIs and configuration options +- Code examples and patterns +- Best practices and conventions +- Troubleshooting guides +- Version-specific documentation + +## 🎯 Best Practices + +### Performance + +- Use CacheBox for expensive operations +- Implement proper database indexing +- Lazy load dependencies when appropriate +- Use async patterns for I/O operations + +### Maintainability + +- Keep handlers thin, move logic to services +- Use events for decoupled communication +- Implement proper error handling +- Write comprehensive tests + +### Security + +- Validate all inputs +- Use parameterized queries +- Implement proper session management +- Regular security audits of dependencies \ No newline at end of file diff --git a/.dockerignore b/resources/docker/.dockerignore similarity index 100% rename from .dockerignore rename to resources/docker/.dockerignore diff --git a/docker/Dockerfile b/resources/docker/Dockerfile similarity index 100% rename from docker/Dockerfile rename to resources/docker/Dockerfile diff --git a/docker/docker-compose.yml b/resources/docker/docker-compose.yml similarity index 100% rename from docker/docker-compose.yml rename to resources/docker/docker-compose.yml diff --git a/resources/rest/Router.bx b/resources/rest/Router.bx new file mode 100644 index 0000000..ed49409 --- /dev/null +++ b/resources/rest/Router.bx @@ -0,0 +1,39 @@ +/** + * This is your application router. From here you can controll all the incoming routes to your application. + * + * https://coldbox.ortusbooks.com/the-basics/routing + */ +class { + + function configure(){ + /** + * -------------------------------------------------------------------------- + * App Routes + * -------------------------------------------------------------------------- + * Here is where you can register the routes for your web application! + * Go get Funky! + */ + + // A nice healthcheck route example + route( "/healthcheck", function( event, rc, prc ){ + return "Ok!" + } ) + + // API Echo + get( "/api/echo", "Echo.index" ) + + // API Authentication Routes + post( "/api/login", "Auth.login" ) + post( "/api/logout", "Auth.logout" ) + post( "/api/register", "Auth.register" ) + + // API Secured Routes + get( "/api/whoami", "Echo.whoami" ) + + // @app_routes@ + + // Conventions-Based Routing + route( ":handler/:action?" ).end() + } + +} diff --git a/resources/rest/apidocs/_schemas/app-response.json b/resources/rest/apidocs/_schemas/app-response.json new file mode 100644 index 0000000..f34fea7 --- /dev/null +++ b/resources/rest/apidocs/_schemas/app-response.json @@ -0,0 +1,26 @@ +{ + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The data packet", + "type": "object", + "properties": {} + } + } +} \ No newline at end of file diff --git a/resources/rest/apidocs/_schemas/app-user.json b/resources/rest/apidocs/_schemas/app-user.json new file mode 100644 index 0000000..04e9536 --- /dev/null +++ b/resources/rest/apidocs/_schemas/app-user.json @@ -0,0 +1,36 @@ +{ + "description": "The user that is logged in", + "type": "object", + "properties": { + "id": { + "description": "The User ID", + "type": "integer" + }, + "firstName": { + "description": "The user's first name", + "type": "string" + }, + "lastName": { + "description": "The user's last name", + "type": "integer" + }, + "username": { + "description": "The user's username", + "type": "string" + }, + "permissions": { + "description": "The collection of permissions", + "type": "array", + "items" : { + "type" : "string" + } + }, + "roles": { + "description": "The collection of roles", + "type": "array", + "items" : { + "type" : "string" + } + } + } +} diff --git a/resources/rest/apidocs/_schemas/app-validation.json b/resources/rest/apidocs/_schemas/app-validation.json new file mode 100644 index 0000000..2c4d329 --- /dev/null +++ b/resources/rest/apidocs/_schemas/app-validation.json @@ -0,0 +1,33 @@ +{ + "description" : "An invalid field will contain an array of information messages", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "MESSAGE" :{ + "type" : "string", + "description" : "The human readable error message" + }, + "VALIDATIONDATA" :{ + "type" : "boolean", + "description" : "The validation data attached" + }, + "ERRORMETADATA" :{ + "type" : "object", + "description" : "Any error metdata attached" + }, + "REJECTEDVALUE" :{ + "type" : "string", + "description" : "The rejected value if any" + }, + "VALIDATIONTYPE" :{ + "type" : "string", + "description" : "The contraint type applied" + }, + "FIELD" :{ + "type" : "string", + "description" : "The field name that caused the validation" + } + } + } +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/login/example.200.json b/resources/rest/apidocs/auth/login/example.200.json new file mode 100644 index 0000000..b7152bc --- /dev/null +++ b/resources/rest/apidocs/auth/login/example.200.json @@ -0,0 +1,8 @@ +{ + "data": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1ODkzMDUwMzEsInNjb3BlcyI6W10sImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NjUxMDAvIiwic3ViIjoxLCJleHAiOjE1ODkzMDg2MzEsImp0aSI6IkQxNkI4Qzg5NTc2OUU5MDcyNUNBQTU3M0I3M0FCNTc2In0.VDsqEDCXtHnrMXZYXqE2To2lQpmQAmHP8asXjhw_c2KsI_1mx2gZETuIrVNXckUl9zdevx2O818PlCkp6znmGw", + "error": false, + "pagination": {}, + "messages": [ + "Bearer token created and it expires in 60 minutes" + ] +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/login/example.401.json b/resources/rest/apidocs/auth/login/example.401.json new file mode 100644 index 0000000..e4e16aa --- /dev/null +++ b/resources/rest/apidocs/auth/login/example.401.json @@ -0,0 +1,49 @@ +{ + "data": { + "lastName": [ + { + "MESSAGE": "The 'lastName' value is required", + "VALIDATIONDATA": true, + "ERRORMETADATA": {}, + "REJECTEDVALUE": "", + "VALIDATIONTYPE": "Required", + "FIELD": "lastName" + } + ], + "firstName": [ + { + "MESSAGE": "The 'firstName' value is required", + "VALIDATIONDATA": true, + "ERRORMETADATA": {}, + "REJECTEDVALUE": "", + "VALIDATIONTYPE": "Required", + "FIELD": "firstName" + } + ], + "PASSWORD": [ + { + "MESSAGE": "The 'PASSWORD' value is required", + "VALIDATIONDATA": true, + "ERRORMETADATA": {}, + "REJECTEDVALUE": "", + "VALIDATIONTYPE": "Required", + "FIELD": "PASSWORD" + } + ], + "USERNAME": [ + { + "MESSAGE": "The 'USERNAME' value is required", + "VALIDATIONDATA": true, + "ERRORMETADATA": {}, + "REJECTEDVALUE": "", + "VALIDATIONTYPE": "Required", + "FIELD": "USERNAME" + } + ] + }, + "error": true, + "pagination": {}, + "messages": [ + "Validation exceptions occurred, please see the data" + ] +} diff --git a/resources/rest/apidocs/auth/login/requestBody.json b/resources/rest/apidocs/auth/login/requestBody.json new file mode 100644 index 0000000..c671dce --- /dev/null +++ b/resources/rest/apidocs/auth/login/requestBody.json @@ -0,0 +1,29 @@ +{ + "description": "Needed fields to login a user", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "description": "The username to use for login in", + "type": "string" + }, + "password": { + "description": "The password to use for login in", + "type": "string" + } + }, + "example": { + "username": "user", + "password": "test" + } + } + } + } +} diff --git a/resources/rest/apidocs/auth/login/responses.json b/resources/rest/apidocs/auth/login/responses.json new file mode 100644 index 0000000..dacb371 --- /dev/null +++ b/resources/rest/apidocs/auth/login/responses.json @@ -0,0 +1,88 @@ +{ + "200": { + "description": "Application registration successful", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The data packet of the registration", + "type": "object", + "properties" : { + "token" : { + "type" : "string", + "description" : "The beaerer token created for the registration" + }, + "user" : { + "$ref" : "../../_schemas/app-user.json" + } + } + } + } + }, + "example": { + "$ref": "example.200.json" + } + } + } + }, + + "401": { + "description": "Validation exception", + "content": { + "application/json": { + "example": { + "$ref": "example.401.json" + }, + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The validation data packet", + "type": "object", + "properties": { + "{invalidField}" : { + "$ref" : "../../_schemas/app-validation.json" + } + } + } + + } + } + } + } + } +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/logout/example.200.json b/resources/rest/apidocs/auth/logout/example.200.json new file mode 100644 index 0000000..d229492 --- /dev/null +++ b/resources/rest/apidocs/auth/logout/example.200.json @@ -0,0 +1,8 @@ +{ + "data": {}, + "error": false, + "pagination": {}, + "messages": [ + "Successfully logged out" + ] +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/logout/example.500.json b/resources/rest/apidocs/auth/logout/example.500.json new file mode 100644 index 0000000..e273346 --- /dev/null +++ b/resources/rest/apidocs/auth/logout/example.500.json @@ -0,0 +1,8 @@ +{ + "data": {}, + "error": true, + "pagination": {}, + "messages": [ + "General application error: Token not found in authorization header or the custom header or the request collection" + ] +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/logout/responses.json b/resources/rest/apidocs/auth/logout/responses.json new file mode 100644 index 0000000..1aa9068 --- /dev/null +++ b/resources/rest/apidocs/auth/logout/responses.json @@ -0,0 +1,29 @@ +{ + "200": { + "description": "Authenticate in the application", + "content": { + "application/json": { + "schema": { + "$ref" : "../../_schemas/app-response.json" + }, + "example": { + "$ref": "example.200.json" + } + } + } + }, + + "500": { + "description": "Invalid token or expired token or no token", + "content": { + "application/json": { + "example": { + "$ref": "example.500.json" + }, + "schema": { + "$ref" : "../../_schemas/app-response.json" + } + } + } + } +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/register/example.200.json b/resources/rest/apidocs/auth/register/example.200.json new file mode 100644 index 0000000..0422615 --- /dev/null +++ b/resources/rest/apidocs/auth/register/example.200.json @@ -0,0 +1,18 @@ +{ + "data": { + "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE1ODkzMTI3NzcsInNjb3BlcyI6W10sImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NjUxMDAvIiwic3ViIjoiRTBDMTlDNjgtRDA5MC00QzY4LUE5NUY4NUIzRDFGRDZCODIiLCJleHAiOjE1ODkzMTYzNzcsImp0aSI6IjQ3Q0QxMDlDNTA4NTdDQTlGRUVBMDY0NzNEQzMxRkY3In0.I3F-MFsy7BbgLYwt6L9FR1HMNu6ZsUpkFQRgbpXdaPBbIbHRznWEw3MTsTKf654g0eO_yE5JVhGYYRzn_VyR6g", + "user": { + "lastName": "majano", + "permissions": [], + "roles": [], + "firstName": "luis", + "id": "E0C19C68-D090-4C68-A95F85B3D1FD6B82", + "username": "testing" + } + }, + "error": false, + "pagination": {}, + "messages": [ + "User registered correctly and Bearer token created and it expires in 60 minutes" + ] +} diff --git a/resources/rest/apidocs/auth/register/example.400.json b/resources/rest/apidocs/auth/register/example.400.json new file mode 100644 index 0000000..4a96f91 --- /dev/null +++ b/resources/rest/apidocs/auth/register/example.400.json @@ -0,0 +1,8 @@ +{ + "data": {}, + "error": true, + "pagination": {}, + "messages": [ + "Invalid or Missing Authentication Credentials" + ] +} \ No newline at end of file diff --git a/resources/rest/apidocs/auth/register/requestBody.json b/resources/rest/apidocs/auth/register/requestBody.json new file mode 100644 index 0000000..4237ac0 --- /dev/null +++ b/resources/rest/apidocs/auth/register/requestBody.json @@ -0,0 +1,41 @@ +{ + "description": "Needed fields to register a user", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "firstName", + "lastName", + "username", + "password" + ], + "properties": { + "firstName": { + "description": "The user's first name", + "type": "string" + }, + "lastName": { + "description": "The user's last name", + "type": "string" + }, + "username": { + "description": "The username to use for login in", + "type": "string" + }, + "password": { + "description": "The password to use for login in", + "type": "string" + } + }, + "example": { + "firstName" : "luis", + "lastName" : "majano", + "username": "user", + "password": "test" + } + } + } + } +} diff --git a/resources/rest/apidocs/auth/register/responses.json b/resources/rest/apidocs/auth/register/responses.json new file mode 100644 index 0000000..926ed6e --- /dev/null +++ b/resources/rest/apidocs/auth/register/responses.json @@ -0,0 +1,75 @@ +{ + "200": { + "description": "Authenticate in the application", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The bearer token created for the user", + "type": "string" + } + } + }, + "example": { + "$ref": "example.200.json" + } + } + } + }, + + "400": { + "description": "Invalid or Missing Authentication Credentials", + "content": { + "application/json": { + "example": { + "$ref": "example.400.json" + }, + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The data packet", + "type": "object", + "properties": {} + } + + } + } + } + } + } +} \ No newline at end of file diff --git a/resources/rest/apidocs/echo/index/example.200.json b/resources/rest/apidocs/echo/index/example.200.json new file mode 100644 index 0000000..bb28334 --- /dev/null +++ b/resources/rest/apidocs/echo/index/example.200.json @@ -0,0 +1,6 @@ +{ + "data": "Welcome to my ColdBox RESTFul Service", + "error": false, + "pagination": {}, + "messages": [] +} \ No newline at end of file diff --git a/resources/rest/apidocs/echo/index/responses.json b/resources/rest/apidocs/echo/index/responses.json new file mode 100644 index 0000000..cbb01d3 --- /dev/null +++ b/resources/rest/apidocs/echo/index/responses.json @@ -0,0 +1,37 @@ +{ + "200": { + "description": "A welcome message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The welcome message", + "type": "string" + } + } + }, + "example": { + "$ref": "example.200.json" + } + } + } + } +} \ No newline at end of file diff --git a/resources/rest/apidocs/echo/whoami/example.200.json b/resources/rest/apidocs/echo/whoami/example.200.json new file mode 100644 index 0000000..de9d0b8 --- /dev/null +++ b/resources/rest/apidocs/echo/whoami/example.200.json @@ -0,0 +1,13 @@ +{ + "data": { + "lastName": "admin", + "permissions": [], + "roles": [], + "firstName": "admin", + "id": 1, + "username": "admin" + }, + "error": false, + "pagination": {}, + "messages": [] +} diff --git a/resources/rest/apidocs/echo/whoami/example.401.json b/resources/rest/apidocs/echo/whoami/example.401.json new file mode 100644 index 0000000..4a96f91 --- /dev/null +++ b/resources/rest/apidocs/echo/whoami/example.401.json @@ -0,0 +1,8 @@ +{ + "data": {}, + "error": true, + "pagination": {}, + "messages": [ + "Invalid or Missing Authentication Credentials" + ] +} \ No newline at end of file diff --git a/resources/rest/apidocs/echo/whoami/responses.json b/resources/rest/apidocs/echo/whoami/responses.json new file mode 100644 index 0000000..3d9d756 --- /dev/null +++ b/resources/rest/apidocs/echo/whoami/responses.json @@ -0,0 +1,74 @@ +{ + "200": { + "description": "Returns the logged in user information", + "content": { + "application/json": { + "example": { + "$ref": "example.200.json" + }, + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "$ref" : "../../_schemas/app-user.json" + } + } + } + } + } + }, + + "401": { + "description": "Invalid or Missing Authentication Credentials", + "content": { + "application/json": { + "example": { + "$ref": "example.401.json" + }, + "schema": { + "type": "object", + "properties": { + "error": { + "description": "Flag to indicate an error.", + "type": "boolean" + }, + "messages": { + "description": "An array of messages related to the request.", + "type": "array", + "items": { + "type": "string" + } + }, + "pagination" : { + "description": "Pagination information.", + "type": "object", + "properties": {} + }, + "data": { + "description": "The data packet", + "type": "object", + "properties": {} + } + + } + } + } + } + } +} \ No newline at end of file diff --git a/resources/rest/config/modules/cbauth.bx b/resources/rest/config/modules/cbauth.bx new file mode 100644 index 0000000..8b09b87 --- /dev/null +++ b/resources/rest/config/modules/cbauth.bx @@ -0,0 +1,30 @@ +class { + + /** + * Configure CBAuth for operation + * https://cbauth.ortusbooks.com/installation-and-usage#configuration + */ + function configure(){ + return { + /** + *-------------------------------------------------------------------------- + * User Service Class + *-------------------------------------------------------------------------- + * The user service class to use for authentication which must implement IUserService + * https://cbauth.ortusbooks.com/iuserservice + * The User object that this class returns must implement IUser as well + * https://cbauth.ortusbooks.com/iauthuser + */ + "userServiceClass" : "UserService", + /** + *------------------------------------------------------------------------- + * Storage Classes + *------------------------------------------------------------------------- + * Which storages to use for tracking session and the request scope + */ + "sessionStorage" : "SessionStorage@cbstorages", + "requestStorage" : "RequestStorage@cbstorages" + }; + } + +} diff --git a/resources/rest/config/modules/cbsecurity.bx b/resources/rest/config/modules/cbsecurity.bx new file mode 100644 index 0000000..ec7afb7 --- /dev/null +++ b/resources/rest/config/modules/cbsecurity.bx @@ -0,0 +1,251 @@ +class { + + function configure(){ + return { + /** + * -------------------------------------------------------------------------- + * Authentication Services + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/authentication + * + * Here you will configure which service is in charge of providing authentication for your application. + * By default we leverage the cbauth module which expects you to connect it to a database via your own User Service. + * + * Available authentication providers: + * - cbauth : Leverages your own UserService that determines authentication and user retrieval + * - basicAuth : Leverages basic authentication and basic in-memory user registration in our configuration + * - custom : Any other service that adheres to our IAuthService interface + */ + authentication : { + // The WireBox ID of the auth service to use which must adhere to the cbsecurity.interfaces.IAuthService interface. + "provider" : "authenticationService@cbauth", + // The name of the variable to use to store an authenticated user in prc scope on all incoming authenticated requests + "prcUserVariable" : "oCurrentUser" + }, + /** + * -------------------------------------------------------------------------- + * Basic Auth + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/basic-auth + * + * If you are using the basicAuth authentication provider, then you can configure it here, else ignore or remove. + */ + basicAuth : { + // Hashing algorithm to use + hashAlgorithm : "SHA-512", + // Iterates the number of times the hash is computed to create a more computationally intensive hash. + hashIterations : 5, + // User storage: The `key` is the username. The value is the user credentials that can include + // { roles: "", permissions : "", firstName : "", lastName : "", password : "" } + users : {} + }, + /** + * -------------------------------------------------------------------------- + * Firewall Settings + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/firewall + * + * The firewall is used to block/check access on incoming requests via security rules or via annotation on handler actions. + * Here you can configure the operation of the firewall and especially what Validator will be in charge of verifying authentication/authorization + * during a matched request. + */ + firewall : { + // Auto load the global security firewall automatically, else you can load it a-la-carte via the `Security` interceptor + "autoLoadFirewall" : true, + // The Global validator is an object that will validate the firewall rules and annotations and provide feedback on either authentication or authorization issues. + "validator" : "JwtAuthValidator@cbsecurity", + // Activate handler/action based annotation security + "handlerAnnotationSecurity" : true, + // The global invalid authentication event or URI or URL to go if an invalid authentication occurs + "invalidAuthenticationEvent" : "echo.onAuthenticationFailure", + // Default Auhtentication Action: override or redirect when a user has not logged in + "defaultAuthenticationAction" : "override", + // The global invalid authorization event or URI or URL to go if an invalid authorization occurs + "invalidAuthorizationEvent" : "echo.onAuthorizationFailure", + // Default Authorization Action: override or redirect when a user does not have enough permissions to access something + "defaultAuthorizationAction" : "override", + // Firewall database event logs. + "logs" : { + "enabled" : false, + "dsn" : "", + "schema" : "", + "table" : "cbsecurity_logs", + "autoCreate" : true + }, + // Firewall Rules, this can be a struct of detailed configuration + // or a simple array of inline rules + "rules" : { + // Use regular expression matching on the rule match types + "useRegex" : true, + // Force SSL for all relocations + "useSSL" : false, + // A collection of default name-value pairs to add to ALL rules + // This way you can add global roles, permissions, redirects, etc + "defaults" : {}, + // You can store all your rules in this inline array + "inline" : [], + // If you don't store the rules inline, then you can use a provider to load the rules + // The source can be a json file, an xml file, model, db + // Each provider can have it's appropriate properties as well. Please see the documentation for each provider. + "provider" : { "source" : "", "properties" : {} } + } + }, + /** + * -------------------------------------------------------------------------- + * Json Web Tokens + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/jwt + * + * Here you configure how JSON Web Tokens are created, validated and stored. + */ + jwt : { + // The issuer authority for the tokens, placed in the `iss` claim + issuer : "", + // The jwt secret encoding key, defaults to getSystemEnv( "JWT_SECRET", "" ) + // This key is only effective within the `config/Coldbox.cfc`. Specifying within a module does nothing. + secretKey : getSystemSetting( "JWT_SECRET", "" ), + // by default it uses the authorization bearer header, but you can also pass a custom one as well. + customAuthHeader : "x-auth-token", + // The expiration in minutes for the jwt tokens + expiration : 60, + // If true, enables refresh tokens, token creation methods will return a struct instead + // of just the access token. e.g. { access_token: "", refresh_token : "" } + enableRefreshTokens : false, + // The default expiration for refresh tokens, defaults to 30 days + refreshExpiration : 10080, + // The Custom header to inspect for refresh tokens + customRefreshHeader : "x-refresh-token", + // If enabled, the JWT validator will inspect the request for refresh tokens and expired access tokens + // It will then automatically refresh them for you and return them back as + // response headers in the same request according to the customRefreshHeader and customAuthHeader + enableAutoRefreshValidator : false, + // Enable the POST > /cbsecurity/refreshtoken API endpoint + enableRefreshEndpoint : true, + // encryption algorithm to use, valid algorithms are: HS256, HS384, and HS512 + algorithm : "HS512", + // Which claims neds to be present on the jwt token or `TokenInvalidException` upon verification and decoding + requiredClaims : [], + // The token storage settings + tokenStorage : { + // enable or not, default is true + "enabled" : true, + // A cache key prefix to use when storing the tokens + "keyPrefix" : "cbjwt_", + // The driver to use: db, cachebox or a WireBox ID + "driver" : "cachebox", + // Driver specific properties + "properties" : { cacheName : "default" } + } + }, + /** + * -------------------------------------------------------------------------- + * Security Headers + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/security-headers + * + * This section is the way to configure cbsecurity for header detection, inspection and setting for common + * security exploits like XSS, ClickJacking, Host Spoofing, IP Spoofing, Non SSL usage, HSTS and much more. + */ + securityHeaders : { + // If you trust the upstream then we will check the upstream first for specific headers + "trustUpstream" : false, + // Content Security Policy + // Content Security Policy (CSP) is an added layer of security that helps to detect and mitigate certain types of attacks, + // including Cross-Site Scripting (XSS) and data injection attacks. These attacks are used for everything from data theft, to + // site defacement, to malware distribution. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP + "contentSecurityPolicy" : { + // Disabled by defautl as it is totally customizable + "enabled" : false, + // The custom policy to use, by default we don't include any + "policy" : "" + }, + // The X-Content-Type-Options response HTTP header is a marker used by the server to indicate that the MIME types advertised in + // the Content-Type headers should be followed and not be changed => X-Content-Type-Options: nosniff + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + "contentTypeOptions" : { "enabled" : true }, + "customHeaders" : { + // Name : value pairs as you see fit. + }, + // Disable Click jacking: X-Frame-Options: DENY OR SAMEORIGIN + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + "frameOptions" : { "enabled" : true, "value" : "SAMEORIGIN" }, + // HTTP Strict Transport Security (HSTS) + // The HTTP Strict-Transport-Security response header (often abbreviated as HSTS) + // informs browsers that the site should only be accessed using HTTPS, and that any future attempts to access it + // using HTTP should automatically be converted to HTTPS. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security, + "hsts" : { + "enabled" : true, + // The time, in seconds, that the browser should remember that a site is only to be accessed using HTTPS, 1 year is the default + "max-age" : "31536000", + // See Preloading Strict Transport Security for details. Not part of the specification. + "preload" : false, + // If this optional parameter is specified, this rule applies to all of the site's subdomains as well. + "includeSubDomains" : false + }, + // Validates the host or x-forwarded-host to an allowed list of valid hosts + "hostHeaderValidation" : { + "enabled" : false, + // Allowed hosts list + "allowedHosts" : "" + }, + // Validates the ip address of the incoming request + "ipValidation" : { + "enabled" : false, + // Allowed IP list + "allowedIPs" : "" + }, + // The Referrer-Policy HTTP header controls how much referrer information (sent with the Referer header) should be included with requests. + // Aside from the HTTP header, you can set this policy in HTML. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy + "referrerPolicy" : { "enabled" : true, "policy" : "same-origin" }, + // Detect if the incoming requests are NON-SSL and if enabled, redirect with SSL + "secureSSLRedirects" : { "enabled" : false }, + // Some browsers have built in support for filtering out reflected XSS attacks. Not foolproof, but it assists in XSS protection. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection, + // X-XSS-Protection: 1; mode=block + "xssProtection" : { "enabled" : true, "mode" : "block" } + }, + /** + * -------------------------------------------------------------------------- + * Security Visualizer + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/visualizer + * + * This is a debugging panel that when active, a developer can visualize security settings and more. + * You can use the `securityRule` to define what rule you want to use to secure the visualizer but make sure the `secured` flag is turned to true. + * You don't have to specify the `secureList` key, we will do that for you. + */ + visualizer : { + "enabled" : false, + "secured" : false, + "securityRule" : {} + }, + /** + * -------------------------------------------------------------------------- + * Cross Site Request Forgery (CSRF) + * -------------------------------------------------------------------------- + * https://coldbox-security.ortusbooks.com/getting-started/configuration/csrf + * + * This section is the way to configure cbsecurity for CSRF detection and mitigation. + */ + csrf : { + // By default we load up an interceptor that verifies all non-GET incoming requests against the token validations + enableAutoVerifier : false, + // A list of events to exclude from csrf verification, regex allowed: e.g. stripe\..* + verifyExcludes : [], + // By default, all csrf tokens have a life-span of 30 minutes. After 30 minutes, they expire and we aut-generate new ones. + // If you do not want expiring tokens, then set this value to 0 + rotationTimeout : 30, + // Enable the /cbcsrf/generate endpoint to generate cbcsrf tokens for secured users. + enableEndpoint : false, + // The WireBox mapping to use for the CacheStorage + cacheStorage : "CacheStorage@cbstorages", + // Enable/Disable the cbAuth login/logout listener in order to rotate keys + enableAuthTokenRotator : true + } + }; + } + +} diff --git a/resources/rest/config/modules/cbswagger.bx b/resources/rest/config/modules/cbswagger.bx new file mode 100644 index 0000000..a0b6b9f --- /dev/null +++ b/resources/rest/config/modules/cbswagger.bx @@ -0,0 +1,88 @@ +class { + + /** + * CBSwagger Configuration + * https://github.com/coldbox-modules/cbswagger + */ + function configure(){ + return { + // The route prefix to search. Routes beginning with this prefix will be determined to be api routes + "routes" : [ "api" ], + // Any routes to exclude + "excludeRoutes" : [], + // The default output format: json or yml + "defaultFormat" : "json", + // A convention route, relative to your app root, where request/response samples are stored ( e.g. resources/apidocs/responses/[module].[handler].[action].[HTTP Status Code].json ) + "samplesPath" : "resources/apidocs", + // Information about your API + "info" : { + // A title for your API + "title" : "ColdBox REST Template", + // A description of your API + "description" : "This API produces amazing results and data.", + // A terms of service URL for your API + "termsOfService" : "", + // The contact email address + "contact" : { + "name" : "API Support", + "url" : "https://www.swagger.io/support", + "email" : "info@ortussolutions.com" + }, + // A url to the License of your API + "license" : { + "name" : "Apache 2.0", + "url" : "https://www.apache.org/licenses/LICENSE-2.0.html" + }, + // The version of your API + "version" : "1.0.0" + }, + // Tags + "tags" : [], + // https://swagger.io/specification/#externalDocumentationObject + "externalDocs" : { + "description" : "Find more info here", + "url" : "https://blog.readme.io/an-example-filled-guide-to-swagger-3-2/" + }, + // https://swagger.io/specification/#serverObject + "servers" : [ + { + "url" : "https://mysite.com/v1", + "description" : "The main production server" + }, + { + "url" : "http://127.0.0.1:60299", + "description" : "The dev server" + } + ], + // An element to hold various schemas for the specification. + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#componentsObject + "components" : { + // Define your security schemes here + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securitySchemeObject + "securitySchemes" : { + "ApiKeyAuth" : { + "type" : "apiKey", + "description" : "User your JWT as an Api Key for security", + "name" : "x-api-key", + "in" : "header" + }, + "bearerAuth" : { + "type" : "http", + "scheme" : "bearer", + "bearerFormat" : "JWT" + } + } + } + + // A default declaration of Security Requirement Objects to be used across the API. + // https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#securityRequirementObject + // Only one of these requirements needs to be satisfied to authorize a request. + // Individual operations may set their own requirements with `@security` + // "security" : [ + // { "APIKey" : [] }, + // { "UserSecurity" : [] } + // ] + }; + } + +} diff --git a/resources/rest/handlers/Auth.bx b/resources/rest/handlers/Auth.bx new file mode 100644 index 0000000..81d9c59 --- /dev/null +++ b/resources/rest/handlers/Auth.bx @@ -0,0 +1,75 @@ +/** + * Authentication Handler + */ +class extends="coldbox.system.RestHandler" { + + // Injection + @inject( "UserService" ) + property name="userService"; + + /** + * Login a user into the application + * + * @x-route (POST) /api/login + * @requestBody ~auth/login/requestBody.json + * @response-default ~auth/login/responses.json##200 + * @response-401 ~auth/login/responses.json##401 + */ + function login( event, rc, prc ){ + param rc.username = ""; + param rc.password = ""; + + // This can throw a InvalidCredentials exception which is picked up by the REST handler + var token = jwtAuth().attempt( rc.username, rc.password ) + + event + .getResponse() + .setData( token ) + .addMessage( + "Bearer token created and it expires in #jwtAuth().getSettings().jwt.expiration# minutes" + ) + } + + /** + * Register a new user in the system + * + * @x-route (POST) /api/register + * @requestBody ~auth/register/requestBody.json + * @response-default ~auth/register/responses.json##200 + * @response-400 ~auth/register/responses.json##400 + */ + function register( event, rc, prc ){ + param rc.firstName = ""; + param rc.lastName = ""; + param rc.username = ""; + param rc.password = ""; + + // Populate, Validate, Create a new user + prc.oUser = userService.create( populateModel( "User" ).validateOrFail() ); + + // Log them in if it was created! + event + .getResponse() + .setData( { + "token" : jwtAuth().fromuser( prc.oUser ), + "user" : prc.oUser.getMemento() + } ) + .addMessage( + "User registered correctly and Bearer token created and it expires in #jwtAuth().getSettings().jwt.expiration# minutes" + ) + } + + /** + * Logout a user + * + * @x-route (POST) /api/logout + * @security bearerAuth,ApiKeyAuth + * @response-default ~auth/logout/responses.json##200 + * @response-500 ~auth/logout/responses.json##500 + */ + function logout( event, rc, prc ){ + jwtAuth().logout(); + event.getResponse().addMessage( "Successfully logged out" ) + } + +} diff --git a/resources/rest/handlers/Echo.bx b/resources/rest/handlers/Echo.bx new file mode 100644 index 0000000..f80b538 --- /dev/null +++ b/resources/rest/handlers/Echo.bx @@ -0,0 +1,43 @@ +/** + * My RESTFul Event Handler + */ +class extends="coldbox.system.RestHandler" { + + // OPTIONAL HANDLER PROPERTIES + this.prehandler_only = "" + this.prehandler_except = "" + this.posthandler_only = "" + this.posthandler_except = "" + this.aroundHandler_only = "" + this.aroundHandler_except = "" + + // REST Allowed HTTP Methods Ex: this.allowedMethods = {delete='POST,DELETE',index='GET'} + this.allowedMethods = {} + + /** + * Say Hello + * + * @x-route (GET) /api/echo + * @response-default ~echo/index/responses.json##200 + */ + function index( event, rc, prc ){ + event.getResponse().setData( "Welcome to my ColdBox RESTFul Service" ) + } + + + /** + * A secured route that shows you your information + * + * @x-route (GET) /api/whoami + * @security bearerAuth,ApiKeyAuth + * @response-default ~echo/whoami/responses.json##200 + * @response-401 ~echo/whoami/responses.json##401 + */ + @secured + function whoami( event, rc, prc ){ + event.getResponse().setData( + jwtAuth().getUser().getMemento() + ) + } + +} diff --git a/resources/rest/handlers/Main.bx b/resources/rest/handlers/Main.bx new file mode 100644 index 0000000..dedddfd --- /dev/null +++ b/resources/rest/handlers/Main.bx @@ -0,0 +1,38 @@ +/** + * Main Application Event Handler + */ +class extends="coldbox.system.EventHandler" { + + /** + * -------------------------------------------------------------------------- + * Implicit Actions + * -------------------------------------------------------------------------- + * All the implicit actions below MUST be declared in the config/Coldbox.bx in order to fire. + * https://coldbox.ortusbooks.com/getting-started/configuration/coldbox.cfc/configuration-directives/coldbox#implicit-event-settings + */ + + function onAppInit( event, rc, prc ){ + } + + function onRequestStart( event, rc, prc ){ + } + + function onRequestEnd( event, rc, prc ){ + } + + function onSessionStart( event, rc, prc ){ + } + + function onSessionEnd( event, rc, prc ){ + var sessionScope = event.getValue( "sessionReference" ); + var applicationScope = event.getValue( "applicationReference" ); + } + + function onException( event, rc, prc ){ + event.setHTTPHeader( statusCode = 500 ); + // Grab Exception From private request collection, placed by ColdBox Exception Handling + var exception = prc.exception; + // Place exception handler below: + } + +} diff --git a/resources/rest/models/.gitkeep b/resources/rest/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/rest/models/User.bx b/resources/rest/models/User.bx new file mode 100644 index 0000000..f22654d --- /dev/null +++ b/resources/rest/models/User.bx @@ -0,0 +1,30 @@ +/** + * A user in the system. + * + * This user is based off the Auth User included in cbsecurity, which implements already several interfaces and properties. + * - https://coldbox-security.ortusbooks.com/usage/authentication-services#iauthuser + * - https://coldbox-security.ortusbooks.com/jwt/jwt-services#jwt-subject-interface + * + * It also leverages several delegates for Validation, Population, Authentication, Authorization and JWT Subject. + */ +@transientCache( false ) +class + extends ="cbsecurity.models.auth.User" + delegates =" + Validatable@cbvalidation, + Population@cbDelegates, + Auth@cbSecurity, + Authorizable@cbSecurity, + JwtSubject@cbSecurity + " +{ + + /** + * Constructor + */ + function init(){ + super.init() + return this + } + +} diff --git a/resources/rest/models/UserService.bx b/resources/rest/models/UserService.bx new file mode 100644 index 0000000..bf3f63b --- /dev/null +++ b/resources/rest/models/UserService.bx @@ -0,0 +1,133 @@ +/** + * This service provides user authentication, retrieval and much more. + * Implements the CBSecurity IUserService: https://coldbox-security.ortusbooks.com/usage/authentication-services#iuserservice + */ +@singleton +class{ + + /** + * -------------------------------------------------------------------------- + * DI + * -------------------------------------------------------------------------- + */ + + @inject( "wirebox:populator" ) + property name="populator"; + + /** + * -------------------------------------------------------------------------- + * Properties + * -------------------------------------------------------------------------- + */ + + /** + * TODO: Mock users, remove when coding + */ + property name="mockUsers"; + + /** + * Constructor + */ + function init(){ + // We are mocking only 1 user right now, update as you see fit + variables.mockUsers = [ + { + "id" : 1, + "firstName" : "admin", + "lastName" : "admin", + "username" : "admin", + "password" : "admin", + "roles" : [], + "permissions" : [] + } + ]; + + return this; + } + + /** + * Construct a new user object via WireBox Providers + */ + @provider( "User" ) + User function new(){ + } + + /** + * Create a new user in the system + * + * @user The user to create + * + * @return The created user + */ + User function create( required user ){ + arguments.user.setId( createUUID() ); + + variables.mockUsers.append( { + "id" : arguments.user.getId(), + "firstName" : arguments.user.getFirstName(), + "lastName" : arguments.user.getLastName(), + "username" : arguments.user.getUsername(), + "password" : arguments.user.getPassword(), + "roles" : arguments.user.getRoles(), + "permissions" : arguments.user.getPermissions() + } ); + return arguments.user; + } + + /** + * Verify if the incoming username/password are valid credentials. + * + * @username The username + * @password The password + */ + boolean function isValidCredentials( required username, required password ){ + var oTarget = retrieveUserByUsername( arguments.username ); + if ( !oTarget.isLoaded() ) { + return false; + } + + // Check Password Here: Remember to use bcrypt + return ( oTarget.getPassword().compareNoCase( arguments.password ) == 0 ); + } + + /** + * Retrieve a user by username + * + * @return User that implements JWTSubject and/or IAuthUser + */ + function retrieveUserByUsername( required username ){ + return variables.mockUsers + .filter( function( record ){ + return arguments.record.username == username; + } ) + .reduce( function( result, record ){ + return variables.populator.populateFromStruct( + target : arguments.result, + memento : arguments.record, + ignoreTargetLists: true + ); + }, new () ); + } + + /** + * Retrieve a user by unique identifier + * + * @id The unique identifier + * + * @return User that implements JWTSubject and/or IAuthUser + */ + User function retrieveUserById( required id ){ + return variables.mockUsers + .filter( function( record ){ + return arguments.record.id == id; + } ) + .reduce( function( result, record ){ + return variables.populator.populateFromStruct( + target : arguments.result, + memento : arguments.record, + ignoreTargetLists: true + ); + }, new () ); + } + +} diff --git a/resources/rest/specs/integration/AuthTests.bx b/resources/rest/specs/integration/AuthTests.bx new file mode 100644 index 0000000..a83b2bd --- /dev/null +++ b/resources/rest/specs/integration/AuthTests.bx @@ -0,0 +1,131 @@ +@autowire +class extends="coldbox.system.testing.BaseTestCase" { + + @inject( "provider:JwtService@cbsecurity" ) + property name="jwtService"; + + @inject( "provider:authenticationService@cbauth" ) + property name="cbauth"; + + /*********************************** LIFE CYCLE Methods ***********************************/ + + function beforeAll(){ + super.beforeAll(); + } + + function afterAll(){ + super.afterAll(); + } + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + describe( "RESTFul Authentication", () => { + beforeEach( ( currentSpec ) => { + // Setup as a new ColdBox request, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST. + setup(); + + // Make sure nothing is logged in to start our calls + cbauth.logout(); + jwtService.getTokenStorage().clearAll(); + } ); + + story( "I want to authenticate a user and receive a JWT token", () => { + given( "a valid username and password", () => { + then( "I will be authenticated and will receive the JWT token", () => { + // Use a user in the seeded db + var event = this.post( + route = "/api/login", + params = { username : "admin", password : "admin" } + ); + var response = event.getPrivateValue( "Response" ); + expect( response.getError() ).toBeFalse( response.getMessages().toString() ); + expect( response.getData() ).toBeString(); + + // debug( response.getData() ); + + var decoded = jwtService.decode( response.getData() ); + expect( decoded.sub ).toBe( 1 ); + expect( decoded.exp ).toBeGTE( dateAdd( "h", 1, decoded.iat ) ); + } ); + } ); + given( "invalid username and password", () => { + then( "I will receive a 401 exception ", () => { + var event = this.post( + route = "/api/login", + params = { username : "invalid", password : "invalid" } + ); + var response = event.getPrivateValue( "Response" ); + expect( response.getError() ).toBeTrue(); + expect( response.getStatusCode() ).toBe( 401 ); + } ); + } ); + } ); + + story( "I want to register into the system", () => { + given( "valid registration details", () => { + then( "I should register, log in and get a token", () => { + // Use a user in the seeded db + var event = this.post( + route = "/api/register", + params = { + firstName : "luis", + lastName : "majano", + username : "lmajano@coldbox.org", + password : "lmajano" + } + ); + var response = event.getPrivateValue( "Response" ); + expect( response.getError() ).toBeFalse( response.getMessages().toString() ); + expect( response.getData() ).toHaveKey( "token,user" ); + + // debug( response.getData() ); + + var decoded = jwtService.decode( response.getData().token ); + expect( decoded.sub ).toBe( response.getData().user.id ); + expect( decoded.exp ).toBeGTE( dateAdd( "h", 1, decoded.iat ) ); + } ); + } ); + given( "invalid registration details", () => { + then( "I should get an error message", () => { + var event = this.post( route = "/api/register", params = {} ); + var response = event.getPrivateValue( "Response" ); + // debug( response.getMemento() ); + expect( response.getError() ).toBeTrue(); + expect( response.getStatusCode() ).toBe( 400 ); + } ); + } ); + } ); + + story( "I want to be able to logout from the system using my JWT token", () => { + given( "a valid incoming jwt token", () => { + then( "my token should become invalidated and I will be logged out", () => { + // Log in first to get a valid token to logout with + var token = jwtService.attempt( "admin", "admin" ); + var payload = jwtService.decode( token ); + expect( cbauth.isLoggedIn() ).toBeTrue(); + + // Now Logout + var event = this.post( route = "/api/logout", params = { "x-auth-token" : token } ); + + var response = event.getPrivateValue( "Response" ); + expect( response.getError() ).toBeFalse( response.getMessages().toString() ); + expect( response.getStatusCode() ).toBe( 200 ); + expect( cbauth.isLoggedIn() ).toBeFalse(); + } ); + } ); + given( "an invalid incoming jwt token", () => { + then( "I should see an error message", () => { + // Now Logout + var event = this.post( route = "/api/logout", params = { "x-auth-token" : "123" } ); + + var response = event.getPrivateValue( "Response" ); + expect( response.getError() ).toBeTrue( response.getMessages().toString() ); + // debug( response.getStatusCode( 500 ) ); + } ); + } ); + } ); + } ); + } + +} diff --git a/resources/rest/specs/integration/EchoTests.bx b/resources/rest/specs/integration/EchoTests.bx new file mode 100755 index 0000000..6e9b520 --- /dev/null +++ b/resources/rest/specs/integration/EchoTests.bx @@ -0,0 +1,61 @@ +ο»Ώ@appMapping( "/app" ) +class extends="coldbox.system.testing.BaseTestCase" { + + /*********************************** LIFE CYCLE Methods ***********************************/ + + function beforeAll(){ + super.beforeAll(); + // do your own stuff here + } + + function afterAll(){ + // do your own stuff here + super.afterAll(); + } + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + describe( "My RESTFUl Service", () => { + beforeEach( ( currentSpec ) => { + // Setup as a new ColdBox request, VERY IMPORTANT. ELSE EVERYTHING LOOKS LIKE THE SAME REQUEST. + setup(); + } ); + + it( "can handle global exceptions", () => { + var event = execute( + event = "echo.onError", + renderResults = true, + eventArguments = { + exception : { + message : "unit test", + detail : "unit test", + stacktrace : "" + } + } + ); + + var response = event.getPrivateValue( "response" ); + expect( response.getError() ).toBeTrue(); + expect( response.getStatusCode() ).toBe( 500 ); + } ); + + it( "can handle an echo", () => { + prepareMock( getRequestContext() ).$( "getHTTPMethod", "GET" ); + var event = execute( route = "echo/index" ); + var response = event.getPrivateValue( "response" ); + expect( response.getError() ).toBeFalse(); + expect( response.getData() ).toBe( "Welcome to my ColdBox RESTFul Service" ); + } ); + + it( "can handle missing actions", () => { + prepareMock( getRequestContext() ).$( "getHTTPMethod", "GET" ); + var event = execute( route = "echo/bogus" ); + var response = event.getPrivateValue( "response" ); + expect( response.getError() ).tobeTrue(); + expect( response.getStatusCode() ).toBe( 404 ); + } ); + } ); + } + +} diff --git a/resources/rest/specs/unit/UserServiceTest.bx b/resources/rest/specs/unit/UserServiceTest.bx new file mode 100644 index 0000000..8f8e8d8 --- /dev/null +++ b/resources/rest/specs/unit/UserServiceTest.bx @@ -0,0 +1,62 @@ +class extends="coldbox.system.testing.BaseTestCase" { + + /*********************************** LIFE CYCLE Methods ***********************************/ + + function beforeAll(){ + super.beforeAll(); + } + + function afterAll(){ + super.afterAll(); + } + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + describe( "UserService", () => { + beforeEach( ( currentSpec ) => { + setup(); + model = getInstance( "UserService" ); + } ); + + it( "can be created", () => { + expect( model ).toBeComponent(); + } ); + + it( "can get a valid mock user by id", () => { + var oUser = model.retrieveUserById( 1 ); + expect( oUser.getId() ).toBe( 1 ); + expect( oUser.isLoaded() ).toBeTrue(); + } ); + + it( "can get a new mock user with invalid id", () => { + var oUser = model.retrieveUserById( 100 ); + expect( oUser.getId() ).toBe( "" ); + expect( oUser.isLoaded() ).toBeFalse(); + } ); + + it( "can get a valid mock user by username", () => { + var oUser = model.retrieveUserByUsername( "admin" ); + expect( oUser.getId() ).toBe( 1 ); + expect( oUser.isLoaded() ).toBeTrue(); + } ); + + it( "can get a new mock user with invalid username", () => { + var oUser = model.retrieveUserByUsername( "bogus@admin" ); + expect( oUser.getId() ).toBe( "" ); + expect( oUser.isLoaded() ).toBeFalse(); + } ); + + it( "can validate valid credentials", () => { + var result = model.isValidCredentials( "admin", "admin" ); + expect( result ).toBeTrue(); + } ); + + it( "can validate invalid credentials", () => { + var result = model.isValidCredentials( "badadmin", "dd" ); + expect( result ).toBeFalse(); + } ); + } ); + } + +} diff --git a/resources/rest/specs/unit/UserTest.bx b/resources/rest/specs/unit/UserTest.bx new file mode 100644 index 0000000..4ac70db --- /dev/null +++ b/resources/rest/specs/unit/UserTest.bx @@ -0,0 +1,35 @@ +/** + * The base model test case will use the 'model' annotation as the instantiation path + * and then create it, prepare it for mocking and then place it in the variables scope as 'model'. It is your + * responsibility to update the model annotation instantiation path and init your model. + */ +@model( "models.User" ) +class extends="coldbox.system.testing.BaseModelTest" { + + /*********************************** LIFE CYCLE Methods ***********************************/ + + function beforeAll(){ + super.beforeAll(); + + // setup the model + super.setup(); + + // init the model object + model.init(); + } + + function afterAll(){ + super.afterAll(); + } + + /*********************************** BDD SUITES ***********************************/ + + function run(){ + describe( "A User", () => { + it( "can be created", () => { + expect( model ).toBeComponent(); + } ); + } ); + } + +} diff --git a/resources/vite/.babelrc b/resources/vite/.babelrc new file mode 100644 index 0000000..0b2a13d --- /dev/null +++ b/resources/vite/.babelrc @@ -0,0 +1,7 @@ +{ + "plugins": [ + ], + "presets" : [ + "@babel/preset-env" + ] +} diff --git a/resources/vite/assets/css/app.css b/resources/vite/assets/css/app.css new file mode 100644 index 0000000..a461c50 --- /dev/null +++ b/resources/vite/assets/css/app.css @@ -0,0 +1 @@ +@import "tailwindcss"; \ No newline at end of file diff --git a/resources/vite/assets/js/app.js b/resources/vite/assets/js/app.js new file mode 100644 index 0000000..a3ac4a4 --- /dev/null +++ b/resources/vite/assets/js/app.js @@ -0,0 +1,4 @@ +import { createApp } from "vue"; +import Hello from "./components/Hello.vue"; + +createApp( Hello ).mount( "#app" ); diff --git a/resources/vite/assets/js/components/Hello.vue b/resources/vite/assets/js/components/Hello.vue new file mode 100644 index 0000000..aa9ec8c --- /dev/null +++ b/resources/vite/assets/js/components/Hello.vue @@ -0,0 +1,7 @@ + diff --git a/resources/vite/layouts/Main.bxm b/resources/vite/layouts/Main.bxm new file mode 100755 index 0000000..1bf6c91 --- /dev/null +++ b/resources/vite/layouts/Main.bxm @@ -0,0 +1,25 @@ + + + + + + + + Welcome to Coldbox! + + + + + + + + #vite( [ "resources/assets/css/app.css", "resources/assets/js/app.js" ] )# + + +
+ + +
+ + +
diff --git a/resources/vite/package.json b/resources/vite/package.json new file mode 100644 index 0000000..9b2304b --- /dev/null +++ b/resources/vite/package.json @@ -0,0 +1,17 @@ +{ + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build --emptyOutDir" + }, + "dependencies": { + "vue": "^3.5.22", + "tailwindcss": "^4.1.14" + }, + "devDependencies": { + "vite": "^6.3.6", + "@vitejs/plugin-vue": "6.0.1", + "@tailwindcss/vite": "^4.1.14", + "coldbox-vite-plugin": "^3.0.2" + } +} diff --git a/resources/vite/vite.config.mjs b/resources/vite/vite.config.mjs new file mode 100644 index 0000000..52e3011 --- /dev/null +++ b/resources/vite/vite.config.mjs @@ -0,0 +1,16 @@ +import { defineConfig } from "vite"; +import vue from "@vitejs/plugin-vue"; +import coldbox from "coldbox-vite-plugin"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [ + vue(), + tailwindcss(), + coldbox({ + input: [ "resources/assets/css/app.css", "resources/assets/js/app.js" ], + refresh: true, + publicDirectory: "public/includes" + }) + ], +}); \ No newline at end of file diff --git a/server.json b/server.json index dd4c4a3..3ec8bc8 100644 --- a/server.json +++ b/server.json @@ -15,10 +15,11 @@ "enable":true }, "aliases":{ + "/coldbox/system/exceptions":"./lib/coldbox/system/exceptions/", "/tests":"./tests/" } }, "scripts":{ - "onServerInitialInstall":"install bx-esapi,bx-mysql" + "onServerInitialInstall":"install bx-esapi" } } diff --git a/tests/runner.bxm b/tests/runner.bxm index 5f9190a..e026b92 100644 --- a/tests/runner.bxm +++ b/tests/runner.bxm @@ -15,7 +15,7 @@ - + diff --git a/tests/specs/integration/MainSpec.bx b/tests/specs/integration/MainSpec.bx index abbea00..ce17f31 100755 --- a/tests/specs/integration/MainSpec.bx +++ b/tests/specs/integration/MainSpec.bx @@ -4,7 +4,7 @@ * Extends the integration class: coldbox.system.testing.BaseTestCase * * so you can test your ColdBox application headlessly. The 'appMapping' points by default to - * the '/root' mapping created in the test folder Application.cfc. Please note that this + * the '/app' mapping created in the test folder Application.cfc. Please note that this * Application.cfc must mimic the real one in your root, including ORM settings if needed. * * The 'execute()' method is used to execute a ColdBox event, with the following arguments