برنامه نویسی

سیستم احراز هویت با استفاده از Rust (actix-web) و sveltekit – Backend Intro

معرفی

سلام بچه ها! اینجا مدتی است. من در مدتی که نبودم کمی زنگ زدگی را یاد گرفتم و برخی از چیزهایی را که یاد گرفتم به اشتراک خواهم گذاشت.

سیستم احراز هویت بخشی جدایی ناپذیر از برنامه های کاربردی مدرن است. این بسیار مهم است که تقریباً همه برنامه های مدرن نوعی از آن را دارند. به دلیل ماهیت حیاتی آنها، چنین سیستم هایی باید ایمن باشند و باید از توصیه های OWAP® در مورد امنیت وب و هش رمز عبور و همچنین ذخیره سازی برای جلوگیری از حملاتی مانند حملات Preimage و Dictionary (که در الگوریتم های SHA رایج است) پیروی کنند. برای نشان دادن برخی از توصیه‌ها، ما یک سیستم احراز هویت مبتنی بر جلسه قوی در Rust و یک برنامه فرانت‌اند تکمیلی خواهیم ساخت. برای این سری مقالات، ما از Actix-Web Rust و چند جعبه عالی برای سرویس Backend استفاده خواهیم کرد. SvelteKit برای قسمت جلویی استفاده خواهد شد. با این حال، باید توجه داشت که آنچه که ما خواهیم ساخت تا حد زیادی چارچوبی است. در نتیجه، می‌توانید تصمیم بگیرید که axum، rocket، warp یا هر چارچوب وب rust دیگری را برای backend انتخاب کنید و react، vue یا هر چارچوب جاوا اسکریپت دیگری را برای frontend انتخاب کنید. حتی می توانید از سرخدار، دانه یا برخی موتورهای قالب مانند MiniJinja یا tera در قسمت جلویی استفاده کنید. این کاملا به شما بستگی دارد. تمرکز ما بیشتر بر روی مفاهیم خواهد بود.

توجه: ما از کتاب صفر تا تولید در زنگ، به شدت با برخی ویژگی‌ها و تغییرات اضافی استفاده خواهیم کرد.

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

توجه: این آموزش به چند مقاله کوتاه تقسیم می شود. حداقل یک (1) مقاله هر هفته بارگذاری می شود تا کل مجموعه کامل شود.

مشخصات مورد نیاز سیستم

در طول این مجموعه آموزشی، ما در جهت اجرای این الزامات کار خواهیم کرد:

یک سیستم احراز هویت کاربر بسازید که در آن کاربر با ترکیب ایمیل/رمز عبور احراز هویت می کند. آدرس های ایمیل باید باشد منحصر بفرد و تایید شده است پس از ثبت نام و تأیید، با ارسال ایمیل های تأیید با زمان محدود ایمیل ها باید از HTML پشتیبانی کنند. تا زمان تایید، هیچ کاربری اجازه ورود به سیستم را ندارد. حملات زمانی باید توسط ارسال نامه ها به صورت ناهمزمان. هش رمز عبور باید قوی باشد و فقط رمزهای عبور هش شده باید در پایگاه داده ذخیره شوند. قابلیت بازنشانی گذرواژه باید با استفاده از تأیید آدرس پست الکترونیکی گنجانده شده و پذیرفته شود. یک ویژگی به روز رسانی نمایه کاربر محافظت شده باید اضافه شود تا فقط کاربران تأیید شده و مجاز بتوانند به آن دسترسی داشته باشند. نمایه کاربر باید شامل یک تصویر کوچک باشد که باید در AWS S3 ذخیره شود.

😲 این خیلی بود، نه؟! از این انتها هم هست 😫😩 . از مشخصات، ما ملزم به داشتن مقداری سرگرمی هستیم. ما به سمت بالا حرکت خواهیم کرد و قلمرو بارگذاری تصویر در AWS S3، تأیید ایمیل، تولید و تخریب توکن، برخی از الگوها و تعداد زیادی از موارد دیگر را ترسیم خواهیم کرد.

پشته فناوری

برای تأکید، پشته فناوری ما شامل موارد زیر است:

  • Backend – برخی از جعبه هایی که مورد استفاده قرار خواهند گرفت عبارتند از:

  • Frontend – برخی از ابزارهایی که مورد استفاده قرار خواهند گرفت عبارتند از:

فرض

یک پیش نیاز ساده برای دنبال کردن، آشنایی با زبان برنامه نویسی Rust است – مانند درک ساختارها، مدل مالکیت، بررسی کننده قرض، سیستم ماژول و سایر موارد. – جاوا اسکریپت (Typescript) و CSS. شما نیازی به متخصص بودن ندارید – من در هیچ یک از فناوری ها نیستم.

کد منبع

کد منبع این سری در github میزبانی می شود یا بیشتر از طریق:

یک سیستم احراز هویت تمام پشته با استفاده از rust، sveltekit و Typescript

ساختار اولیه پروژه

شما می توانید این قالب کامل استارت را از github دریافت کنید.

من ساختار خدمات وب Rust را از صفر تا تولید در Rust به ارث بردم. من عاشق این ساختار شده ام و به احتمال زیاد از آن برای اکثر پروژه های وب Rust خود صرف نظر از چارچوب انتخابی خود استفاده خواهم کرد. این الگوی شروع در اینجا موجود است و نحوه ساخت آن به طور خلاصه مورد بحث قرار خواهد گرفت. من شما را تشویق می کنم که کتاب صفر تا تولید در زنگ را بردارید. فوق العاده است!!! در حال حاضر، backend ساختار به شکل زیر است:

├── Cargo.lock
├── Cargo.toml
├── settings
│   ├── base.yaml
│   ├── development.yaml
│   └── production.yaml
├── src
│   ├── lib.rs
│   ├── main.rs
│   ├── routes
│   │   ├── health.rs
│   │   └── mod.rs
│   ├── settings.rs
│   ├── startup.rs
│   └── telemetry.rs
└── tests
وارد حالت تمام صفحه شوید

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

مرحله 1: یک پروژه جدید ایجاد کنید و برخی از وابستگی ها را نصب کنید

دایرکتوری ایجاد کنید که کل برنامه (هم جلو و هم باطن) را در خود جای دهد. به مال خودم زنگ زدم rust-auth. دایرکتوری را به پوشه تازه ایجاد شده تغییر دهید و دستور زیر را در ترمینال خود صادر کنید:

~/rust-auth$ cargo new backend
وارد حالت تمام صفحه شوید

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

این یک پروژه جدید به نام ایجاد می کند backend با Cargo.toml، Cargo.lock، و src/main.rs فایل های ایجاد شده آن را در ویرایشگر انتخابی خود باز کنید. خود را بسازید Cargo.toml فایل شبیه به این است:

# Cargo.toml

[package]
name = "backend"
version = "0.1.0"
authors = ["Your name <your email>"]
edition = "2021"

[lib]
path = "src/lib.rs"

[[bin]]
path = "src/main.rs"
name = "backend"

[dependencies]
actix-web = "4"
config = { version = "0.13.3", features = ["yaml"] }
dotenv = "0.15.0"
serde = "1.0.160"
tokio = { version = "1.27.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.37"
tracing-subscriber = { version = "0.3.17", features = [
    "fmt",
    "std",
    "env-filter",
    "registry",
    'json',
    'tracing-log',
] }
وارد حالت تمام صفحه شوید

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

اضافه کردیم authors به [package] بخش. سپس یک بخش جدید ایجاد کردیم، [lib]، که به مسیر پروژه اشاره می کند lib.rs فایل. یک پروژه می تواند تنها یک مورد داشته باشد lib.rs فایل. بعد بخش باینری است، [[bin]]. دو براکت مربع داخل .toml فایل ها به معنای آرایه هستند. به این دلیل استفاده شد که می‌توانیم بیش از یک بسته باینری در پروژه Rust داشته باشیم. این دو بخش جدید نوشتن تست یکپارچه یکپارچه را برای ما آسان‌تر می‌کنند که در آن تست‌ها “مستقل” از چارچوب وب مورد استفاده هستند. سپس [dependencies] بخش. جعبه های اولیه ای را که استفاده خواهیم کرد ثبت کردیم. config به تغییر آسان کمک می کند .yaml یا .json فایل‌هایی که حاوی برخی تنظیمات در سطح برنامه هستند، مانند متغیرهای موجود در جنگو settings.py فایل، به ساختارهای زنگ. dotenv بارگذاری متغیرهای محیطی از a .env فایل. serde چارچوب سریال‌سازی/آسیال‌زدایی عمومی Rust است. tokio یک زمان اجرا استاندارد صنعتی برای نوشتن برنامه های کاربردی قابل اعتماد، ناهمزمان و باریک با زنگ زدگی است. در زمان اجرا یا زمانی که برنامه ما در حال تولید است، در نهایت باید درخواست ها و پاسخ ها را ثبت کنیم. گاهی اوقات، کاربران ما شکایت می کنند یا برنامه ما خراب می شود. ما نمی توانیم فقط بفهمیم که چرا وضعیت خاصی از هوا رخ می دهد. ما به یک نقطه مرجع برای رفع اشکال برنامه خود نیاز داریم. در اکوسیستم زنگ، tracing و گسترش آن tracing-subscriber به طور گسترده برای این مورد استفاده می شود. Telemetry چیزی است که به آن می گویند.

مرحله 2: اسکلت پروژه را بسازید

درون src پوشه، دستورات زیر را برای ایجاد چند فایل و پوشه صادر کنید:

~/rust-auth/backend$ touch src/lib.rs src/startup.rs src/settings.rs src/telemetry.rs

~/rust-auth/backend$ mkdir src/routes && touch src/routes/mod.rs src/routes/health.rs
وارد حالت تمام صفحه شوید

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

برای اینکه فایل‌ها و پوشه‌های ایجاد شده شناسایی شوند، باید آنها را به ماژول تبدیل کنیم lib.rs:

// src/lib.rs

pub mod routes;
pub mod settings;
pub mod startup;
pub mod telemetry;
وارد حالت تمام صفحه شوید

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

بیا شروع کنیم با telemetry.rs. آن را به شکل زیر در بیاورید:

// src/telemetry.rs

use tracing_subscriber::layer::SubscriberExt;

pub fn get_subscriber(debug: bool) -> impl tracing::Subscriber + Send + Sync {
    let env_filter = if debug {
        "trace".to_string()
    } else {
        "info".to_string()
    };
    let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(env_filter));

    let stdout_log = tracing_subscriber::fmt::layer().pretty();
    let subscriber = tracing_subscriber::Registry::default()
        .with(env_filter)
        .with(stdout_log);

    let json_log = if !debug {
        let json_log = tracing_subscriber::fmt::layer().json();
        Some(json_log)
    } else {
        None
    };

    let subscriber = subscriber.with(json_log);

    subscriber
}

pub fn init_subscriber(subscriber: impl tracing::Subscriber + Send + Sync) {
    tracing::subscriber::set_global_default(subscriber).expect("Failed to set subscriber");
}
وارد حالت تمام صفحه شوید

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

ما در حال پیکربندی هستیم tracing_subscriberسطح و قالب بسته به اینکه آیا برنامه ما در حال تولید است یا خیر. اگر debug است true، سپس در حالت توسعه هستیم. در غیر این صورت، در تولید. ما میخواهیم JSON خروجی در تولید، زیرا تجزیه آن آسان تر است. سپس، ردیابی را در تابع دیگری بر اساس مشترک داده شده مقداردهی اولیه کردیم.

بعد، settings.rs:

// src/settings.rs

/// Global settings for exposing all preconfigured variables
#[derive(serde::Deserialize, Clone)]
pub struct Settings {
    pub application: ApplicationSettings,
    pub debug: bool,
}

/// Application's specific settings to expose `port`,
/// `host`, `protocol`, and possible URL of the application
/// during and after development
#[derive(serde::Deserialize, Clone)]
pub struct ApplicationSettings {
    pub port: u16,
    pub host: String,
    pub base_url: String,
    pub protocol: String,
}

/// The possible runtime environment for our application.
pub enum Environment {
    Development,
    Production,
}

impl Environment {
    pub fn as_str(&self) -> &'static str {
        match self {
            Environment::Development => "development",
            Environment::Production => "production",
        }
    }
}

impl TryFrom<String> for Environment {
    type Error = String;

    fn try_from(s: String) -> Result<Self, Self::Error> {
        match s.to_lowercase().as_str() {
            "development" => Ok(Self::Development),
            "production" => Ok(Self::Production),
            other => Err(format!(
                "{} is not a supported environment. Use either `development` or `production`.",
                other
            )),
        }
    }
}

/// Multipurpose function that helps detect the current environment the application
/// is running using the `APP_ENVIRONMENT` environment variable.
///
/// \`\`\`
/// APP_ENVIRONMENT = development | production.
/// \`\`\`
///
/// After detection, it loads the appropriate .yaml file
/// then it loads the environment variable that overrides whatever is set in the .yaml file.
/// For this to work, you the environment variable MUST be in uppercase and starts with `APP`,
/// a `_` separator then the category of settings,
/// followed by `__` separator,  and then the variable, e.g.
/// `APP__APPLICATION_PORT=5001` for `port` to be set as `5001`
pub fn get_settings() -> Result<Settings, config::ConfigError> {
    let base_path = std::env::current_dir().expect("Failed to determine the current directory");
    let settings_directory = base_path.join("settings");

    // Detect the running environment.
    // Default to `development` if unspecified.
    let environment: Environment = std::env::var("APP_ENVIRONMENT")
        .unwrap_or_else(|_| "development".into())
        .try_into()
        .expect("Failed to parse APP_ENVIRONMENT.");
    let environment_filename = format!("{}.yaml", environment.as_str());
    let settings = config::Config::builder()
        .add_source(config::File::from(settings_directory.join("base.yaml")))
        .add_source(config::File::from(
            settings_directory.join(environment_filename),
        ))
        // Add in settings from environment variables (with a prefix of APP and '__' as separator)
        // E.g. `APP_APPLICATION__PORT=5001 would set `Settings.application.port`
        .add_source(
            config::Environment::with_prefix("APP")
                .prefix_separator("_")
                .separator("__"),
        )
        .build()?;

    settings.try_deserialize::<Settings>()
}
وارد حالت تمام صفحه شوید

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

ما چند ساختار و یک enum داریم. این ساختارها مستقیماً به .yaml فایل هایی که به زودی ایجاد خواهیم کرد. بخش عمده ای از کار در این است settings.rs فایل در get_settings تابع. هر زمان که به برخی از متغیرهای تنظیمات نیاز داشته باشیم، با فراخوانی این تابع آنها را دریافت خواهیم کرد. با نگاهی به داخل، ابتدا سعی می کنیم مسیر دایرکتوری را که در آن قرار داریم به دست آوریم .yaml فایل ها قرار دارند. سپس تشخیص می دهیم که آیا در حال توسعه هستیم یا خیر. به طور پیش فرض، ما فرض می کنیم که برنامه در حال توسعه است. برای تغییر آن به تولید، باید تنظیم کنید APP_ENVIRONMENT=production در شما .env فایل یا هر روش دیگری که متغیرهای محیط خود را تنظیم می کنید. از آنجایی که محیط های توسعه و تولید برخی از متغیرها را به اشتراک می گذارند – ما آنها را در آنها ذخیره می کنیم base.yaml – ما از خود استفاده می کنیم config جعبه تا ابتدا آن پیکربندی‌های رایج را قبل از بارگیری تنظیمات خاص محیط بارگیری کنید. این به این دلیل است که ما احتمالاً آن پیکربندی‌های رایج را بر اساس هر محیطی نادیده می‌گیریم. برخی از تنظیمات در وجود دارد .yaml فایل هایی که ممکن است بخواهیم مقادیر آنها را با استفاده از متغیرهای محیطی تغییر دهیم. توکن ها، رمزهای عبور و secret_key چند نمونه هستند. ما می خواهیم آنهایی که از طریق متغیرهای محیطی تنظیم شده اند اولویت داشته باشند. مثلاً اگر تنظیم کنیم debug: true که در base.yaml اما در تولید، ما می خواهیم debug: false. ما فقط می توانیم انجام دهیم APP_DEBUG=true در ما .env را فایل کنید و این یکی را لغو می کند base.yaml. به پیشوند توجه کنید، APP_. لازم است که چنین چیزی به عنوان متغیر تنظیم شناخته شود. شما می توانید پیشوند را نیز تغییر دهید. با پیشرفت بیشتر در مورد این تفاوت های ظریف بیشتر می آموزیم. حال، بیایید ایجاد کنیم settings/ دایرکتوری در ریشه پروژه ما نیز ایجاد خواهیم کرد base.yaml، development.yaml و production.yaml در آن:

~/rust-auth/backend$ mkdir settings && touch settings/base.yaml settings/development.yaml settings/production.yaml
وارد حالت تمام صفحه شوید

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

فعلا بساز settings/base.yaml به نظر می رسد این است:

# settings/base.yaml
application:
  port: 5000
وارد حالت تمام صفحه شوید

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

settings/development.yaml:

# settings/development.yaml

application:
  protocol: http
  host: 127.0.0.1
  base_url: "http://127.0.0.1"

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

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

و، settings/production.yaml:

# settings/production.yaml

application:
  protocol: https
  host: 0.0.0.0
  base_url: ""

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

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

بعدی است src/startup.rs:

// src/startup.rs

pub struct Application {
    port: u16,
    server: actix_web::dev::Server,
}

impl Application {
    pub async fn build(settings: crate::settings::Settings) -> Result<Self, std::io::Error> {
        let address = format!(
            "{}:{}",
            settings.application.host, settings.application.port
        );

        let listener = std::net::TcpListener::bind(&address)?;
        let port = listener.local_addr().unwrap().port();
        let server = run(listener).await?;

        Ok(Self { port, server })
    }

    pub fn port(&self) -> u16 {
        self.port
    }

    pub async fn run_until_stopped(self) -> Result<(), std::io::Error> {
        self.server.await
    }
}

async fn run(listener: std::net::TcpListener) -> Result<actix_web::dev::Server, std::io::Error> {
    let server = actix_web::HttpServer::new(move || {
        actix_web::App::new().service(crate::routes::health_check)
    })
    .listen(listener)?
    .run();

    Ok(server)
}
وارد حالت تمام صفحه شوید

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

کل برنامه ما را راه اندازی می کند و در آن انجام می شود run_until_stopped روش از Application ساخت. انگیزه نوشتن به این صورت برای تست آسان است. این تنها راه برای شروع نیست actix-web سرور تمرین معمولی حداقل برای شروع بسیار کوتاهتر است. اما این کاملاً یک تصمیم طراحی است که اختیاری است.

وارد شویم src/main.rs:

// src/main.rs

#[tokio::main]
async fn main() -> std::io::Result<()> {
    dotenv::dotenv().ok();

    let settings = backend::settings::get_settings().expect("Failed to read settings.");

    let subscriber = backend::telemetry::get_subscriber(settings.clone().debug);
    backend::telemetry::init_subscriber(subscriber);

    let application = backend::startup::Application::build(settings).await?;

    tracing::event!(target: "backend", tracing::Level::INFO, "Listening on http://127.0.0.1:{}/", application.port());

    application.run_until_stopped().await?;
    Ok(())
}
وارد حالت تمام صفحه شوید

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

src/main.rs نقطه ورود برنامه های Rust است. ما انتخاب می کنیم #[tokio::main] زمان اجرا شما می توانید استفاده کنید #[actix_web::main] بجای. بعد، آوردیم dotenv به بازی برای کمک به بارگیری همه متغیرهای محیطی در ما .env فایل. سپس تنظیمات خود را همانطور که در آن نوشته شده است دریافت می کنیم src/settings.rs. سپس تله متری مقداردهی اولیه می شود. سپس کل برنامه ما ساخته شد و متعاقباً اجرا شد، اما قبل از اجرا، به توسعه‌دهنده درگاهی را که برنامه ما با استفاده از آن اجرا می‌شود، اطلاع دادیم. tracing::event کلان. ما می‌توانیم پورت را در اینجا دریافت کنیم زیرا آن را در دسترس خود قرار داده‌ایم src/startup.rs. با این کار، ممکن است این فایل را لمس نکنیم، src/main.rs، دوباره در طول این مجموعه. نقطه تماس ما خواهد بود src/startup.rs.

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

هدایت به src/routes/health.rs و آن را به شکل زیر در آورید:

// src/routes/health.rs

#[tracing::instrument]
#[actix_web::get("/health-check/")]
pub async fn health_check() -> actix_web::HttpResponse {
    tracing::event!(target: "backend", tracing::Level::DEBUG, "Accessing health-check endpoint.");
    actix_web::HttpResponse::Ok().json("Application is safe and healthy.")
}
وارد حالت تمام صفحه شوید

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

ما یک نقطه پایانی ساده برای بررسی آنلاین بودن یا نبودن داریم. می توانید ببینید که نوشتن یک نقطه پایانی API در actix-web چقدر آسان است. جدا از ابزار دقیق، می توانید یک نقطه پایانی درخواست GET “کاملا کاربردی” را تنها با 3 خط کد سیم کشی کنید!!!

با نگاهی به نقطه پایانی، استفاده کردیم #[tracing::instrument] برای کمک به حفظ گزارش تمام درخواست ها در این تابع. این ابزار دقیق است. سپس استفاده می کنیم #[actix_web::get("/health-check/")] فقط به آن علامت دهد GET درخواست ها مجاز است /health-check/. هر روش دیگری مردود خواهد بود. یکی از دلایل استفاده از actix-web پشتیبانی بومی آن از توابع ناهمزمان همراه با این واقعیت است که بسیار سریع است. ما تابع خود را ناهمگام کردیم و انتظار داریم که تابع یک پاسخ HTTP برگرداند، actix_web::HttpResponse. راه های دیگری نیز برای رسیدن به این هدف وجود دارد، اما من به دلیل کوتاه بودن این روش را ترجیح می دهم. سپس پیامی را برمی گردانیم، Application is safe and healthy.، در قالب JSON به کاربر با استفاده از وضعیت HTTP Ok، 200. روش های دیگری برای پاسخ HTTP در actix-web موجود است و ما با برخی از آنها مواجه خواهیم شد.

در مرحله بعد، باید این روش را در دسترس قرار دهیم. باز کن src/routes/mod.rs:

// src/routes/mod.rs

mod health;

pub use health::health_check;
وارد حالت تمام صفحه شوید

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

این باعث می شود آن را برای عموم در دسترس قرار دهد. سپس آن را به عنوان یک سرویس در ثبت نام کردیم src/startup.rs استفاده كردن crate::routes::health_check:

// src/startup.rs

...
async fn run(listener: std::net::TcpListener) -> Result<actix_web::dev::Server, std::io::Error> {
    let server = actix_web::HttpServer::new(move || {
       actix_web::App::new().service(crate::routes::health_check)
    })
    .listen(listener)?
    .run();

    Ok(server)
}
وارد حالت تمام صفحه شوید

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

همین برای اولین مقاله از سری!! همه شما را در بعدی می بینم.

دیگر

از این مقاله لذت بردید؟ در نظر بگیرید که برای یک کار، چیزی ارزشمند یا خرید یک قهوه با من تماس بگیرید. همچنین می توانید با من در لینکدین و دنبال کنید توییتر. بد نیست اگر به اشتراک گذاری این مقاله برای پوشش گسترده تر کمک کنید. قدردانش خواهم بود…

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

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

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

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