Build an Instagram clone with Strapi and Svelte (PART 4)
Tags: webdev, tutorial, svelte, typescript
Published: 2020-11-13 18:37:20

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](https://github.com/arnu515/quickstagram)!

## 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.

```javascript
   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."
            );
        }
    }
```

## 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](https://strapi.io/documentation/v3.x/) is there to help! First, we need to create a content type called `token`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1w7o89rz9f6uq97nuz3i.png)

> 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.

```js
// 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);
};
```

### Using the user from the token instead of from the data

Now, we have to edit the [controllers](https://strapi.io/documentation/v3.x/concepts/controllers.html#core-controllers) of `posts` and `comments` model. The controllers are located in: `api/<modelname>/controllers/comment.js`

```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> });
  },
};
```

> This is code copy-pasted from the documentation, so [head there](https://strapi.io/documentation/v3.x/concepts/controllers.html#core-controllers) 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](https://heroku.com). 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](https://devcenter.heroku.com/articles/heroku-cli). Login to the heroku cli by typing:
```sh
heroku login
```

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.
```sh
heroku create appname
```

> 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:
```sh
heroku addons:create heroku-postgresql:hobby-dev
npm install pg-connection-string --save
heroku config:set NODE_ENV=production
npm install pg --save
```

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

```js
// 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,
      },
    },
  },
});
```

Now, add and commit all files
```sh
git add .
git commit -m "init"
```

Deploy to heroku with:
```sh
git push heroku master
```

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](https://cloudinary.com) or log in. We need to set some environment variables on heroku:
```sh
heroku config:set CLOUDINARY_NAME=<your cloudinary name>
heroku config:set CLOUDINARY_KEY=<cloudinary apikey>
heroku config:set CLOUDINARY_SECRET=<cloudinary api secret>
```

Let's also install a library that will help strapi upload directly to cloudinary:
```sh
npm i strapi-provider-upload-cloudinary
```

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:

```js
// 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"),
    },
  },
});
```

Finally, commit your changes and deploy:
```sh
git add .
git commit -m "add cloudinary support"
git push heroku master
```

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](https://vercel.com), 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:

```sh
npm i -g vercel
vercel login
```

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

```sh
vercel --prod
```

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](https://quickstagram.vercel.app) for example.

## Conclusion

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

The github repo is available [here](https://github.com/arnu515/quickstagram). 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 👋