Redis
 Computer >> コンピューター >  >> プログラミング >> Redis

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

この投稿では、Upstash、SvelteKit、Firebase Storage を使用して、Jira Kanban Board に代わるオープンソースをどのように構築したかについて説明します。

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

使用するもの

  • SvelteKit(UI および API ルート)
  • Upstash(CRUD オペレーション)
  • Tailwind CSS(スタイリング)
  • Firebase ストレージ (アセット [画像、PDF など] ストレージ)
  • Auth.js による SvelteKit 認証

必要なもの

  • データベースを作成するための Upstash アカウント
  • ストレージ コンテナを作成するための Firebase アカウント
  • OAuth 認証情報を取得するための Google OAuth 2.0 のセットアップ

Upstash Redis のセットアップ

Upstash アカウントを作成してログインしたら、[Redis] タブに移動してデータベースを作成します。

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

データベースを作成したら、「詳細」タブに移動します。 「データベースに接続する」セクションが見つかるまで下にスクロールします。コンテンツをコピーし、安全な場所に保存します。

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

また、REST API セクションが見つかるまで下にスクロールし、.env ボタンを選択します。コンテンツをコピーし、安全な場所に保存します。

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

プロジェクトのセットアップ

セットアップするには、アプリ リポジトリのクローンを作成し、このチュートリアルに従ってそこに含まれる内容をすべて学習するだけです。プロジェクトをフォークするには、次を実行します。

git clone https://github.com/rishi-raj-jain/jira-sveltekit-firebase-storage-upstash-starter
cd jira-sveltekit-firebase-storage-upstash-starter
npm install

リポジトリのクローンを作成したら、.env ファイルを作成します。上記のセクションで保存したアイテムを追加します。

次のようになります:

# .env
 
# Obtained from Google OAuth 2.0 setup
# https://support.google.com/cloud/answer/6158849?hl=en
GOOGLE_ID="..."
GOOGLE_SECRET="..."
 
# SvelteKit Auth
AUTH_SECRET="..." # A random 32 char string
AUTH_TRUST_HOST=true
 
# Obtained from Upstash as from the steps done above
UPSTASH_REDIS_REST_URL="your_upstash_redis_rest__url_from_above"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest__token_from_above"
// firebase-adminsdk.json
// with the firebase config obtained from your firebase project
// Read more about firebase config
// https://firebase.google.com/docs/web/learn-more#config-object
 
{
 "type": "...",
 "project_id": "...",
 "private_key_id": "...",
 "private_key": "...",
 "client_email": "...",
 "client_id": "...",
 "auth_uri": "...",
 "token_uri": "...",
 "auth_provider_x509_cert_url": "...",
 "client_x509_cert_url": "...",
 "universe_domain": "...",
 "storageBucket": "..."
}

これらの手順を完了すると、次のコマンドを使用してローカル環境を起動できるようになります。

npm run dev

リポジトリの構造

これはプロジェクトのメイン フォルダー構造です。 CRUD オペレーション、SvelteKit Auth、およびファイル アップロード ハンドラーを扱うこの投稿でさらに説明するファイルと、それらが参照されているファイルを赤で囲みました。

Firebase、Upstash、SvelteKit を使用したオープンソース JIRA クローンの構築

ユーザー認証による SvelteKit の Edge 機能の保護

Auth.js のチームによる素晴らしい成果 は、SvelteKit を使用した認証をシームレスな操作にしました。プロジェクトは以下を実装します:

Google OAuth 2.0 を使用したすべてのページの認証

SvelteKit のサーバー フックを使用して、(任意のページへの) すべての受信リクエストに認証を適用します。

// File: @/hooks.server.ts
 
import Google from "@auth/core/providers/google";
import { SvelteKitAuth } from "@auth/sveltekit";
import type { Handle } from "@sveltejs/kit";
import { GOOGLE_ID, GOOGLE_SECRET } from "$env/static/private";
 
// Read more on
// https://kit.svelte.dev/docs/hooks#server-hooks-handle
export const handle = SvelteKitAuth({
 // @ts-ignore
 providers: [Google({ clientId: GOOGLE_ID, clientSecret: GOOGLE_SECRET })],
}) satisfies Handle;

SvelteKit のサーバー ローカルを使用したエッジ機能の承認

SvelteKit の Server Locals を使用すると、ユーザーがサーバー側のみの操作で認証されているかどうかをチェックするためにオプトインできます。以下は、新しい問題の作成時にユーザーが認証されているかどうかを検証するためにこれを使用する例です。

import { json } from '@sveltejs/kit'
import { isAuth } from '@/lib/auth'
import type { RequestEvent } from './$types'
import { getTask, getTasks } from '@/lib/issues'
import type { LayoutServerLoadEvent } from '../routes/$types'
import type { RequestEvent, ServerLoadEvent } from '@sveltejs/kit'
 
// Get user session if available in event locals
const isAuth = async (event: LayoutServerLoadEvent | ServerLoadEvent | RequestEvent) => {
 const session = await event.locals.getSession()
 if (session?.user?.image) {
 return { session }
 }
 return false
}
 
export async function GET(event: RequestEvent) {
 // If user is not authenticated throw a 403
 if (!(await isAuth(event))) {
 return new Response(undefined, {
 status: 403
 })
 }
 const url = event.url
 const idSearchParam = url.searchParams.get('id')
 if (idSearchParam) {
 const res = await getTask(idSearchParam)
 return json(res)
 } else if (url.searchParams.get('all')) {
 const res = await getTasks()
 return json(res)
 }
 return new Response(JSON.stringify({ code: 0, error: 'Invalid Request.' }), {
 status: 400,
 headers: {
 'content-type': 'application/json'
 }
 })
}

Upstash Redis を介した CRUD オペレーションの問題

このセクションでは、カンバン ボード上の各課題のデータの取得、更新、削除がどのように行われるかについて詳しく説明します。私たちは常に Upstash DB を使用しています (@upstash/redis 経由) ) データを取得、表示、更新します。

getTask:課題データ関数の取得

getTask  関数は Upstash の hget を使用します  id 経由 関連する問題のデータを Upstash に API リクエストするためのキーとして使用され、一意の id で識別されます。 。その問題が存在しない場合 (またはエラーがある場合)、関数は { code: 0 } を持つオブジェクトを返すように設定されます。  これにより、ユーザーは SvelteKit の動的ルートで自動的に 404 (問題が見つかりません) にリダイレクトされます。

type Task = { [key: string]: any } | null;
 
// Get Issue Data
// File: @/lib/issues/get.ts
export async function getTask(id: string) {
 try {
 const redis = (await import("../upstash/setup")).default;
 const task: Task = await redis.hget("issues", id);
 if (!task) {
 return {
 code: 0,
 error: "No such issue found.",
 };
 }
 return { ...task, code: 1 };
 } catch (e: any) {
 const error = e.message || e.toString();
 console.log(error);
 return {
 code: 0,
 error,
 };
 }
}

同様に、残りの CRUD 操作は次のとおりです。

// Create Issue
// File: @/lib/issues/create.ts
export async function createTask(info: any) {
 try {
 const redis = (await import("../upstash/setup")).default;
 const id =
 Math.random().toString().slice(2) + new Date().getUTCMilliseconds();
 await redis.hset("issues", { [id]: info });
 return { code: 1, id, message: "Issue Created Succesfully ✅" };
 } catch (e: any) {
 const error = e.message || e.toString();
 console.log(error);
 return {
 code: 0,
 error,
 };
 }
}
// Delete Issue
// File: @/lib/issues/delete.ts
export async function deleteTask(id: string) {
 try {
 const redis = (await import("../upstash/setup")).default;
 await redis.hdel("issues", id);
 return { code: 1, message: "Deleted Succesfully!" };
 } catch (e: any) {
 const error = e.message || e.toString();
 console.log(error);
 return {
 code: 0,
 error,
 };
 }
}
// Update Issue Data
// File: @/lib/issues/update.ts
export async function updateTask(info: any, id: string) {
 try {
 const redis = (await import("../upstash/setup")).default;
 if (id) {
 const task = await redis.hget("issues", id);
 if (task) {
 await redis.hset("issues", { [id]: info });
 return { code: 1, message: "Updated Successfully" };
 }
 }
 return {
 code: 0,
 error: "No such issue was found.",
 };
 } catch (e: any) {
 const error = e.message || e.toString();
 console.log(error);
 return {
 code: 0,
 error,
 };
 }
}

レート制限

エッジでレート制限を実装するには、Upstash Redis を使用します。 データベース クライアントと @upstash/ratelimit というレート リミッタ ライブラリ .

// Reference Function to ratelimiting
// File: @/lib/upstash/ratelimit.ts
import { Ratelimit } from "@upstash/ratelimit";
 
import redis from "./setup";
 
export const ratelimit = {
 upload: new Ratelimit({
 redis,
 limiter: Ratelimit.slidingWindow(2, "60s"),
 }),
 issues: new Ratelimit({
 redis,
 limiter: Ratelimit.slidingWindow(5, "60s"),
 }),
};

レート制限を使用すると、次のことを達成できました。

A.ユーザーごとの 1 分間あたりの発行数の制限

レート制限を使用すると、認証されたユーザーごとに 1 分あたり 5 つの問題の作成を制限できます。認証されたユーザーのユーザー電子メールに基づいて、このレート制限を適用できます。

// File: @/routes/api/issue/+server.ts
// Issue Creation POST API SvelteKit Handler
import { ratelimit } from "@/lib/upstash/ratelimit";
 
export async function POST(event: RequestEvent) {
 const user = await isAuth(event);
 if (!user) {
 return new Response(undefined, {
 status: 403,
 });
 }
 if (user.session.user?.email) {
 // Look at the user email of authenticated user at edge
 // Rate limit 5 issues creation per minute
 const result = await ratelimit.issues.limit(user.session.user.email);
 if (!result.success) {
 return new Response(
 JSON.stringify({
 code: 0,
 error: `You can't create more than 5 issues per minute.`,
 }),
 {
 status: 403,
 headers: {
 "content-type": "application/json",
 },
 },
 );
 }
 const { info } = await event.request.json();
 const res = await createTask(info);
 return json(res);
 }
 return new Response(undefined, {
 status: 403,
 });
}

B.ユーザーごと、問題ごと、1 分あたりのファイルアップロード数を制限する

レート制限を使用すると、ファイルのアップロードを、認証されたユーザーごと、タスクごと、1 分あたり 2 つまでに制限できます。このレート制限は、認証されたユーザーのユーザー電子メールとタスクの ID に基づいて適用できます。アップロードが正常に完了すると、ファイル URL が追加された状態で Upstash DB 内のタスクが更新されます。

// File: @/routes/api/content/+server.ts
// File Upload POST API SvelteKit Handler
import { ratelimit } from "@/lib/upstash/ratelimit";
 
export async function POST(event: RequestEvent) {
 // User Authentication Code
 if (user.session.user?.email) {
 // Validate User, Task ID and if a file is uploaded
 // Look at the user email of authenticated user and task's ID at edge
 // Rate limit 2 uploads per minute
 const result = await ratelimit.upload.limit(
 `${user.session.user.email}_${taskID}`,
 );
 if (!result.success) {
 return new Response(
 JSON.stringify({
 code: 0,
 error: `You can't upload more than 2 files per issue per minute.`,
 }),
 {
 status: 403,
 headers: {
 "content-type": "application/json",
 },
 },
 );
 }
 // File upload code
 // Continue reading the blog to see how
 // file uploads are being taken care of
 }
 return new Response(undefined, {
 status: 403,
 });
}

Firebase Storage を使用したファイルのアップロードとダウンロードの処理

このセクションでは、問題のファイルのアップロードとダウンロードが SvelteKit のエッジで安全かつ認証された方法でどのように処理されるかについて詳しく説明します。ファイルのフェッチとアップロードには Firebase (v9) Storage を利用します。

ああ、でもストレージとして Cloudflare R2 を使用しないのはなぜですか?

Cloudflare R2 の無料ストレージ プランとその利点に対する多くのコミュニティの支持を見てきましたが、私を困惑させたのは、システムを試す前に Cloudflare を自由に使えるようにクレジット カードを置く必要があるということでした。このため、他のストレージ ソリューションについて熟考し、5 GB の無料ストレージを提供する Firebase Storage にたどり着きました。これを超えた場合、私の承認と状況の把握なしに、クレジット カードに請求する代わりにサービスが停止されます。

Firebase ストレージにファイルをアップロードするための SvelteKit Edge 機能

次の Edge 関数では、POST リクエスト イベントを調べており、ユーザーが認証されている場合は、taskID を取得します。 と file イベントのフォームデータから。それが完了したら、ファイル サイズが 5 MB 未満の場合に続行するかどうかをさらに評価します。すべての前提条件が満たされたら、一意の ID を作成し、ファイルのアップロード先となる一意のフォルダーへの Firebase の参照を作成します。ファイルが firebase にアップロードされるとすぐに、アップロードされたファイルへのアクセスに使用できる URL が返されます。この一意の URL を files に追加します。 問題のデータのキー。

// File: @/routes/api/content/+server.ts
// File Upload POST API SvelteKit Handler
import { initializeApp } from "firebase/app";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
 
import fireBaseConfig from "../../../../firebase-adminsdk.json";
 
export async function POST(event: RequestEvent) {
 // User Authentication Code
 if (user.session.user?.email) {
 const app = initializeApp(fireBaseConfig);
 const storage = getStorage(app);
 const data = await event.request.formData();
 const taskID = data.get("taskID");
 const file = data.get("file");
 
 // ...Validate User, Task ID and if a file is uploaded
 // ...Rate Limiting Code
 
 // File Size Restriction(s)
 if (file.size > 5 * 1024 * 1024) {
 return new Response(
 JSON.stringify({
 code: 0,
 error: "File size exceeds the limit of 5 MB.",
 }),
 {
 status: 400,
 headers: {
 "content-type": "application/json",
 },
 },
 );
 }
 
 // Start File Upload Code
 try {
 // Create a unique ID
 const fileId = uuidv4();
 // If uploaded is not a File type
 if (!(file instanceof File)) return;
 // Create a ref to firebase storage
 const storageRef = ref(storage, `uploads/${fileId}/${file.name}`);
 // Obtain the arrayBuffer of the file uploaded
 const fileBuffer = await file.arrayBuffer();
 // Upload file to Firebase Storage in bytes using Uint8Array
 const { metadata } = await uploadBytes(
 storageRef,
 new Uint8Array(fileBuffer),
 );
 const { fullPath } = metadata;
 // No fullPath is received, the API errored out
 if (!fullPath) {
 return new Response(
 JSON.stringify({
 code: 0,
 error: `<span>There was some error while uploading the file.</span> <span class="mt-1 text-xs text-gray-500">Report an issue with the current URL that you are on and with the code XXX.</span>`,
 }),
 {
 status: 403,
 headers: {
 "content-type": "application/json",
 },
 },
 );
 }
 // If a file is uploaded successfully, append the file to list of attachments to the issue's data
 const { code, ...taskValues } = await getTask(taskID);
 if (code === 1) {
 if (taskValues) {
 if (taskValues.hasOwnProperty("files")) {
 taskValues["files"].push(
 `https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`,
 );
 } else {
 taskValues["files"] = [
 `https://storage.googleapis.com/${storageRef.bucket}/${storageRef.fullPath}`,
 ];
 }
 }
 // Update the task's data in Upstash
 await updateTask(taskValues, taskID);
 }
 return json({
 code: 1,
 message: "Uploaded Successfully",
 });
 } catch (error) {
 return new Response(
 JSON.stringify({ code: 0, error: error.message || error.toString() }),
 {
 status: 403,
 headers: {
 "content-type": "application/json",
 },
 },
 );
 }
 }
 return new Response(undefined, {
 status: 403,
 });
}

Firebase ストレージからファイルのパブリック URL をダウンロードする SvelteKit Edge 関数

覚えているとおり、問題の files には Firebase から返された一意の URL が追加されました。 鍵。その一意の URL を、元のファイルを取得するための SvelteKit の Edge 関数への GET リクエストの画像パラメーターとして受け取ります。 Firebase のライブラリから getDownloadURL 関数を利用して、オリジナル メディアのパブリック URL を取得します。

// File: @/routes/api/content/+server.ts
// File Upload GET API SvelteKit Handler
import { initializeApp } from "firebase/app";
import { getDownloadURL, getStorage, ref, uploadBytes } from "firebase/storage";
 
import fireBaseConfig from "../../../../firebase-adminsdk.json";
 
export async function GET(event: RequestEvent) {
 if (!(await isAuth(event))) {
 return new Response(undefined, {
 status: 403,
 });
 }
 const url = event.url;
 const image = url.searchParams.get("image");
 if (image) {
 try {
 const app = initializeApp(fireBaseConfig);
 const storage = getStorage(app);
 const fileRef = ref(storage, image);
 const imagePublicURL = await getDownloadURL(fileRef);
 return json({ code: 1, image: imagePublicURL });
 } catch (error) {
 return new Response(
 JSON.stringify({ code: 0, error: error.message || error.toString() }),
 {
 status: 500,
 headers: {
 "content-type": "application/json",
 },
 },
 );
 }
 }
 return new Response(JSON.stringify({ code: 0, error: "Invalid Request." }), {
 status: 400,
 headers: {
 "content-type": "application/json",
 },
 });
}

すでにお気づきかと思いますが、アップロードできるメディアは複数ある可能性があるため、画像とビデオの簡単なケースを処理するために、次の if else をフロントエンドに追加しました。

<!-- File: @/routes/issue/[slug]/+page.svelte -->
 
{#each fieldFiles as file}
<div class="mt-8 w-full border border-white/25 p-3">
 {#if /\.(mp4|mov|mkv)/i.test(file)}
 <video class="h-auto w-full" src="{file}" controls>
 <track kind="captions" />
 </video>
 {:else}
 <img alt="{file}" src="{file}" class="h-auto w-full" />
 {/if}
</div>
{/each}

しかし、なぜ Jira Kanban Board に代わるオープンソースが必要なのでしょうか?

高額な料金を支払ったソリューションを購入する代わりに、Jira Kanban Board のオープンソース代替品を選択するメリットが数多くあります。

  • 大幅なコスト削減:オープンソースの代替手段を使用することの最も重要な利点の 1 つは、コストの削減です。 Jira のような有料のカンバン ボード ソリューションとは異なり、SvelteKit、TailwindCSS、Firebase Storage、Upstash のサーバーレス DB、レート制限で構築されたオープンソースの代替ソリューションは、ライセンス料なしで使用できます。
  • 無制限のカスタマイズ性:オープンソースの代替手段を使用すると、コードベースを完全に制御でき、特定のニーズに応じてかんばんボードをカスタマイズできます。この柔軟性は、カスタマイズ オプションが限られている有料ソリューションでは実現できないことがよくあります。
  • 統合の容易さ:API の機能を活用して、かんばんボードをプロジェクト管理システム、バージョン管理ツール、通知サービスなどに接続できます。さらに、プロジェクトのオープンソースの性質により、開発者はその機能を拡張し、特定の要件に合わせたプラグインや統合を作成できます。

結論

結論として、このプロジェクトは、粒度レート制限の実装、CRUD データ操作、ファイルの取得とアップロードのための Firebase Storage API の実装、すべて Upstash の @upstash/redis を使用してエッジで行われる貴重な経験を提供しました。 図書館!


  1. .NET Core での Redis 統合をマスターする:実践ガイド

    はじめに Redis は、最新のアプリケーションのキャッシュ、セッション ストレージ、メッセージ ブローカリング、パブリッシュ/サブスクライブ通信に広く使用されている高速なメモリ内キー/値ストアです。軽量でパフォーマンスが高く、開発者にとって使いやすいため、スケーラブルなシステムを構築するための有力な選択肢となります。 .NET Core で開発していて、Redis をアプリケーションに統合したい場合、この記事では StackExchange.Redis ライブラリを使用して基本的な Redis 操作を実行する方法を説明します。 Windows システムで作業している場合は、Redis

  2. Envflow の構築:Laravel および Upstash Redis と環境変数を安全に共有する

    私は最近、Nuno Maduro のプロジェクト Pyre にインスピレーションを受けています。これは、必要な人に短期間の秘密メッセージを送信できる Web アプリです。これらのメッセージは保存時に暗号化されており、あなたまたは復号化リンクを共有した人だけがメッセージを読むことができます。 私は Upstash Redis で同様のものを構築したかったので、環境変数の共有というニッチな分野を選択しました。きっと、ある時点でチームメイトと秘密を共有する必要があり、それを安全に行うことができるシステムを構築する方法を疑問に思ったことがあると思います。 Envflow を紹介します。これは私が構