Struggling to create a custom dashboard and an api controller with back office authentication

I was browsing though the Umbraco documentation, but sadly wasn’t able to find any useful help for my situation (maybe I didn’t search in the correct places, in that case sorry!)

We’re currently running an Umbraco 16 project for our client. We want to create a custom dashboard to trigger an import from within the Umbraco Backoffice (content section, custom dashboard was the idea).

Currently I have two significant files in my project (custom dashboard and custom backoffice api endpoint) and I was wondering if anyone can help me figure out how I can send the authentication from the logged in user to the backoffice, making it possible to trigger the custom backoffice endpoint with the logged in user to authorize the request.

Here’s my custom dashboard (using Lit):

import { LitElement, css, html } from 'lit'
import { customElement, property } from 'lit/decorators.js'

@customElement('import-dashboard')
export class ImportDashboard extends LitElement {
  @property({ type: Number })
  responseMessage = '';

  render() {
    return html`
      <div class="card">
        <button @click=${this.callApi} part="button">Start Import</button>
        <div>${this.responseMessage}</div>
      </div>
    `
  }

  private async callApi() {
    this.responseMessage = 'Import started...';
    try {
      const res = await fetch('/umbraco/backoffice/api/import/startImport', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        }
      });
      const json = await res.json();
      this.responseMessage = `Import finished: ${json}`;
    } catch(err: any) {
      this.responseMessage = `Error: ${err}`;
    }
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'import-dashboard': ImportDashboard
  }
}

And here’s my custom backoffice api endpoint:

[ApiController]
[EndpointGroupName("Import")]
[Route("/umbraco/backoffice/api/import")]
[JsonOptionsName(Constants.JsonOptionsNames.DeliveryApi)]
public class ForceManualImportController(IImportService importService, IConfiguration configuration, ILogger<ForceManualImportController> logger) : UmbracoAuthorizedController
{
    [HttpPost]
    [Route("startImport")]
    [ProducesResponseType(typeof(string), StatusCodes.Status200OK)]
    [Authorize(Roles = "admin")]
    public async Task<IActionResult> StartImport()
    {
        logger.LogInformation("Starting import");

        var importSettings = new ImportSettings
        {
            AlgoliaUpdate = true,
            ImportAdvisors = true,
            ImportCategories = true,
            ImportArrangements = true,
            ImportCourses = true,
            ImportLocations = true,
            ImportSchedules = true,
            ImportStudies = true,
            ImportTeachers = true,
            IndexLocationForStudies = true,
        };

        if (await importService.Import(importSettings))
        {
            logger.LogInformation("Import succesful");
            return Ok("Import succesful");
        }

        logger.LogWarning("Import aborted");
        return Ok("Import is aborted.");
    }
}

If I click the button on the dashboard, it keeps telling me 401 Unauthorized, which makes me think that I should add some sort of an authentication header, but I have no clue where to grab the currently logged in user information for the authentication to be completed.

Any pointers or people who have ran into the same issue and were able to fix it?

Thanks in advance!

The article you are looking for is here:

There are several examples (including Fetch) for you to peruse.

But you could also run the extensions template which has a dashboard and custom api controller example:

dotnet new umbraco-extension -ex

(The -ex argument includes the examples)

For some reason, the entrypoint.ts from the Umbraco 16 extension template for me doesn’t work for me. This works:

import { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { client } from '../api/client.gen';

import type {
    UmbEntryPointOnInit,
    UmbEntryPointOnUnload,
} from "@umbraco-cms/backoffice/extension-api";

// load up the manifests here
export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {
    // Will use only to add in Open API config with generated TS OpenAPI HTTPS Client
    // Do the OAuth token handshake stuff
    _host.consumeContext(UMB_AUTH_CONTEXT, async (authContext) => {
        if (!authContext) {
            console.error("Auth context is not defined!");
            return;
        }

        // Get the token info from Umbraco
        const config = authContext.getOpenApiConfiguration();

        client.setConfig({
            baseUrl: config.base,
            credentials: config.credentials
        });

        // For every request being made, add the token to the headers
        // Can't use the setConfig approach above as its set only once and
        // tokens expire and get refreshed
        client.interceptors.request.use(async (request, _options) => {
            const token = await config.token();
            request.headers.set('Authorization', `Bearer ${token}`);
            return request;
        });
    });
};

export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
    console.log("Goodbye from my extension 👋");
};

Did you perhaps accidentally call the token function in setConfig with parentheses (token())? It is supposed to work like this;

    client.setConfig({
      auth: config?.token ?? undefined,
      baseUrl: config?.base ?? "",
      credentials: config?.credentials ?? "same-origin",
    });

The hey-api client will then call the function when needed.

@jacob, in the Umbraco 16.0.0 dotnet template, the entrypoint.ts file looks like this:

import type {
  UmbEntryPointOnInit,
  UmbEntryPointOnUnload,
} from "@umbraco-cms/backoffice/extension-api";

// load up the manifests here
export const onInit: UmbEntryPointOnInit = (_host, _extensionRegistry) => {
  console.log("Hello from my extension 🎉");
};

export const onUnload: UmbEntryPointOnUnload = (_host, _extensionRegistry) => {
  console.log("Goodbye from my extension 👋");
};

I dunno, but to me it feels like we are missing some things here…? So I added the stuff I had in there from the Umbraco 15 template.

Thanks a lot @jacob and @LuukPeters.

The client is still unsure about running Umbraco 16. I see that these examples don’t seem to work for Umbraco 15 extensions. Is that true? And if it is, can you maybe help me in the umbraco 15 context around authentication? Would love to get this to work.

I actually use this in Umbraco 16, so this example should be a good starting point.

The idea is to generate the models and endpoints using hey-api and you don’t have to worry about authentication anymore with that endpoint. What doesn’t seem to be working?

Why is the client unsure about using Umbraco 16? It’s hardly that much different to Umbraco 15 and 15 is already in the support phase of support and will go into the security phase on august 14th. I don’t see much reason to not go to 16, except if you want to wait for the first update in case there are serious issues.

Hey @LuukPeters, I gave it a shot and it works. Thank you very much.

One question I had when I was playing around with the code was that “Example Dashboard” seems to be the label and pathname even though we’re trying to change it to “Import Dashboard”.

Do you have any idea where we can configure that title? When I have a look at the code and search within files, I only see the generated files containing the name “Example Dashboard”. I even removed the generated files and made the build generate the files again, but still the label and pathname stays as “example dashboard”

The reason why our client is hesitant is because we don’t want to rush, we want to carefully test the new Umbraco version and we’re probably going to release this during the current sprint or just afterwards.

Found the problem and fixed it. Had Visual Studio search not active on *.ts files, so the manifest.ts file wasn’t found. Eventually found where I can change the label and pathname.

Thanks again gentlemen, we have a happy client again :slight_smile:

1 Like

You have to run the template with the -ex flag to generate that extra piece of code.

Hmm, I thought I did. I’ll check tomorrow.

Glad to see that the issue here has been solved!

I thought it might be useful to drop a link in for GitHub - LottePitcher/opinionated-package-starter: Get a head start when creating Umbraco Packages as it gives a good working example for the overall structure of a Belissima extension, along with some examples for API generation, dashboards and authentication.