برنامه نویسی

9 نکته برای ایجاد یک برنامه وب امن با tRPC، Next.js، Prisma، Turbo و NextAuth

Summarize this content to 400 words in Persian Lang
من اخیرا Minute: یک برنامه ردیابی زمان منبع باز برای افرادی که از T3 Stack به عنوان پروژه آخر هفته استفاده می کنند ساخته ام. در این مقاله، نکات امنیتی را که در حین ساخت Minute یاد گرفتم، معرفی می کنم.

ساختار دایرکتوری

این بخش به طور مستقیم به امنیت مربوط نمی شود، اما ابتدا توضیح داده شده است تا درک بخش های زیر آسان تر شود.

من Turborepo را برای مدیریت کد منبع خود به عنوان monorepo پذیرفتم. با توجه به ساخت یک پروژه مونورپو، فکر می کنم سخت ترین قسمت این است که چگونه کد منبع را جدا کنیم و از چه ساختار دایرکتوری استفاده کنیم.

در ابتدا به فکر ایجاد یک ساختار دایرکتوری بر اساس Clean Architecture بودم، اما پس از شروع، نظرم تغییر کرد. این به این دلیل است که اگرچه معماری پاک یک رویکرد شناخته شده و سنجیده است، اما برای Minute کمی بیش از حد مهندسی شده است.

من در نهایت ساختار دایرکتوری را به این صورت ساده کردم:

من برخی از بسته ها را توضیح می دهم، به جز آنهایی که واضح هستند.

prisma/

لینک GitHub

این بسته شامل فایل طرحواره Prisma، فایل های مهاجرت و Prisma Client می باشد. قابل ذکر است که Prisma Client از چندین بسته وارد می شود: trpc، سرویس، و برنامه ها/وب. من توصیه می کنم یک بسته prisma/ ایجاد کنید زیرا درک فایل های مرتبط با Prisma را آسان تر می کند.

schema/

لینک GitHub

این شامل طرحواره‌های Zod برای هر مدل Prisma و برخی ثابت‌های مرتبط با اعتبارسنجی است، مانند MAX_NAME_LENGTH، REGEXP، و غیره. اینها برای اعتبارسنجی استفاده می‌شوند.

services/

لینک GitHub

این شامل توابعی است که یک رویه واحد را اجرا می کند، مانند درج یا به روز رسانی یک مدل پس از تأیید پارامترها.

trpc/

لینک GitHub

این شامل میان افزار tRPC، روترها و رویه ها می شود. همه رویه‌ها فاقد منطق هستند و پردازش را به سرویس مناسب واگذار می‌کنند. با انجام این کار می توانم در آینده بدون تغییر منطق به کتابخانه دیگری (مثلاً serverAction) در صورت نیاز سوئیچ کنم.

اعتبارسنجی پارامترها در هر بسته

اعتبار سنجی مهمترین بخش محافظت از یک برنامه وب است. بنابراین، من می‌خواستم پارامترهای ورودی ارسال شده توسط کاربران و خروجی‌ها را در بسته‌های trpc و سرویس اعتبارسنجی کنم. این رویکرد مشابه برنامه نویسی قراردادی است.

در برنامه نویسی قرارداد، هر نقطه ورودی (بسته) باید مقادیر ورودی/خروجی را تأیید کند، و اگر اعتبارسنجی ناموفق باشد، تابع خطا می دهد و فرآیند باقی مانده را به دلایل امنیتی اجرا نمی کند.

خوشبختانه، tRPC از قبل این قابلیت را دارد که ورودی ها و خروجی ها را اعتبارسنجی کند، کتابخانه Zod نیز دارای ویژگی مشابهی به نام z.function() است.

بنابراین من یک تابع کمکی به نام ساختم contract، که یک wrapper کوچک از z.function().

import type { ZodTypeAny } from “zod”;
import { z } from “zod”;

export const contract =
I extends ZodTypeAny,
O extends ZodTypeAny,
F extends (input: z.inferI>) => z.inferO>,
>(
{ input, output }: { input: I; output: O },
implement: F,
) => {
return z.function().args(input).returns(output).implement(implement);
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و از آن در هر بسته خدماتی استفاده کنید.

export const getUser = (db: PrismaClient) =>
contract(
{
// the userId should be UUID otherwise it throws an error.
input: z.strictObject({
userId: z.string().uuid(),
}),
// the result object should be the user object.
output: z.promise(userSchema),
},
async (input) => {
const user = await db.user.findFirst({
select: {
id: true,
name: true,
image: true,
createdAt: true,
updatedAt: true,
},
where: {
id: input.userId,
},
});
if (user === null) {
throw Error(“The user does not exist.”);
}
return user;
},
);

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بسته trpc همچنین داده های ورودی/خروجی را تأیید می کند.

export const tasksRouter = router({
getTasksInFolder: protectedProcedure
.input(
z.strictObject({
folderId: z.string().uuid(),
}),
)
.output(z.array(taskSchema))
.query(async ({ input, ctx }) => {
return getTasksInFolder(ctx.db)({
userId: ctx.currentUserId,
folderId: input.folderId,
});
}),

})

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من عاشق این رویکرد هستم. به ویژه، اعتبار سنجی خروجی به اندازه اعتبار سنجی ورودی مهم است. به عنوان مثال، تصور کنید به طور تصادفی داده های حساس (مانند sessionToken) را به یک شیء نتیجه اضافه کرده ایم. اعتبار سنجی خروجی به جای ارسال نتیجه به کاربر، یک خطا ایجاد می کند.

تنظیم قالب خطای سفارشی tRPC

از آنجایی که tRPC فرمت‌کننده خطای پیش‌فرض را ارائه می‌کند، می‌توانید بدون نیاز به راه‌اندازی فرمت‌کننده خطای سفارشی از tRPC استفاده کنید. با این حال، من توصیه می‌کنم یک قالب‌کننده خطای سفارشی تنظیم کنید.

اگر خطایی رخ دهد، tRPC an را برمی گرداند پیغام خطا به عنوان یک پاسخ به طور پیش فرض این معمولاً مشکلی نیست، اما برخی از کتابخانه ها در صورت بروز خطا، پیام های پرمخاطب را به error.message اضافه می کنند.

به عنوان مثال، کتابخانه Zod یک پیام مفید به error.message اضافه می کند.

این پیام هنگام اشکال زدایی مفید است، اما این خطر وجود دارد که یک مهاجم بتواند حدس بزند چه خطاهایی در داخل رخ داده است. بنابراین توصیه می‌کنم یک قالب‌کننده سفارشی مانند زیر تنظیم کنید:

initTRPC.contexttypeof createInnerContext>().create({
transformer: superjson,
errorFormatter({ shape }) {
// Hide a message to avoid exposing server errors.
return { …shape, message: “” };
},
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با انجام این کار، tRPC همیشه در صورت بروز خطا، یک رشته خالی را برمی گرداند.

ENV ها را با T3 Env مدیریت کنید

T3 Env برای مدیریت نوع ایمن متغیرهای محیط برنامه استفاده می شود. از آنجایی که به ندرت پیش می‌آید که برنامه‌ای هیچ متغیر محیطی نداشته باشد، این کتابخانه را به همه کسانی که از Next.js استفاده می‌کنند توصیه می‌کنم.

اگر قصد استفاده از این را دارید، من نیز توصیه می کنم طبق این هشدار در اسناد رسمی، طرحواره ها را به دو فایل تقسیم کنید: یکی برای عمومی (مشتری) و دیگری برای مخفی (سرور).

این به این دلیل است که اگر طرحواره را جدا نکنید، طرح‌های اعتبارسنجی (کلیدها) برای متغیرهای سرور به مشتری ارسال می‌شود. البته، مقادیر ENV در معرض نمایش نیستند، بنابراین اجباری نیست. با این حال، طرح‌واره‌های اعتبارسنجی می‌توانند در مورد نوع کتابخانه‌هایی که برنامه به صورت داخلی استفاده می‌کند (مثلاً NEXTAUTH_***).

اگر از آن استفاده نمی کنید، fetchCache را غیرفعال کنید

هنگام ایجاد Minute، یک برنامه ردیابی زمان منبع باز، تصمیم گرفتم درخواست‌های API را در سمت کلاینت انجام دهم و درخواستی در SSR (RSC) انجام ندهم، زیرا همه صفحات داده‌های خصوصی را مدیریت می‌کنند و نیاز به احراز هویت دارند، و می‌خواستم از نگرانی در مورد مسائل مربوط به حافظه پنهان جلوگیری کنم. .

اگر می‌خواهید ذخیره داده‌های واکشی شده را غیرفعال کنید، تنظیم کنید only-no-store یا force-no-store برای واکشی کش در صفحه.

export const fetchCache = “only-no-store”;

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چه زمانی force-no-store ارسال می‌شود، Next.js داده‌های واکشی شده را در حافظه پنهان نمی‌کند، حتی اگر a را ارائه کنید force-cache گزینه در اجزای فرزند. متقابلا، only-no-store همچنین cache را غیرفعال می کند و در صورت ارائه یک خطا، خطا می دهد force-cache گزینه.

من شخصا استفاده از آن را توصیه می کنم only-no-store زیرا اگر شما یا شخص دیگری به طور ناخواسته ذخیره پنهان را فعال کنید، به شما هشدار می دهد و به شما امکان می دهد متوجه اشتباه شوید.

Error: cache: ‘force-cache’ used on fetch for https://… with ‘export const fetchCache=”only-no-store”

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هدر CSP را اضافه کنید

هدر CSP برای کاهش خطر حملات XSS ضروری است. خوشبختانه، در اینجا دستورالعمل نحوه تنظیم هدر CSP در اسناد Next.js آمده است.

به غیر از هدر CSP، توصیه می کنم هدرهای امنیتی دیگری را اضافه کنید. کتابخانه هایی به طور خاص برای افزودن این سرصفحه ها وجود دارد (مثلاً next-safe).

پیشگیری CSRF را اضافه کنید

اگر برنامه Next.js شما دارای مسیرهای API است، افزودن اعتبارسنجی سرصفحه Origin در میان افزار برای جلوگیری از حملات CSRF ارزشمند است. مرورگرهای مدرن منشایی را که باعث درخواست در سرآیند Origin قبل از ارسال درخواست API شده است را شامل می‌شود که می‌توان از آن برای تعیین اینکه آیا درخواست از همان مبدا است استفاده کرد.

const hasValidOrigin = (req: NextRequest) => {
const origin = req.headers.get(“Origin”);
return origin === null || origin === ‘https://YOUR-ORIGIN’;
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

FYI: این تکنیک در ServerAction نیز استفاده می شود.

برای کسانی که از tRPC استفاده می کنند، توصیه می شود اعتبارسنجی را اضافه کنند تا اطمینان حاصل شود که نوع محتوا JSON است. این رویکرد در tRPC v11 پیاده‌سازی خواهد شد، اما از آنجایی که نسخه 11 در حال حاضر در نسخه بتا است، می‌توانید این اعتبار را با پیاده‌سازی خودتان اضافه کنید.

const hasJsonContentType = (req: NextRequest) => {
const contentType = req.headers.get(“Content-Type”);
return (
typeof contentType === “string” &&
contentType.startsWith(“application/json”)
);
};

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

علاوه بر این، اگر از کوکی‌ها برای ذخیره شناسه‌های جلسه استفاده می‌کنید، توصیه می‌کنم ویژگی‌های HttpOnly، Secure و SameSite کوکی را بررسی کنید. (من معتقدم که NextAuth این ویژگی ها را به طور پیش فرض به درستی تنظیم می کند.)

هدر Cache-Control را اضافه کنید

هدر Cache-Control برای کنترل اینکه آیا داده های پاسخ را می توان کش کرد یا نه استفاده می شود. در مورد Minute، تمام پاسخ‌های tRPC خصوصی هستند و من نمی‌خواهم آنها را در حافظه پنهان نگه دارم.

برای اضافه کردن هدر Cache-Control، به سادگی تنظیمات را به next.config.js اضافه کنید.

/** @type {import(‘next’).NextConfig} */
const nextConfig = {
reactStrictMode: true,
headers() {
return [
{
source: “/api/:path*”,
headers: [
{
key: “Cache-Control”,
value: “private, no-store, no-cache, must-revalidate”,
},
{ key: “Pragma”, value: “no-cache” },
],
},
];
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که نمی‌توانید هدرهای Cache-Control را در next.config.js برای صفحات یا دارایی‌ها تنظیم کنید. Next.js به طور خودکار هدرهای Cache-Control مناسب را به هر صفحه اضافه می کند (شاید، امیدوارم).

فقط از سرور استفاده کنید

در پروژه Next.js شما، فایل‌هایی وجود دارند که قرار است فقط در سرور استفاده شوند و ممکن است نخواهید آنها را در معرض مرورگر مشتری قرار دهید. در این صورت می توانید از بسته سرور استفاده کنید.

اگر فایلی وارد شود server-only بسته ها و از کلاینت وارد می شود، برای جلوگیری از نشت کد سمت سرور، خطا ایجاد می کند. من توصیه می کنم یک واردات اضافه کنید server-only به هر فایلی که بدیهی است نیازی به استفاده توسط مشتری نیست. به عنوان مثال، در […nextauth]/route.ts):

// Prevent importing from client-side code.
import “server-only”;

import NextAuth from “next-auth”

const handler = NextAuth({

})

export { handler as GET, handler as POST }

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کد Backend را تست کنید

تست های Backend برای سالم نگه داشتن برنامه شما ضروری هستند. در مورد Minute، از آنجایی که من از Prisma در باطن (trpc و بسته سرویس) استفاده کردم، مجبور شدم Prisma Client را مسخره کنم یا از یک کانتینر Docker برای راه اندازی پایگاه داده برای آزمایش برای نوشتن تست های Backend استفاده کنم. اسناد Prisma هر دو گزینه را معرفی می کنند: تست واحد و تست ادغام. من تست ادغام را پذیرفتم زیرا دقیق تر از تست واحد است زیرا از یک پایگاه داده واقعی استفاده می کند.

اگر قصد دارید تست های یکپارچه سازی بنویسید، ممکن است بخواهید هر بار که آزمایش اجرا می شود پایگاه داده را بازنشانی کنید. در آن صورت، گزینه –force-reset مفید خواهد بود.

// package.json
{

“scripts”: {
“db:reset”: “prisma db push –force-reset”
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همچنین، اگر در تلاش برای ایجاد داده‌های ساختگی برای آزمایش هستید، من FactoryJS را توصیه می‌کنم که برای ایجاد اشیا یا رکوردهای Prisma ساده است.

وقتی تست می نویسید، توصیه می کنم هم موارد رایج و هم موارد لبه را پوشش دهید. مثلا اگر شناسه داده شده نامعتبر باشد چه؟

describe(“deleteChart”, () => {
describe(“when a user has the chart”, () => {
it(“deletes the chart and its items”, async () => {
const user = await userFactory.create();
const chart = await chartFactory.vars({ user: () => user }).create();
await expect(
deleteChart(db)({
id: chart.id,
userId: user.id,
}),
).resolves.toBeUndefined();
await expect(
db.chart.findFirst({
where: {
id: chart.id,
},
}),
).resolves.toBeNull();
});
});

describe(“when the id is invalid”, () => {
it(“throws an error”, async () => {
const user = await userFactory.create();
await expect(
deleteChart(db)({
id: ‘INVALID’,
userId: user.id,
}),
).rejects.toThrow(“The chart does not exist.”);
});
});
});

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

نتیجه

من نکاتی را که در طول توسعه Minute پیدا کردم، معرفی کردم. با این حال، مطمئن نیستم که همه نکات مهم را فهرست کرده باشم (احتمالا نه، زیرا این کتابخانه ها پیچیده هستند و روز به روز تغییر می کنند). اما حداقل معتقدم این مقاله برای همه کسانی که قصد دارند با Next.js، Prisma و غیره برنامه بسازند مفید خواهد بود.

امیدوارم این مقاله برای شما مفید و جالب باشد!

اگر علاقه مند به Minute هستید، می توانید از لینک زیر به مخزن دسترسی داشته باشید. ما همچنین از مشارکت ها استقبال می کنیم.

⏰ برنامه ردیابی زمان منبع باز برای افراد.

⏰دقیقه

برنامه ردیابی زمان منبع باز برای افراد.

در باره

دقیقه یک برنامه ردیابی زمان منبع باز است.برخلاف سایر برنامه‌های ردیابی زمان، Minute عمدتاً بر استفاده فردی متمرکز است و برای کمک به کاربران در بررسی نحوه گذراندن زمان و استفاده معنادارتر از آن طراحی شده است.

اسکرین شات ها

امکانات

پوشه ها

ورودی‌های زمان ردیابی‌شده خود را با پوشه‌ها مدیریت کنید و زمان مصرف اخیر خود را در هر پوشه در صفحه گزارش تجزیه و تحلیل کنید. همچنین می توانید پوشه های ایجاد شده و ورودی های زمان را در هر زمان از نوار کناری مشاهده و ویرایش کنید.

دسته بندی ها

چندین پوشه را در یک دسته گروه بندی کنید و از آنها برای تجزیه و تحلیل در صفحه گزارش استفاده کنید. به عنوان مثال، دسته بندی هایی را برای زمانی که می خواهید کاهش دهید و زمانی که می خواهید افزایش دهید ایجاد کنید و از آنها در صفحه گزارش استفاده کنید.

نمودارهای سفارشی

از پوشه ها و دسته بندی های ایجاد شده برای نمایش افزایش و کاهش زمان استفاده در نمودار استفاده کنید. موارد نمایش داده شده در نمودار…

من اخیرا Minute: یک برنامه ردیابی زمان منبع باز برای افرادی که از T3 Stack به عنوان پروژه آخر هفته استفاده می کنند ساخته ام. در این مقاله، نکات امنیتی را که در حین ساخت Minute یاد گرفتم، معرفی می کنم.


ساختار دایرکتوری

این بخش به طور مستقیم به امنیت مربوط نمی شود، اما ابتدا توضیح داده شده است تا درک بخش های زیر آسان تر شود.

من Turborepo را برای مدیریت کد منبع خود به عنوان monorepo پذیرفتم. با توجه به ساخت یک پروژه مونورپو، فکر می کنم سخت ترین قسمت این است که چگونه کد منبع را جدا کنیم و از چه ساختار دایرکتوری استفاده کنیم.

در ابتدا به فکر ایجاد یک ساختار دایرکتوری بر اساس Clean Architecture بودم، اما پس از شروع، نظرم تغییر کرد. این به این دلیل است که اگرچه معماری پاک یک رویکرد شناخته شده و سنجیده است، اما برای Minute کمی بیش از حد مهندسی شده است.

من در نهایت ساختار دایرکتوری را به این صورت ساده کردم:

من برخی از بسته ها را توضیح می دهم، به جز آنهایی که واضح هستند.

prisma/

لینک GitHub

این بسته شامل فایل طرحواره Prisma، فایل های مهاجرت و Prisma Client می باشد. قابل ذکر است که Prisma Client از چندین بسته وارد می شود: trpc، سرویس، و برنامه ها/وب. من توصیه می کنم یک بسته prisma/ ایجاد کنید زیرا درک فایل های مرتبط با Prisma را آسان تر می کند.

schema/

لینک GitHub

این شامل طرحواره‌های Zod برای هر مدل Prisma و برخی ثابت‌های مرتبط با اعتبارسنجی است، مانند MAX_NAME_LENGTH، REGEXP، و غیره. اینها برای اعتبارسنجی استفاده می‌شوند.

services/

لینک GitHub

این شامل توابعی است که یک رویه واحد را اجرا می کند، مانند درج یا به روز رسانی یک مدل پس از تأیید پارامترها.

trpc/

لینک GitHub

این شامل میان افزار tRPC، روترها و رویه ها می شود. همه رویه‌ها فاقد منطق هستند و پردازش را به سرویس مناسب واگذار می‌کنند. با انجام این کار می توانم در آینده بدون تغییر منطق به کتابخانه دیگری (مثلاً serverAction) در صورت نیاز سوئیچ کنم.


اعتبارسنجی پارامترها در هر بسته

اعتبار سنجی مهمترین بخش محافظت از یک برنامه وب است. بنابراین، من می‌خواستم پارامترهای ورودی ارسال شده توسط کاربران و خروجی‌ها را در بسته‌های trpc و سرویس اعتبارسنجی کنم. این رویکرد مشابه برنامه نویسی قراردادی است.

در برنامه نویسی قرارداد، هر نقطه ورودی (بسته) باید مقادیر ورودی/خروجی را تأیید کند، و اگر اعتبارسنجی ناموفق باشد، تابع خطا می دهد و فرآیند باقی مانده را به دلایل امنیتی اجرا نمی کند.

خوشبختانه، tRPC از قبل این قابلیت را دارد که ورودی ها و خروجی ها را اعتبارسنجی کند، کتابخانه Zod نیز دارای ویژگی مشابهی به نام z.function() است.

بنابراین من یک تابع کمکی به نام ساختم contract، که یک wrapper کوچک از z.function().

import type { ZodTypeAny } from "zod";
import { z } from "zod";

export const contract = 
  I extends ZodTypeAny,
  O extends ZodTypeAny,
  F extends (input: z.inferI>) => z.inferO>,
>(
  { input, output }: { input: I; output: O },
  implement: F,
) => {
  return z.function().args(input).returns(output).implement(implement);
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

و از آن در هر بسته خدماتی استفاده کنید.

export const getUser = (db: PrismaClient) =>
  contract(
    {
      // the userId should be UUID otherwise it throws an error.
      input: z.strictObject({
        userId: z.string().uuid(),
      }),
      // the result object should be the user object.
      output: z.promise(userSchema),
    },
    async (input) => {
      const user = await db.user.findFirst({
        select: {
          id: true,
          name: true,
          image: true,
          createdAt: true,
          updatedAt: true,
        },
        where: {
          id: input.userId,
        },
      });
      if (user === null) {
        throw Error("The user does not exist.");
      }
      return user;
    },
  );
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بسته trpc همچنین داده های ورودی/خروجی را تأیید می کند.

export const tasksRouter = router({
  getTasksInFolder: protectedProcedure
    .input(
      z.strictObject({
        folderId: z.string().uuid(),
      }),
    )
    .output(z.array(taskSchema))
    .query(async ({ input, ctx }) => {
      return getTasksInFolder(ctx.db)({
        userId: ctx.currentUserId,
        folderId: input.folderId,
      });
    }),
...
})
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من عاشق این رویکرد هستم. به ویژه، اعتبار سنجی خروجی به اندازه اعتبار سنجی ورودی مهم است. به عنوان مثال، تصور کنید به طور تصادفی داده های حساس (مانند sessionToken) را به یک شیء نتیجه اضافه کرده ایم. اعتبار سنجی خروجی به جای ارسال نتیجه به کاربر، یک خطا ایجاد می کند.


تنظیم قالب خطای سفارشی tRPC

از آنجایی که tRPC فرمت‌کننده خطای پیش‌فرض را ارائه می‌کند، می‌توانید بدون نیاز به راه‌اندازی فرمت‌کننده خطای سفارشی از tRPC استفاده کنید. با این حال، من توصیه می‌کنم یک قالب‌کننده خطای سفارشی تنظیم کنید.

اگر خطایی رخ دهد، tRPC an را برمی گرداند پیغام خطا به عنوان یک پاسخ به طور پیش فرض این معمولاً مشکلی نیست، اما برخی از کتابخانه ها در صورت بروز خطا، پیام های پرمخاطب را به error.message اضافه می کنند.

به عنوان مثال، کتابخانه Zod یک پیام مفید به error.message اضافه می کند.

کتابخانه Zod یک پیام مفید اضافه می کند

این پیام هنگام اشکال زدایی مفید است، اما این خطر وجود دارد که یک مهاجم بتواند حدس بزند چه خطاهایی در داخل رخ داده است. بنابراین توصیه می‌کنم یک قالب‌کننده سفارشی مانند زیر تنظیم کنید:

initTRPC.contexttypeof createInnerContext>().create({
  transformer: superjson,
  errorFormatter({ shape }) {
    // Hide a message to avoid exposing server errors.
    return { ...shape, message: "" };
  },
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با انجام این کار، tRPC همیشه در صورت بروز خطا، یک رشته خالی را برمی گرداند.

tRPC یک رشته خالی برمی گرداند


ENV ها را با T3 Env مدیریت کنید

T3 Env برای مدیریت نوع ایمن متغیرهای محیط برنامه استفاده می شود. از آنجایی که به ندرت پیش می‌آید که برنامه‌ای هیچ متغیر محیطی نداشته باشد، این کتابخانه را به همه کسانی که از Next.js استفاده می‌کنند توصیه می‌کنم.

اگر قصد استفاده از این را دارید، من نیز توصیه می کنم طبق این هشدار در اسناد رسمی، طرحواره ها را به دو فایل تقسیم کنید: یکی برای عمومی (مشتری) و دیگری برای مخفی (سرور).

این به این دلیل است که اگر طرحواره را جدا نکنید، طرح‌های اعتبارسنجی (کلیدها) برای متغیرهای سرور به مشتری ارسال می‌شود. البته، مقادیر ENV در معرض نمایش نیستند، بنابراین اجباری نیست. با این حال، طرح‌واره‌های اعتبارسنجی می‌توانند در مورد نوع کتابخانه‌هایی که برنامه به صورت داخلی استفاده می‌کند (مثلاً NEXTAUTH_***).


اگر از آن استفاده نمی کنید، fetchCache را غیرفعال کنید

هنگام ایجاد Minute، یک برنامه ردیابی زمان منبع باز، تصمیم گرفتم درخواست‌های API را در سمت کلاینت انجام دهم و درخواستی در SSR (RSC) انجام ندهم، زیرا همه صفحات داده‌های خصوصی را مدیریت می‌کنند و نیاز به احراز هویت دارند، و می‌خواستم از نگرانی در مورد مسائل مربوط به حافظه پنهان جلوگیری کنم. .

اگر می‌خواهید ذخیره داده‌های واکشی شده را غیرفعال کنید، تنظیم کنید only-no-store یا force-no-store برای واکشی کش در صفحه.

export const fetchCache = "only-no-store";
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

چه زمانی force-no-store ارسال می‌شود، Next.js داده‌های واکشی شده را در حافظه پنهان نمی‌کند، حتی اگر a را ارائه کنید force-cache گزینه در اجزای فرزند. متقابلا، only-no-store همچنین cache را غیرفعال می کند و در صورت ارائه یک خطا، خطا می دهد force-cache گزینه.

من شخصا استفاده از آن را توصیه می کنم only-no-store زیرا اگر شما یا شخص دیگری به طور ناخواسته ذخیره پنهان را فعال کنید، به شما هشدار می دهد و به شما امکان می دهد متوجه اشتباه شوید.

Error: cache: 'force-cache' used on fetch for https://... with 'export const fetchCache="only-no-store"
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


هدر CSP را اضافه کنید

هدر CSP برای کاهش خطر حملات XSS ضروری است. خوشبختانه، در اینجا دستورالعمل نحوه تنظیم هدر CSP در اسناد Next.js آمده است.

به غیر از هدر CSP، توصیه می کنم هدرهای امنیتی دیگری را اضافه کنید. کتابخانه هایی به طور خاص برای افزودن این سرصفحه ها وجود دارد (مثلاً next-safe).


پیشگیری CSRF را اضافه کنید

اگر برنامه Next.js شما دارای مسیرهای API است، افزودن اعتبارسنجی سرصفحه Origin در میان افزار برای جلوگیری از حملات CSRF ارزشمند است. مرورگرهای مدرن منشایی را که باعث درخواست در سرآیند Origin قبل از ارسال درخواست API شده است را شامل می‌شود که می‌توان از آن برای تعیین اینکه آیا درخواست از همان مبدا است استفاده کرد.

const hasValidOrigin = (req: NextRequest) => {
  const origin = req.headers.get("Origin");
  return origin === null || origin === 'https://YOUR-ORIGIN';
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

FYI: این تکنیک در ServerAction نیز استفاده می شود.

برای کسانی که از tRPC استفاده می کنند، توصیه می شود اعتبارسنجی را اضافه کنند تا اطمینان حاصل شود که نوع محتوا JSON است. این رویکرد در tRPC v11 پیاده‌سازی خواهد شد، اما از آنجایی که نسخه 11 در حال حاضر در نسخه بتا است، می‌توانید این اعتبار را با پیاده‌سازی خودتان اضافه کنید.

const hasJsonContentType = (req: NextRequest) => {
  const contentType = req.headers.get("Content-Type");
  return (
    typeof contentType === "string" &&
    contentType.startsWith("application/json")
  );
};
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

علاوه بر این، اگر از کوکی‌ها برای ذخیره شناسه‌های جلسه استفاده می‌کنید، توصیه می‌کنم ویژگی‌های HttpOnly، Secure و SameSite کوکی را بررسی کنید. (من معتقدم که NextAuth این ویژگی ها را به طور پیش فرض به درستی تنظیم می کند.)


هدر Cache-Control را اضافه کنید

هدر Cache-Control برای کنترل اینکه آیا داده های پاسخ را می توان کش کرد یا نه استفاده می شود. در مورد Minute، تمام پاسخ‌های tRPC خصوصی هستند و من نمی‌خواهم آنها را در حافظه پنهان نگه دارم.

برای اضافه کردن هدر Cache-Control، به سادگی تنظیمات را به next.config.js اضافه کنید.

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  headers() {
    return [
      {
        source: "/api/:path*",
        headers: [
          {
            key: "Cache-Control",
            value: "private, no-store, no-cache, must-revalidate",
          },
          { key: "Pragma", value: "no-cache" },
        ],
      },
    ];
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که نمی‌توانید هدرهای Cache-Control را در next.config.js برای صفحات یا دارایی‌ها تنظیم کنید. Next.js به طور خودکار هدرهای Cache-Control مناسب را به هر صفحه اضافه می کند (شاید، امیدوارم).


فقط از سرور استفاده کنید

در پروژه Next.js شما، فایل‌هایی وجود دارند که قرار است فقط در سرور استفاده شوند و ممکن است نخواهید آنها را در معرض مرورگر مشتری قرار دهید. در این صورت می توانید از بسته سرور استفاده کنید.

اگر فایلی وارد شود server-only بسته ها و از کلاینت وارد می شود، برای جلوگیری از نشت کد سمت سرور، خطا ایجاد می کند. من توصیه می کنم یک واردات اضافه کنید server-only به هر فایلی که بدیهی است نیازی به استفاده توسط مشتری نیست. به عنوان مثال، در […nextauth]/route.ts):

// Prevent importing from client-side code.
import "server-only";

import NextAuth from "next-auth"

const handler = NextAuth({
  ...
})

export { handler as GET, handler as POST }
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


کد Backend را تست کنید

تست های Backend برای سالم نگه داشتن برنامه شما ضروری هستند. در مورد Minute، از آنجایی که من از Prisma در باطن (trpc و بسته سرویس) استفاده کردم، مجبور شدم Prisma Client را مسخره کنم یا از یک کانتینر Docker برای راه اندازی پایگاه داده برای آزمایش برای نوشتن تست های Backend استفاده کنم. اسناد Prisma هر دو گزینه را معرفی می کنند: تست واحد و تست ادغام. من تست ادغام را پذیرفتم زیرا دقیق تر از تست واحد است زیرا از یک پایگاه داده واقعی استفاده می کند.

اگر قصد دارید تست های یکپارچه سازی بنویسید، ممکن است بخواهید هر بار که آزمایش اجرا می شود پایگاه داده را بازنشانی کنید. در آن صورت، گزینه –force-reset مفید خواهد بود.

// package.json
{
  ...
  "scripts": {
    "db:reset": "prisma db push --force-reset"
  }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همچنین، اگر در تلاش برای ایجاد داده‌های ساختگی برای آزمایش هستید، من FactoryJS را توصیه می‌کنم که برای ایجاد اشیا یا رکوردهای Prisma ساده است.

وقتی تست می نویسید، توصیه می کنم هم موارد رایج و هم موارد لبه را پوشش دهید. مثلا اگر شناسه داده شده نامعتبر باشد چه؟

describe("deleteChart", () => {
  describe("when a user has the chart", () => {
    it("deletes the chart and its items", async () => {
      const user = await userFactory.create();
      const chart = await chartFactory.vars({ user: () => user }).create();
      await expect(
        deleteChart(db)({
          id: chart.id,
          userId: user.id,
        }),
      ).resolves.toBeUndefined();
      await expect(
        db.chart.findFirst({
          where: {
            id: chart.id,
          },
        }),
      ).resolves.toBeNull();
    });
  });

  describe("when the id is invalid", () => {
    it("throws an error", async () => {
      const user = await userFactory.create();
      await expect(
        deleteChart(db)({
          id: 'INVALID',
          userId: user.id,
        }),
      ).rejects.toThrow("The chart does not exist.");
    });
  });
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید


نتیجه

من نکاتی را که در طول توسعه Minute پیدا کردم، معرفی کردم. با این حال، مطمئن نیستم که همه نکات مهم را فهرست کرده باشم (احتمالا نه، زیرا این کتابخانه ها پیچیده هستند و روز به روز تغییر می کنند). اما حداقل معتقدم این مقاله برای همه کسانی که قصد دارند با Next.js، Prisma و غیره برنامه بسازند مفید خواهد بود.

امیدوارم این مقاله برای شما مفید و جالب باشد!

اگر علاقه مند به Minute هستید، می توانید از لینک زیر به مخزن دسترسی داشته باشید. ما همچنین از مشارکت ها استقبال می کنیم.

⏰ برنامه ردیابی زمان منبع باز برای افراد.


دقیقه

برنامه ردیابی زمان منبع باز برای افراد.

در باره

دقیقه یک برنامه ردیابی زمان منبع باز است.
برخلاف سایر برنامه‌های ردیابی زمان، Minute عمدتاً بر استفاده فردی متمرکز است و برای کمک به کاربران در بررسی نحوه گذراندن زمان و استفاده معنادارتر از آن طراحی شده است.

اسکرین شات ها

max width: 100%;

max width: 100%;

امکانات

پوشه ها

max width: 100%;

ورودی‌های زمان ردیابی‌شده خود را با پوشه‌ها مدیریت کنید و زمان مصرف اخیر خود را در هر پوشه در صفحه گزارش تجزیه و تحلیل کنید. همچنین می توانید پوشه های ایجاد شده و ورودی های زمان را در هر زمان از نوار کناری مشاهده و ویرایش کنید.

دسته بندی ها

max width: 100%;

چندین پوشه را در یک دسته گروه بندی کنید و از آنها برای تجزیه و تحلیل در صفحه گزارش استفاده کنید. به عنوان مثال، دسته بندی هایی را برای زمانی که می خواهید کاهش دهید و زمانی که می خواهید افزایش دهید ایجاد کنید و از آنها در صفحه گزارش استفاده کنید.

نمودارهای سفارشی

max width: 100%;

از پوشه ها و دسته بندی های ایجاد شده برای نمایش افزایش و کاهش زمان استفاده در نمودار استفاده کنید. موارد نمایش داده شده در نمودار…

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا