سلام دوست عزیز😃👋

به مسابقه «بله‌کمپ ۷ - مرحله دوم (DevOps)» خوش آمدی!

نکات مفید برای شرکت در مسابقه:

  • هرگونه استفاده از ابزارهای تولید کد، مثل chatGPT و... در مسابقات کوئرا ممنوع است و بعد از شناسایی از لیست شرکت‌کنندگان مسابقه حذف می‌شوید.
  • هر گونه ارتباط با سایر شرکت‌کنندگان ممنوع است.
  • می‌توانید سوال‌ها و مشکلات خود را از بخش «سوال بپرسید» با ما در میان بگذارید.
  • این مسابقه دارای سناریو یک‌ پارچه است، یعنی پروژه سوالات شبیه به هم و دنباله‌دار هستند و روی یک فضا پیش می‌روند.
  • در این مسابقه تلاش شده تا مهارت شما هم در سطح توسعه دهنده یا همان developer و هم در سطح operation سنجیده شود. برای همین شما در برخی سوالات مجبور به توسعه و پیاده‌سازی هستید. در این گونه سوالات تلاش شده تا شما را محدود به استفاده از زبان برنامه‌نویسی مخصوصی نکنیم و دست شما را باز بگذاریم. بنابراین می‌توانید با زبان دلخواه خود به حل سوالات بپردازید.

موفق باشید و بهتون خوش بگذره 😉✌

سامانه بلاگ بَله‌آباد


سلیب پس از ترجمه‌ی نقشه متوجه شد که گنج جایی در آبادی «بله‌آباد» است که در پشت کوه‌های جزیره پنهان شده‌است. او سریعاً حرکت کرد و هنگامی‌که به پشت کوه‌های «بله‌آباد» رسید، متوجه شد که مردمان این آبادی، سامانه‌ای شبیه سیستم بلاگ Medium ندارند. برای سلیب این موقعیت خوبی بود تا به جای جست‌وجوی شبانه‌روزی به دنبال گنج، از این طریق ثروتی کسب کند و بی‌خیال گنج بشود؛ پس درخواست پیاده‌سازی این سامانه را با مشخصاتی که در ادامه توضیح می‌دهیم، از شما دارد. :)

جزئیات پروژه🔗

پروژه‌ی اولیه را از این لینک دانلود کنید.

در این سؤال، ما با دو بخش کلی در ارتباط هستیم: بخش کاربران و بخش بلاگ‌ها. در ادامه جزئیات هر بخش را بیان می‌کنیم.

بخش کاربران

برای این بخش نیاز است تعدادی REST API شامل endpointهای زیر پیاده‌سازی شوند:

آدرس عنوان
GET / بررسی up بودن سرویس
POST /auth/signup/ ثبت‌نام
POST /auth/login/ ورود به حساب کاربری
POST /auth/logout/ خروج از حساب کاربری

در این API هر کاربر باید یک توکن داشته باشد. این توکن برای هر کاربر ثابت است.

پیاده‌سازی endpointهای موردنیاز بخش کاربر🔗

در همه‌ی endpointها، پاسخ باید به‌صورت JSON باشد.

اکیداً توصیه می‌گردد برای پیاده‌سازی بخش کاربر از JWT استفاده کنید.

اطلاعات ورودی به‌صورت application/x-www-form-urlencoded به endpointها ارسال می‌شوند.

بررسی up بودن سرویس

پاسخ این endpoint باید به‌صورت زیر باشد:

  • کد وضعیت: 200
  • بدنه: {"message":Welcome to Medium API}
ثبت‌نام

سه پارامتر username و password و email به این endpoint ارسال می‌شوند. درصورتی‌که حداقل یکی از پارامترهای username و password ارسال نشده باشد یا برابر با رشته‌ی خالی باشد، پاسخ باید به‌صورت زیر باشد:

هر دو پارامتر username و password خالی:

  • کد وضعیت: 400

  • بدنه:

    extensionFromNamejson
    {
      "error": {
          "username": ["This field may not be blank."],
          "password": ["This field may not be blank."],
          }
    }
    JSON

    پارامتر username خالی:

  • کد وضعیت: 400

  • بدنه:

    extensionFromNamejson
    {
      "error": {
          "username": ["This field may not be blank."]
          }
    }
    JSON

    پارامتر password خالی:

  • کد وضعیت: 400

  • بدنه:

    extensionFromNamejson
    {
      "error": {
          "password": ["This field may not be blank."]
          }
    }
    JSON

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

  • کد وضعیت: 400

  • بدنه:

    extensionFromNamejson
    {
      "error": {
          "username": ["A user with that username already exists."],
          }
    }
    JSON

    در غیر این صورت، کاربر باید ساخته شود، یک توکن یکتا برایش تولید شود و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 201

  • بدنه:

    extensionFromNamejson
    {
      "refresh": "[REFRESH_TOKEN]",
      "access": "[ACCESS_TOKEN]",
    }
    JSON
ورود به حساب کاربری

دو پارامتر username و password باید به این endpoint ارسال شوند. درصورتی‌که حداقل یکی از این پارامترها ارسال نشده باشد یا برابر با رشته‌ی خالی باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 401
  • بدنه: {"error": "Invalid credentials"}

اگر نام کاربری یا رمز عبور نادرست باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 401
  • بدنه: {"error": "Invalid credentials"}

در غیر این صورت، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 200
  • بدنه:
    extensionFromNamejson
    {
      "refresh": "[REFRESH_TOKEN]",
      "access": "[ACCESS_TOKEN]",
    }
    JSON
خروج از حساب کاربری

تنها پارامتر refresh باید به این endpoint ارسال شود. درصورتی‌که این پارامتر ارسال نشده باشد یا برابر با رشته‌ی خالی باشد یا حتی مقدار درستی نداشته باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 400
  • بدنه: {}

در غیر این صورت، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 205
  • بدنه: {}
بخش بلاگ

برای این بخش نیاز است تا تعدادی REST API شامل endpointهای زیر باید پیاده‌سازی شوند:

آدرس عنوان
GET /blogs/ دریافت تمامی بلاگ‌های ثبت شده
POST /blogs/create/ ایجاد بلاگ جدید
PUT /blogs/<int:pk>/ آپدیت بلاگ
DELETE /blogs/<int:pk>/ حذف بلاگ
GET /blogs/<int:pk>/detail/ مشاهده بلاگ
POST /blogs/<int:pk>/like/ لایک کردن بلاگ
GET /blogs/analytics/ اوضاع کلی حساب نویسنده

پیاده‌سازی endpointهای موردنیاز بخش بلاگ🔗

در همه‌ی endpointها، پاسخ باید به‌صورت JSON باشد.

اطلاعات ورودی به‌صورت application/x-www-form-urlencoded به endpointها ارسال می‌شوند.

دریافت تمامی بلاگ‌های ثبت‌شده

این endpoint نیازمند authentication نیست.

نیازی به ارسال هیچ پارامتری به این endpoint نیست و در همه‌ی حالات باید جواب برابر با مقدار زیر باشد:

  • کد وضعیت: 200
  • بدنه:
    extensionFromNamejson
    [
      {
          "id": "[BLOG_ID]",
          "title": "[BLOG_TITLE]",
          "content": "[BLOG_CONTENT]",
          "views": "[BLOG_VIEWS]",
          "likes": "[BLOG_LIKES]",
      },
      ...
    ]
    JSON

بلاگ‌های برگردانده‌شده باید به ترتیب زمان ثبت بلاگ باشند.

ایجاد بلاگ جدید

این endpoint نیازمند authentication است. در ریکوئست ارسالی، مقدار هدر Authorization باید برابر با توکن کاربر باشد (توکن همواره همراه با Bearer ارسال می‌شود.).

پارامتر title و content باید به این endpoint ارسال شود. درصورتی‌که یکی از این پارامترها ارسال نشده باشد یا برابر با رشته‌ی خالی باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 400
  • بدنه:
    extensionFromNamejson
    {
      "error": {
          "title": ["This field may not be blank."],
          "content": ["This field may not be blank."],
          }
    }
    JSON

در غیر این صورت، بلاگ باید ثبت شود و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 201
  • بدنه:
    extensionFromNamejson
    {
      "id": "[BLOG_ID]",
      "title": "[BLOG_TITLE]",
      "content": "[BLOG_CONTENT]",
      "views": "[BLOG_VIEWS]",
      "likes": "[BLOG_LIKES]",
    }
    JSON
آپدیت بلاگ

این endpoint نیازمند authentication است. در ریکوئست ارسالی مقدار هدر Authorization باید برابر با توکن کاربر باشد (توکن همواره همراه با Bearer ارسال می‌شود.).

پارامتر title و content باید به این endpoint ارسال شود. درصورتی‌که یکی از این پارامترها ارسال نشده باشد یا برابر با رشته‌ی خالی باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 400

  • بدنه:

    extensionFromNamejson
    {
      "error": {
          "title": ["This field may not be blank."],
          "content": ["This field may not be blank."],
          }
    }
    JSON

    درصورتی‌که pk موجود در URL در دیتابیس وجود نداشته باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 404

  • بدنه: {}

درصورتی‌که کاربر ارسال‌کننده‌ی ریکوئست برابر با نویسنده‌ی بلاگ نباشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 403
  • بدنه: {}

در غیر این صورت، بلاگ باید آپدیت شود و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 200
  • بدنه:
    extensionFromNamejson
    {
      "id": "[BLOG_ID]",
      "title": "[BLOG_TITLE]",
      "content": "[BLOG_CONTENT]",
      "views": "[BLOG_VIEWS]",
      "likes": "[BLOG_LIKES]",
    }
    JSON
حذف بلاگ

این endpoint نیازمند authentication است. در ریکوئست ارسالی مقدار هدر Authorization باید برابر با توکن کاربر باشد (توکن همواره همراه با Bearer ارسال می‌شود.).

پارامتری به این endpoint ارسال نمی‌شود. در صورتی که pk موجود در URL در دیتابیس وجود نداشته باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 404
  • بدنه: {}

در صورتی که کاربر ارسال‌کننده‌ی ریکوئست برابر با نویسنده بلاگ نباشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 403
  • بدنه: {}

در غیر این صورت، بلاگ باید حذف شود و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 204
  • بدنه: {}
مشاهده‌ی بلاگ

این endpoint نیازمند authentication نیست.

نیازی به ارسال هیچ پارامتری به این endpoint نیست. در همه‌ی حالات، پاسخ باید برابر با مقادیر زیر باشد:

درصورتی‌که pk موجود در URL در دیتابیس وجود نداشته باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 404
  • بدنه: {}

در غیر این‌صورت، بلاگ باید ثبت شود و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 200
  • بدنه:
    extensionFromNamejson
    {
      "id": "[BLOG_ID]",
      "title": "[BLOG_TITLE]",
      "content": "[BLOG_CONTENT]",
      "views": "[BLOG_VIEWS]",
      "likes": "[BLOG_LIKES]",
    }
    JSON
    توجه داشته باشید که درصورتی‌که بلاگ موردنظر وجود داشت و کاربر بدون مشکل می‌توانست آن را مشاهده کند، باید به مقدار viewی بلاگ، یک واحد اضافه کنید و سپس بلاگ را برگردانید. یعنی بازدید فعلی کاربر در میزان بازدید‌های بلاگ برگردانده‌شده باید محاسبه شده باشد.

نکته‌ی ریت لیمیت: در این‌جا نیاز به پیاده‌سازی سازوکاری برای ریت لیمیت داریم. می‌خواهیم هر device_id و ip بتواند تنها 10 بار هر بلاگ را ببیند تا میزان ویو‌های هر بلاگ، شهودی واقعی از میزان دیده‌شدن بلاگ بدهد. تا 10 ریکوئست برای گرفتن یک بلاگ از یک device_id و ip را بدون مشکل باز گردانید و مقدار ویوی بلاگ را هم اضافه کنید. به محض عبور از 10 نیاز است که پاسخ برابر زیر باشد و به مقدار ویوی بلاگ هیچ عددی اضافه نشود.

  • کد وضعیت: 429
  • بدنه: {}
لایک کردن بلاگ

این endpoint نیازمند authentication است. در ریکوئست ارسالی مقدار هدر Authorization باید برابر با توکن کاربر باشد (توکن همواره همراه با Bearer ارسال می‌شود.).

پارامتری به این endpoint ارسال نمی‌شود. درصورتی‌که pk موجود در URL در دیتابیس وجود نداشته باشد، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 404
  • بدنه: {}

درصورتی‌که توکن Authorization وجود نداشت و در اصل یوزر لاگین نبود، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 401
  • بدنه: {}

در غیر این صورت، باید یک عدد به لایک‌های بلاگ اضافه شود و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 200
  • بدنه:
    extensionFromNamejson
    {
      "id": "[BLOG_ID]",
      "title": "[BLOG_TITLE]",
      "content": "[BLOG_CONTENT]",
      "views": "[BLOG_VIEWS]",
      "likes": "[BLOG_LIKES]",
    }
    JSON
اوضاع کلی حساب نویسنده

این endpoint نیازمند authentication است. در ریکوئست ارسالی مقدار هدر Authorization باید برابر با توکن کاربر باشد (توکن همواره همراه با Bearer ارسال می‌شود.).

درصورتی‌که توکن Authorization وجود نداشت و در اصل یوزر لاگین نبود، پاسخ باید به‌صورت زیر باشد:

  • کد وضعیت: 401
  • بدنه: {}

در غیر این صورت، باید مجموع تمام view و likeهای تمام بلاگ‌های نویسنده را محاسبه کند و پاسخ به‌صورت زیر باشد:

  • کد وضعیت: 200
  • بدنه:
    extensionFromNamejson
    {
      "total_views": "int",
      "total_likes": "int"
    }
    JSON

امتیاز ویژه: در این مسئله داکر فایل و داکر کامپوز در اختیار شماست و نحوه‌ی پیاده‌سازی کاملاً بر عهده‌ی خودتان است. اگر دیتابیس مورداستفاده‌تان را ماندگار و یا persist کنید، امتیاز بیشتری از مسئله دریافت می‌کنید.

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

نکات تکمیلی🔗

نصب نیازمندی‌ها و اجرا

برای حل این سؤال می‌توانید از هر زبان و هر تکنولوژی‌ای که می‌خواهید استفاده کنید. به‌صورتی‌که در یک پوشه به نام medium کد برنامه را بنویسید. توجه کنید که حتماً باید Dockerfile مربوط به پروژه‌ی خود را برای ما ارسال کنید.

  • نیازی به persistent بودن داده‌ها نیست! اما برای دریافت امتیاز کامل مسئله باید دیتاها را persistent کنید.
  • سیستم داوری docker-compose.yml تحویلی شما را که در خارج از فولدر medium قرار دارد، با دستور docker-compose up --build اجرا می‌کند.
extensionFromNamedocker-compose.yml
version: "3"

# add your services here
YAML
  • شما مجاز به تغییر یا ارسال docker-compose.yml دلخواه هستید.
  • نام سرویس و کانتیر کد سرور شما در فایل docker-compose.yml حتما باید برابر با medium باشد.
  • سرویس شما باید روی پورت 80 آدرس localhost قابل دسترسی باشد.
  • توصیه می‌کنیم خود APIتان را روی 0.0.0.0:80 اجرا کنید.
  • ورژن فایل داکرکامپوز شما باید حتما برابر با 3 باشد.

نحوه‌ی ارسال پاسخ🔗

شما می‌توانید تمامی محتوای موجود در پوشه‌ی medium را تغییر دهید و هر فایلی که می‌خواهید اضافه یا کم کنید.

├── medium
├──├──  [ALL_YOUR_PROJECT]   # or main.go somefile.js anyfile.php name.any ...
├──├── Dockerfile
└── docker-compose.yml
Plain text

توجه کنید که نام فایل کد شما برای سیستم داوری اهمیتی ندارد و این خود شما هستید که در داکر فایل و داکر کامپوزتان از نام آن برای اجرای پروژه استفاده می‌کنید.

در نهایت پوشه medium را به همراه docker-compose.yml ZIP کرده و ارسال کنید. توجه کنید که پس از extract کردن فایل ZIP شما، باید فایل docker-compose.yml پوشه‌ی medium را ببینیم که درون آن Dockerfile وجود دارد.

ارسال پاسخ برای این سؤال
در حال حاضر شما دسترسی ندارید.