इस पोस्ट में, मैं बात करता हूं कि कैसे मैंने Upstash, SvelteKit, और Firebase Storage का उपयोग करके Jira Kanban बोर्ड का एक ओपन-सोर्स विकल्प बनाया।

हम क्या उपयोग करेंगे
- SvelteKit (UI और API रूट्स)
- Upstash (CRUD ऑपरेशन)
- Tailwind CSS (स्टाइलिंग)
- Firebase Storage (एसेट [इमेज, PDF, आदि] स्टोरेज)
- Auth.js द्वारा SvelteKit Auth
आपको क्या चाहिए
- एक डेटाबेस बनाने के लिए एक Upstash खाता
- एक स्टोरेज कंटेनर बनाने के लिए एक Firebase खाता
- OAuth क्रेडेंशियल प्राप्त करने के लिए एक Google OAuth 2.0 सेटअप
Upstash Redis सेट अप करना
एक बार जब आप एक Upstash खाता बना लेते हैं और लॉग इन हो जाते हैं तो आप Redis टैब पर जाएंगे और एक डेटाबेस बनाएंगे।


अपना डेटाबेस बनाने के बाद, आप Details टैब पर जाएंगे। नीचे स्क्रॉल करें जब तक आपको Connect your database अनुभाग न मिल जाए। सामग्री कॉपी करें और इसे कहीं सुरक्षित सहेजें।

또한, REST API अनुभाग मिलने तक नीचे स्क्रॉल करें और .env बटन चुनें। सामग्री कॉपी करें और इसे कहीं सुरक्षित सहेजें।

प्रोजेक्ट सेट अप करना
सेट अप करने के लिए, बस ऐप रेपो क्लोन करें और इस ट्यूटोरियल का पालन करें ताकि इसमें जो कुछ भी है उसे सीख सकें। प्रोजेक्ट को फोर्क करने के लिए, चलाएं:
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 # Google OAuth 2.0 सेटअप से प्राप्त # https://support.google.com/cloud/answer/6158849?hl=en GOOGLE_ID="..." GOOGLE_SECRET="..." # SvelteKit Auth AUTH_SECRET="..." # एक यादृच्छिक 32 कैरेक्टर स्ट्रिंग AUTH_TRUST_HOST=true # ऊपर किए गए चरणों से Upstash से प्राप्त 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 // अपने फायरबेस प्रोजेक्ट से प्राप्त फायरबेस कॉन्फिग के साथ // फायरबेस कॉन्फिग के बारे में और पढ़ें // 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, और फ़ाइल अपलोड हैंडलर से संबंधित हैं, जिन फ़ाइलों में उनका संदर्भ दिया गया है।

उपयोगकर्ता प्रमाणीकरण द्वारा SvelteKit के एज फ़ंक्शन की रक्षा करना
Auth.js की टीम द्वारा शानदार काम ने SvelteKit के साथ Auth को एक सहज ऑपरेशन बना दिया है। प्रोजेक्ट लागू करता है:
Google OAuth 2.0 का उपयोग करके सभी पेजों पर प्राधिकरण
SvelteKit के सर्वर हुक का उपयोग करके, हम सभी आने वाले अनुरोधों (किसी भी पेज पर) पर Auth लागू करते हैं:
// 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"; // इस पर और पढ़ें // 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 के सर्वर लोकल्स का उपयोग करके, हम किसी भी सर्वर-साइड-ओनली ऑपरेशन में उपयोगकर्ता प्रमाणित है या नहीं, इसकी जांच करने के लिए ऑप्ट-इन कर सकते हैं। नीचे एक नए मुद्दे को बनाते समय उपयोगकर्ता प्रमाणित है या नहीं, इसे मान्य करने में इसका उपयोग करने का उदाहरण है:
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' // इवेंट लोकल्स में उपलब्ध होने पर उपयोगकर्ता सत्र प्राप्त करें 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) { // यदि उपयोगकर्ता प्रमाणित नहीं है तो 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 ऑपरेशन
इस अनुभाग में, हम इस बात पर गहराई से जाएंगे कि Kanban बोर्ड पर प्रत्येक मुद्दे के लिए डेटा लाना, अपडेट करना और हटाना कैसे किया जाता है। हम डेटा लाने, प्रदर्शित करने और ताज़ा करने के लिए लगातार Upstash DB (via @upstash/redis) का उपयोग करते हैं।
getTask: मुद्दा डेटा लाने वाला फ़ंक्शन
getTask फ़ंक्शन Upstash के hget का उपयोग करता है id को कुंजी के रूप में एक अद्वितीय id द्वारा पहचाने गए प्रासंगिक मुद्दा डेटा के लिए Upstash पर एक API अनुरोध करने के लिए। यदि वह मुद्दा मौजूद नहीं है (या कोई त्रुटि है), तो फ़ंक्शन { 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. प्रति उपयोगकर्ता प्रति मिनट मुद्दा निर्माण की संख्या सीमित करें
रेट लिमिटिंग का उपयोग करके, मैं प्रति प्रमाणित उपयोगकर्ता प्रति मिनट पांच मुद्दों के निर्माण को सीमित करने में सक्षम हूं। हम प्रमाणित उपयोगकर्ता के उपयोगकर्ता ईमेल के आधार पर इस रेट लिमिट को लागू करने में सक्षम हैं।
// 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) { // एज पर प्रमाणित उपयोगकर्ता के उपयोगकर्ता ईमेल को देखें // रेट लिमिट 5 मुद्दा निर्माण प्रति मिनट 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. प्रति उपयोगकर्ता प्रति मुद्दा प्रति मिनट फ़ाइल अपलोड की संख्या सीमित करें
रेट लिमिटिंग का उपयोग करके, मैं प्रति प्रमाणित उपयोगकर्ता प्रति कार्य प्रति मिनट 2 तक फ़ाइल अपलोड को सीमित करने में सक्षम हूं। हम प्रमाणित उपयोगकर्ता के उपयोगकर्ता ईमेल और कार्य की आईडी के आधार पर इस रेट लिमिट को लागू करने में सक्षम हैं। जब भी अपलोड सफलतापूर्वक पूरा हो जाता है, हम फ़ाइल 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 // एज पर प्रमाणित उपयोगकर्ता के उपयोगकर्ता ईमेल और कार्य की आईडी को देखें // रेट लिमिट 2 अपलोड प्रति मिनट 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 के निपटान में मेरा क्रेडिट कार्ड रखने की आवश्यकता। इसने मुझे अन्य स्टोरेज समाधानों के बारे में सोचने पर मजबूर किया, और मैं Firebase Storage पर पहुंचा जो मुझे 5 GB मुफ्त स्टोरेज प्रदान करता है, और यदि मैं इसे पार कर जाता हूं, तो मेरी सेवाएं रुक जाएंगी बिना मेरी मंजूरी और जानकारी के मेरे क्रेडिट कार्ड से शुल्क लिए।
Firebase Storage पर फ़ाइल अपलोड करने के लिए SvelteKit एज फ़ंक्शन
निम्नलिखित एज फ़ंक्शन में, हम किसी भी POST अनुरोध इवेंट को देख रहे हैं और यदि उपयोगकर्ता प्रमाणित है, तो हम इवेंट के formData से taskID और file प्राप्त करते हैं। इसके साथ, हम आगे मूल्यांकन करते हैं कि क्या फ़ाइल का आकार 5 MB से कम है तो जारी रखना है। एक बार सभी पूर्वापेक्षाएँ पूरी हो जाने पर, हम एक अद्वितीय आईडी बनाते हैं, और फिर उस अद्वितीय फ़ोल्डर के लिए 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 Storage से फ़ाइल के सार्वजनिक URL को डाउनलोड करने के लिए SvelteKit एज फ़ंक्शन
जैसा कि आपको याद होगा, हमने Firebase द्वारा लौटाए गए अद्वितीय URL को मुद्दे के files कुंजी में जोड़ा था। हम उस अद्वितीय URL को मूल फ़ाइल को पुनः प्राप्त करने के लिए SvelteKit के एज फ़ंक्शन में 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 बोर्ड का एक ओपन सोर्स विकल्प क्यों?
कई लाभ हैं जो आपको भारी भुगतान वाले समाधान खरीदने के बजाय Jira Kanban बोर्ड के ओपन सोर्स विकल्प के साथ जाने के लिए मजबूर करेंगे:
- बहुत लागत बचत: ओपन सोर्स विकल्प का उपयोग करने के सबसे महत्वपूर्ण लाभों में से एक लागत बचत है। Jira जैसे भुगतान वाले Kanban बोर्ड समाधानों के विपरीत, SvelteKit, TailwindCSS, Firebase Storage, Upstash के सर्वरलेस DB, और रेट लिमिटिंग के साथ बनाया गया एक ओपन सोर्स विकल्प बिना किसी लाइसेंस शुल्क के उपयोग किया जा सकता है।
- असीमित अनुकूलन: एक ओपन सोर्स विकल्प के साथ, आपके पास कोडबेस पर पूर्ण नियंत्रण होता है और आप Kanban बोर्ड को अपनी विशिष्ट आवश्यकताओं के अनुसार अनुकूलित कर सकते हैं। यह लचीलापन अक्सर भुगतान वाले समाधानों के साथ संभव नहीं होता है जिनके पास सीमित अनुकूलन विकल्प होते हैं।
- एकीकरण में आसानी: आप अपने Kanban बोर्ड को प्रोजेक्ट मैनेजमेंट सिस्टम, वर्जन कंट्रोल टूल, नोटिफिकेशन सेवाओं, और अधिक के साथ जोड़ने के लिए APIs की शक्ति का लाभ उठा सकते हैं। इसके अतिरिक्त, प्रोजेक्ट की ओपन सोर्स प्रकृति डेवलपर्स को इसकी कार्यक्षमता बढ़ाने और उनकी विशिष्ट आवश्यकताओं के अनुरूप प्लगइन्स या एकीकरण बनाने की अनुमति देती है।
निष्कर्ष
निष्कर्ष में, इस प्रोजेक्ट ने Upstash के @upstash/redis लाइब्रेरी के साथ एज पर ग्रैन्युलर रेट लिमिटिंग, CRUD डेटा ऑपरेशन, फ़ाइलें प्राप्त करने और अपलोड करने के लिए Firebase Storage APIs को लागू करने में मूल्यवान अनुभव प्रदान किया है!