Angular custom guard

Angular custom guard

Merhaba,

Bu yazımda size Angular Guard yapısından ve kullanımından bahsedeceğim. Guard yapısı ile uygulamamızda yapmış olduğumuz sayfalar arası geçişlerimize kontrol mekanizması eklemiş oluyoruz. Hangi kullanıcı bu sayfaya erişebilecek, hangi aşamada sayfadan ayrılabilecek vb. Örneğin sisteme giriş yapan bir kullanıcı tüm sayfalara erişebilirken, giriş yapmayan kullanıcı sadece belirli sayfalarını görebiliyor.

Bu konumuz için Home, Profile sayfaları olan ve birde Login, Logout yapısı olan bir uygulama hazırlayalım ve profile sayfasına sadece login olan kullanıcı erişebilsin ve ayrıca profile sayfasındayken değişiklikleri kayıt etmeden sayfadan ayrılamasın.

Hazırlayacağımız uyglamamız bittiğinde aşağıdaki gibi klasör yapısına sahip olacaktır.

Uygulama klasör yapısı

src
├── app
│   ├── app-routing.module.ts
│   ├── app.component.html
│   ├── app.component.scss
│   ├── app.component.ts
│   ├── app.module.ts
│   ├── components
│   │   ├── login
│   │   │   ├── login.component.html
│   │   │   ├── login.component.scss
│   │   │   └── login.component.ts
│   │   └── profile
│   │       ├── profile.component.html
│   │       ├── profile.component.scss
│   │       └── profile.component.ts
│   ├── guards
│   │   ├── can-activate.guard.ts
│   │   └── can-deactivate-profile.guard.ts
│   └── services
│       ├── auth.service.ts
│       └── data.service.ts
│

Proje oluşturma

İlk olarak projemizi aşağıdaki komut ile oluşturalım ve oluşturduğumuz proje klasörüne geçelim.

ng new angular-guard-usage
cd angular-guard-usage

Servisleri oluşturma

Şimdi ise yapacağımız işlemler için gerekli olan servislerimizi oluşturalım. İlk olarak services klasörünü aşağıdaki komut ile oluşturalım. Daha sonra bu klasör altında gerekli olan servislerimizi oluşturalım.

cd src/app
mkdir services
cd services

Auth servisini oluşturma

Yapacağımız kontroller için gerekli olan auth servisimizi aşağıdaki komut ile oluşturalım.

ng g s auth

Data servisini oluşturma

Kullanacağımız datalarımız için gerekli olan servisimizi aşağıdaki komutla oluşturalım.

ng g s data

Guard oluşturma

Benzer şekilde yine ilk önce guards klasörümüzü oluşturalım ve sonrasında ihtiyacımız olan guard servislerini bu klasör altında oluşturalım.

cd src/app
mkdir guards
cd guards

Kontrol için kullanacağımız ve belirttiğimiz alana erişim ya da oradan ayrılma işlemeni kontrol edeceğimiz active ve deactive guard servislerini oluşturalım.

can-activate guard oluşturma

Aşağıdaki komut ile can-activate guard servisimizi oluşturalım.

ng g guard can-activate

can-deactivate-profile guard oluşturma

Benzer şekilde aşağıdaki komut yardımı ile ihtiyacımız olan guard servisimizi oluşturalım.

ng g guard can-deactivate-profile

Component oluşturma

Hem servislerimiz hemde guard servislerimiz hazır olduğuna göre şimdi kullanacak olduğumuz componentlerimizi oluşturalım. Yine yaptığımız gibi ilk olarak components klasörümüzü oluşturalım ve bu klasör altında gerekli olan componentleri tanımlayalım.

cd src/app
mkdir components
cd components

login componenti oluşturma

Aşağıdaki komut ile login componentimizi oluşturalım.

ng g c login

profile componenti oluşturma

İhtiyacımız olan diğer component olan profil componentinide aşağıdaki komut ile oluşturalım.

ng g c profile

Artık projemizde ihtiyacımız olan tüm servisleri ve componentleri oluşturduk şimdi uygulama klasör yapısındaki sırayla tek tek component ve servislerimizin içeriğini güncelleyelim.

App

app-routing.module.ts içeriği

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LoginComponent } from './components/login/login.component';
import { ProfileComponent } from './components/profile/profile.component';
import { CanActivateGuard } from './guards/can-activate.guard';
import { CanDeactivateProfileGuard } from './guards/can-deactivate-profile.guard';

const routes: Routes = [
  { component: LoginComponent, path: 'login' },
  {
    component: ProfileComponent,
    path: 'profile',
    canActivate: [CanActivateGuard],
    canDeactivate: [CanDeactivateProfileGuard],
  },
  { path: '**', pathMatch: 'full', redirectTo: '' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

app.component.html içeriği

<div class="toolbar" role="banner">
  <img
    width="40"
    alt="Angular Logo"
    src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="
  />
  <span>{{ title }}</span>
</div>

<div class="content" role="main">
  <div class="card highlight-card card-small">
    <svg
      id="rocket"
      xmlns="http://www.w3.org/2000/svg"
      width="101.678"
      height="101.678"
      viewBox="0 0 101.678 101.678"
    >
      <g id="Group_83" data-name="Group 83" transform="translate(-141 -696)">
        <circle
          id="Ellipse_8"
          data-name="Ellipse 8"
          cx="50.839"
          cy="50.839"
          r="50.839"
          transform="translate(141 696)"
          fill="#dd0031"
        />
        <g
          id="Group_47"
          data-name="Group 47"
          transform="translate(165.185 720.185)"
        >
          <path
            id="Path_33"
            data-name="Path 33"
            d="M3.4,42.615a3.084,3.084,0,0,0,3.553,3.553,21.419,21.419,0,0,0,12.215-6.107L9.511,30.4A21.419,21.419,0,0,0,3.4,42.615Z"
            transform="translate(0.371 3.363)"
            fill="#fff"
          />
          <path
            id="Path_34"
            data-name="Path 34"
            d="M53.3,3.221A3.09,3.09,0,0,0,50.081,0,48.227,48.227,0,0,0,18.322,13.437c-6-1.666-14.991-1.221-18.322,7.218A33.892,33.892,0,0,1,9.439,25.1l-.333.666a3.013,3.013,0,0,0,.555,3.553L23.985,43.641a2.9,2.9,0,0,0,3.553.555l.666-.333A33.892,33.892,0,0,1,32.647,53.3c8.55-3.664,8.884-12.326,7.218-18.322A48.227,48.227,0,0,0,53.3,3.221ZM34.424,9.772a6.439,6.439,0,1,1,9.106,9.106,6.368,6.368,0,0,1-9.106,0A6.467,6.467,0,0,1,34.424,9.772Z"
            transform="translate(0 0.005)"
            fill="#fff"
          />
        </g>
      </g>
    </svg>

    <span class="active-page">{{ activePage }}</span>
  </div>

  <div class="card-container">
    <button class="card card-small" routerLink="">
      <span>Home</span>
    </button>

    <button class="card card-small" routerLink="profile">
      <span>Profile</span>
    </button>

    <button
      *ngIf="!authService.isLoggedIn"
      class="card card-small login"
      routerLink="login"
    >
      <span>Login</span>
    </button>

    <button
      *ngIf="authService.isLoggedIn"
      class="card card-small logout"
      (click)="logout()"
    >
      <span>Logout</span>
    </button>
  </div>
</div>

<router-outlet></router-outlet>

app.component.scss içeriği

h1,
h2,
h3,
h4,
h5,
h6 {
  margin: 8px 0;
}

p {
  margin: 0;
}

.toolbar {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 60px;
  display: flex;
  align-items: center;
  background-color: #1976d2;
  color: white;
  font-weight: 600;
}

.toolbar img {
  margin: 0 16px;
}

.content {
  display: flex;
  margin: 82px auto 32px;
  padding: 0 16px;
  max-width: 960px;
  flex-direction: column;
  align-items: center;
}

.card-container {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  margin-top: 16px;
}

.card {
  all: unset;
  border-radius: 4px;
  border: 1px solid #eee;
  background-color: #fafafa;
  height: 40px;
  width: 200px;
  margin: 0 8px 16px;
  padding: 16px;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  transition: all 0.2s ease-in-out;
  line-height: 24px;
}

.card-container .card:not(:last-child) {
  margin-right: 0;
}

.card.card-small {
  height: 16px;
  width: 168px;
}

.card-container .card:not(.highlight-card) {
  cursor: pointer;
}

.card-container .card:not(.highlight-card):hover {
  transform: translateY(-3px);
  box-shadow: 0 4px 17px rgba(0, 0, 0, 0.35);
}

.card.highlight-card {
  background-color: #1976d2;
  color: white;
  font-weight: 600;
  border: none;
  width: auto;
  min-width: 30%;
  position: relative;
}

.card.card.highlight-card span {
  margin-left: 60px;
}

svg#rocket {
  width: 80px;
  position: absolute;
  left: -10px;
  top: -24px;
}

svg#rocket-smoke {
  height: calc(100vh - 95px);
  position: absolute;
  top: 10px;
  right: 180px;
  z-index: -10;
}

.active-page {
  text-transform: capitalize;
}

.login {
  color: green;
  font-weight: bold;
}

.logout {
  color: red;
  font-weight: bold;
}

app.component.ts içeriği

import { Component, OnInit, VERSION } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { AuthService } from './services/auth.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
  public title = `Angular ${VERSION.major} Guard usage`;
  public activePage = 'Home';

  constructor(public authService: AuthService, private router: Router) {}

  public ngOnInit(): void {
    this.router.events.subscribe((event) => {
      if (event instanceof NavigationEnd) {
        const path = event.url.split('/')[1];
        this.activePage = path === '' ? 'home' : path;
      }
    });
  }

  public logout(): void {
    this.authService.logout();
    this.router.navigate(['.']);
  }
}

app.module.ts içeriği

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginComponent } from './components/login/login.component';
import { ProfileComponent } from './components/profile/profile.component';

@NgModule({
  declarations: [AppComponent, LoginComponent, ProfileComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Login

login.component.html içeriği

<p>{{ loginContent }}</p>

login.component.scss içeriği

p{
  display: flex;
  justify-content: center;
}

login.component.ts içeriği

import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { delay } from 'rxjs';
import { AuthService } from 'src/app/services/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
  public loginContent: string = '';

  constructor(private authService: AuthService, private router: Router) {}

  public ngOnInit(): void {
    this.loginContent = 'Logging in...';

    setTimeout(() => {
      this.authService.login();
      this.loginContent = '';
      this.router.navigate(['.']);
    }, 700);
  }
}

Profile

profile.component.html içeriği

<p>profile works!</p>
<div>
  <button (click)="saveChanges()">Save changes</button>
  <button (click)="rollbackChanges()">Rollback changes</button>
</div>

profile.component.scss içeriği

p,
div {
  display: flex;
  justify-content: center;
}

profile.component.ts içeriği

import { Component } from '@angular/core';
import { DataService } from 'src/app/services/data.service';

@Component({
  selector: 'app-profile',
  templateUrl: './profile.component.html',
  styleUrls: ['./profile.component.scss'],
})
export class ProfileComponent {
  public isSaved: boolean = false;

  constructor(private dataService: DataService) {}

  public saveChanges(): void {
    this.dataService.saveChanges();
    this.isSaved = true;
  }

  public rollbackChanges(): void {
    this.dataService.rollbackChanges();
    this.isSaved = false;
  }
}

CanActivate

can-activate.guard.ts içeriği

import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({ providedIn: 'root' })
export class CanActivateGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  public canActivate(): boolean {
    if (!this.authService.isLoggedIn) {
      alert('You should login first...');
      return false;
    }

    return true;
  }
}

CanDeactivate

can-deactivate-profile.guard.ts içeriği

import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { ProfileComponent } from '../components/profile/profile.component';
import { DataService } from '../services/data.service';

@Injectable({
  providedIn: 'root',
})
export class CanDeactivateProfileGuard
  implements CanDeactivate<ProfileComponent>
{
  constructor(private dataService: DataService) {}

  public canDeactivate(component: ProfileComponent): boolean {
    if (!this.dataService.isSaved || !component.isSaved) {
      alert('You should save your changes...');
      return false;
    }

    return true;
  }
}

Auth

auth.service.ts içeriği

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  private _isLoggedIn: boolean = false;

  public get isLoggedIn(): boolean {
    return this._isLoggedIn;
  }

  public login(): void {
    this._isLoggedIn = true;
  }

  public logout(): void {
    this._isLoggedIn = false;
  }
}

Data

data.service.ts içeriği

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DataService {
  private _isSavedChanges: boolean = false;

  public get isSaved(): boolean {
    return this._isSavedChanges;
  }

  public saveChanges(): void {
    this._isSavedChanges = true;
  }

  public rollbackChanges(): void {
    this._isSavedChanges = false;
  }
}

Şimdi projemizde kullandığımız tüm component servis ve guard içeriklerini güncellediğimize göre artık guard kullanımında yaptığımız tanımları ve kontrollerini ele alalım. Bizim örneğinimizde bir sayfaya erişebilmek ve sayfadan ayrılma işlemlerini kontrol etmek için iki guard hazırladık ve bu guardları ilgili route tanımındaki canActivate ve canDeactivate bilgileri içerisine ekledik. Daha sonra CanActivateGuard tanımı içinde sayfaya erişmek için gerekli olan kontrollerimizi yazdık ve son olarak da CanDeactivateProfileGuard içinde ise sayfadan ayrılma işlemininin kontrolünü yazdık.

Kaynak kod ve Demo

Sizde kendinize göre bir guard yazmak ya da bu örnekte yapılan düzenlemeleri özelleştirmek için aşağıdaki GitHub adresine, çalışan uygulama için Demo sayfasına bakabilirsiniz.