DEV Community

Cover image for Build an Instagram clone with Strapi and Svelte (PART 4)
arnu515
arnu515

Posted on

Build an Instagram clone with Strapi and Svelte (PART 4)

Hello again! I'm finally back with part 4! Sorry it took a bit of time. I had a few tests and actually didn't know how to do this part, but now I do, and I'm gonna show you how!

Ofcourse, the code is on the Github repository!

A small bug

If we visit @username, replacing the username to a valid user ofcourse, you can see that we get errors in the console. The fix for that is very simple. In src/routes/userProfile.svelte, you just need to change data.map to data.filter in the getPosts function.

   async function getPosts(): Promise<Post[]> {
        try {
            const { data } = await axios.get<Post[]>(
                getContext("apiUrl") + "/posts"
            );
            // NEW
            return data.filter((post) => {
                if (post.user.username === params.username) return post;
            });
        } catch (err) {
            console.log({ error: err });
            throw new Error(
                "Request failed with status: " +
                    err.response.status +
                    "\nCheck the console for further details."
            );
        }
    }
Enter fullscreen mode Exit fullscreen mode

Securing our app

The way we used to identify the user, i.e. explicitly sending the User ID, was really bad on security (and kinda dumb too :P). We need to get the user's ID from the token itself. And like I mentioned in the last post, you can't do that unless you edit strapi's APIs directly. Fear not, for I will guide you through it. Open your code editor in your Strapi directory. If you get stuck somewhere, the Strapi documentation is there to help! First, we need to create a content type called token.

Alt Text

Make sure the relationship between token and user is Token has and belongs to one User, i.e. the second option (in strapi 3 atleast).

Now that we have our token datatype, we have to write some code to get the user from the token.

// extensions/users-permissions/config/policies/permissions.js
const _ = require('lodash');

module.exports = async (ctx, next) => {
  let role;

  if (ctx.state.user) {
    // request is already authenticated in a different way
    return next();
  }

  if (ctx.request && ctx.request.header && ctx.request.header.authorization) {
    try {
      const {id} = await strapi.plugins['users-permissions'].services.jwt.getToken(ctx);

      if (id === undefined) {
        throw new Error('Invalid token: Token did not contain required fields');
      }

      // fetch authenticated user
      ctx.state.user = await strapi.plugins[
        'users-permissions'
      ].services.user.fetchAuthenticatedUser(id);
    } catch (err) {
      return handleErrors(ctx, err, 'unauthorized');
    }

    if (!ctx.state.user) {
      return handleErrors(ctx, 'User Not Found', 'unauthorized');
    }

    role = ctx.state.user.role;

    if (role.type === 'root') {
      return await next();
    }

    const store = await strapi.store({
      environment: '',
      type: 'plugin',
      name: 'users-permissions',
    });

    if (
      _.get(await store.get({ key: 'advanced' }), 'email_confirmation') &&
      !ctx.state.user.confirmed
    ) {
      return handleErrors(ctx, 'Your account email is not confirmed.', 'unauthorized');
    }

    if (ctx.state.user.blocked) {
      return handleErrors(
        ctx,
        'Your account has been blocked by the administrator.',
        'unauthorized'
      );
    }
  }

  // Retrieve `public` role.
  if (!role) {
    role = await strapi.query('role', 'users-permissions').findOne({ type: 'public' }, []);
  }

  const route = ctx.request.route;
  const permission = await strapi.query('permission', 'users-permissions').findOne(
    {
      role: role.id,
      type: route.plugin || 'application',
      controller: route.controller,
      action: route.action,
      enabled: true,
    },
    []
  );

  if (!permission) {
    return handleErrors(ctx, undefined, 'forbidden');
  }

  // Execute the policies.
  if (permission.policy) {
    return await strapi.plugins['users-permissions'].config.policies[permission.policy](ctx, next);
  }

  // Execute the action.
  await next();
};

const handleErrors = (ctx, err = undefined, type) => {
  throw strapi.errors[type](err);
};
Enter fullscreen mode Exit fullscreen mode

Using the user from the token instead of from the data

Now, we have to edit the controllers of posts and comments model. The controllers are located in: api/<modelname>/controllers/comment.js

"use strict";

const { sanitizeEntity, parseMultipartData } = require("strapi-utils");

/**
 * Read the documentation (https://strapi.io/documentation/v3.x/concepts/controllers.html#core-controllers)
 * to customize this controller
 */

module.exports = {
  async create(ctx) {
    let entity;
    if (ctx.is("multipart")) {
      const { data, files } = parseMultipartData(ctx);
      entity = await strapi.services.<modelname>.create(data, { files });
    } else {
      ctx.request.body.user = ctx.state.user.id;
      entity = await strapi.services.<modelname>.create(ctx.request.body);
    }
    return sanitizeEntity(entity, { model: strapi.models.<modelname> });
  },
};
Enter fullscreen mode Exit fullscreen mode

This is code copy-pasted from the documentation, so head there if you get stuck somewhere.

Deploying

Deploying strapi

Since Strapi is a node.js app, it can be deployed on a server, or using Heroku. We will use the latter because it is both easier, and free. So create an account if you don't have one alredy, and download the Heroku CLI. Login to the heroku cli by typing:

heroku login
Enter fullscreen mode Exit fullscreen mode

This will open up a browser window that will log you in to the heroku CLI.

Now, let's create an application. First, let's initialise git in the strapi folder with git init.

Next, let's create a heroku app.

heroku create appname
Enter fullscreen mode Exit fullscreen mode

If you already have a created app and want to add it as remote to your current git repo,
heroku git:remote -a appname

Let's add the database:

heroku addons:create heroku-postgresql:hobby-dev
npm install pg-connection-string --save
heroku config:set NODE_ENV=production
npm install pg --save
Enter fullscreen mode Exit fullscreen mode

We have to configure strapi to use this database only in production.

// config/env/production/database.js
// create if not present

const parse = require('pg-connection-string').parse;
const config = parse(process.env.DATABASE_URL);

module.exports = ({ env }) => ({
  defaultConnection: 'default',
  connections: {
    default: {
      connector: 'bookshelf',
      settings: {
        client: 'postgres',
        host: config.host,
        port: config.port,
        database: config.database,
        username: config.user,
        password: config.password,
      },
      options: {
        ssl: false,
      },
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Now, add and commit all files

git add .
git commit -m "init"
Enter fullscreen mode Exit fullscreen mode

Deploy to heroku with:

git push heroku master
Enter fullscreen mode Exit fullscreen mode

You can now access the admin panel at appname.herokuapp.com/admin

We're not done yet

We've successfully deployed strapi, but, file uploads do not work. This is because Heroku uses a file-system that refreshes everytime your app goes to sleep, i.e. every so often, so storing files on it is out of the question. We need a third party provider like Amazon S3 or Cloudinary to store our files, and since we're dealing with only images here, we can use Cloudinary. Sign up for a new account on Cloudinary or log in. We need to set some environment variables on heroku:

heroku config:set CLOUDINARY_NAME=<your cloudinary name>
heroku config:set CLOUDINARY_KEY=<cloudinary apikey>
heroku config:set CLOUDINARY_SECRET=<cloudinary api secret>
Enter fullscreen mode Exit fullscreen mode

Let's also install a library that will help strapi upload directly to cloudinary:

npm i strapi-provider-upload-cloudinary
Enter fullscreen mode Exit fullscreen mode

Then, once you're done, we have to add a file named plugins.js in the same directory as our database.js, i.e. config/env/production/plugins.js, this way, it only affects heroku, not local development:

// config/env/production/plugins.js

module.exports = ({ env }) => ({
  upload: {
    provider: "cloudinary",
    providerOptions: {
      cloud_name: env("CLOUDINARY_NAME"),
      api_key: env("CLOUDINARY_KEY"),
      api_secret: env("CLOUDINARY_SECRET"),
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Finally, commit your changes and deploy:

git add .
git commit -m "add cloudinary support"
git push heroku master
Enter fullscreen mode Exit fullscreen mode

We're done with the backend (strapi) deployment. Let's move on to the frontend.

Note: You will have to give permissions to your roles again. I don't know why this happens, but this might be a bug.

Deploying the frontend

Let's deploy the frontend on Vercel, but you can use Netlify or even Heroku. I'll be using vercel since it is the easiest, but all you have to do is build your app and deploy it. Create an account on vercel if you don't have one already.

First, let's quickly change our strapiApiUrl in main.ts to match the URL of strapi on heroku. Next, install vercel and login to it:

npm i -g vercel
vercel login
Enter fullscreen mode Exit fullscreen mode

Deploying is really simple. All you have to do is say:

vercel --prod
Enter fullscreen mode Exit fullscreen mode

If it is your first time deploying, Vercel will ask you a few questions. Choosing the defaults to all of them (except the app name ofcourse) will work. Once it has been deployed, vercel will automatically copy the URL to your clipboard which you can then visit using your browser. Here's mine for example.

Conclusion

And that's it! We're done with this project! Congratulations 🎉

The github repo is available here. If I find any bugs, I will fix them there.

As always, if you have any questions/suggestions, drop them in the comments below! Thanks for sticking with me! Bye 👋

Top comments (1)

Collapse
 
arnu515 profile image
arnu515

I just found a bug that wouldn't let you access images from cloudinary. All you have to do to fix it, is change your img's srcs to:

src={post.image.provider === 'local' ? getContext('apiUrl') + post.image.url : post.image.url}
Enter fullscreen mode Exit fullscreen mode

Checkout the github if you're stuck