StrapiとSvelteによるInstagramクローンの構築(その3 )


こんにちは!私は、このチュートリアルのパート3で戻ります!約束通り、この部分では、投稿を作成したり、コメントを追加したり、画像をアップロードするなどの認証要求を処理します.始めましょう!

If you get stuck, or just want the source code, it is available on Github


フィニッシュ


私たちのアイコンは動作しません.彼らは、ちょうど箱を示します.これは、まだアイコンのフォントを追加していないためです.私たちはCSSを加えました、しかし、また、我々はフォントを入れる必要がありますpublic/webfonts . フォントをダウンロードCDNJS として保存public/webfonts/fa-solid-900.woff2 . あなたのアイコンが正常に表示されるはずです

認証の取り扱い


我々がstrapiで認証するとき、我々はJWTトークンを取り戻します.このトークンは、その後、任意のメール/パスワードを送信することなく、他のリクエストで自分自身を認証するために使用することができます.更新しましょうAuth.svelte コンポーネント
<!-- 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>
さて、我々のアプリは、stradサーバーに移動し、JWTトークンを取得し、後で使用するために私たちのローカルストレージにそれを置く.つくりましょうsrc/auth.ts トークンの取得および削除にはヘルパー関数があります.
// 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;
}

Navbarにおける条件付きレンダリング


ヘルパーメソッドを使いましょうauth.ts ログインしているかどうかを調べます.ユーザーがログインしているならば、私たちはアップロード・ボタン(私が「新しいポスト」に改名された)を示すだけであるためにこれをNavbarで使うことができます:
<!-- 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>

自動リダイレクト


ユーザーがログインしているならば、彼らは訪問します/auth , 彼らは再びログインできます.これを防ぐために、自動的に/posts . 両方でこれをします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>

ロギングアウト


ロギングアウトは非常に簡単です、我々もこれのためにstrapiに連絡する必要はありません!我々はちょうどローカルストレージからトークンを削除する必要があります.私はルートと呼ばれる/logout では、
<!-- 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>
そして、すべてのルートと同様に、我々は2009年にそれを登録する必要がありますApp.svelte .
<!-- src/App.svelte -->

<script lang="ts">
    // ...
    import Logout from "./routes/logout.svelte";
    // ...

    router("/logout", setupRouteParams, () => (page = Logout));
    // ...
</script>

<!-- ... -->

コメントの追加


最初にコメントに集中しましょうsrc/components/onePost.svelte , コメントを追加できるようにする入力を追加しましょう.
<!-- 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}

コメントを追加する不眠症を使用しましょう

Note that we have to specify the post and user 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.


我々は不眠症でこれをしました、しかし、現在、我々のフロントエンドでそれをするコードを加えましょう.修正しますnewComment ファンクション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;
            });
    }
テストしましょう!

新規投稿の作成


今すぐポストに同じ治療を与える!私は、Aをつくるつもりです/new 利用する路線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}
今、我々がする必要があるすべては、このイメージをアップロードし、新しいポストを作成します.エディットnewPost ファンクション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!


デモ


そして、それ!私たちはここで、ほとんどほぼ完了です.これは良いですが、我々はそれをより安全にすることができます.第4で、そして、最終的な部分で、私はあなたにHerokuとVercelの上でstrapiとフロントエンドの両方を展開する方法を示します.デモを見てみましょう.

結論


strapiは本当に楽しかったですが、まだ私のカスタムメイドのバックエンドをミスします.私がStrapiが不足していると思う1つのことは、JWTからユーザ情報を取得する能力です.つまり、そのAPIを編集する必要がなくなります.ユーザIDを取得している方法は、非常に安全でない方法であり、生産のためのものではありません.あなたが第4部でそれをどのように確保するかをあなたに教えます!