[v.4] How to allow user to uplod avatar, cover and update his profile?

System Information
  • Strapi Version: 4.0.2
  • Operating System: Win10
  • Database: PostgreSQL

Basically I’m rewriting my backend (Strapi) to work with v.4 and PostgreSQL (in v.3 I worked with MongoDB). In v.3 I defined a user API, which contained the following router, controller and services:

// api/user/config/routes.json
{
  "routes": [
    {
      "method": "PUT",
      "path": "/user/upload/avatar",
      "handler": "User.uploadAvatar",
      "config": {
        "policies": []
      }
    },
    {
      "method": "PUT",
      "path": "/user/upload/cover",
      "handler": "User.uploadCover",
      "config": {
        "policies": []
      }
    },
    {
      "method": "PUT",
      "path": "/user/update",
      "handler": "User.updateUser",
      "config": {
        "policies": []
      }
    }
  ]
}

// api/user/controllers/User.js
const { parseMultipartData, sanitizeEntity } = require('strapi-utils');
const _ = require('lodash');
const { POINTS, SOCKET_TYPES } = require('../../../utils/settings')

module.exports = {
  async uploadAvatar(ctx) {
    let entity
    const userId = ctx.state.user.id
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
      entity = await strapi.services.user.updateUser(userId, {}, {files})
    } else {
      ctx.request.body.user = ctx.state.user.id;
      entity = await strapi.services.user.updateUser(userId, ctx.request.body);
    }
    return sanitizeEntity(entity, { model: strapi.models.user });
  },
  async uploadCover(ctx) {
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
    } else {
      ctx.request.body.user = ctx.state.user.id;
    }
    return ctx.state.user
  },

  async updateUser(ctx) {
    const userId = ctx.state.user.id
    let entity
    let points = ctx.state.user.points
    switch (ctx.request.body.type) {
      // ...lots of logics here
      // in some cases calls to a service called points, to update user points - await strapi.services.points.updateUserPoints(userId, points)
    }
    return {success: true, user: entity}
  }
}
const mergeSMProfileArrays = (incoming, existing) => {//...}


// api/user/services/user.js
module.exports = {
  updateUser: async (id, data) => {
    return await strapi.query('user', 'users-permissions').update({id}, data)
  }
}

In v.3 that all worked just fine. Now I tried to change it to match all the new v.4 structure. I didn’t find anything regarding updating the user info in the docs. So I tried the following - first I created directory user in the api section. Then inside I created directories controllers, routes, services.

These are my router, user controller and user services:

//api/user/routes/router.js
module.exports = {
  routes: [
    {
      method: "PUT",
      path: "/user/upload/avatar",
      handler: "user.uploadAvatar",
      config: {
        "policies": []
      }
    },
    {
      method: "PUT",
      path: "/user/upload/cover",
      handler: "user.uploadCover",
      config: {
        policies: []
      }
    },
    {
      method: "PUT",
      path: "/user/update",
      handler: "user.updateUser",
      config: {
        policies: []
      }
    }
  ]
}
//api/user/controllers/user.js
const { createCoreController } = require('@strapi/strapi').factories;
const { parseMultipartData, sanitizeEntity } = require('@strapi/utils');
const _ = require('lodash');
const { POINTS, SOCKET_TYPES } = require('../../../utils/settings')

module.exports = createCoreController('api::user.user', ({ strapi }) => ({
  async uploadAvatar(ctx) {
    let entity
    const userId = ctx.state.user.id
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
      entity = await strapi.services.user.updateUser(userId, {}, {files})
      entity = await this.updateUser(userId, {}, {files})
    } else {
      ctx.request.body.user = ctx.state.user.id;
      entity = await this.updateUser(userId, ctx.request.body);
    }
    return sanitizeEntity(entity, { model: strapi.models.user });*/
  },
  async uploadCover(ctx) {
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
    } else {
      ctx.request.body.user = ctx.state.user.id;
    }
    return ctx.state.user
  },

  async updateUser(ctx) {
    const userId = ctx.state.user.id
    let entity
    let points = ctx.state.user.points
    switch (ctx.request.body.type) {
      // same logic as above
    return {success: true, user: entity}
  }
}))
//api/user/services/user.js
const { createCoreService } = require('@strapi/strapi').factories;

module.exports = createCoreService('api::user.user', ({ strapi }) => ({
  async updateUser(id, data) {
    return await strapi.db.query('plugin::user-permissions.user').update({
      where: {id},
      data
    })
  }
}))

And my server crashed. I’m getting the following error:

error: Cannot read properties of undefined (reading 'kind')
TypeError: Cannot read properties of undefined (reading 'kind')
    at Object.isSingleType (C:\work\Web\my-project\backend\node_modules\@strapi\utils\lib\content-types.js:90:25)
    at createController (C:\work\Web\my-project\backend\node_modules\@strapi\strapi\lib\core-api\controller\index.js:36:20)
    at C:\work\Web\my-project\backend\node_modules\@strapi\strapi\lib\factories.js:11:28
    at Object.get (C:\work\Web\my-project\backend\node_modules\@strapi\strapi\lib\core\registries\controllers.js:37:61)
    at Object.get [as api::user.user] (C:\work\Web\my-project\backend\node_modules\@strapi\strapi\lib\core\registries\controllers.js:55:25)
    at C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:4967:32
    at baseForOwn (C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:3032:24)
    at Function.mapKeys (C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:13430:7)
    at removeNamespacedKeys (C:\work\Web\my-project\backend\node_modules\@strapi\strapi\lib\core\domain\module\index.js:11:12)
    at Object.get controllers [as controllers] (C:\work\Web\my-project\backend\node_modules\@strapi\strapi\lib\core\domain\module\index.js:103:14)
    at C:\work\Web\my-project\backend\node_modules\@strapi\plugin-users-permissions\server\services\users-permissions.js:152:28
    at C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:3585:27
    at C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:4967:15
    at baseForOwn (C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:3032:24)
    at C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:4936:18
    at baseMap (C:\work\Web\my-project\backend\node_modules\lodash\lodash.js:3584:7)

Even with the entire content of the methods commented out I’m still getting this error. Only when the controller/user.js and routes/router.js files are completely removed - the server is able to start.

So what am I doing wrong here? How do I enable the end user to upload avatars and update their profiles?

OK, I think I managed to solve this… I think the issue was that I tried to create a core controller. Now inside my api/user/controllers/user.js I just export an object with the required methods:

module.exports = {
  async uploadAvatar(ctx) {
    let entity
    const userId = ctx.state.user.id
    if (ctx.is('multipart')) {
      const { data, files } = parseMultipartData(ctx);
      entity = await strapi.service('api::user.user').updateUser(userId, {}, {files})
    } else {
      ctx.request.body.user = ctx.state.user.id;
      entity = await strapi.service('api::user.user').updateUser(userId, ctx.request.body);
    }
    return sanitizeEntity(entity, { model: strapi.models.user });
  },

  // ...other methods
}

Now I’m still not able to test it fully, especially that there are some other issues I haven’t solved yet (for example: parseMultipartData and sanitizeEntity, which used to be a part of 'strapi/utils' and it looks like now they’re not, and it’s not documented anywhere, or an issue with socket.io, which worked great for me in v3, but there were too many breaking changes in v4 and now it doesn’t), but one problem at a time, and at least now my server was able to start properly.

3 Likes

Could you solve all issues?
I see myself challenged with a similar quest: “i want the authenticated user to be able to modify his own information” with strapi v4. With your code every user can edit all other users informations theoretically, right?

I agree with you. You should use a policy (is-owner) which checks if you are the owner of the record before you are allowed to make changes.

1 Like

The example in the documentation here isn’t working with v4. Can’t create/update/find of undefined in this line of code:

entity = await strapi.services.article.create(ctx.request.body);

Any ideas?

Unfortunately no, I couldn’t. Now I began to test the solution and it doesn’t work. I tried another approach, but unfortunately it also didn’t work. I think I will open a new topic for that question…
Regarding your question about ‘every user can edit all other users information’, then theoretically it shouldn’t happen because of this part:

const userId = ctx.state.user.id
...updateUser(userId, ctx.request.body)

So basically it should update only the user that is connected, with his own ID. But again, my solution so far didn’t work at all.

Hi Igal-Kleiner,

I think your service is declared incorrectly. I get the same [reading ‘kind’] error when I try to call the service.
Maybe this will help a little:

Your service starts with:

module.exports = createCoreService('api::user.user', ({ strapi }) => ({
    async updateUser(id, data) { ... }
}))

Try this:

module.exports = () => ({
	async updateUser(id, data) {
		console.log(">>>>> updateUser", id, data)
		return 'ok'
	}
});