Hello! I'm back with Part 3 of this tutorial! As promised, in this part, we'll deal with authenticated requests like creating posts, adding comments and uploading images. Let's get started!
If you get stuck, or just want the source code, it is available on Github
Fixing fontawesome
Our icons don't work. They just show a box. This is because we haven't added the icon font yet. We did add the CSS, but we also need to put the fonts in public/webfonts
. Download the font from CDNJS and save it as public/webfonts/fa-solid-900.woff2
. Your icons should now show as normal
Dealing with authentication
When we authenticate with Strapi, we get back a JWT token. This token can then be used to authenticate ourselves in other requests without needing to send any email/password. Let's update our Auth.svelte
component:
<!-- src/components/Auth.svelte -->
<script lang="ts">
import Error from "./ErrorAlert.svelte";
import { fade } from "svelte/transition";
import { getContext } from "svelte";
import axios from "axios";
type AuthMode = "login" | "register";
export let authMode: AuthMode = "register";
export let next: string = "/posts";
const apiUrl: string = getContext("apiUrl");
let loginError: string | null = null;
let registerError: string | null = null;
let email = "";
let password = "";
let cpassword = "";
let username = "";
function login() {
email = email.trim();
password = password.trim();
if (!email || !password) {
loginError = "Fill out all fields!";
return;
}
loginError = null;
axios
.post(apiUrl + "/auth/local", {
identifier: email,
password,
})
.then(({ data }) => {
localStorage.setItem("JWT", data.jwt);
localStorage.setItem("user", JSON.stringify(data.user));
// Using window.location.href instead of router.redirect to refresh the page
// so that components like Navbar update too
window.location.href = next;
})
.catch((err) => {
if (err.response) {
loginError = "";
for (let message of err.response.data.message[0].messages) {
loginError += `${message.message}\n`;
}
} else loginError = err;
});
}
function register() {
email = email.trim();
password = password.trim();
cpassword = cpassword.trim();
username = username.trim();
if (!email || !password || !cpassword || !username) {
registerError = "Fill out all fields!";
return;
}
if (password !== cpassword) {
registerError = "Passwords don't match";
return;
}
registerError = null;
axios
.post(apiUrl + "/auth/local/register", {
email,
username,
password,
})
.then(({ data }) => {
localStorage.setItem("JWT", data.jwt);
localStorage.setItem("user", JSON.stringify(data.user));
// Using window.location.href instead of router.redirect to refresh the page
// so that components like Navbar update too
window.location.href = next;
})
.catch((err) => {
if (err.response) {
registerError = "";
for (let message of err.response.data.message[0].messages) {
registerError += `${message.message}\n`;
}
} else registerError = err;
});
}
</script>
<style>
.auth-box {
width: 40%;
margin: 1rem auto;
}
@media (max-width: 600px) {
.auth-box {
width: 80%;
}
}
</style>
<div class="w3-container">
<div class="w3-card-4 w3-border w3-border-black auth-box">
<div class="w3-bar w3-border-bottom w3-border-gray">
<button
style="width: 50%"
on:click={() => (authMode = 'login')}
class="w3-bar-item w3-button w3-{authMode === 'login' ? 'blue' : 'white'} w3-hover-{authMode === 'login' ? 'blue' : 'light-gray'}">Login</button>
<button
style="width: 50%"
on:click={() => (authMode = 'register')}
class="w3-bar-item w3-button w3-{authMode === 'register' ? 'blue' : 'white'} w3-hover-{authMode === 'register' ? 'blue' : 'light-gray'}">Register</button>
</div>
<div class="w3-container">
<h3>{authMode === 'login' ? 'Login' : 'Register'}</h3>
{#if authMode === 'login'}
<form on:submit|preventDefault={login} in:fade>
{#if loginError}
<Error message={loginError} />
{/if}
<div class="w3-section">
<label for="email">Email</label>
<input
type="email"
bind:value={email}
placeholder="Enter your email"
id="email"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="password">Password</label>
<input
type="password"
bind:value={password}
placeholder="Enter your password"
id="password"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Login</button>
<button
class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
on:click={() => (authMode = 'register')}>Register</button>
</div>
</form>
{:else}
<form on:submit|preventDefault={register} in:fade>
{#if registerError}
<Error message={registerError} />
{/if}
<div class="w3-section">
<label for="username">Username</label>
<input
type="text"
bind:value={username}
placeholder="Enter a username"
id="username"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="email">Email</label>
<input
type="email"
bind:value={email}
placeholder="Enter your email"
id="email"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="password">Password</label>
<input
type="password"
bind:value={password}
placeholder="Enter a password"
id="password"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<label for="cpassword">Confirm Password</label>
<input
type="password"
bind:value={cpassword}
placeholder="Re-enter that password"
id="cpassword"
class="w3-input w3-border w3-border-black" />
</div>
<div class="w3-section">
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue">Register</button>
<button
class="w3-button w3-white w3-hover-light-gray w3-border w3-border-black"
on:click={() => (authMode = 'login')}>Login</button>
</div>
</form>
{/if}
</div>
</div>
</div>
Now, our app will go to the Strapi server, get the JWT token, and put it in our local storage for later use. Let's create src/auth.ts
which will have some helper functions to get and remove the token:
// src/auth.ts
import type { User } from "./types";
export function getToken(): string | null {
return localStorage.getItem("JWT") || null;
}
export function clearToken() {
localStorage.removeItem("JWT");
}
export function getUserId(): number | null {
let user: string | User = localStorage.getItem("user");
if (!user) return null;
user = JSON.parse(user);
return (user as User).id;
}
export function getUser(): User | null {
let user: string | User = localStorage.getItem("user");
if (!user) return null;
user = JSON.parse(user);
return user as User;
}
Conditional rendering in the navbar
Let's use the helper methods from auth.ts
to determine if we're logged in or not. We can use this in the navbar to only show the upload button (which I renamed to "New Post") if the user is logged in:
<!-- src/components/Navbar.svelte -->
<script lang="ts">
import { slide } from "svelte/transition";
import { getToken } from "../auth";
const auth = !!getToken();
let active = false;
</script>
<style>
.toggler {
display: none;
}
@media (max-width: 600px) {
.logo {
display: block;
width: 100%;
}
.logo .toggler {
float: right;
display: initial;
}
.nav {
display: flex;
width: 100%;
flex-direction: column;
}
.nav a {
text-align: left;
}
}
</style>
<div class="w3-bar w3-blue">
<div class="logo">
<a
href="/"
class="w3-bar-item w3-text-white w3-button w3-hover-blue">Quickstagram</a>
<button
class="toggler w3-button w3-blue w3-hover-blue"
on:click={() => (active = !active)}>
<i class="fas fa-{active ? 'times' : 'bars'}" /></button>
</div>
<div class="w3-right w3-hide-small">
{#if auth}
<a href="/new" class="w3-bar-item w3-button w3-hover-blue">New post</a>
<a
href="/logout"
class="w3-bar-item w3-button w3-hover-blue">Logout</a>
{:else}
<a
href="/auth?action=login"
class="w3-bar-item w3-button w3-hover-blue">Login</a>
<a
href="/auth?action=register"
class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
{/if}
</div>
{#if active}
<div class="w3-right nav w3-hide-large w3-hide-medium" transition:slide>
{#if auth}
<a href="/new" class="w3-bar-item w3-button w3-hover-blue">New
post</a>
<a
href="/logout"
class="w3-bar-item w3-button w3-hover-blue">Logout</a>
{:else}
<a
href="/auth?action=login"
class="w3-bar-item w3-button w3-hover-blue">Login</a>
<a
href="/auth?action=register"
class="w3-bar-item w3-button w3-purple w3-hover-purple">Register</a>
{/if}
</div>
{/if}
</div>
Automatic redirects
If the user is logged in, and they visit /auth
, they can log in again. To prevent this, we'll automatically redirect them to /posts
. We'll do this in both auth.svelte
and index.svelte
<!-- src/routes/auth.svelte -->
<script lang="ts">
import Auth from "../components/Auth.svelte";
import router from "page";
import { onMount } from "svelte";
import { getToken } from "../auth";
export const params = {};
export let queryString: { action: "login" | "register"; next: string };
onMount(() => {
if (getToken()) router.redirect(queryString.next || "/posts");
});
</script>
<Auth authMode={queryString.action} next={queryString.next} />
<!-- src/routes/index.svelte -->
<script lang="ts">
import { onMount } from "svelte";
import { getToken } from "../auth";
import router from "page";
import Auth from "../components/Auth.svelte";
export const queryString = {};
export const params = {};
onMount(() => {
if (getToken()) router.redirect("/posts");
});
</script>
<div class="w3-container">
<h1 class="w3-center w3-xxxlarge">Quickstagram</h1>
<p class="w3-center w3-large w3-text-gray">Instagram, but quicker!</p>
<div class="w3-center">
<a
href="/auth?action=register"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">Register</a>
<a
href="/auth?action=login"
class="w3-button w3-white w3-border w3-border-black w3-hover-white">Login</a>
</div>
<Auth />
<div class="w3-center">
<a
href="/posts"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue">View
posts</a>
</div>
</div>
Logging out
Logging out is very simple, we don't even need to contact Strapi for this! We just have to delete the token from our local storage. I've created a route called /logout
which will do just that:
<!-- src/routes/logout.svelte -->
<script lang="ts">
import { onMount } from "svelte";
import { clearToken } from "../auth";
export let queryString: { next: string };
onMount(() => {
clearToken();
// Using window.location.href instead of router.redirect to refresh the page
// so that components like Navbar update too
window.location.href = queryString.next || "/";
});
</script>
<h1 class="w3-center w3-xxlarge">Logging you out...</h1>
And as with all routes, we have to register it in App.svelte
.
<!-- src/App.svelte -->
<script lang="ts">
// ...
import Logout from "./routes/logout.svelte";
// ...
router("/logout", setupRouteParams, () => (page = Logout));
// ...
</script>
<!-- ... -->
Adding comments
Let's focus on comments first, since they're easier :P. In src/components/onePost.svelte
, let's add an input that allows us to add comments:
<!-- src/components/onePost.svelte -->
<script lang="ts">
import axios from "axios";
import { getContext } from "svelte";
import router from "page";
import { getToken } from "../auth";
import type { Post, Comment as CommentType } from "../types";
import Comment from "../components/Comment.svelte";
import ErrorAlert from "../components/ErrorAlert.svelte";
export let params: { username: string; postId: string };
const apiUrl = getContext("apiUrl");
const auth = !!getToken();
let commentError: string | null = null;
async function getPost(): Promise<Post> {
try {
const { data } = await axios.get<Post>(
apiUrl + "/posts/" + params.postId
);
if (data.user)
if (data.user.username !== params.username)
router.redirect("/404");
return data;
} catch (err) {
if (err.response.status === 404) router.redirect("/404");
else {
console.log({ error: err });
throw new Error(
"Request failed with status: " +
err.response.status +
"\nCheck the console for further details."
);
}
}
}
async function getComments(post: Post): Promise<CommentType[]> {
try {
let comments: CommentType[] = [];
for (let i = 0; i < post.comments.length; i++) {
const { data } = await axios.get<CommentType>(
apiUrl + "/comments/" + post.comments[i].id
);
comments.push(data);
}
return comments;
} catch (err) {
if (err.response) {
console.log({ err });
if (err.response.status === 404) router.redirect("/404");
else {
console.log({ error: err });
throw new Error(
"Request failed with status: " +
err.response.status +
"\nCheck the console for further details."
);
}
} else throw new Error(err);
}
}
function newComment() {}
</script>
<style>
#comment-form {
display: grid;
grid-template-rows: auto;
grid-template-columns: 80% 20%;
margin: 1rem 0;
}
.post {
width: 50%;
margin: 0 auto;
}
@media (max-width: 992px) {
.post {
width: 70%;
}
}
@media (max-width: 600px) {
.post {
width: 90%;
}
}
</style>
{#await getPost()}
<div class="w3-center w3-section w3-xxlarge w3-spin">
<i class="fas fa-spinner" />
</div>
{:then post}
<div class="w3-card post">
<a
href={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}><img
src={post.image[0].provider === 'local' && getContext('apiUrl') + post.image[0].url}
alt={post.image.alternativeText || 'Post image'}
style="width: 100%" /></a>
<div class="w3-container">
<p class="w3-small w3-text-gray">
<a
href="/@{post.user.username}"
style="text-decoration: none">@{post.user.username}</a>
</p>
<p>{post.content}</p>
</div>
</div>
<div class="w3-card post w3-margin-top">
<header class="w3-container w3-border-bottom w3-border-light-gray">
<h3>Comments</h3>
</header>
<div class="w3-container">
{#if auth}
{#if commentError}
<ErrorAlert message={commentError} />
{/if}
<form on:submit|preventDefault={newComment} id="comment-form">
<input
type="text"
class="w3-input w3-border"
placeholder="Type your comment here"
id="comment" />
<button
class="w3-button w3-blue w3-hover-blue w3-border w3-border-blue"
type="submit">Add</button>
</form>
{/if}
{#await getComments(post)}
<div class="w3-center w3-section w3-xxlarge w3-spin">
<i class="fas fa-spinner" />
</div>
{:then comments}
{#each comments as comment}
<Comment {comment} />
{/each}
{:catch err}
<div
class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
{err}
</div>
{/await}
</div>
</div>
{:catch err}
<div
class="w3-panel w3-pale-red w3-padding w3-leftbar w3-border-red w3-text-red">
{err}
</div>
{/await}
Let's use Insomnia to add a comment:
Note that we have to specify the
post
anduser
id too. Automatically determining them is not possible unless we edit Strapi's code. Also, you need to provide a JWT to authenticate the request.
We've done this in insomnia, but now, let's add code to do it in our frontend. We'll modify our newComment
function in onePost.svelte
// src/routes/onePost.svelte
// script tag
function newComment(postId: number) {
// "as HTMLInputElement" is supported in TypeScript only.
const userId: number | null = getUserId();
if (!userId) {
window.location.href =
"/auth?action=login&next=" + window.location.pathname;
return;
}
const content = (
(document.getElementById("comment") as HTMLInputElement).value || ""
).trim();
if (!content) return;
axios
.post<Comment>(
apiUrl + "/comments",
{
content,
post: postId,
user: userId,
},
{
headers: {
Authorization: "Bearer " + getToken(),
},
}
)
.then(() => window.location.reload())
.catch((error) => {
if (error.response) {
if (
error.response.status === 401 ||
error.response.status === 403
)
window.location.href =
"/auth?action=login&next=" +
window.location.pathname;
else {
commentError = "";
for (let message of error.response.data.message[0]
.messages) {
commentError += `${message.message}\n`;
}
}
} else commentError = error;
});
}
Let's test it!
Creating new posts
Let's now give the same treatment to posts! I'm going to create a /new
route which will be occupied by newPost.svelte
.
<!-- src/components/newPost.svelte -->
<script lang="ts">
import { getContext, onMount } from "svelte";
import { getToken, getUser } from "../auth";
import ErrorAlert from "../components/ErrorAlert.svelte";
import type { User } from "../types";
const apiUrl = getContext("apiUrl");
const user: User = getUser();
onMount(() => {
if (!getToken() || !user)
window.location.href =
"/auth?action=login&next=" + window.location.pathname;
});
let loading = false;
let error: string | null = null;
let file: File;
let content = "";
function chooseFile() {
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.addEventListener("change", ({ target }) => {
if ((target as HTMLInputElement).files.length === 1) {
file = (target as HTMLInputElement).files[0];
}
});
input.click();
}
function newPost() {}
</script>
{#if user}
<h1 class="w3-center w3-xxxlarge">New post</h1>
<p class="w3-center w3-text-gray">Logged in as: {user.username}</p>
<div class="w3-card w3-margin">
{#if loading}
<div class="w3-center w3-container">
<i class="fas fa-spinner fa-spin fa-5x w3-margin" />
<p class="w3-xlarge">Uploading...</p>
</div>
{:else}
<form class="w3-container" on:submit|preventDefault={newPost}>
{#if error}
<ErrorAlert message={error} />
{/if}
<div class="w3-section">
{#if file}
<p>Chosen image: {file.name}</p>
{:else}
<button
type="button"
on:click={chooseFile}
class="w3-button w3-white w3-border">Choose image</button>
{/if}
</div>
<div class="w3-section">
<label for="content">Post content</label>
<textarea
id="content"
rows="5"
bind:value={content}
class="w3-input w3-border" />
</div>
<div class="w3-section">
<button
type="submit"
class="w3-button w3-blue w3-border w3-border-blue w3-hover-blue"
style="width: 100%">Post</button>
</div>
</form>
{/if}
</div>
{/if}
Now, all we need to do, is upload this image, and create a new post. Edit the newPost
function in newPost.svelte
<!-- src/routes/newPost.svelte -->
<script lang="ts">
// ...
function newPost() {
if (!content || !content.trim()) {
error = "Enter some content";
return;
}
if (!file) {
error = "Please choose a file";
return;
}
if (file.type.split("/")[0] !== "image") {
error = "Please choose an image";
return;
}
content = content.trim();
let fd = new FormData();
fd.append("files", file);
loading = true;
// uploading file
axios
.post<ImageType[]>(apiUrl + "/upload", fd, {
headers: {
"Content-Type": "multipart/formdata",
Authorization: "Bearer " + getToken(),
},
})
.then(({ data }) => {
const imageId: number = data[0].id;
// creating the post itself
axios
.post<Post>(
apiUrl + "/posts",
{
image: imageId,
user: getUserId(),
content,
},
{
headers: {
Authorization: "Bearer " + getToken(),
},
}
)
.then(({ data }) => {
window.location.href = `/@${data.user.username}/${data.id}`;
})
.catch((err) => {
if (err.response) {
if (
err.response.status === 401 ||
err.response.status === 400
)
window.location.href =
"/auth?action=login&next=" +
window.location.pathname;
else {
error = "";
for (let message of err.response.data.message[0]
.messages) {
error += `${message.message}\n`;
}
}
} else error = err;
});
})
.catch((err) => {
if (err.response) {
if (
err.response.status === 401 ||
err.response.status === 400
)
window.location.href =
"/auth?action=login&next=" +
window.location.pathname;
else {
error = "";
for (let message of err.response.data.message[0]
.messages) {
error += `${message.message}\n`;
}
}
} else error = err;
});
}
</script>
<!-- ... -->
Remember to import whenever required!
Demo
And that's it! We're done here, well almost. This is good, but we can make it more secure. In the fourth, and final part, I'll show you how to deploy both strapi and our frontend on Heroku and Vercel respectively. Let's look at a demo:
Conclusion
Strapi was really fun to work with, but I still do miss my custom made backend. One thing I think that Strapi is missing, is the ability to get the user info from a JWT out of the box, i.e. without needing to edit its API. You can see that the way we're getting a user id is a very insecure way and not meant for production. I'll show you how you can secure it in the 4th part. Here's the 4th part!
Top comments (4)
Great article again! Can't wait to see the next part.
Thank you so much! But, the next part will take a bit of time.
Take your time ;).
Part 4 is out! dev.to/arnu515/build-an-instagram-...