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."
);
}
}
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
.
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);
};
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> });
},
};
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
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
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
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,
},
},
},
});
Now, add and commit all files
git add .
git commit -m "init"
Deploy to heroku with:
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 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>
Let's also install a library that will help strapi upload directly to cloudinary:
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:
// 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:
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, 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
Deploying is really simple. All you have to do is say:
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 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)
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
'ssrc
s to:Checkout the github if you're stuck