Angular + Pilvio API Authentication

    In this tutorial, we will create an Angular application that communicates with the Pilvio API using an HTTP interceptor to manage infrastructure securely.

    What we will build

    • Angular 17+ with standalone components
    • An HTTP interceptor that adds authentication headers
    • A service for managing Pilvio VMs and StorageVault buckets

    Prerequisites

    • Pilvio account and API token (see overview)
    • Node.js 20+ and Angular CLI (npm install -g @angular/cli)

    Step 1: Create the project

    ng new pilvio-angular-app --standalone --style=css --routing
    cd pilvio-angular-app
    

    Step 2: Environment configuration

    Create the file src/environments/environment.ts:

    export const environment = {
      production: false,
      // Backend proxy URL — mitte kunagi otse Pilvio API URL frontendis!
      apiUrl: 'http://localhost:3001/pilvio',
      sessionSecret: 'sinu-arendus-saladus',
    };
    

    Create the file src/environments/environment.prod.ts:

    export const environment = {
      production: true,
      apiUrl: '/pilvio',
      sessionSecret: '',  // Tootmises hallatakse serveripoolselt
    };
    

    Step 3: Auth interceptor

    Create the file src/app/interceptors/auth.interceptor.ts:

    import { HttpInterceptorFn } from '@angular/common/http';
    import { environment } from '../../environments/environment';
    
    export const authInterceptor: HttpInterceptorFn = (req, next) => {
      // Lisa autentimise päis ainult Pilvio API päringutele
      if (req.url.startsWith(environment.apiUrl)) {
        const cloned = req.clone({
          setHeaders: {
            'Authorization': `Bearer ${environment.sessionSecret}`,
          },
        });
        return next(cloned);
      }
      return next(req);
    };
    

    Register the interceptor in the src/app/app.config.ts file:

    import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
    import { provideRouter } from '@angular/router';
    import { provideHttpClient, withInterceptors } from '@angular/common/http';
    import { routes } from './app.routes';
    import { authInterceptor } from './interceptors/auth.interceptor';
    
    export const appConfig: ApplicationConfig = {
      providers: [
        provideZoneChangeDetection({ eventCoalescing: true }),
        provideRouter(routes),
        provideHttpClient(withInterceptors([authInterceptor])),
      ],
    };
    

    Step 4: Pilvio service

    Create the file src/app/services/pilvio.service.ts:

    import { Injectable } from '@angular/core';
    import { HttpClient, HttpParams } from '@angular/common/http';
    import { Observable } from 'rxjs';
    import { environment } from '../../environments/environment';
    
    export interface PilvioVM {
      uuid: string;
      name: string;
      status: string;
      os_name: string;
      os_version: string;
      vcpu: number;
      memory: number;
      private_ipv4: string;
      created_at: string;
    }
    
    export interface PilvioBucket {
      name: string;
      size_bytes: number;
      num_objects: number;
      billing_account_id: number;
    }
    
    export interface PilvioLocation {
      slug: string;
      display_name: string;
      is_default: boolean;
      country_code: string;
    }
    
    @Injectable({ providedIn: 'root' })
    export class PilvioService {
      private baseUrl = environment.apiUrl;
    
      constructor(private http: HttpClient) {}
    
      // VM-id
      listVMs(): Observable<PilvioVM[]> {
        return this.http.get<PilvioVM[]>(`${this.baseUrl}/user-resource/vm/list`);
      }
    
      getVM(uuid: string): Observable<PilvioVM> {
        return this.http.get<PilvioVM>(`${this.baseUrl}/user-resource/vm`, {
          params: new HttpParams().set('uuid', uuid),
        });
      }
    
      startVM(uuid: string): Observable<any> {
        return this.http.post(`${this.baseUrl}/user-resource/vm/start`, `uuid=${uuid}`, {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        });
      }
    
      stopVM(uuid: string): Observable<any> {
        return this.http.post(`${this.baseUrl}/user-resource/vm/stop`, `uuid=${uuid}`, {
          headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        });
      }
    
      // StorageVault (S3 bucketid)
      listBuckets(): Observable<PilvioBucket[]> {
        return this.http.get<PilvioBucket[]>(`${this.baseUrl}/storage/bucket/list`);
      }
    
      // Asukohad
      listLocations(): Observable<PilvioLocation[]> {
        return this.http.get<PilvioLocation[]>(`${this.baseUrl}/config/locations`);
      }
    }
    

    Step 5: Infrastructure dashboard

    Create the file src/app/components/infra-dashboard.component.ts:

    import { Component, OnInit } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { PilvioService, PilvioVM, PilvioBucket } from '../services/pilvio.service';
    
    @Component({
      selector: 'app-infra-dashboard',
      standalone: true,
      imports: [CommonModule],
      template: `
        <div class="dashboard">
          <h1>Pilvio infrastruktuuri haldus</h1>
    
          <div *ngIf="error" class="error">{{ error }}</div>
    
          <!-- VM-id -->
          <section>
            <h2>Virtuaalmasinad ({{ vms.length }})</h2>
            <button (click)="loadVMs()" [disabled]="loadingVMs">Värskenda</button>
    
            <table *ngIf="vms.length > 0">
              <thead>
                <tr>
                  <th>Nimi</th>
                  <th>Staatus</th>
                  <th>OS</th>
                  <th>Ressursid</th>
                  <th>IP</th>
                  <th>Tegevused</th>
                </tr>
              </thead>
              <tbody>
                <tr *ngFor="let vm of vms">
                  <td>{{ vm.name }}</td>
                  <td [class]="'status-' + vm.status">{{ vm.status }}</td>
                  <td>{{ vm.os_name }} {{ vm.os_version }}</td>
                  <td>{{ vm.vcpu }} vCPU / {{ formatMemory(vm.memory) }}</td>
                  <td><code>{{ vm.private_ipv4 }}</code></td>
                  <td>
                    <button *ngIf="vm.status === 'stopped'" (click)="startVM(vm.uuid)" class="btn-start">
                      Käivita
                    </button>
                    <button *ngIf="vm.status === 'running'" (click)="stopVM(vm.uuid)" class="btn-stop">
                      Peata
                    </button>
                  </td>
                </tr>
              </tbody>
            </table>
          </section>
    
          <!-- StorageVault bucketid -->
          <section>
            <h2>StorageVault bucketid ({{ buckets.length }})</h2>
            <table *ngIf="buckets.length > 0">
              <thead>
                <tr>
                  <th>Nimi</th>
                  <th>Objektide arv</th>
                  <th>Maht</th>
                </tr>
              </thead>
              <tbody>
                <tr *ngFor="let b of buckets">
                  <td>{{ b.name }}</td>
                  <td>{{ b.num_objects }}</td>
                  <td>{{ formatBytes(b.size_bytes) }}</td>
                </tr>
              </tbody>
            </table>
          </section>
        </div>
      `,
      styles: [`
        .dashboard { max-width: 1000px; margin: 0 auto; padding: 20px; }
        .error { color: #ef4444; padding: 10px; background: #fef2f2; border-radius: 4px; margin-bottom: 16px; }
        table { width: 100%; border-collapse: collapse; margin-top: 12px; }
        th, td { padding: 10px; text-align: left; border-bottom: 1px solid #e5e7eb; }
        th { font-weight: 600; background: #f9fafb; }
        code { background: #f3f4f6; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
        section { margin-bottom: 32px; }
        .status-running { color: #16a34a; font-weight: 600; }
        .status-stopped { color: #dc2626; font-weight: 600; }
        .btn-start { background: #22c55e; color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
        .btn-stop { background: #ef4444; color: white; border: none; padding: 4px 12px; border-radius: 4px; cursor: pointer; }
      `],
    })
    export class InfraDashboardComponent implements OnInit {
      vms: PilvioVM[] = [];
      buckets: PilvioBucket[] = [];
      loadingVMs = false;
      error: string | null = null;
    
      constructor(private pilvio: PilvioService) {}
    
      ngOnInit() {
        this.loadVMs();
        this.loadBuckets();
      }
    
      loadVMs() {
        this.loadingVMs = true;
        this.pilvio.listVMs().subscribe({
          next: (data) => { this.vms = data; this.loadingVMs = false; },
          error: () => { this.error = 'VM-ide laadimine ebaõnnestus'; this.loadingVMs = false; },
        });
      }
    
      loadBuckets() {
        this.pilvio.listBuckets().subscribe({
          next: (data) => this.buckets = data,
          error: () => this.error = 'Bucketi laadimine ebaõnnestus',
        });
      }
    
      startVM(uuid: string) {
        this.pilvio.startVM(uuid).subscribe({ next: () => this.loadVMs() });
      }
    
      stopVM(uuid: string) {
        this.pilvio.stopVM(uuid).subscribe({ next: () => this.loadVMs() });
      }
    
      formatMemory(mb: number): string {
        return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb} MB`;
      }
    
      formatBytes(bytes: number): string {
        if (bytes < 1024) return `${bytes} B`;
        if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
        if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
        return `${(bytes / 1073741824).toFixed(1)} GB`;
      }
    }
    

    Step 6: Routing

    Update the file src/app/app.routes.ts:

    import { Routes } from '@angular/router';
    import { InfraDashboardComponent } from './components/infra-dashboard.component';
    
    export const routes: Routes = [
      { path: '', component: InfraDashboardComponent },
    ];
    

    Step 7: Deploy

    The backend proxy setup is identical to the Vue.js tutorial.

    ng build --configuration production
    scp -r dist/pilvio-angular-app/browser/* deploy@SINU_FLOATING_IP:/var/www/angular-app/
    

    Next steps: Use StorageVault for file management or add a database to your VM.