ভূমিকা
এই আর্টিকেলে আমরা একটি মিনিমাল কিন্তু পুরোপুরি ফাংশনাল অথেন্টিকেটেড REST API সার্ভিস বিল্ড করব, Next.js API Routes এবং Upstash Redis ব্যবহার করে, যা আমরা আমাদের ডেটা, ইউজার অথেন্টিকেশন, এবং JWT হ্যান্ডলিং-এর জন্য সুপারফাস্ট স্টোরেজ/ক্যাশ সিস্টেম হিসেবে ব্যবহার করব। অনুগ্রহ করে সচেতন থাকুন যে এই প্রজেক্টে কোনো ফ্রন্ট এন্ড থাকবে না, কিন্তু এটি শুধু API এক্সপোজ করবে যা বিভিন্ন ক্লায়েন্ট দিয়ে কোয়েরি করা যাবে।
পূর্বশর্ত
এই টিউটোরিয়াল ফলো করতে আপনাকে নিচের موارد দরকার:
- একটি Upstash অ্যাকাউন্ট — ফ্রি অ্যাকাউন্টের জন্য সাইন আপ করুন
- Redis-এর বেসিক জ্ঞান
- Next.js API Routes-এর বেসিক জ্ঞান
- অথেন্টিকেশন এবং অথরাইজেশন ওয়ার্কফ্লোর বেসিক জ্ঞান
- HTTP রিকোয়েস্ট করার জন্য আপনার পছন্দের একটি টুল
Upstash Redis কী?
Upstash হলো একটি সার্ভারলেস ইন-মেমরি ক্লাউড ডাটাবেস যা Redis-ভিত্তিক। আমরা এটি ব্যবহার করব আমাদের API দ্বারা সার্ভ করা ডেটা স্টোর করার জন্য, আমাদের ইউজার বেস এবং ইউজার টোকেনও এখানে স্টোর করব।
আমরা কী বিল্ড করব
আমরা একটি REST API সার্ভিস কোড করব যা ক্লায়েন্ট অ্যাপ্লিকেশনগুলোর ডেটা রিকোয়েস্ট করার অনুমতি দেবে (এই নির্দিষ্ট কেসে একটি মুভি লিস্ট); আমরা JWT ব্যবহার করে এন্ডপয়েন্টগুলো সিকিউর করব, আমরা একটি API লগইন সার্ভিস কোড করব টোকেন পাওয়ার জন্য এবং আমরা একটি রিফ্রেশ টোকেন ওয়ার্কফ্লোও ইমপ্লিমেন্ট করব।
আমরা ফোকাস করব না ক্লায়েন্ট ডেভেলপমেন্টে (যেহেতু আমরা একটি 'অনঅপিনিয়নেটেড' সার্ভিস বিল্ড করছি), কিন্তু আমরা আমাদের সার্ভিসের স্পেসিফিকেশন দিব যাতে কেউ এর জন্য ক্লায়েন্ট বিল্ড করতে পারে।
রিপোজিটরি এবং ডেমো
ফলো করার জন্য আপনি প্রজেক্ট রিপোজিটরি ক্লোন করতে পারেন
GitHub-এ সোর্স
আপনি নিচের URL-এ ডেমো ট্রাই করতে পারেন:
https://upstash-dwov9jbiq-popland.vercel.app/api/auth/signin
সার্ভিসের সাথে কানেক্ট করতে, একটি POST HTTP রিকোয়েস্ট করুন ইউজারনেম (me@home.org) এবং পাসওয়ার্ড (password) পাস করে নিচের উদাহরণ মতো (Postman ব্যবহার করে):
Redis ডাটাবেস সেট আপ করা
সবার আগে, আপনাকে Upstash Redis-এ সাইন-আপ করতে হবে (টেস্টের জন্য ফ্রি প্ল্যান কাজ করবে), একবার লগইন করলে কনসোলে আপনি একটি নতুন ডাটাবেস তৈরি করতে পারেন:
"Create database"-এ ক্লিক করুন, নাম দিন MovieManager, এবং এটি Global সেট করুন। এখন Upstash CLI ব্যবহার করে কিছু ডামি ডেটা যোগ করুন
আমরা কিছু মুভি Redis হ্যাশ হিসেবে যোগ করব (ये মূলত অবজেক্ট) HMSET কমান্ড ব্যবহার করে:
hmset movie:'Dr. Strangelove' director 'Stanley Kubrick' year 1964
hmset movie:'2001: A Space Odyssey' director 'Stanley Kubrick' year 1968
hmset movie:'Pulp Fiction' director 'Quentin Tarantino' year 1994
hmset movie:'Django Unchained' director 'Quentin Tarantino' year 2012
আমরা একটি ইউজারও যোগ করব যেটি ডেটা এক্সেস করার জন্য অথরাইজড থাকবে, ইউজারটিও একটি Redis হ্যাশ হবে:
hmset user:'me@home.org' password $2b$10$zctxUVDyy3jzvSp68oKpMOnkyra4R.NzOFVh9aii3Y43X7XtetoyK level 0
দ্রষ্টব্য: পাসওয়ার্ডটি bcrypt দিয়ে এনক্রিপ্ট করা (প্লেইন টেক্সটে এটি password), সাধারণত যেসব ইউজারকে API এক্সেস দরকার তারা একটি ওয়েবসাইটের মাধ্যমে রেজিস্টার করে (বা ওয়েবসাইট থেকে ক্রেডেনশিয়াল নেয়), এই উদাহরণে আমরা কোনো রেজিস্টার এন্ডপয়েন্ট দিচ্ছি না
Upstash CLI-এ দেওয়া প্রতিটি কমান্ড আপনাকে OK রিপ্লাই দেবে, যদি সব ঠিক থাকে এবং আপনি Data Browser-এ যান এবং Hash সিলেক্ট করেন, তবে আপনি ইনসার্ট করা ডেটার লিস্ট দেখতে পাবেন
অথরাইজেশন ওয়ার্কফ্লো
যেমন আগে বলা হয়েছে, আমাদের এন্ডপয়েন্টগুলো পাবলিকলি এক্সেসIBLE নয়, তাই আমাদের ইউজার অথেন্টিকেশন এবং অথরাইজেশনের একটি উপায় লাগবে। অথেন্টিকেশনের জন্য আমরা একটি লগইন এন্ডপয়েন্ট দিচ্ছি; অথরাইজেশনের জন্য, প্রোটেক্টেড এন্ডপয়েন্টটি একটি Authorization Header চায় যা রিকোয়েস্টের সাথে পাঠাতে হবে। বিস্তারিত ওয়ার্কফ্লো নিচে:
- ইউজার সাইন-ইন এন্ডপয়েন্টে রিকোয়েস্ট করে, ইউজারনেম এবং পাসওয়ার্ড পোস্ট করে
- সার্ভার ইউজারকে অথেন্টিকেট করার চেষ্টা করে, যদি ইউজার ভ্যালিড হয় তবে সার্ভার একটি JWT (JSON Web Token) এবং একটি রিফ্রেশ টোকেন তৈরি করে এবং পাঠিয়ে দেয়, রিফ্রেশ টোকেনটি আমাদের Upstash Redis ইন্সট্যান্সেও স্টোর হয়
- ক্লায়েন্ট টোকেনগুলো পায় এবং কোথাও সংরক্ষণ করে (কীভাবে/কোথায় সংরক্ষণ করবে সেটা ক্লায়েন্টের দায়িত্ব)
- ক্লায়েন্ট একটি প্রোটেক্টেড এন্ডপয়েন্টে রিকোয়েস্ট করে, হেডারে JWT পাঠিয়ে
- সার্ভার JWT পায়, ভেরিফাই করে, এবং যদি ভেরিফাইড হয় তবে ক্লায়েন্ট যে ডেটা চেয়েছিল সেটা পাঠিয়ে দেয়
- একবার JWT এক্সপায়ার হয়ে যাওয়া বা এক্সপায়ারের নিকট আসলে, ক্লায়েন্ট রি-লগইন ছাড়া একটি নতুন JWT পেতে পারে, নির্দিষ্ট এন্ডপয়েন্টে রিফ্রেশ টোকেন পাঠিয়ে
- সার্ভার রিফ্রেশ টোকেন পায়, ভেরিফাই করে, এবং যদি ভেরিফিকেশন পজিটিভ হয় তবে একটি নতুন JWT এবং রিফ্রেশ টোকেন ইস্যু করে, সেগুলো ক্লায়েন্টকে পাঠিয়ে দেয়, এবং নতুন রিফ্রেশ টোকেন আবার স্টোর করে
JWT এবং রিফ্রেশ টোকেন একই ফরম্যাটে, Почywiz একই ইনফরমেশন থাকে কিন্তু দুটি আলাদা কী ব্যবহার করে (যেগুলো আমরা আমাদের .env ফাইলে সেট আপ করব) এবং দুটি আলাদা এক্সপায়ারেশন: JWT-এর জন্য ছোট একটি (যেহেতু এটি সেশনের সময় সবচেয়ে বেশি ব্যবহৃত টোকেন, আমরা এটিকে শীঘ্র এক্সপায়ার করি, যাতে ইন্টারসেপ্ট হলে কম ক্ষতি হয়) এবং রিফ্রেশ টোকেনের জন্য বড় একটি। দুটির ডিউরেশন নির্ভর করে আপনি কতটা সিকিউর থাকতে চান; সাধারণত JWT এক ঘন্টার কম এক্সপায়ার হয়, এবং রিফ্রেশ টোকেন এক মাস পর্যন্ত টিকতে পারে। যদি দুটো টোকেনই এক্সপায়ার হয়ে যায়, তবে ইউজারকে আবার লগইন করতে হবে।
প্রজেক্ট সেট আপ করা
Upstash Redis ডাটাবেস নিয়ে কাজ শেষ হলে আমরা আমাদের প্রজেক্ট ইনিশিয়ালাইজ করতে পারি; প্রথমে, একটি নতুন Next.js প্রজেক্ট তৈরি করুন:
npx create-next-app upstash-jwt
তারপর নতুন তৈরি ফোল্ডার upstash-jwt-এ যান এবং প্রয়োজনীয় মডিউল ইনস্টল করুন:
npm i bcrypt jsonwebtoken @upstash/redis
আপনার কী এবং ডেটা স্টোর করার জন্য একটি .env.local ফাইল তৈরি করুন এবং সঠিক ডেটা দিয়ে পূরণ করুন
SECRET_TOKEN=
SECRET_RTOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
JWT জেনারেট করার জন্য SECRET_TOKEN এবং SECRET_RTOKEN তৈরি করুন, দয়া করে মনে রাখবেন যে এই কীগুলো গোপন রাখতে হবে এবং খুব র্যান্ডম/গেস করা কঠিন হতে হবে, আপনি 64-বিট হেক্স স্ট্রিং ব্যবহার করতে পারেন। UPSTASH_REDIS_REST_URL এবং UPSTASH_REDIS_REST_TOKEN আপনার Upstash কনসোল থেকে, Details ট্যাব, Rest API সেকশন থেকে পান:
এখন আমরা আমাদের এন্ডপয়েন্টগুলো লে-আউট করতে পারি:
POST /auth/signin এটি ইউজারকে লগইন করায়, ইমেল এবং পাসওয়ার্ড JSON অবজেক্ট হিসেবে পাস করতে হবে {"email":"email", "password": "password"}, এটি ইউজারের ইনফরমেশন, একটি JWT, এবং একটি রিফ্রেশ টোকেন সহ একটি JSON অবজেক্ট রিটার্ন করে।
GET /movies/ মুভির লিস্ট JSON অবজেক্ট হিসেবে রিটার্ন করে, এটিতে একটি ভ্যালিড JWT লাগে যা হেডারে এই ফরম্যাটে পাস করতে হবে: Authorization: Bearer xxx
GET /movies/$ID $ID আইডি 가진 মুভির ডিটেইলস রিটার্ন করে
POST /auth/refresh একটি নতুন JWT জেনারেট করে এবং রিটার্ন করে, রিফ্রেশ টোকেনটি refreshToken প্যারামিটার হিসেবে পাস করতে হবে।
API রুটের কোড
সাইন-ইন এন্ডপয়েন্ট দিয়ে শুরু করি, pages/api/auth/signin.js ফাইলটি নিচের মতো তৈরি করুন:
import bcrypt from "bcrypt";
import { addToList, generateAccessToken, generateRefreshToken, redis, } from "../../../utils";
export default async (req, res) => {
if (req.method === "GET") {
res.status(405).send("Not Allowed");
} else {
console.log(req.body.user);
try {
const user = await redis.hgetall(`user:${req.body.user}`);
if (user) {
const validPassword = bcrypt.compare(req.body.password, user.password);
if (validPassword) {
const token = generateAccessToken(req.body.user, user.level);
const refreshToken = generateRefreshToken(req.body.user, user.level);
const refresh = await addToList(req.body.user, refreshToken);
const content = { user: req.body.user, level: user.level };
res.status(200).json({ message: "Logged in", content: content, JWT: token, refresh: refreshToken });
} else {
res.status(400).json({ error: "Invalid Password" });
}
} else {
res.status(401).json({ error: "User not found" });
}
} catch (error) {
res.status(500).send("Internal Server Error");
}
}
};
আমাদের সাইন-ইন এন্ডপয়েন্ট শুধু POST এক্সেপ্ট করবে দুটি প্যারামিটার: user এবং password। সবার আগে, আমরা চেক করি ইউজার আমাদের Redis ডাটাবেসে আছে কিনা:
const user = await redis.hgetall(`user:${req.body.user}`);
যদি ইউজার থাকে তবে এনক্রিপ্টেড পাসওয়ার্ড কমপেয়ার করি:
const validPassword = bcrypt.compare(req.body.password, user.password);
এই পয়েন্টে, যদি পাসওয়ার্ড ম্যাচ করে, তবে আমরা ধরে নিতে পারি ইউজার অথেন্টিকেটেড এবং আমরা একটি JWT এবং একটি রিফ্রেশ টোকেন পাঠিয়ে দিতে পারি, আমরা রিফ্রেশ টোকেনও আমাদের Redis ইন্সট্যান্সে স্টোর করি। এটি করার জন্য আমরা কিছু ফাংশন ব্যবহার করি যা একটি এক্সটার্নাল ফাইল utils.js-এ আছে
ক্লায়েন্টের দায়িত্ব হলো রিটার্ন করা টোকেনগুলো সংরক্ষণ করা, অথরাইজেশনের সময় সেগুলো ব্যবহার করা, এবং এক্সপায়ার হলে রিফ্রেশ করা
আমাদের একটি ফাংশন আছে আমাদের টোকেন জেনারেট করার জন্য generateAccessToken, একটি রিফ্রেশ টোকেন জেনারেট করার জন্য generateRefreshToken, এবং একটি রিফ্রেশ টোকেন আমাদের Redis-এ স্টোর করার জন্য addToList। এই utils.js ফাইলটি অন্যান্য ইউটিলিটি ফাংশন এবং রেফারেন্সও রাখবে (যেমন Redis কানেকশন, টোকেন ভেরিফিকেশন এবং রিফ্রেশ, ইত্যাদি):
import { Redis } from "@upstash/redis";
import jwt from "jsonwebtoken";
export const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
export function generateAccessToken(username, email, level) {
return jwt.sign(
{ user: username, email: email, level: level },
process.env.SECRET_TOKEN,
{ expiresIn: "1h" },
);
}
export function generateRefreshToken(username, email, level) {
return jwt.sign(
{ user: username, email: email, level: level },
process.env.SECRET_RTOKEN,
{ expiresIn: "30d" },
);
}
export async function addToList(user, refresher) {
try {
await redis.hset("refresh:" + user, { refresh: refresher });
} catch (error) {
console.log(error);
}
}
export async function tokenRefresh(refreshtoken, res) {
var decoded = "";
try {
decoded = jwt.verify(refreshtoken, process.env.SECRET_RTOKEN);
} catch (error) {
return res.status(401).send("Can't refresh. Invalid Token");
}
if (decoded) {
try {
const rtoken = await redis.hget("refresh:" + decoded.user, "refresh");
console.log(rtoken);
if (rtoken !== refreshtoken) {
return res.status(401).send("Can't refresh. Invalid Token");
} else {
const user = await redis.hgetall(`user:${decoded.user}`);
console.log(user);
const token = generateAccessToken(decoded.user, user.level);
const refreshToken = generateRefreshToken(decoded.user, user.level);
const refresh = await addToList(decoded.user, refreshToken);
const content = { user: decoded.user, level: user.level };
return { message: "Token Refreshed", content: content, JWT: token, refresh: refreshToken };
}
} catch (error) {
console.log(error);
}
}
}
export async function verifyToken(token, res) {
try {
const decoded = jwt.verify(token, process.env.SECRET_TOKEN);
return decoded;
} catch (err) {
return res.status(405).send("Token is invalid");
}
}
এখন, আমরা একটি টুল (যেমন Postman) ব্যবহার করে সাইন-ইন প্রসেস টেস্ট করতে পারি, https://localhost:3000/api/auth/signin-এ পোস্ট করে এবং ইউজারনেম (me@home.org) এবং পাসওয়ার্ড (password) পাস করে, আপনাকে একটি JSON অবজেক্ট ফেরত মেলবে যাতে ইউজার ডিটেইলসের পাশাপাশি একটি JWT এবং একটি রিফ্রেশ টোকেন থাকবে:
যদি সব ঠিক থাকে, তবে আপনার Redis ডাটাবেসে আপনাকে নতুন তৈরি হওয়া রিফ্রেশ টোকেনের জন্য একটি নতুন Hash এন্ট্রি দেখতে मिलবে:
এরপর, আমরা অথেন্টিকেশন প্রসেস সম্পূর্ণ করছি, একটি টোকেন রিফ্রেশ রুট refresh.js কোড করে
import { redis, tokenRefresh } from "../../../utils";
export default async (req, res) => {
if (req.method === "GET") {
res.status(405).send("Not Allowed");
} else {
console.log(req.body.refresh);
const refresp = await tokenRefresh(req.body.refresh, res);
res.status(200).json(refresp);
}
};
এটি utils.js-র tokenRefresh ফাংশনটি ব্যবহার করে এটি শুরু করে টোকেনটি ভ্যালিড এবং ডিকোডযোগ্য কিনা চেক করে, তারপর Redis-এ চেক করে ইউজারের রিফ্রেশ টোকেন আছে কিনা (যেটি আমরা addToList দিয়ে আগে স্টোর করেছিলাম), যদি সব ঠিক থাকে তবে একটি নতুন JWT, একটি নতুন রিফ্রেশ টোকেন জেনারেট করে (এবং আবার Redis-এ স্টোর করে) এবং সব কিছু ক্লায়েন্টকে পাঠিয়ে দেয়।
আমরা আমাদের টুল দিয়ে এই এন্ডপয়েন্ট টেস্ট করতে পারি, https://localhost:3000/api/auth/refresh-এ পোস্ট করে এবং রিফ্রেশ টোকেন প্যারামিটার হিসেবে পাস করে:
এখন, আমাদের হיפותেটিক্যাল ক্লায়েন্ট লগইন করতে পারবে এবং তার টোকেন রিফ্রেশ করতে পারবে, চলুন দেখি টোকেনটি কীভাবে অথেন্টিকেটেড রিকোয়েস্ট করার জন্য ব্যবহার করা যায়।
একটি নতুন API রুট তৈরি করুন: api/movies/[[...id]].js যা মুভির লিস্ট পাওয়ার জন্য এবং মুভি ডিটেইল পাওয়ার জন্য ব্যবহার হবে:
import { redis, verifyToken } from "../../../utils";
export default async (req, res) => {
var id;
console.log(req.query);
if (req.query.id) {
id = req.query.id[0];
}
var decoded = "";
const authHeader = req.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
return res.status(403).send("A token is required for authentication");
} else {
decoded = await verifyToken(token, res);
}
if (decoded) {
if (id) {
try {
const result = await redis.hgetall(id);
console.log(result);
return res.status(200).json(result);
} catch (error) {
return res.status(500).send("Internal Server Error");
}
} else {
try {
const result = await redis.scan(0, { match: "movie:*" });
return res.status(200).json(result);
} catch (error) {
return res.status(500).send("Internal Server Error");
}
}
}
};
আমাদের utils.js-র verifyToken ফাংশন ব্যবহার করে আমরা আমাদের API এন্ডপয়েন্টে অ্যাক্সেস শুধু ভ্যালিড টোকেন প্রদানকারী ইউজারদের জন্য সীমাবদ্ধ করতে পারি। আমরা কয়েকটি স্যাম্পল কোয়েরি করলাম, প্রথমটি মুভির লিস্ট পাওয়ার জন্য
const result = await redis.scan(0, { match: 'movie:*' });
এবং দ্বিতীয়টি একটি সিঙ্গল মুভির ডিটেইল পাওয়ার জন্য, URL রিকোয়েস্টের আইডি প্যারামিটার ভিত্তিক করে:
const result = await redis.hgetall(id);
দুইটি রিকোয়েস্টই ইউজার স্ট্যাটাসের ওপর ডিপেন্ডেন্ট যা verifyToken দিয়ে চেক করা হয়েছে কিন্তু আপনি মিক্স অ্যান্ড ম্যাচ করতে পারেন, উদাহরণস্বরূপ লিস্টটি পাবলিক রাখা যেতে পারে এবং ডিটেইলস প্রোটেক্টেড। যেহেতু আমাদের ইউজারে একটি লেভেলও স্টোর করা আছে (এবং টোকেনেও) আমরা আরও অথরাইজেশন লেভেল रखতে পারি। চলুন মুভির লিস্ট পাওয়ার চেষ্টা করি:
একটি সিঙ্গল মুভির ডিটেইলস:
ক্লায়েন্টের দৃষ্টিভঙ্গি
যেমন আমরা আগে বলেছি, আমরা শুধু সার্ভার পার্টে ফোকাস করেছি, এটিই একটি API-এর মূল স্কোপ, এটি অ্যাবস্ট্র্যাক্ট হওয়া উচিত, এটি একটি ওয়েবসাইট নয়। ক্লায়েন্ট কীভাবে ডেটা রিকোয়েস্ট করবে (প্রোগ্রামিং ল্যাঙ্গুয়েজ, লাইব্রেরি, ইত্যাদি) সেটা ক্লায়েন্ট ডেভেলপারের চয়েস, আমরা এন্ডপয়েন্টের লিস্ট দিচ্ছি, আমাদের এন্ডপয়েন্ট কী এক্সপেক্ট করছে, এবং আমাদের এন্ডপয়েন্ট ক্লায়েন্টকে কী রিটার্ন করছে। ডেটা হ্যান্ডলিং, রিফ্রেশ ডিলে, ইত্যাদি সব ক্লায়েন্টের Страটেজি।
আগামী কী
এটি শুধু একটি প্রোটেক্টেড API-এর ওয়ার্কফ্লোর বেসিক উদাহরণ। এখান থেকে এটি শুধু উন্নত হইতে পারে: Redis-এ ডেটা কীভাবে স্টোর হয় সেটা অপ্টিমাইজ করা, অন্য একটি Redis ইন্সট্যান্সে ইউজার ডেটা স্টোর করে লগইন সিকিউরিটি বাড়ানো, ইউজার যা পাঠাচ্ছে সেটা চেক ও ভ্যালিডেট করা সবার আগে, আরও এন্ডপয়েন্ট যোগ করা, GraphQL ফরম্যাটে ডেটা রিটার্ন করা, আপনার API-র জন্য একটি ক্লায়েন্ট বিল্ড করা, প্রতি ঘন্টা मैক্সিমাম কল সংখ্যা সীমাবদ্ধ করা, লেভেল ব্যবহার করে অ্যাক্সেস সীমাবদ্ধ করা... এক্সটেনশন এবং উন্নতির কোনো সীমা নেই!