Auth Controller

AuthController یک کلاس انتزاعی است که از Controller ارث‌بری می‌کند. این کلاس قرارداد (contract) احراز هویت و مجوزدهی را برای محافظت از مسیرها فراهم می‌کند. آن را یک‌بار برای هر اپلیکیشن پیاده‌سازی می‌کنید و به هر مسیری که نیاز به ورود (login) دارد ضمیمه می‌کنید.

متدهای انتزاعی

متد چه زمانی اجرا می‌شود چه چیزی باید بازگردانده شود
auth() در هر درخواست به یک مسیر محافظت‌شده true برای اجازه دادن، false برای رد کردن (و مدیریت پاسخ)
authApi() در درخواست‌ها به endpointهای محافظت‌شده /api/ true برای اجازه دادن
checkLogin() توسط auth() برای اعتبارسنجی session فراخوانی می‌شود یک record به‌شکل ({bool success, String message, T? user})
checkPermission() پس از موفقیت auth()، route.permissions را بررسی می‌کند true اگر کاربر مجوز لازم را داشته باشد
loginPost() مدیریت POST به مسیر فرم ورود پاسخ HTML
register() مدیریت ارسال فرم ثبت‌نام، در صورتی که اپلیکیشن شما از ثبت‌نام خودکار پشتیبانی کند پاسخ HTML
newUser() ایجاد یک حساب کاربری جدید (مثلاً از یک پنل مدیریت) پاسخ HTML
logout() مدیریت مسیر خروج (logout) هدایت مجدد یا HTML
updateAuth(email, password, user) پس از ورود موفق ذخیره session/کوکی
removeAuth() هنگام خروج پاک کردن session/کوکی
index() به ارث‌رسیده از Controller — handler پیش‌فرض اگر مسیر اکشن index/controller مخصوص به خودش را نداشته باشد هر چیزی که این مسیر باید نمایش دهد

اگر اپلیکیشن شما نیازی به ثبت‌نام ندارد، معمول است که register()/newUser() را طوری بگذارید که UnimplementedError() پرتاب کنند، همان‌طور که پروژه مثال این کار را انجام می‌دهد — به مثال زیر مراجعه کنید.

مثال کامل

این auth controller از پروژه مثال فینچ است:

import 'package:finch/finch_route.dart';
import 'package:finch/finch_tools.dart';
import '../models/mock_user_model.dart';
import '../route/web_route.dart';

class AppAuthController extends AuthController<MockUserModel> {
  MockUserModel? userLogined;

  @override
  Future<bool> auth() async {
    var res = await checkLogin();
    if (!res.success) {
      // درخواست‌های API یک JSON 403 دریافت می‌کنند، درخواست‌های وب یک هدایت مجدد دریافت می‌کنند
      if (rq.isApiEndpoint) {
        await rq.renderError(
          403,
          toData: true,
          params: {'message': 'Please login.', 'success': false},
        );
      } else {
        await rq.redirect('/example/form');
      }
      return false;
    }
    updateAuth(res.user!.email, res.user!.password, res.user!);
    return true;
  }

  @override
  Future<bool> authApi() async {
    var auth = rq.authorization;
    var mockUser = MockUserModel();

    if (auth.type == AuthType.basic) {
      String email = auth.getBasicUsername();
      String password = auth.getBasicPassword();
      return email == mockUser.email && password == mockUser.password;
    } else if (auth.type == AuthType.bearer) {
      return auth.value == '${mockUser.email} ${mockUser.password}';
    }
    return false;
  }

  @override
  Future<({bool success, String message, MockUserModel? user})>
      checkLogin() async {
    var mockUser = MockUserModel();
    var userSession = rq.getSession('user', def: '');

    if (userSession == mockUser.email) {
      return (success: true, message: 'Success.', user: mockUser);
    }
    return (success: false, message: 'Please login.', user: mockUser);
  }

  @override
  Future<bool> checkPermission() async {
    if (rq.route == null || userLogined == null) return false;

    var permission = userLogined!.permission;
    if (rq.route!.permissions.isNotEmpty &&
        !rq.route!.permissions.contains(permission)) {
      return false;
    }
    return true;
  }

  @override
  Future<String> loginPost() async {
    var formLogin = LoginForm();
    await formLogin.check(
      onInvalid: (p0) {},
      onValid: (p0) {
        var mockUser = MockUserModel();
        var email = formLogin.get<String>('email', def: '');
        var password = formLogin.get<String>('password', def: '');
        if (email == mockUser.email && password == mockUser.password) {
          updateAuth(email, password, mockUser);
        } else {
          rq.addParam('errorLogin', 'form.validation.loginError'.tr);
        }
      },
    );
    return homeController.renderView('example/form');
  }

  @override
  Future<String> logout() {
    removeAuth();
    return rq.redirect('/example/form');
  }

  @override
  Future<String> newUser() => throw UnimplementedError();

  @override
  Future<String> register() => throw UnimplementedError();

  @override
  void removeAuth() {
    rq.session.remove('user');
    rq.removeCookie('user');
    userLogined = null;
  }

  @override
  void updateAuth(String email, String password, MockUserModel user) {
    userLogined = user;
    rq.addSession('user', email);
  }
}

ضمیمه کردن به یک مسیر

final authController = AppAuthController();

FinchRoute(
  key: 'example.panel',
  path: 'panel',
  methods: Methods.ALL,
  auth: authController,
  permissions: ['admin'],
  index: homeController.exampleAuth,
),

جریان درخواست برای یک مسیر محافظت‌شده:

  1. مسیر مطابقت پیدا می‌کند →
  2. auth() فراخوانی می‌شود → اگر false برگرداند، درخواست متوقف می‌شود
  3. checkPermission() فراخوانی می‌شود → اگر false برگرداند، درخواست متوقف می‌شود
  4. handler کنترلر / index اجرا می‌شود

هدر Authorization

برای مسیرهای API، هدر Authorization را از طریق rq.authorization بخوانید:

var auth = rq.authorization;

if (auth.type == AuthType.bearer) {
  String token = auth.value;
  // اعتبارسنجی token
}

if (auth.type == AuthType.basic) {
  String user = auth.getBasicUsername();
  String pass = auth.getBasicPassword();
  // اعتبارسنجی اعتبارنامه‌ها (credentials)
}

AuthType همچنین مقادیر digest، hawk، aws و akamai را برای سازگاری با آینده (forward compatibility) تعریف می‌کند، اما امروز فقط basic و bearer واقعاً parse می‌شوند — هر scheme دیگری (یا یک هدر گم‌شده/نادرست) به AuthType.none resolve می‌شود.

Permissions

Permissions یک کلاس کوچک شامل ثابت‌های از پیش تعریف‌شده رشته‌ای دسترسی (permission) است که می‌توانید به‌جای hardcode کردن رشته‌ها در همه‌جا از آن استفاده کنید:

class Permissions {
  static final String none = 'none';
  static final String superAdmin = 'super-admin';
}
FinchRoute(
  key: 'admin.panel',
  path: 'admin/panel',
  auth: authController,
  permissions: [Permissions.superAdmin],
  index: adminController.panel,
),

این کلاس فقط همین دو ثابت را تعریف می‌کند — برای هر چیزی فراتر از none/super-admin، رشته‌های دسترسی خودتان را تعریف کنید (یا Permissions را extend کنید) و آن‌ها را به همان شکل درون checkPermission() بررسی کنید، همان‌طور که AppAuthController.checkPermission() پروژه مثال در بالا این کار را انجام می‌دهد.