Build an Angular App Leveraging Sitefinity Headless Capabilities, Part II

Build an Angular App Leveraging Sitefinity Headless Capabilities, Part II

Posted on May 22, 2019 0 Comments
Build an Angular App Leveraging Sitefinity Headless Capabilities_870x450

In the second part of our tutorial, learn how to set up authentication for an Angular app that consumes Sitefinity CMS OData services using Sitefinity’s easy-to-use OData SDK.

You are probably already acquainted with our previous blog post about building an Angular app with the help of Sitefinity CMS headless capabilities. If so, you know by now how to use the Sitefinity OData SDK for read operations, and for consuming data from Sitefinity and displaying it in an Angular app.

In this blog post, we will demonstrate how to create authentication for your Angular app, utilizing Sitefinity authentication features. You will also learn how to perform write operations via the SDK by creating comments, testimonials, and by uploading images. You will use the QuantumHeadlessAngularApp you previously created. You can also use a ready version of this app, located in the GitHub repo.

Following is an overview of what you need to do to create authentication for your app.

Set OData Service Permissions

Once you create a Sitefinity website, you can access all content types via the Default OData service, located under Administration -> WebServices. To access the API reference page for the service, click the Use in your app link on the right. Of course, depending on your scenario, you can use your own custom service, different than the default one.

By default, Sitefinity OData web services are secured and their access permissions are set to Administrators only. The other available permission options are Everyone and Authenticated. As you would expect, the Authenticated option provides read and write permissions only to authenticated users. On the other hand, the Everyone option provides read permissions to everyone and write permissions only to authenticated users.

Since your application needs to be visible to everyone, but comments and testimonials to be created only by Sitefinity users, you need to switch to the Everyone option for your OData service.

Set Up Authentication

By default, Sitefinity CMS uses claims-based identification. In this scenario, the identity of users is authenticated by Security Token Service (STS). The STS issues a token, containing the claims that the user makes about their identity. When requesting a protected resource from Sitefinity via HTTP, you need to obtain an authentication token and pass it as an Authorization header. Since you are working with the OData SDK, you need to pass the authentication token to a designated authentication property of the SDK Sitefinity instance.

By following the documentation guidelines, you use the oidc-client library to manage the overall security context of the application. This includes signing in, signing out, issuing an authentication token from the STS, and providing identity information about the current user. Usually applications have more than one provider but in this example, you will only be working with the Sitefinity STS as an Identity provider.

Let’s get going!

Prerequisites

  1. You are familiar with how to create an Angular app in Sitefinity.
    For details, see the first part of this tutorial.
  2. You downloaded the Angular app from GitHub (edit branch).
  3. Optionally, get acquainted with how to secure Angular apps.

Initial Setup

Add the oidc-client as an npm dependency and install with:

npm install oidc-client --save

Setting Up Authentication

Authentication for your app basically consists of:

  • Authentication guard
    Responsible for intercepting route requests and checking the identity of the user
  • Authentication provider
    Provides several authentication methods from the oidc-client
  • Authentication service
    Initializes the provider and handles the authentication process
Let’s start with the implementation of the provider. The oidc-client library provides a UserManager class with an API for signing in, signing out, management of user claims returned from the OIDC provider, and access token management. The UserManager class also provides an instance of the User object. The User object encapsulates the client-side information about a signed in user.

To use these classes, you need to create your own oidc provider with a manager property of type UserManager. The provider methods call the ones from the UserManager API. To set up the UserManager, you first configure the class with a settings object as a parameter. The settings object has the following properties:

  • Authority
    The URL of the OIDC/OAuth2 provider, which in Sitefinity is Sitefinity/Authenticate/OpenID.
  • client_id
    The client application identifier as registered with the OIDC/OAuth2 provider. In this case, you will be using “sitefinity,” which is the out-of-the-box client for interacting with Sitefinity STS via the implicit client flow.
  • response_type
    The response you want to get from the provider, which in this case you want to receive as a token.
  • Scope
    The requested scope from the provider. You need to be able to access the user profile.
  • AutomaticSilentRenew
    A boolean value, indicating whether there needs to be an automatic token renewal. During the authentication process, a cookie and an authentication token are issued to the client. Usually, the token expires before the cookie, which requires renewal of the token with another request to the STS. This can happen while the system is being used. To prevent loss of data for the user, the subsequent token request can be performed silently, so you set this property to true to indicate you want to have silent token renewal.
  • post_logout_redirect_uri
    The OIDC/OAuth2 post-logout redirect URI. This is where you want to redirect users after logout, which in this case is the route of sign-out-redirect.component.ts.
  • redirect_uri
    The redirect URI of your client application is to receive a response from the OIDC/OAuth2 provider. The redirect URI is where you want your users to go after login, which in this case is the route of the sign-in-redirect.component.ts.
  • silent_redirect_uri
    The URL for the page containing the code that handles the silent renewal. This is an HTML page (silent-renewal.html).


There are two ways to sign in users - via a redirect to the Identity server’s login page or silently.

  1. The first user authentication is performed with a redirect to the login page, where the user is prompted to enter their credentials. As a result, an authentication token is issued via the signingRedirect method of the UserManager class. You pass the current URL to the signinRedirect property to make sure the user returns to the same page after they log in.
    private authenticateWithRedirects(returnUrl: string): Observable<void> { 
      const signIn = this.manager.signinRedirect({ data: returnUrl }); 
      return observableFrom(signIn); 
    } 

    After logging in, the user is redirected to the URL that you set in the redirect_uri property of the UserManager settings.

    However, you want to be able to return to the application page from which login or logout was initiated. To do this, you create two components – sign-in-redirect.component and sign-out-redirect component. A few steps back, you actually pointed the redirect_uri and the post_logout_redirect_uri properties of the settings object to the routes of these components. Thus, after login or logout, users are redirected to the corresponding component. The code of the two components is quite similar. The login and logout methods of the UserManager class accept the URL of the page you want your users to return to after the action is performed.

    After login, users are redirected to the route of the sign-in-redirect.component. In the component, on ngOnInit, you subscribe to the signingRedirectCallback. The observable returns the user, as well as the URL passed to the signinRedirect method as a state property of the user. Finally, navigate back to the route where the user initially was. The logic on logout is similar to that of login. 

    ngOnInit() { 
      const redirect = new UserManager({}).signinRedirectCallback(); 
      const redirectAsObservable = observableFrom(redirect); 
      redirectAsObservable.subscribe((args: SignInResponse) => { 
        const state = args.state; 
        this.router.navigateByUrl(state); 
      }); 
    } 

  2. In case the user session expires while browsing the app, perform an attempt for silent authentication:


    private authenticateSilent(returnUrl: string): Observable<void> { 
      const signInSilent = observableFrom(this.manager.signinSilent()); 
      signInSilent.subscribe(() => { 
        this.router.navigateByUrl(returnUrl); 
      }); 
     
      return signInSilent.pipe(map(x => <any>x)); 
    }

    On successful silent login, redirect back to the page from which the silent login was initiated.


Once the user is logged in, you get the authentication token from the user object and emit it though the token property of the provider.

The provider also exposes a signout method, a getUser method, and an isLoggedIn method. The first two call methods of the UserManager class. The isLoggedIn method gets the user and the session and returns a boolean value. It returns true if the user session hasn't expired and the subject identifier of the user and the session are the same.

isLoggedIn(): Observable<boolean> { 
  const user = observableFrom(this.manager.getUser()); 
  const session = observableFrom(this.manager.querySessionStatus()); 
 
  return observableCombineLatest(user, session).pipe(map(data => { 
    const [user, session] = data; 
    if (user && session) { 
      if (!user.expired && user.profile.sub === session.sub) { 
        return true; 
      } 
    } 
 
    return false; 
  }), catchError(() => { 
    return observableOf(false); 
  })); 
} 

Authentication Service

The authentication service initializes the provider and sets the authentication token to the Sitefinity object of the SDK. It also exposes generic authentication methods. Currently there is just one provider, but the authentication service makes it easy to plug in more. The only condition for the providers is to implement a common interface—AuthProviders.

Authentication Guard

As mentioned earlier, the idea is to allow unauthenticated users to see the content of your application, whereas allow only authenticated to create content—post comments and create testimonials. This is why you need an authentication guard for the protected routes.

In the CanActivate method of the authentication guard you check whether the user is logged in and if not, redirect them to the Sitefinity login page. You also pass the current route to the signIn method to make sure that the user is redirected back to the same page. 

canActivate (next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | boolean { 
  const isLoggedInSubject = new ReplaySubject<boolean>(1); 
  this.authService.init().subscribe(() => { 
    this.authService.isLoggedIn().subscribe((isLoggedIn) => { 
      if(!isLoggedIn) { 
        this.authService.signIn(state.url).subscribe(); 
      } 
 
      isLoggedInSubject.next(isLoggedIn); 
      isLoggedInSubject.complete(); 
    }) 
  }); 
 
  return isLoggedInSubject.asObservable(); 
} 

In your application, this guard is used to protect the ‘testimonial’ create route.

The News details component has a Comments section. We will get back to the implementation of this section a later in the tutorial. What is important for the authentication at this point is that if a user is not logged in, they do not see the Submit a comment form. They only see the already submitted comments and a link to the login page. To achieve this, you use the authentication service to check if the user is logged in.

By now, the authentication for your app is all set up. Let’s move on to the write operations.

Comments

Comments component

To add a Comments form to the News details component, you create a comments component, which consists of the list of already submitted comments and a comment submit form. The comment component has two main methods:

submitComment(form: any) { 
  if(form.valid) { 
    this.creatingComment = true; 
    this.commentsService.createComment(newsItemsDataOptions, this.model, this.commentableItemId).subscribe(isCommentCreated => { 
      this.isCommentCreated = isCommentCreated; 
      this.getComments(); 
    }); 
  } 
}

SubmitComment calls the comments service and in case the comment is successfully submitted, updates the list of comments with the new comment.

Comments Service

The getComments method of the comments service returns an Observable of comments. Comments are exposed as a complex property to the content types that support comments. Therefore, the request through the SDK is a little different than the requests made so far:

getComments(contentItemsDataOptions: DataOptions, contentItemId: string, skip?: number, take?: number ): Observable<Comment[]> { 
  const commentSubject = new ReplaySubject<Comment[]>(1); 
  this.sitefinity.instance.data(contentItemsDataOptions).getSingle({ 
    key: contentItemId, 
    action: 'Comments', 
    successCb: data => { return commentSubject.next(data.value as Comment[])}, 
    failureCb: data => console.log(data) 
  }); 
  return commentSubject.asObservable(); 
} 


You call the getSingle method of the Sitefinity instance and pass the Id of the news item as key. In the action property, you specify the name of the complex property—Comments.

The createComment method calls create on the Sitefinity object and passes ‘postcomment’ as an action.

getComments(contentItemsDataOptions: DataOptions, contentItemId: string, skip?: number, take?: number ): Observable<Comment[]> { 
  const commentSubject = new ReplaySubject<Comment[]>(1); 
  this.sitefinity.instance.data(contentItemsDataOptions).getSingle({ 
    key: contentItemId, 
    action: 'Comments', 
    successCb: data => { return commentSubject.next(data.value as Comment[])}, 
    failureCb: data => console.log(data) 
  }); 
  return commentSubject.asObservable(); 
} 

Testimonials

Testimonials is a dynamic type in the Quantum project. In your app, testimonials are displayed in a carousel on the Showcases page.

Testimonials Component

The testimonials component is a carousel component, displaying all testimonials for the site. For the carousel functionality, you are using the ngx-bootstrap npm package that contains all core bootstrap components as Angular components.

The implementation of this component is similar to the implementation of the news list component.You have a Submit testimonial button, leading to the Create testimonial form. As mention earlier, the testimonial route is a protected one and is accessible to users only after authentication.

Testimonial Form

The testimonial form component is a simple form with required Author, Photo, and Quote fields. The Photo field is an input field of type file. When the user submits a valid form, the createTestimonial method of the testimonial service is called, passing the contents of the form.

Testimonial Service

The testimonial service has two methods—createTestimonial and getTestimonial. We will focus on the createTestimonial method. This method receives a testimonial object as a parameter. To create a testimonial, you first want to upload the image file. You do this by getting the ID of the library where you want to store the image, in this case you upload it to the Default library. Next, you call the upload method of the ImageService by passing the library ID and the file.

The image is uploaded with a batch operation that passes either a success, a progress, or a reject callback to the batch constructor, as well as a configuration object that contains information about the content provider and the language. Next, you call the upload method of the transaction by passing the file, a safe object URL, the content type, and an additional primitive imageProperties. In the success callback, you create a results object, containing the booleans for the status of the upload, a string property for the error message, and a result that contains the uploaded image.

 

public uploadImage(libraryId: string, imageProperties: any ): Observable<any> {
    const upload = { success: false, failure: false, result: null, errorMessage: null };
    const resultSubject = new ReplaySubject<any>(1);

    const success = (result) => {
      const { data } = result.data[0].response[0];

      if (result.isSuccessful) {
        upload.result = data;
        upload.success = true;
        resultSubject.next(upload);
        resultSubject.complete();
      } else {
        upload.failure = true;
        upload.errorMessage = data.error;
        resultSubject.next(upload);
      }
    };
    const reject = (result) => {
      upload.failure = true;
      resultSubject.next(upload);
    };

    const progress = () => {};

    const batch = this.sitefinity.instance.batch(success, reject, progress, {
      providerName: "OpenAccessDataProvider",
      cultureName: "en"
    });

    const transaction = batch.beginTransaction();
    const file = imageProperties.File || imageProperties.file;
    const url = window.URL.createObjectURL(file);
    const safeUrl = this.sanitizer.bypassSecurityTrustUrl(url);
    const imagePrimitives: ImagePrimitives = {
      ParentId: libraryId,
      DirectUpload: true,
      Height: imageProperties.height,
      Width: imageProperties.width,
      Title: imageProperties.File.name || imageProperties.file
    };

    const uploadedFile = transaction.upload({
      entitySet: "images",
      data: file,
      dataUrl: safeUrl,
      contentType: file.type,
      fileName: file.name,
      uploadProperties: imagePrimitives
    });

    transaction.operation({
      entitySet: "images",
      key: uploadedFile,
      data: {
        action: "Publish"
      }
    });
    batch.endTransaction(transaction);
    batch.execute();

    return resultSubject.asObservable();
    }

Let’s go back to the createTestimonial method. You subscribe to the uploadImage method of the Image service and once the image is uploaded and received, you create a new batch for the testimonial creation. Next, you call transcation.Create() by passing the type of the content you want to create and the primitive properties of the item.

  createTestimonial(testimonial: Testimonial): Observable<boolean> {
    const isTestimonialCreated = new ReplaySubject<boolean>(1);
    const sortedFields = this.imageService.sortFieldValues(testimonial);
    const primitiveFields = sortedFields.primitives;
    const relationalFields = sortedFields.relational;

    this.imageService.getLibraryByTitle("Default library").subscribe((library: any) => {
      const parentId = library.RootId ? library.RootId : library.Id;
      this.imageService.uploadImage(parentId, relationalFields["Photo"]).subscribe((upload => {
        if (upload.success) {
          const success = (result) => {
            if (result.isSuccessful) {
              isTestimonialCreated.next(true);
            } else {
              isTestimonialCreated.next(false);
            }
          };
          const failure = () => {
            isTestimonialCreated.next(false);
          };
          const batch = this.sitefinity.instance.batch(success, failure, { providerName: testimonialDataOptions.providerName, cultureName: testimonialDataOptions.cultureName });
          const transaction = batch.beginTransaction();
          const entitySet = "testimonials";
          const operation = { action: "Publish" };
          const testimonialItemId = transaction.create({
            entitySet,
            data: primitiveFields
          });

          this.imageService.associateRelatedImage("Photo", relationalFields["Photo"], entitySet, upload.result.Id, testimonialItemId, transaction);

          transaction.operation({
            entitySet: entitySet,
            key: testimonialItemId,
            data: operation
          });

          batch.endTransaction(transaction);
          batch.execute();
        } else {
          isTestimonialCreated.next(false);
        }
      }));
    });
    return isTestimonialCreated.asObservable();
  }

The uploaded image is a complex object, related to the testimonial though content links. To associate the image to the testimonial, you call the associateRelatedImage method of the Image service. You first destroy any existing relations for the related image field. Then, create a new relation. The createRelated method accepts an object with the following properties:

  • EntitySet
    The content type of our item—testimonials

  • Key
    The ID returned by the create transaction

  • NavigationProperty
    The name of the related Image property on the Testimonials type

  • Link
    A URL containing the type of the relatedContent (images) and the ID of the particular relatedItem (the uploaded image)


public associateRelatedImage(relationalFieldName: string, relationalField: {}, entitySet: string, itemId: any, relationId: string, transaction: any) { 
    transaction.destroyRelated({ 
      entitySet: entitySet, 
      key: relationId, 
      navigationProperty: relationalFieldName 
    }); 
    const relationLink = this.settingsService.url + endpoint + 'images(' + itemId + ')'; 
 
    transaction.createRelated({ 
      entitySet: entitySet, 
      key: relationId, 
      navigationProperty: relationalFieldName, 
      link: relationLink 
  }); 
} 

The last thing to do is end the transaction and execute the batch. As a result, if you go back to Showcases, you should see the newly create testimonial item.

We hope you found this tutorial useful. If you have any questions, please drop us a line in the comments below. If you're new to Sitefinity, you can learn more here or jump right in and start your free 30-day trial today.

Zheyna Peleva headshot

Zheyna Peleva

Jen Peleva was a Principal frontend developer for the Sitefinity CMS.

Comments

Comments are disabled in preview mode.
Topics

Sitefinity Training and Certification Now Available.

Let our experts teach you how to use Sitefinity's best-in-class features to deliver compelling digital experiences.

Learn More
Latest Stories
in Your Inbox

Subscribe to get all the news, info and tutorials you need to build better business apps and sites

Loading animation