diff --git a/config/Router.cfc b/config/Router.cfc
index ca024b9..1b2c9fa 100644
--- a/config/Router.cfc
+++ b/config/Router.cfc
@@ -13,6 +13,13 @@ component {
} )
.toHandler( "API" );
+ // User avatar endpoint
+ route( "/api/v1/users/:id/avatar" )
+ .withAction( {
+ "GET" : "avatar"
+ } )
+ .toHandler( "api.v1.Users" );
+
apiResources(
resource = "/api/v1/users",
handler = "api.v1.Users"
diff --git a/handlers/api/v1/Users.cfc b/handlers/api/v1/Users.cfc
index 71e1524..affc024 100644
--- a/handlers/api/v1/Users.cfc
+++ b/handlers/api/v1/Users.cfc
@@ -18,12 +18,31 @@ component extends="BaseAPIHandler" secured="StacheboxUser"{
}
+ // ( GET ) /api/v1/users/:id/avatar - Returns user avatar
+ function avatar( event, rc, prc ){
+
+ var user = getInstance( "User@stachebox" ).getOrFail( rc.id );
+ var userMemento = user.getMemento( includes = "avatar" );
+
+ prc.response.setData({
+ "id": user.getId(),
+ "avatar": userMemento.avatar ?: javacast( "null", 0 )
+ });
+
+ }
+
// ( POST ) /api/v1/users
function create( event, rc, prc ) secured="StacheboxAdministrator"{
var user = getInstance( "User@stachebox" )
.new( rc )
.encryptPassword()
.validateOrFail();
+
+ // Process and validate avatar if provided
+ if( !isNull( user.getAvatar() ) && len( user.getAvatar() ) ){
+ user.processAvatar();
+ }
+
prc.response.setData(
user
.save()
@@ -40,6 +59,12 @@ component extends="BaseAPIHandler" secured="StacheboxUser"{
user.encryptPassword();
}
+ // Process and validate avatar if provided
+ // Check if user has avatar after population (not just if it's in request)
+ if( !isNull( user.getAvatar() ) && len( user.getAvatar() ) ){
+ user.processAvatar();
+ }
+
prc.response.setData( user.save().getMemento() );
}
diff --git a/models/User.cfc b/models/User.cfc
index a750f7f..0e8471d 100644
--- a/models/User.cfc
+++ b/models/User.cfc
@@ -57,7 +57,6 @@ component
"isActive",
"isApproved",
"isAdministrator",
- "avatar",
"allowLogin"
],
"defaults" :{
@@ -145,7 +144,7 @@ component
function save(){
var userDoc = newDocument().new(
index=getSearchIndexName()
- ).populate( this.getMemento( includes="password,resetToken" ) );
+ ).populate( this.getMemento( includes="password,resetToken,avatar" ) );
if( !isNull( getId() ) && len( getId() ) ){
userDoc.setId( getId() );
@@ -170,6 +169,81 @@ component
return this;
}
+ function processAvatar(){
+ if( !isNull( variables.avatar ) && len( variables.avatar ) ){
+ try {
+ // Check if it's a valid data URI
+ if( !reFindNoCase( "^data:image/", variables.avatar ) ){
+ throw(
+ type = "ValidationException",
+ message = "Invalid avatar format. Must be a base64-encoded image."
+ );
+ }
+
+ // Extract the base64 data (everything after the comma)
+ var base64Data = listLast( variables.avatar, "," );
+
+ // Decode base64 to binary
+ var imageData = toBinary( base64Data );
+
+ // Validate max upload size (10MB for raw upload)
+ var maxUploadSize = 10 * 1024 * 1024; // 10MB
+ if( arrayLen( imageData ) > maxUploadSize ){
+ throw(
+ type = "ValidationException",
+ message = "Avatar image is too large. Maximum upload size is 10MB."
+ );
+ }
+
+ // Create image object from binary data
+ var img = imageNew( imageData );
+
+ // Get image dimensions
+ var width = imageGetWidth( img );
+ var height = imageGetHeight( img );
+
+ // Resize if necessary (max 200x200, maintaining aspect ratio)
+ if( width > 200 || height > 200 ){
+ imageScaleToFit( img, 200, 200 );
+ }
+
+ // Ensure it's exactly 200x200 by cropping from center
+ var currentWidth = imageGetWidth( img );
+ var currentHeight = imageGetHeight( img );
+
+ if( currentWidth != 200 || currentHeight != 200 ){
+ var cropX = max( 0, floor( ( currentWidth - 200 ) / 2 ) );
+ var cropY = max( 0, floor( ( currentHeight - 200 ) / 2 ) );
+ imageCrop( img, cropX, cropY, min( 200, currentWidth ), min( 200, currentHeight ) );
+ }
+
+ // Convert to PNG for consistent format (supports transparency, lossless)
+ // Write to byte array and convert to base64
+ var baos = createObject( "java", "java.io.ByteArrayOutputStream" ).init();
+
+ // Write the image as PNG to the byte array output stream
+ var ImageIO = createObject( "java", "javax.imageio.ImageIO" );
+ var bufferedImage = imageGetBufferedImage( img );
+ ImageIO.write( bufferedImage, "png", baos );
+
+ // Convert to base64 and create data URI
+ var processedBase64 = toBase64( baos.toByteArray() );
+ variables.avatar = "data:image/png;base64," & processedBase64;
+
+ } catch( any e ){
+ // Log the full error for debugging
+ writeLog( file="application", text="Avatar processing error: #e.message# - Detail: #e.detail#" );
+
+ throw(
+ type = "ValidationException",
+ message = "Failed to process avatar image: #e.message# #e.detail#",
+ extendedInfo = serializeJSON( { "avatar": ["Failed to process image: #e.message#"] } )
+ );
+ }
+ }
+ return this;
+ }
+
function setFirstName( string firstName ){
variables.firstName = trim( arguments.firstName ?: "" );
return this;
diff --git a/resources/assets/js/api/users.js b/resources/assets/js/api/users.js
index daf5a65..ba80899 100644
--- a/resources/assets/js/api/users.js
+++ b/resources/assets/js/api/users.js
@@ -13,6 +13,7 @@ const defaultAPI = Axios.create({
export const finalAPI = {
apiInstance : defaultAPI,
fetch :( id, params, token ) => defaultAPI.get( '/' + id, { params : params, headers : { 'Authorization' : 'Bearer ' + token } } ),
+ fetchAvatar :( id, token ) => defaultAPI.get( '/' + id + '/avatar', { headers : { 'Authorization' : 'Bearer ' + token } } ),
list :( params, token ) => defaultAPI.get( '', { params : params, headers : { 'Authorization' : 'Bearer ' + token } } ),
create : ( params, token ) => defaultAPI.post( '', JSON.stringify( params ), { headers : { 'Authorization' : 'Bearer ' + token } } ),
update : ( params, token ) => defaultAPI.put( '/' + params.id, JSON.stringify( params ), { headers : { 'Authorization' : 'Bearer ' + token } } ),
diff --git a/resources/assets/js/layouts/inc/Header.vue b/resources/assets/js/layouts/inc/Header.vue
index 6472165..c8b299c 100644
--- a/resources/assets/js/layouts/inc/Header.vue
+++ b/resources/assets/js/layouts/inc/Header.vue
@@ -33,7 +33,7 @@
>
@@ -71,6 +71,7 @@