"Our goal with Angular 2 is to make the best possible set of tools for
building web apps not constrained by maintaining backwards compatibility
with existing APIs." —Angular blog
احراز هویت و اعتبارسنجی کاربران در برنامههای Angular - قسمت اول - معرفی و ایجاد ساختار برنامه
dotnet new angular --auth Individual
dotnet new react --auth Individual //or dotnet new reactredux --auth Individual
سازماندهی برنامههای Angular توسط ماژولها
- یک نمونهاش در سری «احراز هویت و اعتبارسنجی کاربران در برنامههای Angular» بحث شدهاست؛ با این کدها.
├───authentication │ ├───access-denied │ └───login ├───core │ ├───component │ │ └───header │ ├───models │ └───services ├───dashboard │ ├───call-protected-api │ └───protected-page ├───page-not-found ├───shared │ └───directives └───welcome
- بستگی به توزیع نهایی برنامه دارد. آیا قرار است برنامهی Angular بر روی یک پورت دیگر و یا یک دومین دیگر به صورت مجزایی ارائه شود؟ بله. در اینحالت باید CORS فعال شود (یک مثال فعالسازی آن). در غیراینصورت اگر فایلهای نهایی Angular در پوشهی wwwroot برنامهی وب کپی میشوند، نیازی به تغییر اضافهتری نیست.
فایل package.json به صورت زیر میباشد
{ "name": "mainmodule", "version": "1.0.0", "scripts": { "build": "webpack --progress", "build:prod": "webpack -p --progress", "serve": "webpack-dev-server --inline --progress ", "postinstall": "typings install" }, "dependencies": { "@angular/common": "2.0.0-rc.5", "@angular/compiler": "2.0.0-rc.5", "@angular/core": "2.0.0-rc.5", "@angular/forms": "0.3.0", "@angular/http": "2.0.0-rc.5", "@angular/platform-browser": "2.0.0-rc.5", "@angular/platform-browser-dynamic": "2.0.0-rc.5", "@angular/router": "3.0.0-rc.1", "core-js": "^2.4.0", "reflect-metadata": "^0.1.3", "rxjs": "5.0.0-beta.6", "typings": "^1.4.0", "zone.js": "^0.6.12" }, "devDependencies": { "html-webpack-plugin": "^2.22.0", "ts-loader": "^0.9.0", "typescript": "^2.0.3", "typings": "^1.4.0", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.2" } }
با توجه به خطای connect ECONNREFUSED 10.10.34.36:443 که در زمان ذخیره package.json به دلیل وجود typings install رخ میداد از vpn استفاده کردم و ذخیره فایل درست انجام شد ولی بازهم با اجرای npm run serve خطای بالا رخ میدهد.
آیا به یادگیری یا ادامهی استفاده از AngularJS خواهید پرداخت؟
البته Angular به همراه دایرکتیو ویژهای به نام ngModel است که two-way data binding را با import ماژول ویژهی فرمها میسر میکند:
که آن نیز در اصل از جمع Property Binding و Event Binding تشکیل شدهاست:
<input [ngModel]="username" (ngModelChange)="username = $event">
<input [(ngModel)]='username' />
انقیاد به خواص یا Property binding
فرض کنید دو کامپوننت والد و فرزند را ایجاد کردهایم:
در کامپوننت والد، مقداری را توسط متد deposit هربار 100 آیتم افزایش میدهیم:
import { Component, OnInit } from "@angular/core"; @Component({ selector: "app-parent", templateUrl: "./parent.component.html", styleUrls: ["./parent.component.css"] }) export class ParentComponent implements OnInit { amount = 500; constructor() { } ngOnInit() { } deposit() { this.amount += 100; } }
<h2>Custom two way data binding</h2> <div class="panel panel-primary"> <div class="panel-heading"> <h2 class="panel-title">Parnet Component</h2> </div> <div class="panel-body"> <label>Available amount:</label> {{amount}} <button (click)="deposit()" class="btn btn-success">Deposit 100</button> <div> <app-child [amount]="amount"> </app-child> </div> </div> </div>
کامپوننت فرزند به صورت ذیل تعریف میشود:
import { Component, OnInit, Input } from "@angular/core"; @Component({ selector: "app-child", templateUrl: "./child.component.html", styleUrls: ["./child.component.css"] }) export class ChildComponent implements OnInit { @Input() amount: number; constructor() { } ngOnInit() { } withdraw() { this.amount -= 100; } }
با این قالب:
<div class="panel panel-default"> <div class="panel-heading"> <h2 class="panel-title">Child Component</h2> </div> <div class="panel-body"> <label>Amount available: </label> {{amount}} <button (click)="withdraw()" class="btn btn-danger">Withdraw 100</button> </div> </div>
در اینجا زمانیکه data binding را به صورت ذیل تعریف میکنیم:
<app-child [amount]="amount"> </app-child>
این ارتباط نیز یک طرفهاست. برای مثال اگر بر روی دکمهی Deposit والد کلیک کنیم:
مقدار افزایش یافتهی در والد، به فرزند نیز منتقل میشود و نمایش داده خواهد شد. اما اگر بر روی دکمهی withdraw فرزند کلیک کنیم:
تغییر صورت گرفته، به والد انعکاس پیدا نمیکند. برای اطلاع رسانی به والد، به انقیاد به رخدادها نیاز داریم.
انقیاد به رخدادها یا Event binding
یک کامپوننت میتواند به رخدادهای صادر شدهی توسط کامپوننتی دیگر گوش فرا دهد:
import { Component, OnInit, Input, Output, EventEmitter } from "@angular/core"; @Component({ selector: "app-child", templateUrl: "./child.component.html", styleUrls: ["./child.component.css"] }) export class ChildComponent implements OnInit { @Input() amount: number; @Output() amountChange = new EventEmitter(); constructor() { } ngOnInit() { } withdraw() { this.amount -= 100; this.amountChange.emit(this.amount); } }
اکنون در قالب کامپوننت والد، این رخداد را درون یک () معرفی خواهیم کرد:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
اکنون اگر برنامه را آزمایش کنیم، با کلیک بر روی دکمهی withdraw فرزند، مقدار کاهش یافته به والد نیز منعکس میشود:
پیاده سازی syntax ویژهی Banana in a box
تا اینجا پیاده سازی two way data-binding سفارشی به پایان میرسد. اما تعریف طولانی:
<app-child [amount]="amount" (amountChange)="this.amount= $event"> </app-child>
<app-child [(amount)]="amount"> </app-child>
نکتهی ویژهی آن، وجود پسوند Change در نام رخداد تعریف شدهاست:
@Input() amount: number; @Output() amountChange = new EventEmitter();
بنابراین به صورت خلاصه جهت تعریف یک انقیاد دو طرفه سفارشی:
- ابتدا باید انقیاد به یک خاصیت ورودی x را تعریف کرد.
- سپس نیاز است انقیاد به یک رخداد خروجی همنام، که نام آن، پسوند Change را اضافهتر دارد، یعنی xChange را تعریف کرد.
- اکنون میتوان two-way data binding syntax ویژهای را به نام banana in a box بر روی ایندو تعریف کرد[(x)].
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید.
- ViewUsers
- CreateUser
- EditUser
- DeleteUser
همانطور که مشاهده میکنید، المنتهایی در صفحه وجود دارند که کاربر X نباید آنها را مشاهده کند. از جمله دکمه حذف کاربر و دکمه ایجاد کاربر. برای مخفی کردن آنها چه راهحلی را میتوان ارائه داد؟ شاید بخواهید برای اینکار از ngIf* استفاده کنید. برای اینکار کافی است دست بکار شوید تا مشکلاتی را که در این روش به آنها بر میخورید، متوجه شوید. از جمله این مشکلات میتوان به پیچیدگی بسیار زیاد و وجود کدهای تکراری در هر کامپوننت اشاره کرد (در بهترین حالت هر کامپوننت باید سرویس حاوی دسترسیها را در خود تزریق کرده و با استفاده از توابعی، وجود یا عدم وجود دسترسی را بررسی کند).
راهحل بهتر، استفاده از یک Directive سفارشی است. همچنین ماژول Ng2Permission علاوه بر فراهم کردن Directive جهت مدیریت المنتهای روی صفحه، امکاناتی را جهت نگهداری و تعریف دسترسیهای جدید و همچنین محافظت از Routeها فراهم کرده است. این ماژول الهام گرفته از ماژول angular-permission میباشد.
کافی است با استفاده از دستور زیر این ماژول را نصب کنید:
npm install angular2-permission --save
بعد از نصب، ماژول Ng2Permission را در قسمت imports در ماژول اصلی برنامه، اضافه کنید.
import { Ng2Permission } from 'angular2-permission'; @NgModule({ imports: [ Ng2Permission ] })
مدیریت دسترسیها
توضیحات | امضاء |
تعریف دسترسیهای جدید | define(permissions: Array<string>): void |
افزودن دسترسی جدید | add(permission: string ) : void |
حذف دسترسی مشخص شده | remove(permission: string ) : void |
برسی اینکه دسترسی قبلا تعریف شده است؟ | hasDefined( permission : string ) : boolean |
برسی اینکه حداقل یکی از دسترسیهای ورودی قبلا تعریف شده است؟ | hasOneDefined(permissions: Array < string > ) : boolean |
حذف تمامی دسترسیها | clearStore( ) : void |
دریافت تمامی دسترسیهای تعریف شده | get store ( ) : Array < string> |
Emitter جهت تغییر در دسترسیها | get permissionStoreChangeEmitter ( ) : EventEmitter< an y> |
برای مثال جهت تعریف دسترسیهای جدید، کافی است سرویس PermissionService را تزریق کرده و با استفاده از متدهای define اقدام به اینکار کنید:
import { PermissionService } from 'angular2-permission'; @Component({ […] }) export class LoginComponent implements OnInit { constructor(private _permissionService: PermissionService) { this._permissionService.define(['ViewUsers', 'CreateUser', 'EditUser', 'DeleteUser']); } }
آنچه که واضح است، این است که لیست دسترسیها میتوانند از سمت سرور تامین شوند.
محافظت از مسیرهای تعریف شده
import { PermissionGuard, IPermissionGuardModel } from 'angular2-permission'; […] const routes: Routes = [ { path: 'login', component: LoginComponent, children: [] }, { path: 'users', component: UserListComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['ViewUsers'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }, { path: 'users/create', component: UserCreateComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['CreateUser'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }, { path: '403', component: AccessDeniedComponent, children: [] } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { }
توضیحات | خصوصیت |
فقط دسترسیهای تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت. | Only |
تمامی دسترسیهای تعریف شده، به جز دسترسیهای تعریف شده در این قسمت، امکان پیمایش به این مسیر را خواهند داشت. | Except |
در صورتیکه تقاضای غیر مجازی جهت پیمایش به این مسیر صادر شد، کاربر را به این مسیر هدایت میکند. | RedirectTo |
{ path: 'users/create', component: UserCreateComponent, canActivate: [PermissionGuard], data: { Permission: { Only: ['Admin', 'CreateUser'], RedirectTo: '403' } as IPermissionGuardModel }, children: [] }
محافظت از المنتهای صفحه
توضیحات | Input type | Directive |
فقط دسترسیهای تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند. | Arrayy<string> | hasPermission |
تمامی دسترسیهای موجود، به جز دسترسیهای تعریف شده در این دایرکتیو امکان مشاهده (به صورت پیش فرض) المنت را دارند. | Array<string> | exceptPermission |
استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت دارد. | string | Function | onAuthorizedPermission |
استراتژی برخورد با المنت، هنگامیکه کاربر دسترسی به المنت را ندارد. | string | Function | onUnauthorizedPermission |
<button type="button" [hasPermission]="['DeleteUser']"> <span aria-hidden="true"></span> Delete </button>
در صورتیکه بیش از یک دسترسی مد نظر باشد، با کاما از هم جدا خواهند شد.
<button type="button" [hasPermission]="[ 'Admin', 'DeleteUser']"> <span aria-hidden="true"></span> Delete </button>
<button type="button" [exceptPermission]="['GeustUser']"> <span aria-hidden="true"></span> Delete </button>
<button type="button" [hasPermission]="['GeustUser']" onAuthorizedPermission="enable" onUnauthorizedPermission="disable"> <span aria-hidden="true"></span> Delete </button>
رفتار | مقدار |
حذف خصوصیت disabled از المنت | enable |
افزودن خصوصیت disabled به المنت | disable |
تنظیم استایل display به inherit | show |
تنظیم استایل display به none | hide |
@Component({ selector: 'app-user-list', templateUrl: './user-list.component.html', styleUrls: ['./user-list.component.css'] }) export class UserListComponent { constructor() { } OnAuthorizedPermission(element: ElementRef) { element.nativeElement.style.visibility ="inherit"; } OnUnauthorizedPermission(element: ElementRef) { element.nativeElement.style.visibility = "hidden"; } }
<button [hasPermission]="['CreateUser']" [onAuthorizedPermission]="OnAuthorizedPermission" [onUnauthorizedPermission]="OnUnauthorizedPermission"> <span aria-hidden="true"></span> Add New User </button>