Merge branch 'main' into feature/document-user-permission

This commit is contained in:
Mads Rasmussen
2024-04-10 11:34:39 +02:00
committed by GitHub
279 changed files with 4590 additions and 1625 deletions

View File

@@ -27,10 +27,18 @@ Third-party licenses
---
Lucide License
ISC License
ISC License <https://lucide.dev/license>
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
---
Simple Icons
CC0 1.0 Universal license <https://creativecommons.org/publicdomain/zero/1.0/>
The person who associated a work with this deed has dedicated the work to the public domain by waiving all of his or her rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law.
You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.

View File

@@ -1,19 +0,0 @@
import { expect, test } from '@playwright/test';
test('login', async ({ page }) => {
await page.goto('/');
// Fill input[name="email"]
await page.locator('input[name="email"]').fill('test@umbraco.com');
// Fill input[name="password"]
await page.locator('input[name="password"]').fill('test123456');
// Wait for console.log to be called
page.on('console', (message) => {
expect(message.text()).toBe('login');
});
// Click [aria-label="Login"]
await page.locator('[aria-label="Login"]').click();
});

View File

@@ -1,15 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Umbraco Auth</title>
<script type="module" src="/src/index.ts"></script>
<base href="/" />
</head>
<body class="uui-font uui-text" style="margin: 0; padding: 0; overflow: hidden">
<umb-login></umb-login>
</body>
</html>

View File

@@ -1,20 +0,0 @@
{
"name": "@umbraco-cms/login",
"version": "0.0.0",
"license": "MIT",
"author": {
"name": "Umbraco HQ",
"email": "backoffice@umbraco.com"
},
"type": "module",
"module": "dist/main.js",
"files": [
"dist"
],
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:watch": "vite build --watch",
"preview": "vite preview"
}
}

View File

@@ -1,108 +0,0 @@
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig = {
testDir: './e2e',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run dev',
port: 5173,
reuseExistingServer: !process.env.CI,
},
};
export default config;

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#3544B1;}
.st1{fill:#3544B1;}
</style>
<path class="st1" d="M0,20C0,8.9,9,0,20,0s20,9,20,20s-9,20-20,20C8.9,40,0,31,0,20L0,20z M19.6,26.8c-1.6,0-3.1-0.1-4.6-0.4
c-1.1-0.2-2.1-1-2.5-2c-0.5-1-0.7-2.6-0.7-4.8c0-1.1,0.1-2.3,0.2-3.4c0.1-1.1,0.3-2,0.4-2.7l0.1-0.7c0,0,0,0,0-0.1
c0-0.2-0.1-0.4-0.3-0.4l-2.6-0.4H9.6c-0.2,0-0.4,0.1-0.4,0.3c0,0.2-0.1,0.3-0.1,0.7c-0.1,0.8-0.3,1.5-0.4,2.6
c-0.2,1.2-0.3,2.4-0.3,3.5c-0.1,0.8-0.1,1.6,0,2.5c0.1,2.2,0.4,3.9,1.1,5.2c0.7,1.3,1.9,2.2,3.5,2.8c1.6,0.6,3.9,0.9,6.9,0.8h0.4
c2.9,0,5.2-0.3,6.9-0.8c1.6-0.6,2.8-1.5,3.5-2.8c0.7-1.3,1.1-3.1,1.1-5.2c0.1-0.8,0.1-1.6,0-2.5c0-1.2-0.1-2.4-0.3-3.5
c-0.1-1.1-0.3-1.8-0.4-2.6c-0.1-0.4-0.1-0.5-0.1-0.7c0-0.2-0.2-0.3-0.4-0.3h-0.1l-2.6,0.4c-0.2,0-0.3,0.2-0.3,0.4c0,0,0,0,0,0.1
l0.1,0.7c0.1,0.7,0.3,1.6,0.4,2.7c0.1,1.1,0.2,2.3,0.2,3.4c0,2.2-0.2,3.8-0.7,4.8c-0.5,1-1.4,1.8-2.5,2c-1.5,0.3-3.1,0.5-4.6,0.4
L19.6,26.8z"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 316"><path d="M.2 157.8C.3 70.6 71.1-.1 158.3.1S316.2 71 316.1 158.2s-70.8 157.7-157.9 157.7C70.8 315.9.1 245.1.2 157.8zm154.7 54.1c-12.3.4-24.5-.7-36.5-3.3-8.8-1.8-16.3-7.8-19.9-16-3.6-8.2-5.3-20.9-5.2-38.1.1-9 .6-17.9 1.7-26.8 1-8.7 2.1-15.8 3.1-21.5l1.1-5.6v-.5c0-1.6-1.1-2.9-2.6-3.2l-20.4-3.2h-.4c-1.5 0-2.8 1-3.1 2.5-.3 1.3-.6 2.3-1.2 5.4-1.2 6-2.2 11.8-3.4 20.4-1.3 9.3-2 18.6-2.3 27.9-.4 6.5-.4 13 0 19.6.5 17.3 3.4 31.1 8.9 41.4 5.5 10.3 14.7 17.8 27.7 22.3s31.2 6.8 54.4 6.7h2.9c23.3.1 41.4-2.1 54.4-6.7 13-4.5 22.2-12 27.7-22.3s8.4-24.1 8.9-41.4c.4-6.5.4-13 0-19.6-.3-9.3-1-18.7-2.3-27.9-1.2-8.4-2.3-14.3-3.4-20.4-.6-3.1-.8-4.1-1.2-5.4-.3-1.4-1.6-2.5-3.1-2.5h-.5l-20.4 3.2c-1.6.3-2.7 1.6-2.7 3.2v.5l1.1 5.6c1 5.6 2.1 12.8 3.1 21.5 1 8.9 1.6 17.9 1.7 26.8.2 17.1-1.6 29.8-5.2 38.1-3.6 8.2-11 14.2-19.8 16.1-12 2.5-24.2 3.6-36.5 3.3l-6.6-.1zm932.3-43.9c0-30.4 8.6-51.7 43.8-51.7s43.8 21.3 43.8 51.7-8.6 51.7-43.8 51.7-43.8-21.3-43.8-51.7zm65.3 0c0-21.1-2.7-33.1-21.5-33.1s-21.5 12-21.5 33.1 2.8 33.1 21.5 33.1c18.8 0 21.5-12 21.5-33.1zm-672.1 47.8c.5.9 1.5 1.5 2.5 1.4h8.2c1.6 0 2.9-1.3 2.9-2.9v-92.7c0-1.6-1.3-2.9-2.9-2.9h-16.3c-1.6 0-2.9 1.3-2.9 2.9v73.6c-7 3.9-14.9 5.9-22.8 5.8-10.4 0-15.6-4.5-15.6-14.6v-64.8c0-1.6-1.3-2.9-2.9-2.9h-16.4c-1.6 0-2.9 1.3-2.9 2.9v66.7c0 18.9 8.9 31.3 33.9 31.3 11.4-.1 22.6-3.7 32-10.2l2.9 6.5.3-.1zm184.1-68.1c0-18.7-9.3-31.4-32.6-31.4-11.3 0-22.3 3.4-31.6 9.8-4.1-6.1-12-9.8-25.3-9.8-10.7.2-21.1 3.8-29.7 10.2l-2.9-6.5c-.5-.9-1.5-1.5-2.5-1.4h-8.3c-1.6 0-2.9 1.3-2.9 2.9v92.8c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9v-73.5c6.2-3.8 13.4-5.8 20.7-5.8 8.9 0 14 3.3 14 12.6v66.7c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9v-73.6c6.2-3.9 13.4-5.9 20.7-5.8 8.6 0 14 3.3 14 12.6v66.7c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9l.1-66.5zm50.4 61.7c9.3 6.9 20.5 10.5 32 10.2 28.8 0 39.4-19.3 39.4-51.7s-10.7-51.7-39.4-51.7c-9.4 0-18.5 2.7-26.4 7.7V94.2c0-1.6-1.2-2.9-2.8-3h-16.6c-1.6 0-2.9 1.3-2.9 2.9v120.3c0 1.6 1.3 2.9 2.9 2.9h8.2c1 0 2-.5 2.5-1.4l3.1-6.5zm26.8-8.5c-7.5 0-14.8-2-21.3-5.8v-54.4c6.5-3.8 13.8-5.8 21.3-5.8 19.3 0 22.3 14.8 22.3 32.9s-2.8 33.1-22.3 33.1zM868 135.7c-2.5-.3-5.1-.5-7.7-.5-8.8-.4-17.5 1.7-25.3 5.9v73.2c0 1.6-1.3 2.9-2.9 2.9h-16.3c-1.6 0-2.9-1.3-2.9-2.9v-92.7c0-1.6 1.3-2.9 2.9-2.9h8.2c1 0 2 .5 2.5 1.4l2.9 6.5c8.9-6.8 19.9-10.4 31.2-10.2 2.6 0 5.2.2 7.7.6 1.4 0 2.7 2.4 2.7 4v11.8c0 1.6-1.3 2.9-2.9 2.9h-.2m56.7 36.1c-9.8 1.2-15.6 4.9-15.6 15.2 0 7.5 3.3 14.6 15.2 14.6 7.5.1 14.9-2.2 21.1-6.5v-25.5l-20.7 2.2zm26.1 37.6c-8.5 6.7-19 10.3-29.8 10.2-25.5 0-33.9-15.8-33.9-31.6 0-21.3 13.8-30.4 36.1-32.1l22.1-1.8v-4.9c0-10.1-4.7-14-19.3-14-9.2 0-18.3 1.5-26.9 4.5h-.9c-1.6 0-2.9-1.3-2.9-2.9v-13.1c0-1.2.7-2.4 1.9-2.8 9.8-3.3 20.1-5 30.5-4.9 32.3 0 39.8 14.2 39.8 35.1V214c0 1.6-1.3 2.9-2.9 2.9h-8.2c-1 0-2-.5-2.5-1.4l-3.1-6.1zM1063 197h.9c1.6 0 2.9 1.3 2.9 2.9V213c0 1.2-.7 2.3-1.8 2.7-8.1 2.9-16.7 4.3-25.4 4.1-34.9 0-45.7-20.9-45.7-51.7s10.7-51.7 45.7-51.7c8.6-.2 17.1 1.1 25.2 4 1.1.4 1.9 1.5 1.8 2.7v13.1c0 1.6-1.3 2.9-2.9 2.9h-.9c-7.1-2.2-14.6-3.3-22-3.1-19.1 0-24.7 13.1-24.7 32.2s5.5 32.1 24.7 32.1c7.5.1 14.9-1 22-3.3" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 315.89 315.89"><path fill="#fff" d="M0 157.74a157.95 157.95 0 11158 158.15A157.95 157.95 0 010 157.74zm154.74 54.09a155.41 155.41 0 01-36.5-3.29 27.92 27.92 0 01-19.94-16q-5.35-12.34-5.21-38.1a243 243 0 011.69-26.84q1.55-13 3.09-21.46l1.07-5.59a2 2 0 000-.49 3.2 3.2 0 00-2.65-3.17l-20.37-3.22h-.44a3.19 3.19 0 00-3.11 2.48c-.35 1.31-.56 2.27-1.17 5.38-1.16 6-2.24 11.85-3.43 20.38a264.17 264.17 0 00-2.3 27.94 145.24 145.24 0 000 19.57q.72 25.94 8.9 41.42t27.72 22.3q19.53 6.81 54.43 6.66h2.91q34.94.15 54.41-6.66t27.71-22.3q8.17-15.53 8.91-41.42a145.24 145.24 0 000-19.57 266.84 266.84 0 00-2.3-27.94c-1.2-8.44-2.27-14.26-3.44-20.38-.61-3.11-.81-4.07-1.16-5.38a3.21 3.21 0 00-3.12-2.48h-.52l-20.38 3.18a3.2 3.2 0 00-2.68 3.17 4 4 0 000 .49l1.08 5.59q1.55 8.48 3.12 21.46a245.68 245.68 0 011.65 26.84q.27 25.69-5.21 38.07a27.9 27.9 0 01-19.76 16.07 155.19 155.19 0 01-36.48 3.29z"/></svg>

Before

Width:  |  Height:  |  Size: 942 B

View File

@@ -1,72 +0,0 @@
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('umb-auth-layout')
export class UmbAuthLayoutElement extends LitElement {
render() {
return html`
<div id="background"></div>
<div id="logo">
<img src="umbraco_logomark_white.svg" alt="Umbraco" />
</div>
<div id="container">
<uui-box id="box">
<slot></slot>
</uui-box>
</div>
`;
}
static styles: CSSResultGroup = [
css`
#background {
position: fixed;
overflow: hidden;
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
background-image: url('login.jpeg');
width: 100vw;
height: 100vh;
}
#logo {
position: fixed;
top: var(--uui-size-space-5);
left: var(--uui-size-space-5);
height: 30px;
}
#logo img {
height: 100%;
}
#container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
}
#box {
width: 500px;
padding: var(--uui-size-space-6) var(--uui-size-space-5) var(--uui-size-space-5) var(--uui-size-space-5);
}
#email,
#password {
width: 100%;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-auth-layout': UmbAuthLayoutElement;
}
}

View File

@@ -1,10 +0,0 @@
import { LoginRequestModel, IUmbAuthContext, LoginResponse } from './types.js';
export class UmbAuthLegacyContext implements IUmbAuthContext {
readonly #AUTH_URL = '/umbraco/backoffice/umbracoapi/authentication/postlogin';
readonly supportsPersistLogin = true;
login(data: LoginRequestModel): Promise<LoginResponse> {
throw new Error('Method not implemented.');
}
}

View File

@@ -1,13 +0,0 @@
import { UmbAuthRepository } from './auth.repository.js';
import { LoginRequestModel, IUmbAuthContext } from './types.js';
export class UmbAuthContext implements IUmbAuthContext {
readonly #AUTH_URL = '/umbraco/management/api/v1/security/back-office';
readonly supportsPersistLogin = false;
#authRepository = new UmbAuthRepository(this.#AUTH_URL);
public async login(data: LoginRequestModel) {
return this.#authRepository.login(data);
}
}

View File

@@ -1,44 +0,0 @@
import { LoginRequestModel } from './types.js';
export class UmbAuthRepository {
#authURL = '';
constructor(authUrl: string) {
this.#authURL = authUrl;
}
public async login(data: LoginRequestModel) {
const request = new Request(this.#authURL + '/login', {
method: 'POST',
body: JSON.stringify({
username: data.username,
password: data.password,
}),
headers: {
'Content-Type': 'application/json',
},
});
const response = await fetch(request);
//TODO: What kind of data does the old backoffice expect?
//NOTE: this conditionally adds error and data to the response object
return {
status: response.status,
...(!response.ok && { error: this.#getErrorText(response) }),
...(response.ok && { data: 'WHAT DATA SHOULD BE RETURNED?' }),
};
}
#getErrorText(response: Response) {
switch (response.status) {
case 401:
return 'Oops! It seems like your login credentials are invalid or expired. Please double-check your username and password and try again.';
case 500:
return "We're sorry, but the server encountered an unexpected error. Please refresh the page or try again later..";
default:
return response.statusText;
}
}
}

View File

@@ -1,40 +0,0 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { customElement } from 'lit/decorators.js';
@customElement('umb-external-login-provider-test')
export class UmbExternalLoginProviderTestElement extends LitElement {
render() {
return html`
<b>Custom External Login Provider</b>
<p>This is an example of a custom external login provider using the external login provider extension point</p>
<uui-button label="My custom login provider" look="primary"></uui-button>
`;
}
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
padding: var(--uui-size-space-5);
border: 1px solid var(--uui-color-border);
background: var(--uui-color-surface-alt);
border-radius: var(--uui-border-radius);
}
p {
margin: 0;
}
`,
];
}
export default UmbExternalLoginProviderTestElement;
declare global {
interface HTMLElementTagNameMap {
'umb-external-login-provider-test': UmbExternalLoginProviderTestElement;
}
}

View File

@@ -1,53 +0,0 @@
import { css, html, LitElement } from 'lit';
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { customElement } from 'lit/decorators.js';
@customElement('umb-external-login-provider-test2')
export class UmbExternalLoginProviderTest2Element extends LitElement {
render() {
return html`
<b>Another Custom External Login Provider</b>
<p>This is an example of another custom external login provider</p>
<uui-form-layout-item>
<uui-label id="emailLabel" for="email" slot="label" required>Email</uui-label>
<uui-input
type="email"
id="email"
name="email"
placeholder="Enter your email..."
required
required-message="Email is required"></uui-input>
</uui-form-layout-item>
<uui-button label="Custom login" look="primary"></uui-button>
`;
}
static styles = [
UUITextStyles,
css`
:host {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-4);
padding: var(--uui-size-space-5);
border: 1px solid var(--uui-color-border);
background: var(--uui-color-surface-alt);
border-radius: var(--uui-border-radius);
}
p {
margin: 0;
}
uui-input {
width: 100%;
}
`,
];
}
export default UmbExternalLoginProviderTest2Element;
declare global {
interface HTMLElementTagNameMap {
'umb-external-login-provider-test2': UmbExternalLoginProviderTest2Element;
}
}

View File

@@ -1,29 +0,0 @@
// TODO: could these be renamed as login providers?
import type { ManifestExternalLoginProvider } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestExternalLoginProvider> = [
{
type: 'externalLoginProvider',
alias: 'Umb.ExternalLoginProvider.Test',
name: 'Test External Login Provider',
elementName: 'umb-external-login-provider-test',
element: () => import('./external-login-provider-test.element.js'),
weight: 2,
meta: {
label: 'Test External Login Provider',
pathname: 'test/test/test',
},
},
{
type: 'externalLoginProvider',
alias: 'Umb.ExternalLoginProvider.Test2',
name: 'Test External Login Provider 2',
elementName: 'umb-external-login-provider-test2',
element: () => import('./external-login-provider-test2.element.js'),
weight: 1,
meta: {
label: 'Test External Login Provider 2',
pathname: 'test/test/test',
},
},
];

View File

@@ -1,7 +0,0 @@
import 'element-internals-polyfill';
import '@umbraco-ui/uui-css/dist/uui-css.css';
import '../../../src/css/umb-css.css';
import '@umbraco-ui/uui';
import './login.element.js';

View File

@@ -1,148 +0,0 @@
import { UUITextStyles } from '@umbraco-ui/uui-css';
import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import type { UUIButtonState } from '@umbraco-ui/uui';
import type { IUmbAuthContext } from './types.js';
import { UmbAuthLegacyContext } from './auth-legacy.context.js';
import { UmbAuthContext } from './auth.context.js';
import './auth-layout.element.js';
@customElement('umb-login')
export default class UmbLoginElement extends LitElement {
#authContext: IUmbAuthContext;
@property({ type: String, attribute: 'return-url' })
returnUrl = '';
@property({ type: Boolean })
isLegacy = false;
@state()
private _loginState: UUIButtonState = undefined;
@state()
private _loginError = '';
constructor() {
super();
if (this.isLegacy) {
this.#authContext = new UmbAuthLegacyContext();
} else {
this.#authContext = new UmbAuthContext();
}
}
#handleSubmit = async (e: SubmitEvent) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
if (!form) return;
if (!form.checkValidity()) return;
const formData = new FormData(form);
const username = formData.get('email') as string;
const password = formData.get('password') as string;
const persist = formData.has('persist');
this._loginState = 'waiting';
const response = await this.#authContext.login({ username, password, persist });
this._loginError = response.error || '';
this._loginState = response.error ? 'failed' : 'success';
if (response.error) return;
location.href = this.returnUrl;
};
get #greeting() {
return [
'Happy super Sunday',
'Happy marvelous Monday',
'Happy tubular Tuesday',
'Happy wonderful Wednesday',
'Happy thunderous Thursday',
'Happy funky Friday',
'Happy Saturday',
][new Date().getDay()];
}
render() {
return html`
<umb-auth-layout>
<div class="uui-text">
<h1 class="uui-h3">${this.#greeting}</h1>
<uui-form>
<form id="LoginForm" name="login" @submit="${this.#handleSubmit}">
<uui-form-layout-item>
<uui-label id="emailLabel" for="email" slot="label" required>Email</uui-label>
<uui-input
type="email"
id="email"
name="email"
label="Email"
required
required-message="Email is required"></uui-input>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-label id="passwordLabel" for="password" slot="label" required>Password</uui-label>
<uui-input-password
id="password"
name="password"
label="Password"
required
required-message="Password is required"></uui-input-password>
</uui-form-layout-item>
${this.#authContext.supportsPersistLogin
? html`<uui-form-layout-item>
<uui-checkbox name="persist" label="Remember me">Remember me</uui-checkbox>
</uui-form-layout-item>`
: nothing}
<uui-form-layout-item>${this.#renderErrorMessage()}</uui-form-layout-item>
<uui-button
type="submit"
label="Login"
look="primary"
color="positive"
state=${this._loginState}></uui-button>
</form>
</uui-form>
</div>
</umb-auth-layout>
`;
}
#renderErrorMessage() {
if (!this._loginError || this._loginState !== 'failed') return nothing;
return html`<p class="text-danger">${this._loginError}</p>`;
}
static styles: CSSResultGroup = [
UUITextStyles,
css`
#email,
#password {
width: 100%;
}
.text-danger {
color: var(--uui-color-danger-standalone);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-login': UmbLoginElement;
}
}

View File

@@ -1,21 +0,0 @@
import { expect, fixture, html } from '@open-wc/testing';
import { type UmbTestRunnerWindow, defaultA11yConfig } from '@umbraco-cms/internal/test-utils';
import UmbLoginElement from './login.element.js';
describe('UmbLogin', () => {
let element: UmbLoginElement;
beforeEach(async () => {
element = await fixture(html`<umb-login></umb-login>`);
});
it('is defined with its own instance', () => {
expect(element).to.be.instanceOf(UmbLoginElement);
});
if ((window as UmbTestRunnerWindow).__UMBRACO_TEST_RUN_A11Y_TEST) {
it('passes the a11y audit', async () => {
await expect(element).to.be.accessible(defaultA11yConfig);
});
}
});

View File

@@ -1,16 +0,0 @@
export type LoginRequestModel = {
username: string;
password: string;
persist: boolean;
};
export interface IUmbAuthContext {
login(data: LoginRequestModel): Promise<LoginResponse>;
supportsPersistLogin: boolean;
}
export type LoginResponse = {
data?: string;
error?: string;
status: number;
};

View File

@@ -1,6 +0,0 @@
/// <reference types="vite/client" />
/*
interface ImportMetaEnv {
}
*/

View File

@@ -1,12 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": false
},
"include": ["src/**/*.ts"],
"references": [
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -1,9 +0,0 @@
{
"compilerOptions": {
"composite": true,
"module": "esnext",
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,32 +0,0 @@
import { defineConfig } from 'vite';
import viteTSConfigPaths from 'vite-tsconfig-paths';
// https://vitejs.dev/config/
export default defineConfig({
build: {
lib: {
entry: 'src/index.ts',
formats: ['es'],
fileName: 'main',
},
target: 'esnext',
sourcemap: true,
rollupOptions: {
external: [/^@umbraco-cms\/backoffice\//],
output: {
manualChunks: {
uui: ['@umbraco-ui/uui'],
},
},
},
outDir: '../../../Umbraco.Cms.StaticAssets/wwwroot/umbraco/auth',
emptyOutDir: true,
},
server: {
fs: {
// Allow serving files from the global node_modules folder
allow: ['.', '../../node_modules'],
},
},
plugins: [viteTSConfigPaths()],
});

View File

@@ -6,12 +6,13 @@ const path = pathModule.default;
const getDirName = path.dirname;
const glob = globModule.default;
const moduleDirectory = 'src/shared/icon-registry';
const moduleDirectory = 'src/packages/core/icon-registry';
const iconsOutputDirectory = `${moduleDirectory}/icons`;
const umbracoSvgDirectory = `${moduleDirectory}/svgs`;
const iconMapJson = `${moduleDirectory}/icon-dictionary.json`;
const lucideSvgDirectory = 'node_modules/lucide-static/icons';
const simpleIconsSvgDirectory = 'node_modules/simple-icons/icons';
const run = async () => {
var icons = await collectDictionaryIcons();
@@ -30,7 +31,7 @@ const collectDictionaryIcons = async () => {
// Lucide:
fileJSON.lucide.forEach((iconDef) => {
if(iconDef.file && iconDef.name) {
if (iconDef.file && iconDef.name) {
const path = lucideSvgDirectory + "/" + iconDef.file;
try {
@@ -50,15 +51,46 @@ const collectDictionaryIcons = async () => {
};
icons.push(icon);
} catch(e) {
console.log(`Could not load file: '${path}'`);
} catch (e) {
console.log(`[Lucide] Could not load file: '${path}'`);
}
}
});
// SimpleIcons:
fileJSON.simpleIcons.forEach((iconDef) => {
if (iconDef.file && iconDef.name) {
const path = simpleIconsSvgDirectory + "/" + iconDef.file;
try {
const rawData = readFileSync(path);
let svg = rawData.toString()
const iconFileName = iconDef.name;
// SimpleIcons need to use fill="currentColor"
const pattern = /fill=/g;
if (!pattern.test(svg)) {
svg = svg.replace(/<path/g, '<path fill="currentColor"');
}
const icon = {
name: iconDef.name,
legacy: iconDef.legacy,
fileName: iconFileName,
svg,
output: `${iconsOutputDirectory}/${iconFileName}.js`,
};
icons.push(icon);
} catch (e) {
console.log(`[SimpleIcons] Could not load file: '${path}'`);
}
}
});
// Umbraco:
fileJSON.umbraco.forEach((iconDef) => {
if(iconDef.file && iconDef.name) {
if (iconDef.file && iconDef.name) {
const path = umbracoSvgDirectory + "/" + iconDef.file;
try {
@@ -75,8 +107,8 @@ const collectDictionaryIcons = async () => {
};
icons.push(icon);
} catch(e) {
console.log(`Could not load file: '${path}'`);
} catch (e) {
console.log(`[Umbraco] Could not load file: '${path}'`);
}
}
});
@@ -104,7 +136,7 @@ const collectDiskIcons = async (icons) => {
const iconName = iconFileName;
// Only append not already defined icons:
if(!icons.find(x => x.name === iconName)) {
if (!icons.find(x => x.name === iconName)) {
const icon = {
name: iconName,

File diff suppressed because it is too large Load Diff

View File

@@ -62,6 +62,7 @@
"./property-action": "./dist-cms/packages/core/property-action/index.js",
"./property-editor": "./dist-cms/packages/core/property-editor/index.js",
"./property": "./dist-cms/packages/core/property/index.js",
"./recycle-bin": "./dist-cms/packages/core/recycle-bin/index.js",
"./relation-type": "./dist-cms/packages/relations/relation-types/index.js",
"./relations": "./dist-cms/packages/relations/relations/index.js",
"./repository": "./dist-cms/packages/core/repository/index.js",
@@ -89,6 +90,7 @@
"./webhook": "./dist-cms/packages/webhook/index.js",
"./workspace": "./dist-cms/packages/core/workspace/index.js",
"./external/backend-api": "./dist-cms/external/backend-api/index.js",
"./external/base64-js": "./dist-cms/external/base64-js/index.js",
"./external/dompurify": "./dist-cms/external/dompurify/index.js",
"./external/lit": "./dist-cms/external/lit/index.js",
"./external/marked": "./dist-cms/external/marked/index.js",
@@ -118,7 +120,6 @@
"url": "https://umbraco.com"
},
"scripts": {
"auth:test:e2e": "npx playwright test --config apps/auth/",
"backoffice:test:e2e": "npx playwright test",
"build-storybook": "npm run wc-analyze && storybook build",
"build:for:cms": "npm run build && node ./devops/build/copy-to-cms.js",
@@ -147,7 +148,7 @@
"preview": "vite preview --open",
"storybook:build": "npm run wc-analyze && storybook build",
"storybook": "npm run wc-analyze && storybook dev -p 6006",
"test:e2e": "npm run auth:test:e2e && npm run backoffice:test:e2e",
"test:e2e": "npm run backoffice:test:e2e",
"test:dev": "web-test-runner --config ./web-test-runner.dev.config.mjs",
"test:dev-watch": "web-test-runner --watch --config ./web-test-runner.dev.config.mjs",
"test:watch": "web-test-runner --watch",
@@ -164,11 +165,11 @@
"npm": ">=10.1 < 11"
},
"dependencies": {
"@openid/appauth": "^1.3.1",
"@types/dompurify": "^3.0.5",
"@types/uuid": "^9.0.8",
"@umbraco-ui/uui": "1.7.2",
"@umbraco-ui/uui-css": "1.7.2",
"base64-js": "^1.5.1",
"dompurify": "^3.0.9",
"element-internals-polyfill": "^1.3.10",
"lit": "^3.1.2",
@@ -199,10 +200,10 @@
"@types/mocha": "^10.0.1",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^7.1.0",
"@web/dev-server-esbuild": "^1.0.1",
"@web/dev-server-esbuild": "^1.0.2",
"@web/dev-server-import-maps": "^0.2.0",
"@web/dev-server-rollup": "^0.6.1",
"@web/test-runner": "^0.18.0",
"@web/test-runner": "^0.18.1",
"@web/test-runner-playwright": "^0.11.0",
"babel-loader": "^9.1.3",
"eslint": "^8.56.0",
@@ -223,10 +224,11 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remark-gfm": "^3.0.1",
"rollup": "^4.12.0",
"rollup-plugin-esbuild": "^6.1.0",
"rollup-plugin-import-css": "^3.4.0",
"rollup": "^4.14.1",
"rollup-plugin-esbuild": "^6.1.1",
"rollup-plugin-import-css": "^3.5.0",
"rollup-plugin-web-worker-loader": "^1.6.1",
"simple-icons": "^11.11.0",
"storybook": "^7.6.17",
"tiny-glob": "^0.2.9",
"tsc-alias": "^1.8.8",

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 316"><path d="M.2 157.8C.3 70.6 71.1-.1 158.3.1S316.2 71 316.1 158.2s-70.8 157.7-157.9 157.7C70.8 315.9.1 245.1.2 157.8zm154.7 54.1c-12.3.4-24.5-.7-36.5-3.3-8.8-1.8-16.3-7.8-19.9-16-3.6-8.2-5.3-20.9-5.2-38.1.1-9 .6-17.9 1.7-26.8 1-8.7 2.1-15.8 3.1-21.5l1.1-5.6v-.5c0-1.6-1.1-2.9-2.6-3.2l-20.4-3.2h-.4c-1.5 0-2.8 1-3.1 2.5-.3 1.3-.6 2.3-1.2 5.4-1.2 6-2.2 11.8-3.4 20.4-1.3 9.3-2 18.6-2.3 27.9-.4 6.5-.4 13 0 19.6.5 17.3 3.4 31.1 8.9 41.4 5.5 10.3 14.7 17.8 27.7 22.3s31.2 6.8 54.4 6.7h2.9c23.3.1 41.4-2.1 54.4-6.7 13-4.5 22.2-12 27.7-22.3s8.4-24.1 8.9-41.4c.4-6.5.4-13 0-19.6-.3-9.3-1-18.7-2.3-27.9-1.2-8.4-2.3-14.3-3.4-20.4-.6-3.1-.8-4.1-1.2-5.4-.3-1.4-1.6-2.5-3.1-2.5h-.5l-20.4 3.2c-1.6.3-2.7 1.6-2.7 3.2v.5l1.1 5.6c1 5.6 2.1 12.8 3.1 21.5 1 8.9 1.6 17.9 1.7 26.8.2 17.1-1.6 29.8-5.2 38.1-3.6 8.2-11 14.2-19.8 16.1-12 2.5-24.2 3.6-36.5 3.3l-6.6-.1zm932.3-43.9c0-30.4 8.6-51.7 43.8-51.7s43.8 21.3 43.8 51.7-8.6 51.7-43.8 51.7-43.8-21.3-43.8-51.7zm65.3 0c0-21.1-2.7-33.1-21.5-33.1s-21.5 12-21.5 33.1 2.8 33.1 21.5 33.1c18.8 0 21.5-12 21.5-33.1zm-672.1 47.8c.5.9 1.5 1.5 2.5 1.4h8.2c1.6 0 2.9-1.3 2.9-2.9v-92.7c0-1.6-1.3-2.9-2.9-2.9h-16.3c-1.6 0-2.9 1.3-2.9 2.9v73.6c-7 3.9-14.9 5.9-22.8 5.8-10.4 0-15.6-4.5-15.6-14.6v-64.8c0-1.6-1.3-2.9-2.9-2.9h-16.4c-1.6 0-2.9 1.3-2.9 2.9v66.7c0 18.9 8.9 31.3 33.9 31.3 11.4-.1 22.6-3.7 32-10.2l2.9 6.5.3-.1zm184.1-68.1c0-18.7-9.3-31.4-32.6-31.4-11.3 0-22.3 3.4-31.6 9.8-4.1-6.1-12-9.8-25.3-9.8-10.7.2-21.1 3.8-29.7 10.2l-2.9-6.5c-.5-.9-1.5-1.5-2.5-1.4h-8.3c-1.6 0-2.9 1.3-2.9 2.9v92.8c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9v-73.5c6.2-3.8 13.4-5.8 20.7-5.8 8.9 0 14 3.3 14 12.6v66.7c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9v-73.6c6.2-3.9 13.4-5.9 20.7-5.8 8.6 0 14 3.3 14 12.6v66.7c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9l.1-66.5zm50.4 61.7c9.3 6.9 20.5 10.5 32 10.2 28.8 0 39.4-19.3 39.4-51.7s-10.7-51.7-39.4-51.7c-9.4 0-18.5 2.7-26.4 7.7V94.2c0-1.6-1.2-2.9-2.8-3h-16.6c-1.6 0-2.9 1.3-2.9 2.9v120.3c0 1.6 1.3 2.9 2.9 2.9h8.2c1 0 2-.5 2.5-1.4l3.1-6.5zm26.8-8.5c-7.5 0-14.8-2-21.3-5.8v-54.4c6.5-3.8 13.8-5.8 21.3-5.8 19.3 0 22.3 14.8 22.3 32.9s-2.8 33.1-22.3 33.1zM868 135.7c-2.5-.3-5.1-.5-7.7-.5-8.8-.4-17.5 1.7-25.3 5.9v73.2c0 1.6-1.3 2.9-2.9 2.9h-16.3c-1.6 0-2.9-1.3-2.9-2.9v-92.7c0-1.6 1.3-2.9 2.9-2.9h8.2c1 0 2 .5 2.5 1.4l2.9 6.5c8.9-6.8 19.9-10.4 31.2-10.2 2.6 0 5.2.2 7.7.6 1.4 0 2.7 2.4 2.7 4v11.8c0 1.6-1.3 2.9-2.9 2.9h-.2m56.7 36.1c-9.8 1.2-15.6 4.9-15.6 15.2 0 7.5 3.3 14.6 15.2 14.6 7.5.1 14.9-2.2 21.1-6.5v-25.5l-20.7 2.2zm26.1 37.6c-8.5 6.7-19 10.3-29.8 10.2-25.5 0-33.9-15.8-33.9-31.6 0-21.3 13.8-30.4 36.1-32.1l22.1-1.8v-4.9c0-10.1-4.7-14-19.3-14-9.2 0-18.3 1.5-26.9 4.5h-.9c-1.6 0-2.9-1.3-2.9-2.9v-13.1c0-1.2.7-2.4 1.9-2.8 9.8-3.3 20.1-5 30.5-4.9 32.3 0 39.8 14.2 39.8 35.1V214c0 1.6-1.3 2.9-2.9 2.9h-8.2c-1 0-2-.5-2.5-1.4l-3.1-6.1zM1063 197h.9c1.6 0 2.9 1.3 2.9 2.9V213c0 1.2-.7 2.3-1.8 2.7-8.1 2.9-16.7 4.3-25.4 4.1-34.9 0-45.7-20.9-45.7-51.7s10.7-51.7 45.7-51.7c8.6-.2 17.1 1.1 25.2 4 1.1.4 1.9 1.5 1.8 2.7v13.1c0 1.6-1.3 2.9-2.9 2.9h-.9c-7.1-2.2-14.6-3.3-22-3.1-19.1 0-24.7 13.1-24.7 32.2s5.5 32.1 24.7 32.1c7.5.1 14.9-1 22-3.3" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,171 @@
import {
UMB_AUTH_CONTEXT,
UMB_MODAL_APP_AUTH,
UMB_STORAGE_REDIRECT_URL,
type UmbUserLoginState,
} from '@umbraco-cms/backoffice/auth';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { filter, firstValueFrom, skip } from '@umbraco-cms/backoffice/external/rxjs';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { UMB_MODAL_MANAGER_CONTEXT } from '@umbraco-cms/backoffice/modal';
import type { ManifestAuthProvider } from '@umbraco-cms/backoffice/extension-registry';
export class UmbAppAuthController extends UmbControllerBase {
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#firstTimeLoggingIn = true;
constructor(host: UmbControllerHost) {
super(host);
this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
this.#authContext = context;
// Observe the user's authorization state and start the authorization flow if the user is not authorized
this.observe(
context.isAuthorized.pipe(
// Skip the first since it is always false
skip(1),
// Only continue if the value is false
filter((x) => !x),
),
() => {
this.#firstTimeLoggingIn = false;
this.makeAuthorizationRequest('timedOut');
},
'_authState',
);
});
}
/**
* Checks if the user is authorized.
* If not, the authorization flow is started.
*/
async isAuthorized(): Promise<boolean> {
if (!this.#authContext) {
throw new Error('[Fatal] Auth context is not available');
}
const isAuthorized = this.#authContext.getIsAuthorized();
if (isAuthorized) {
return true;
}
// Make a request to the auth server to start the auth flow
return this.makeAuthorizationRequest();
}
/**
* Starts the authorization flow.
* It will check which providers are available and either redirect directly to the provider or show a provider selection screen.
*/
async makeAuthorizationRequest(userLoginState: UmbUserLoginState = 'loggingIn'): Promise<boolean> {
if (!this.#authContext) {
throw new Error('[Fatal] Auth context is not available');
}
// Save location.href so we can redirect to it after login
if (location.href !== this.#authContext.getPostLogoutRedirectUrl()) {
window.sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, location.href);
}
// Figure out which providers are available
const availableProviders = await firstValueFrom(this.#authContext.getAuthProviders());
// If the user is timed out, we can show the login modal directly
if (userLoginState === 'timedOut') {
const selected = await this.#showLoginModal(userLoginState, availableProviders);
if (!selected) {
return false;
}
return this.#updateState();
}
if (availableProviders.length === 0) {
throw new Error('[Fatal] No auth providers available');
}
if (availableProviders.length === 1) {
// One provider available (most likely the Umbraco provider), so initiate the authorization request to the default provider
this.#authContext.makeAuthorizationRequest(availableProviders[0].forProviderName);
return this.#updateState();
}
// Check if any provider is redirecting directly to the provider
const redirectProvider =
userLoginState === 'loggingIn'
? availableProviders.find((provider) => provider.meta?.behavior?.autoRedirect)
: undefined;
if (redirectProvider) {
// Redirect directly to the provider
this.#authContext.makeAuthorizationRequest(redirectProvider.forProviderName);
return this.#updateState();
}
// Show the provider selection screen
const selected = await this.#showLoginModal(userLoginState, availableProviders);
if (!selected) {
return false;
}
return this.#updateState();
}
async #showLoginModal(
userLoginState: UmbUserLoginState,
availableProviders: Array<ManifestAuthProvider>,
): Promise<boolean> {
if (!this.#authContext) {
throw new Error('[Fatal] Auth context is not available');
}
// Check if any provider denies local login
const denyLocalLogin = availableProviders.some((provider) => provider.meta?.behavior?.denyLocalLogin);
if (denyLocalLogin) {
// Unregister the Umbraco provider
umbExtensionsRegistry.unregister('Umb.AuthProviders.Umbraco');
}
// Show the provider selection screen
const modalManager = await this.getContext(UMB_MODAL_MANAGER_CONTEXT);
const selected = await modalManager
.open(this._host, UMB_MODAL_APP_AUTH, {
data: {
userLoginState,
},
modal: {
backdropBackground: this.#firstTimeLoggingIn
? 'var(--umb-auth-backdrop, url("/umbraco/backoffice/assets/umbraco_logo_white.svg") 20px 20px / 200px no-repeat, radial-gradient(circle, rgba(2,0,36,1) 0%, rgba(40,58,151,.9) 50%, rgba(0,212,255,1) 100%))'
: 'var(--umb-auth-backdrop-timedout, rgba(0,0,0,0.75))',
},
})
.onSubmit()
.catch(() => undefined);
if (!selected?.providerName) {
return false;
}
this.#authContext.makeAuthorizationRequest(selected.providerName, selected.loginHint);
return true;
}
#updateState() {
if (!this.#authContext) {
throw new Error('[Fatal] Auth context is not available');
}
// Reinitialize the auth flow (load the state from local storage)
this.#authContext.setInitialState();
// The authorization flow is finished, so let the caller know if the user is authorized
return this.#authContext.getIsAuthorized();
}
}

View File

@@ -1,8 +1,10 @@
import { onInit } from '../../packages/core/entry-point.js';
import type { UmbAppErrorElement } from './app-error.element.js';
import { UmbAppContext } from './app.context.js';
import { UmbServerConnection } from './server-connection.js';
import { UmbAppAuthController } from './app-auth.controller.js';
import type { UMB_AUTH_CONTEXT } from '@umbraco-cms/backoffice/auth';
import { UMB_STORAGE_REDIRECT_URL, UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { UmbAuthContext } from '@umbraco-cms/backoffice/auth';
import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UUIIconRegistryEssential } from '@umbraco-cms/backoffice/external/uui';
import { UmbIconRegistry } from '@umbraco-cms/backoffice/icon';
@@ -11,6 +13,8 @@ import type { Guard, UmbRoute } from '@umbraco-cms/backoffice/router';
import { pathWithoutBasePath } from '@umbraco-cms/backoffice/router';
import { OpenAPI, RuntimeLevelModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbContextDebugController } from '@umbraco-cms/backoffice/debug';
import { UmbServerExtensionRegistrator } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
@customElement('umb-app')
export class UmbAppElement extends UmbLitElement {
@@ -21,7 +25,12 @@ export class UmbAppElement extends UmbLitElement {
* @remarks This is the base URL of the Umbraco server, not the base URL of the backoffice.
*/
@property({ type: String })
serverUrl = window.location.origin;
set serverUrl(url: string) {
OpenAPI.BASE = url;
}
get serverUrl() {
return OpenAPI.BASE;
}
/**
* The base path of the backoffice.
@@ -29,7 +38,6 @@ export class UmbAppElement extends UmbLitElement {
* @attr
*/
@property({ type: String })
// TODO: get from base element or maybe move to UmbAuthContext.#getRedirectUrl since it is only used there
backofficePath = '/umbraco';
/**
@@ -48,6 +56,13 @@ export class UmbAppElement extends UmbLitElement {
component: () => import('../upgrader/upgrader.element.js'),
guards: [this.#isAuthorizedGuard()],
},
{
path: 'logout',
resolve: () => {
this.#authContext?.clearTokenStorage();
this.#authController.makeAuthorizationRequest('loggedOut');
},
},
{
path: '**',
component: () => import('../backoffice/backoffice.element.js'),
@@ -56,17 +71,18 @@ export class UmbAppElement extends UmbLitElement {
];
#authContext?: typeof UMB_AUTH_CONTEXT.TYPE;
#umbIconRegistry = new UmbIconRegistry();
#uuiIconRegistry = new UUIIconRegistryEssential();
#serverConnection?: UmbServerConnection;
#authController = new UmbAppAuthController(this);
constructor() {
super();
new UmbContextDebugController(this);
OpenAPI.BASE = window.location.origin;
this.#umbIconRegistry.attach(this);
this.#uuiIconRegistry.attach(this);
new UmbIconRegistry().attach(this);
new UUIIconRegistryEssential().attach(this);
new UmbContextDebugController(this);
}
connectedCallback(): void {
@@ -75,19 +91,17 @@ export class UmbAppElement extends UmbLitElement {
}
async #setup() {
if (this.serverUrl === undefined) throw new Error('No serverUrl provided');
/* All requests to the server requires the base URL to be set.
We make sure it happens before we get the server status.
TODO: find the right place to set this
*/
OpenAPI.BASE = this.serverUrl;
this.#serverConnection = await new UmbServerConnection(this.serverUrl).connect();
this.#authContext = new UmbAuthContext(this, this.serverUrl, this.backofficePath, this.bypassAuth);
new UmbAppContext(this, { backofficePath: this.backofficePath, serverUrl: this.serverUrl });
// Register Core extensions (this is specifically done here because we need these extensions to be registered before the application is initialized)
onInit(this, umbExtensionsRegistry);
// Register public extensions
await new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPublicExtensions();
// Try to initialise the auth flow and get the runtime status
try {
// If the runtime level is "install" we should clear any cached tokens
@@ -136,7 +150,6 @@ export class UmbAppElement extends UmbLitElement {
// Instruct all requests to use the auth flow to get and use the access_token for all subsequent requests
OpenAPI.TOKEN = () => this.#authContext!.getLatestToken();
OpenAPI.WITH_CREDENTIALS = true;
OpenAPI.CREDENTIALS = 'include';
}
#redirect() {
@@ -184,24 +197,7 @@ export class UmbAppElement extends UmbLitElement {
}
#isAuthorizedGuard(): Guard {
return () => {
if (!this.#authContext) {
throw new Error('[Fatal] AuthContext requested before it was initialized');
}
if (this.#authContext.getIsAuthorized()) {
return true;
}
// Save location.href so we can redirect to it after login
window.sessionStorage.setItem(UMB_STORAGE_REDIRECT_URL, location.href);
// Make a request to the auth server to start the auth flow
this.#authContext.makeAuthorizationRequest();
// Return false to prevent the route from being rendered
return false;
};
return () => this.#authController.isAuthorized() ?? false;
}
#errorPage(errorMsg: string, error?: unknown) {

View File

@@ -14,7 +14,6 @@ import './components/index.js';
const CORE_PACKAGES = [
import('../../packages/audit-log/umbraco-package.js'),
import('../../packages/block/umbraco-package.js'),
import('../../packages/core/umbraco-package.js'),
import('../../packages/data-type/umbraco-package.js'),
import('../../packages/dictionary/umbraco-package.js'),
import('../../packages/documents/umbraco-package.js'),
@@ -45,7 +44,6 @@ export class UmbBackofficeElement extends UmbLitElement {
/**
* Backoffice extension registry.
* This enables to register and unregister extensions via DevTools, or just via querying this element via the DOM.
* @type {UmbExtensionsRegistry}
*/
public extensionRegistry = umbExtensionsRegistry;
@@ -53,9 +51,11 @@ export class UmbBackofficeElement extends UmbLitElement {
super();
new UmbBackofficeContext(this);
new UmbBundleExtensionInitializer(this, umbExtensionsRegistry);
new UmbEntryPointExtensionInitializer(this, umbExtensionsRegistry);
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerAllExtensions();
new UmbServerExtensionRegistrator(this, umbExtensionsRegistry).registerPrivateExtensions();
// So far local packages are this simple to registerer, so no need for a manager to do that:
CORE_PACKAGES.forEach(async (packageImport) => {

View File

@@ -1,7 +1,7 @@
import { UMB_BACKOFFICE_CONTEXT } from '../backoffice.context.js';
import type { UmbBackofficeContext } from '../backoffice.context.js';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, repeat, ifDefined } from '@umbraco-cms/backoffice/external/lit';
import type { ManifestSection } from '@umbraco-cms/backoffice/extension-registry';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { UmbExtensionManifestInitializer } from '@umbraco-cms/backoffice/extension-api';
@@ -62,7 +62,11 @@ export class UmbBackofficeHeaderSectionsElement extends UmbLitElement {
<uui-tab
?active="${this._currentSectionAlias === section.alias}"
href="${`section/${section.manifest?.meta.pathname}`}"
label="${section.manifest?.meta.label ?? section.manifest?.name ?? ''}"></uui-tab>
label="${ifDefined(
section.manifest?.meta.label
? this.localize.string(section.manifest?.meta.label)
: section.manifest?.name,
)}"></uui-tab>
`,
)}
</uui-tab-group>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

View File

@@ -1013,6 +1013,18 @@ export default {
lockoutWillOccur: 'Du har været inaktiv, og du vil blive logget ud om',
renewSession: 'Forny for at gemme dine ændringer',
},
login: {
greeting0: 'Velkommen',
greeting1: 'Velkommen',
greeting2: 'Velkommen',
greeting3: 'Velkommen',
greeting4: 'Velkommen',
greeting5: 'Velkommen',
greeting6: 'Velkommen',
instruction: 'Log ind på Umbraco',
signInWith: 'Log ind med {0}',
timeout: 'Du er blevet logget ud på grund af inaktivitet, vil du logge ind igen?',
},
main: {
dashboard: 'Skrivebord',
sections: 'Sektioner',

View File

@@ -5,61 +5,66 @@ export default {
assigndomain: 'Culture and Hostnames',
auditTrail: 'Audit Trail',
browse: 'Browse Node',
changeDocType: 'Change Document Type',
changeDataType: 'Change Data Type',
copy: 'Copy',
changeDocType: 'Change Document Type',
chooseWhereToCopy: 'Choose where to copy',
chooseWhereToImport: 'Choose where to import',
chooseWhereToMove: 'Choose where to move',
copy: 'Copy to',
create: 'Create',
export: 'Export',
createPackage: 'Create Package',
createblueprint: 'Create Document Blueprint',
createGroup: 'Create group',
createPackage: 'Create Package',
delete: 'Delete',
deployCompare: 'Compare',
deployPartialRestore: 'Partial restore',
deployQueueForTransfer: 'Queue for transfer',
deployRestore: 'Workspace restore',
deployTransferNow: 'Transfer now',
deployTreeRestore: 'Tree restore',
disable: 'Disable',
editContent: 'Edit content',
editSettings: 'Edit settings',
emptyrecyclebin: 'Empty recycle bin',
enable: 'Enable',
export: 'Export',
exportDocumentType: 'Export Document Type',
folderCreate: 'Create folder',
folderDelete: 'Delete folder',
folderRename: 'Rename folder',
import: 'Import',
importdocumenttype: 'Import Document Type',
importPackage: 'Import Package',
infiniteEditorChooseWhereToCopy: 'Choose where to copy the selected item(s)',
infiniteEditorChooseWhereToMove: 'Choose where to move the selected item(s)',
liveEdit: 'Edit in Canvas',
logout: 'Exit',
move: 'Move',
move: 'Move to',
notify: 'Notifications',
protect: 'Restrict Public Access',
publish: 'Publish',
unpublish: 'Unpublish',
refreshNode: 'Reload',
republish: 'Republish entire site',
remove: 'Remove',
rename: 'Rename',
republish: 'Republish entire site',
resendInvite: 'Resend Invitation',
restore: 'Restore',
chooseWhereToCopy: 'Choose where to copy',
chooseWhereToMove: 'Choose where to move',
chooseWhereToImport: 'Choose where to import',
toInTheTreeStructureBelow: 'to in the tree structure below',
infiniteEditorChooseWhereToCopy: 'Choose where to copy the selected item(s)',
infiniteEditorChooseWhereToMove: 'Choose where to move the selected item(s)',
wasMovedTo: 'was moved to',
wasCopiedTo: 'was copied to',
wasDeleted: 'was deleted',
rights: 'Permissions',
rollback: 'Rollback',
sendtopublish: 'Send To Publish',
sendToTranslate: 'Send To Translation',
setGroup: 'Set group',
sort: 'Sort',
translate: 'Translate',
update: 'Update',
setPermissions: 'Set permissions',
sort: 'Sort children of',
toInTheTreeStructureBelow: 'to in the tree structure below',
translate: 'Translate',
trash: 'Trash',
unlock: 'Unlock',
createblueprint: 'Create Document Blueprint',
resendInvite: 'Resend Invitation',
deployQueueForTransfer: 'Queue for transfer',
deployRestore: 'Workspace restore',
deployTreeRestore: 'Tree restore',
deployPartialRestore: 'Partial restore',
deployTransferNow: 'Transfer now',
deployCompare: 'Compare',
unpublish: 'Unpublish',
update: 'Update',
wasCopiedTo: 'was copied to',
wasDeleted: 'was deleted',
wasMovedTo: 'was moved to',
},
actionCategories: {
content: 'Content',
@@ -745,6 +750,7 @@ export default {
deleted: 'Deleted',
deleting: 'Deleting...',
design: 'Design',
details: 'Details',
dictionary: 'Dictionary',
dimensions: 'Dimensions',
discard: 'Discard',
@@ -1010,6 +1016,18 @@ export default {
lockoutWillOccur: "You've been idle and logout will automatically occur in",
renewSession: 'Renew now to save your work',
},
login: {
greeting0: 'Welcome',
greeting1: 'Welcome',
greeting2: 'Welcome',
greeting3: 'Welcome',
greeting4: 'Welcome',
greeting5: 'Welcome',
greeting6: 'Welcome',
instruction: 'Sign in to Umbraco',
signInWith: 'Sign in with {0}',
timeout: 'Your session has timed out. Please sign in again below.',
},
main: {
dashboard: 'Dashboard',
sections: 'Sections',
@@ -1800,6 +1818,7 @@ export default {
createDate: 'User created',
changePassword: 'Change your password',
changePhoto: 'Change photo',
configureMfa: 'Configure MFA',
emailRequired: 'Required - enter an email address for this user',
newPassword: 'New password',
newPasswordFormatLengthTip: 'Minimum %0% character(s) to go!',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2_00000159465072208785227250000008040336475015305350_"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1256.6 344.8"
style="enable-background:new 0 0 1256.6 344.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#283A97;}
</style>
<g id="Layer_1-2">
<g>
<path class="st0" d="M5,172.2C5.1,79.7,80.2,4.9,172.6,5s167.3,75.2,167.2,167.6c-0.1,92.4-75,167.2-167.4,167.2
C79.9,339.7,5,264.7,5,172.2L5,172.2L5,172.2z M169,229.5c-13,0.4-26-0.8-38.7-3.5c-9.4-1.9-17.3-8.2-21.2-17
c-3.8-8.7-5.6-22.2-5.5-40.4c0.1-9.5,0.7-19,1.8-28.4c1.1-9.2,2.2-16.8,3.2-22.8l1.1-5.9c0-0.2,0-0.3,0-0.5c0-1.7-1.2-3.1-2.8-3.4
l-21.6-3.4H85c-1.6,0-2.9,1.1-3.3,2.6c-0.4,1.4-0.6,2.4-1.2,5.7c-1.2,6.4-2.4,12.6-3.6,21.6c-1.3,9.8-2.1,19.7-2.4,29.6
c-0.5,6.9-0.5,13.8,0,20.7c0.5,18.3,3.7,33,9.4,43.9s15.6,18.8,29.4,23.6c13.8,4.8,33,7.2,57.7,7.1h3.1
c24.7,0.1,43.9-2.2,57.7-7.1c13.8-4.8,23.6-12.7,29.4-23.6c5.8-11,8.9-25.6,9.4-43.9c0.5-6.9,0.5-13.8,0-20.7
c-0.3-9.9-1.1-19.8-2.4-29.6c-1.3-9-2.4-15.1-3.6-21.6c-0.6-3.3-0.9-4.3-1.2-5.7c-0.4-1.5-1.7-2.6-3.3-2.6h-0.6l-21.6,3.4
c-1.6,0.3-2.8,1.7-2.8,3.4c0,0.2,0,0.3,0,0.5l1.2,5.9c1.1,6,2.2,13.6,3.3,22.7c1.1,9.4,1.7,18.9,1.8,28.4
c0.2,18.2-1.7,31.6-5.5,40.4c-3.8,8.7-11.6,15-20.9,17c-12.7,2.7-25.7,3.9-38.7,3.5L169,229.5L169,229.5z"/>
<g>
<path class="st0" d="M1155.2,186.1c0-33.4,9.6-56.9,48.2-56.9s48.2,23.5,48.2,56.9s-9.6,56.9-48.2,56.9
S1155.2,219.5,1155.2,186.1z M1227.1,186.1c0-23.2-3-36.5-23.6-36.5s-23.6,13.3-23.6,36.5s3.1,36.5,23.6,36.5
S1227.1,209.2,1227.1,186.1z"/>
<path class="st0" d="M487.2,238.7c0.6,1,1.6,1.6,2.8,1.6h9c1.8,0,3.2-1.4,3.2-3.2l0,0V135c0-1.8-1.4-3.2-3.2-3.2h-18
c-1.8,0-3.2,1.4-3.2,3.2v81c-7.7,4.3-16.4,6.5-25.2,6.3c-11.5,0-17.2-5-17.2-16.1V135c0-1.8-1.4-3.2-3.2-3.2h-17.9
c-1.8,0-3.2,1.4-3.2,3.2v73.4c0,20.8,9.8,34.5,37.3,34.5c12.6-0.1,24.9-4.1,35.2-11.3l3.2,7.2L487.2,238.7L487.2,238.7z"/>
<path class="st0" d="M689.8,163.7c0-20.6-10.2-34.5-35.9-34.5c-12.4,0-24.5,3.8-34.8,10.7c-4.6-6.7-13.3-10.7-27.8-10.7
c-11.8,0.2-23.3,4.2-32.8,11.3l-3.2-7.2l0,0c-0.6-1-1.6-1.7-2.8-1.7h-9c-1.8,0-3.2,1.4-3.2,3.2v102.1c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-80.8c6.9-4.1,14.8-6.3,22.8-6.3c9.8,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-81.1c6.8-4.2,14.8-6.4,22.8-6.4c9.6,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2l0,0L689.8,163.7L689.8,163.7z"/>
<path class="st0" d="M745.4,231.7c10.2,7.5,22.5,11.5,35.2,11.3c31.7,0,43.4-21.3,43.4-56.9s-11.7-56.9-43.4-56.9
c-10.3,0.1-20.4,3-29.1,8.5v-32.8c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8,0-3.2,1.4-3.2,3.2v132.3c0,1.8,1.4,3.2,3.2,3.2h9
c1.2,0,2.2-0.6,2.8-1.6l0,0L745.4,231.7L745.4,231.7z M774.9,222.3c-8.2,0-16.3-2.1-23.4-6.3v-59.9c7.1-4.1,15.2-6.3,23.4-6.3
c21.3,0,24.5,16.3,24.5,36.2S796.2,222.3,774.9,222.3L774.9,222.3L774.9,222.3z"/>
<path class="st0" d="M913.9,150.5c-2.8-0.4-5.6-0.6-8.5-0.5c-9.7-0.4-19.3,1.9-27.8,6.5v80.6c0,1.8-1.4,3.2-3.2,3.2h-18
c-1.8,0-3.2-1.4-3.2-3.2V135c0-1.8,1.4-3.2,3.2-3.2h9c1.2,0,2.2,0.6,2.8,1.6l0,0l3.2,7.2c9.9-7.5,22-11.5,34.4-11.3
c2.8,0,5.7,0.2,8.5,0.7l0,0c1.7,0,3,2.7,3,4.4v13c0,1.8-1.4,3.2-3.2,3.2h-0.2"/>
<path class="st0" d="M976.1,190.2c-10.7,1.3-17.2,5.4-17.2,16.7c0,8.3,3.6,16.1,16.7,16.1c8.3,0.1,16.4-2.4,23.2-7.2v-28.1
L976.1,190.2L976.1,190.2z M1004.8,231.7C995.5,239,984,243,972.1,243c-28,0-37.3-17.4-37.3-34.8c0-23.5,15.2-33.4,39.7-35.4
l24.4-1.9v-5.4c0-11.1-5.2-15.4-21.3-15.4c-10.1,0-20.1,1.7-29.6,4.9c-0.3,0.1-0.7,0.1-1,0c-1.8,0-3.2-1.4-3.2-3.2v-14.4
c0-1.4,0.8-2.6,2.1-3.1l0,0c10.8-3.6,22.1-5.4,33.5-5.4c35.6,0,43.8,15.6,43.8,38.7v69.3c0,1.8-1.4,3.2-3.2,3.2h-9
c-1.2,0-2.2-0.6-2.8-1.6l0,0L1004.8,231.7L1004.8,231.7z"/>
<path class="st0" d="M1128.6,218c0.3,0,0.7,0,1,0c1.8,0,3.2,1.4,3.2,3.2v14.4c0,1.3-0.8,2.5-2,3l0,0c-8.9,3.2-18.4,4.8-27.9,4.6
c-38.5,0-50.3-23-50.3-56.9s11.8-56.9,50.3-56.9c9.4-0.2,18.9,1.2,27.8,4.4l0,0c1.2,0.5,2,1.7,2,3v14.5c0,1.8-1.4,3.2-3.2,3.2
c-0.4,0.1-0.7,0.1-1.1,0l0,0c-7.8-2.5-16-3.7-24.2-3.6c-21.1,0-27.2,14.4-27.2,35.4s6.1,35.4,27.2,35.4
c8.2,0.1,16.4-1.1,24.2-3.6"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1 +1,51 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1180 316"><path d="M.2 157.8C.3 70.6 71.1-.1 158.3.1S316.2 71 316.1 158.2s-70.8 157.7-157.9 157.7C70.8 315.9.1 245.1.2 157.8zm154.7 54.1c-12.3.4-24.5-.7-36.5-3.3-8.8-1.8-16.3-7.8-19.9-16-3.6-8.2-5.3-20.9-5.2-38.1.1-9 .6-17.9 1.7-26.8 1-8.7 2.1-15.8 3.1-21.5l1.1-5.6v-.5c0-1.6-1.1-2.9-2.6-3.2l-20.4-3.2h-.4c-1.5 0-2.8 1-3.1 2.5-.3 1.3-.6 2.3-1.2 5.4-1.2 6-2.2 11.8-3.4 20.4-1.3 9.3-2 18.6-2.3 27.9-.4 6.5-.4 13 0 19.6.5 17.3 3.4 31.1 8.9 41.4 5.5 10.3 14.7 17.8 27.7 22.3s31.2 6.8 54.4 6.7h2.9c23.3.1 41.4-2.1 54.4-6.7 13-4.5 22.2-12 27.7-22.3s8.4-24.1 8.9-41.4c.4-6.5.4-13 0-19.6-.3-9.3-1-18.7-2.3-27.9-1.2-8.4-2.3-14.3-3.4-20.4-.6-3.1-.8-4.1-1.2-5.4-.3-1.4-1.6-2.5-3.1-2.5h-.5l-20.4 3.2c-1.6.3-2.7 1.6-2.7 3.2v.5l1.1 5.6c1 5.6 2.1 12.8 3.1 21.5 1 8.9 1.6 17.9 1.7 26.8.2 17.1-1.6 29.8-5.2 38.1-3.6 8.2-11 14.2-19.8 16.1-12 2.5-24.2 3.6-36.5 3.3l-6.6-.1zm932.3-43.9c0-30.4 8.6-51.7 43.8-51.7s43.8 21.3 43.8 51.7-8.6 51.7-43.8 51.7-43.8-21.3-43.8-51.7zm65.3 0c0-21.1-2.7-33.1-21.5-33.1s-21.5 12-21.5 33.1 2.8 33.1 21.5 33.1c18.8 0 21.5-12 21.5-33.1zm-672.1 47.8c.5.9 1.5 1.5 2.5 1.4h8.2c1.6 0 2.9-1.3 2.9-2.9v-92.7c0-1.6-1.3-2.9-2.9-2.9h-16.3c-1.6 0-2.9 1.3-2.9 2.9v73.6c-7 3.9-14.9 5.9-22.8 5.8-10.4 0-15.6-4.5-15.6-14.6v-64.8c0-1.6-1.3-2.9-2.9-2.9h-16.4c-1.6 0-2.9 1.3-2.9 2.9v66.7c0 18.9 8.9 31.3 33.9 31.3 11.4-.1 22.6-3.7 32-10.2l2.9 6.5.3-.1zm184.1-68.1c0-18.7-9.3-31.4-32.6-31.4-11.3 0-22.3 3.4-31.6 9.8-4.1-6.1-12-9.8-25.3-9.8-10.7.2-21.1 3.8-29.7 10.2l-2.9-6.5c-.5-.9-1.5-1.5-2.5-1.4h-8.3c-1.6 0-2.9 1.3-2.9 2.9v92.8c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9v-73.5c6.2-3.8 13.4-5.8 20.7-5.8 8.9 0 14 3.3 14 12.6v66.7c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9v-73.6c6.2-3.9 13.4-5.9 20.7-5.8 8.6 0 14 3.3 14 12.6v66.7c0 1.6 1.3 2.9 2.9 2.9h16.3c1.6 0 2.9-1.3 2.9-2.9l.1-66.5zm50.4 61.7c9.3 6.9 20.5 10.5 32 10.2 28.8 0 39.4-19.3 39.4-51.7s-10.7-51.7-39.4-51.7c-9.4 0-18.5 2.7-26.4 7.7V94.2c0-1.6-1.2-2.9-2.8-3h-16.6c-1.6 0-2.9 1.3-2.9 2.9v120.3c0 1.6 1.3 2.9 2.9 2.9h8.2c1 0 2-.5 2.5-1.4l3.1-6.5zm26.8-8.5c-7.5 0-14.8-2-21.3-5.8v-54.4c6.5-3.8 13.8-5.8 21.3-5.8 19.3 0 22.3 14.8 22.3 32.9s-2.8 33.1-22.3 33.1zM868 135.7c-2.5-.3-5.1-.5-7.7-.5-8.8-.4-17.5 1.7-25.3 5.9v73.2c0 1.6-1.3 2.9-2.9 2.9h-16.3c-1.6 0-2.9-1.3-2.9-2.9v-92.7c0-1.6 1.3-2.9 2.9-2.9h8.2c1 0 2 .5 2.5 1.4l2.9 6.5c8.9-6.8 19.9-10.4 31.2-10.2 2.6 0 5.2.2 7.7.6 1.4 0 2.7 2.4 2.7 4v11.8c0 1.6-1.3 2.9-2.9 2.9h-.2m56.7 36.1c-9.8 1.2-15.6 4.9-15.6 15.2 0 7.5 3.3 14.6 15.2 14.6 7.5.1 14.9-2.2 21.1-6.5v-25.5l-20.7 2.2zm26.1 37.6c-8.5 6.7-19 10.3-29.8 10.2-25.5 0-33.9-15.8-33.9-31.6 0-21.3 13.8-30.4 36.1-32.1l22.1-1.8v-4.9c0-10.1-4.7-14-19.3-14-9.2 0-18.3 1.5-26.9 4.5h-.9c-1.6 0-2.9-1.3-2.9-2.9v-13.1c0-1.2.7-2.4 1.9-2.8 9.8-3.3 20.1-5 30.5-4.9 32.3 0 39.8 14.2 39.8 35.1V214c0 1.6-1.3 2.9-2.9 2.9h-8.2c-1 0-2-.5-2.5-1.4l-3.1-6.1zM1063 197h.9c1.6 0 2.9 1.3 2.9 2.9V213c0 1.2-.7 2.3-1.8 2.7-8.1 2.9-16.7 4.3-25.4 4.1-34.9 0-45.7-20.9-45.7-51.7s10.7-51.7 45.7-51.7c8.6-.2 17.1 1.1 25.2 4 1.1.4 1.9 1.5 1.8 2.7v13.1c0 1.6-1.3 2.9-2.9 2.9h-.9c-7.1-2.2-14.6-3.3-22-3.1-19.1 0-24.7 13.1-24.7 32.2s5.5 32.1 24.7 32.1c7.5.1 14.9-1 22-3.3" fill="#fff"/></svg>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.8.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_2_00000109717856507224707600000004572449649498936508_"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1256.6 344.8"
style="enable-background:new 0 0 1256.6 344.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g id="Layer_1-2">
<g>
<path class="st0" d="M5,172.2C5.1,79.7,80.2,4.9,172.6,5s167.3,75.2,167.2,167.6c-0.1,92.4-75,167.2-167.4,167.2
C79.9,339.7,4.9,264.7,5,172.2L5,172.2L5,172.2z M169,229.5c-13,0.4-26-0.8-38.7-3.5c-9.4-1.9-17.3-8.2-21.2-17
c-3.8-8.7-5.6-22.2-5.5-40.4c0.1-9.5,0.7-19,1.8-28.4c1.1-9.2,2.2-16.8,3.2-22.8l1.1-5.9c0-0.2,0-0.3,0-0.5c0-1.7-1.2-3.1-2.8-3.4
l-21.6-3.4H85c-1.6,0-2.9,1.1-3.3,2.6c-0.4,1.4-0.6,2.4-1.2,5.7c-1.2,6.4-2.4,12.6-3.6,21.6c-1.3,9.8-2.1,19.7-2.4,29.6
c-0.5,6.9-0.5,13.8,0,20.7c0.5,18.3,3.7,33,9.4,43.9s15.6,18.8,29.4,23.6c13.8,4.8,33,7.2,57.7,7.1h3.1
c24.7,0.1,43.9-2.2,57.7-7.1c13.8-4.8,23.6-12.7,29.4-23.6c5.8-11,8.9-25.6,9.4-43.9c0.5-6.9,0.5-13.8,0-20.7
c-0.3-9.9-1.1-19.8-2.4-29.6c-1.3-8.9-2.4-15.1-3.6-21.6c-0.6-3.3-0.9-4.3-1.2-5.7c-0.4-1.5-1.7-2.6-3.3-2.6h-0.6l-21.6,3.4
c-1.6,0.3-2.8,1.7-2.8,3.4c0,0.2,0,0.3,0,0.5l1.2,5.9c1.1,6,2.2,13.6,3.3,22.7c1.1,9.4,1.7,18.9,1.8,28.4
c0.2,18.2-1.7,31.6-5.5,40.4c-3.8,8.7-11.6,15-20.9,17c-12.7,2.7-25.7,3.9-38.7,3.5L169,229.5L169,229.5z"/>
<g>
<path class="st0" d="M1155.2,186.1c0-33.4,9.6-56.9,48.2-56.9s48.2,23.5,48.2,56.9s-9.6,56.9-48.2,56.9
S1155.2,219.5,1155.2,186.1z M1227.1,186.1c0-23.2-3-36.5-23.6-36.5s-23.6,13.3-23.6,36.5s3.1,36.5,23.6,36.5
S1227.1,209.2,1227.1,186.1z"/>
<path class="st0" d="M487.2,238.7c0.6,1,1.6,1.6,2.8,1.6h9c1.8,0,3.2-1.4,3.2-3.2l0,0V135c0-1.8-1.4-3.2-3.2-3.2h-18
c-1.8,0-3.2,1.4-3.2,3.2v81c-7.7,4.3-16.4,6.5-25.2,6.3c-11.5,0-17.2-5-17.2-16.1V135c0-1.8-1.4-3.2-3.2-3.2h-17.9
c-1.8,0-3.2,1.4-3.2,3.2v73.4c0,20.8,9.8,34.5,37.3,34.5c12.6-0.1,24.9-4.1,35.2-11.3l3.2,7.2L487.2,238.7L487.2,238.7z"/>
<path class="st0" d="M689.8,163.7c0-20.6-10.2-34.5-35.9-34.5c-12.4,0-24.5,3.8-34.8,10.7c-4.6-6.7-13.3-10.7-27.8-10.7
c-11.8,0.2-23.3,4.2-32.8,11.3l-3.2-7.2l0,0c-0.6-1-1.6-1.7-2.8-1.7h-9c-1.8,0-3.2,1.4-3.2,3.2v102.1c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-80.8c6.9-4.1,14.8-6.3,22.8-6.3c9.8,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2v-81.1c6.8-4.2,14.8-6.4,22.8-6.4c9.6,0,15.4,3.6,15.4,14v73.4c0,1.8,1.4,3.2,3.2,3.2h18
c1.8,0,3.2-1.4,3.2-3.2l0,0L689.8,163.7L689.8,163.7z"/>
<path class="st0" d="M745.4,231.7c10.2,7.5,22.5,11.5,35.2,11.3c31.7,0,43.4-21.3,43.4-56.9s-11.7-56.9-43.4-56.9
c-10.3,0.1-20.4,3-29.1,8.5v-32.9c0-1.8-1.4-3.2-3.2-3.2h-18c-1.8,0-3.2,1.4-3.2,3.2v132.3c0,1.8,1.4,3.2,3.2,3.2h9
c1.2,0,2.2-0.6,2.8-1.6l0,0L745.4,231.7L745.4,231.7z M774.9,222.3c-8.2,0-16.3-2.1-23.4-6.3v-59.9c7.1-4.1,15.2-6.3,23.4-6.3
c21.3,0,24.5,16.3,24.5,36.2S796.2,222.3,774.9,222.3L774.9,222.3L774.9,222.3z"/>
<path class="st0" d="M913.9,150.5c-2.8-0.4-5.6-0.6-8.5-0.5c-9.7-0.4-19.3,1.9-27.8,6.5v80.6c0,1.8-1.4,3.2-3.2,3.2h-18
c-1.8,0-3.2-1.4-3.2-3.2V135c0-1.8,1.4-3.2,3.2-3.2h9c1.2,0,2.2,0.6,2.8,1.6l0,0l3.2,7.2c9.9-7.5,22-11.5,34.4-11.3
c2.8,0,5.7,0.2,8.5,0.7l0,0c1.7,0,3,2.7,3,4.4v13c0,1.8-1.4,3.2-3.2,3.2L913.9,150.5"/>
<path class="st0" d="M976.1,190.2c-10.7,1.3-17.2,5.4-17.2,16.7c0,8.3,3.6,16.1,16.7,16.1c8.3,0.1,16.4-2.4,23.2-7.2v-28.1
L976.1,190.2L976.1,190.2z M1004.8,231.7C995.5,239,984,243,972.1,243c-28,0-37.3-17.4-37.3-34.8c0-23.5,15.2-33.4,39.7-35.4
l24.4-1.9v-5.4c0-11.1-5.2-15.4-21.3-15.4c-10.1,0-20.1,1.7-29.6,4.9c-0.3,0.1-0.7,0.1-1,0c-1.8,0-3.2-1.4-3.2-3.2v-14.4
c0-1.4,0.8-2.6,2.1-3.1l0,0c10.8-3.6,22.1-5.4,33.5-5.4c35.6,0,43.8,15.6,43.8,38.7v69.3c0,1.8-1.4,3.2-3.2,3.2h-9
c-1.2,0-2.2-0.6-2.8-1.6l0,0L1004.8,231.7L1004.8,231.7z"/>
<path class="st0" d="M1128.6,218c0.3,0,0.7,0,1,0c1.8,0,3.2,1.4,3.2,3.2v14.4c0,1.3-0.8,2.5-2,3l0,0c-8.9,3.2-18.4,4.8-27.9,4.6
c-38.5,0-50.3-23-50.3-56.9s11.8-56.9,50.3-56.9c9.4-0.2,18.9,1.2,27.8,4.4l0,0c1.2,0.5,2,1.7,2,3v14.5c0,1.8-1.4,3.2-3.2,3.2
c-0.4,0.1-0.7,0.1-1.1,0l0,0c-7.8-2.5-16-3.7-24.2-3.6c-21.1,0-27.2,14.4-27.2,35.4s6.1,35.4,27.2,35.4
c8.2,0.1,16.4-1.1,24.2-3.6"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -152,6 +152,7 @@ export { LogLevelModel } from './models/LogLevelModel';
export type { LogMessagePropertyPresentationModel } from './models/LogMessagePropertyPresentationModel';
export type { LogMessageResponseModel } from './models/LogMessageResponseModel';
export type { LogTemplateResponseModel } from './models/LogTemplateResponseModel';
export type { ManifestResponseModel } from './models/ManifestResponseModel';
export type { MediaCollectionResponseModel } from './models/MediaCollectionResponseModel';
export type { MediaConfigurationResponseModel } from './models/MediaConfigurationResponseModel';
export type { MediaItemResponseModel } from './models/MediaItemResponseModel';
@@ -209,7 +210,6 @@ export type { OutOfDateStatusResponseModel } from './models/OutOfDateStatusRespo
export { OutOfDateTypeModel } from './models/OutOfDateTypeModel';
export type { PackageConfigurationResponseModel } from './models/PackageConfigurationResponseModel';
export type { PackageDefinitionResponseModel } from './models/PackageDefinitionResponseModel';
export type { PackageManifestResponseModel } from './models/PackageManifestResponseModel';
export type { PackageMigrationStatusResponseModel } from './models/PackageMigrationStatusResponseModel';
export type { PagedAllowedDocumentTypeModel } from './models/PagedAllowedDocumentTypeModel';
export type { PagedAllowedMediaTypeModel } from './models/PagedAllowedMediaTypeModel';
@@ -411,6 +411,7 @@ export { IndexerResource } from './services/IndexerResource';
export { InstallResource } from './services/InstallResource';
export { LanguageResource } from './services/LanguageResource';
export { LogViewerResource } from './services/LogViewerResource';
export { ManifestResource } from './services/ManifestResource';
export { MediaResource } from './services/MediaResource';
export { MediaTypeResource } from './services/MediaTypeResource';
export { MemberResource } from './services/MemberResource';

View File

@@ -3,7 +3,7 @@
/* tslint:disable */
/* eslint-disable */
export type PackageManifestResponseModel = {
export type ManifestResponseModel = {
name: string;
version?: string | null;
extensions: Array<any>;

View File

@@ -0,0 +1,52 @@
/* generated using openapi-typescript-codegen -- do no edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { ManifestResponseModel } from '../models/ManifestResponseModel';
import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request';
export class ManifestResource {
/**
* @returns any Success
* @throws ApiError
*/
public static getManifestManifest(): CancelablePromise<Array<ManifestResponseModel>> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/manifest/manifest',
errors: {
401: `The resource is protected and requires an authentication token`,
},
});
}
/**
* @returns any Success
* @throws ApiError
*/
public static getManifestManifestPrivate(): CancelablePromise<Array<ManifestResponseModel>> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/manifest/manifest/private',
errors: {
401: `The resource is protected and requires an authentication token`,
},
});
}
/**
* @returns any Success
* @throws ApiError
*/
public static getManifestManifestPublic(): CancelablePromise<Array<ManifestResponseModel>> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/manifest/manifest/public',
});
}
}

View File

@@ -5,7 +5,6 @@
import type { CreatePackageRequestModel } from '../models/CreatePackageRequestModel';
import type { PackageConfigurationResponseModel } from '../models/PackageConfigurationResponseModel';
import type { PackageDefinitionResponseModel } from '../models/PackageDefinitionResponseModel';
import type { PackageManifestResponseModel } from '../models/PackageManifestResponseModel';
import type { PagedPackageDefinitionResponseModel } from '../models/PagedPackageDefinitionResponseModel';
import type { PagedPackageMigrationStatusResponseModel } from '../models/PagedPackageMigrationStatusResponseModel';
import type { UpdatePackageRequestModel } from '../models/UpdatePackageRequestModel';
@@ -195,31 +194,6 @@ export class PackageResource {
});
}
/**
* @returns any Success
* @throws ApiError
*/
public static getPackageManifest(): CancelablePromise<Array<PackageManifestResponseModel>> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/package/manifest',
errors: {
401: `The resource is protected and requires an authentication token`,
},
});
}
/**
* @returns any Success
* @throws ApiError
*/
public static getPackageManifestPublic(): CancelablePromise<Array<PackageManifestResponseModel>> {
return __request(OpenAPI, {
method: 'GET',
url: '/umbraco/management/api/v1/package/manifest/public',
});
}
/**
* @returns any Success
* @throws ApiError

View File

@@ -0,0 +1 @@
export { fromByteArray } from 'base64-js';

View File

@@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,122 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import { DefaultCrypto } from './crypto_utils.js';
import type { Crypto } from './crypto_utils.js';
import { log } from './logger.js';
import type { StringMap } from './types.js';
/**
* Represents an AuthorizationRequest as JSON.
*/
export interface AuthorizationRequestJson {
response_type: string;
client_id: string;
redirect_uri: string;
scope: string;
state?: string;
extras?: StringMap;
internal?: StringMap;
}
/**
* Generates a cryptographically random new state. Useful for CSRF protection.
*/
const SIZE = 10; // 10 bytes
const newState = function (crypto: Crypto): string {
return crypto.generateRandom(SIZE);
};
/**
* Represents the AuthorizationRequest.
* For more information look at
* https://tools.ietf.org/html/rfc6749#section-4.1.1
*/
export class AuthorizationRequest {
static RESPONSE_TYPE_TOKEN = 'token';
static RESPONSE_TYPE_CODE = 'code';
// NOTE:
// Both redirect_uri and state are actually optional.
// However AppAuth is more opionionated, and requires you to use both.
clientId: string;
redirectUri: string;
scope: string;
responseType: string;
state: string;
extras?: StringMap;
internal?: StringMap;
/**
* Constructs a new AuthorizationRequest.
* Use a `undefined` value for the `state` parameter, to generate a random
* state for CSRF protection.
*/
constructor(
request: AuthorizationRequestJson,
private crypto: Crypto = new DefaultCrypto(),
private usePkce: boolean = true,
) {
this.clientId = request.client_id;
this.redirectUri = request.redirect_uri;
this.scope = request.scope;
this.responseType = request.response_type || AuthorizationRequest.RESPONSE_TYPE_CODE;
this.state = request.state || newState(crypto);
this.extras = request.extras;
// read internal properties if available
this.internal = request.internal;
}
setupCodeVerifier(): Promise<void> {
if (!this.usePkce) {
return Promise.resolve();
} else {
const codeVerifier = this.crypto.generateRandom(128);
const challenge: Promise<string | undefined> = this.crypto.deriveChallenge(codeVerifier).catch((error) => {
log('Unable to generate PKCE challenge. Not using PKCE', error);
return undefined;
});
return challenge.then((result) => {
if (result) {
// keep track of the code used.
this.internal = this.internal || {};
this.internal['code_verifier'] = codeVerifier;
this.extras = this.extras || {};
this.extras['code_challenge'] = result;
// We always use S256. Plain is not good enough.
this.extras['code_challenge_method'] = 'S256';
}
});
}
}
/**
* Serializes the AuthorizationRequest to a JavaScript Object.
*/
toJson(): Promise<AuthorizationRequestJson> {
// Always make sure that the code verifier is setup when toJson() is called.
return this.setupCodeVerifier().then(() => {
return {
response_type: this.responseType,
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: this.scope,
state: this.state,
extras: this.extras,
internal: this.internal,
};
});
}
}

View File

@@ -0,0 +1,161 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { AuthorizationRequest } from './authorization_request.js';
import type { AuthorizationError, AuthorizationResponse } from './authorization_response.js';
import type { AuthorizationServiceConfiguration } from './authorization_service_configuration.js';
import type { Crypto } from './crypto_utils.js';
import { log } from './logger.js';
import type { QueryStringUtils } from './query_string_utils.js';
import type { StringMap } from './types.js';
/**
* This type represents a lambda that can take an AuthorizationRequest,
* and an AuthorizationResponse as arguments.
*/
export type AuthorizationListener = (
request: AuthorizationRequest,
response: AuthorizationResponse | null,
error: AuthorizationError | null,
) => void;
/**
* Represents a structural type holding both authorization request and response.
*/
export interface AuthorizationRequestResponse {
request: AuthorizationRequest;
response: AuthorizationResponse | null;
error: AuthorizationError | null;
}
/**
* Authorization Service notifier.
* This manages the communication of the AuthorizationResponse to the 3p client.
*/
export class AuthorizationNotifier {
private listener: AuthorizationListener | null = null;
setAuthorizationListener(listener: AuthorizationListener) {
this.listener = listener;
}
/**
* The authorization complete callback.
*/
onAuthorizationComplete(
request: AuthorizationRequest,
response: AuthorizationResponse | null,
error: AuthorizationError | null,
): void {
if (this.listener) {
// complete authorization request
this.listener(request, response, error);
}
}
}
// TODO(rahulrav@): add more built in parameters.
/* built in parameters. */
export const BUILT_IN_PARAMETERS = ['redirect_uri', 'client_id', 'response_type', 'state', 'scope'];
/**
* Defines the interface which is capable of handling an authorization request
* using various methods (iframe / popup / different process etc.).
*/
export abstract class AuthorizationRequestHandler {
constructor(
public utils: QueryStringUtils,
protected crypto: Crypto,
) {}
// notifier send the response back to the client.
protected notifier: AuthorizationNotifier | null = null;
/**
* A utility method to be able to build the authorization request URL.
*/
protected buildRequestUrl(configuration: AuthorizationServiceConfiguration, request: AuthorizationRequest) {
// build the query string
// coerce to any type for convenience
const requestMap: StringMap = {
redirect_uri: request.redirectUri,
client_id: request.clientId,
response_type: request.responseType,
state: request.state,
scope: request.scope,
};
// copy over extras
if (request.extras) {
for (const extra in request.extras) {
if (Object.prototype.hasOwnProperty.call(request.extras, extra)) {
// check before inserting to requestMap
if (BUILT_IN_PARAMETERS.indexOf(extra) < 0) {
requestMap[extra] = request.extras[extra];
}
}
}
}
const query = this.utils.stringify(requestMap);
const baseUrl = configuration.authorizationEndpoint;
const url = `${baseUrl}?${query}`;
return url;
}
/**
* Completes the authorization request if necessary & when possible.
*/
completeAuthorizationRequestIfPossible(): Promise<void> {
// call complete authorization if possible to see there might
// be a response that needs to be delivered.
log(`Checking to see if there is an authorization response to be delivered.`);
if (!this.notifier) {
log(`Notifier is not present on AuthorizationRequest handler.
No delivery of result will be possible`);
}
return this.completeAuthorizationRequest().then((result) => {
if (!result) {
log(`No result is available yet.`);
}
if (result && this.notifier) {
this.notifier.onAuthorizationComplete(result.request, result.response, result.error);
}
});
}
/**
* Sets the default Authorization Service notifier.
*/
setAuthorizationNotifier(notifier: AuthorizationNotifier): AuthorizationRequestHandler {
this.notifier = notifier;
return this;
}
/**
* Makes an authorization request.
*/
abstract performAuthorizationRequest(
configuration: AuthorizationServiceConfiguration,
request: AuthorizationRequest,
): void;
/**
* Checks if an authorization flow can be completed, and completes it.
* The handler returns a `Promise<AuthorizationRequestResponse>` if ready, or a `Promise<null>`
* if not ready.
*/
protected abstract completeAuthorizationRequest(): Promise<AuthorizationRequestResponse | null>;
}

View File

@@ -0,0 +1,79 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents the AuthorizationResponse as a JSON object.
*/
export interface AuthorizationResponseJson {
code: string;
state: string;
}
/**
* Represents the AuthorizationError as a JSON object.
*/
export interface AuthorizationErrorJson {
error: string;
error_description?: string;
error_uri?: string;
state?: string;
}
/**
* Represents the Authorization Response type.
* For more information look at
* https://tools.ietf.org/html/rfc6749#section-4.1.2
*/
export class AuthorizationResponse {
code: string;
state: string;
constructor(response: AuthorizationResponseJson) {
this.code = response.code;
this.state = response.state;
}
toJson(): AuthorizationResponseJson {
return { code: this.code, state: this.state };
}
}
/**
* Represents the Authorization error response.
* For more information look at:
* https://tools.ietf.org/html/rfc6749#section-4.1.2.1
*/
export class AuthorizationError {
error: string;
errorDescription?: string;
errorUri?: string;
state?: string;
constructor(error: AuthorizationErrorJson) {
this.error = error.error;
this.errorDescription = error.error_description;
this.errorUri = error.error_uri;
this.state = error.state;
}
toJson(): AuthorizationErrorJson {
return {
error: this.error,
error_description: this.errorDescription,
error_uri: this.errorUri,
state: this.state,
};
}
}

View File

@@ -0,0 +1,81 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { Requestor } from './xhr.js';
import { FetchRequestor } from './xhr.js';
/**
* Represents AuthorizationServiceConfiguration as a JSON object.
*/
export interface AuthorizationServiceConfigurationJson {
authorization_endpoint: string;
token_endpoint: string;
revocation_endpoint: string;
end_session_endpoint?: string;
userinfo_endpoint?: string;
}
/**
* The standard base path for well-known resources on domains.
* See https://tools.ietf.org/html/rfc5785 for more information.
*/
const WELL_KNOWN_PATH = '.well-known';
/**
* The standard resource under the well known path at which an OpenID Connect
* discovery document can be found under an issuer's base URI.
*/
const OPENID_CONFIGURATION = 'openid-configuration';
/**
* Configuration details required to interact with an authorization service.
*
* More information at https://openid.net/specs/openid-connect-discovery-1_0-17.html
*/
export class AuthorizationServiceConfiguration {
authorizationEndpoint: string;
tokenEndpoint: string;
revocationEndpoint: string;
userInfoEndpoint?: string;
endSessionEndpoint?: string;
constructor(request: AuthorizationServiceConfigurationJson) {
this.authorizationEndpoint = request.authorization_endpoint;
this.tokenEndpoint = request.token_endpoint;
this.revocationEndpoint = request.revocation_endpoint;
this.userInfoEndpoint = request.userinfo_endpoint;
this.endSessionEndpoint = request.end_session_endpoint;
}
toJson() {
return {
authorization_endpoint: this.authorizationEndpoint,
token_endpoint: this.tokenEndpoint,
revocation_endpoint: this.revocationEndpoint,
end_session_endpoint: this.endSessionEndpoint,
userinfo_endpoint: this.userInfoEndpoint,
};
}
static fetchFromIssuer(openIdIssuerUrl: string, requestor?: Requestor): Promise<AuthorizationServiceConfiguration> {
const fullUrl = `${openIdIssuerUrl}/${WELL_KNOWN_PATH}/${OPENID_CONFIGURATION}`;
const requestorToUse = requestor || new FetchRequestor();
return requestorToUse
.xhr<AuthorizationServiceConfigurationJson>({ url: fullUrl, dataType: 'json', method: 'GET' })
.then((json) => new AuthorizationServiceConfiguration(json));
}
}

View File

@@ -0,0 +1,98 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as base64 from '@umbraco-cms/backoffice/external/base64-js';
import { AppAuthError } from './errors.js';
const HAS_CRYPTO = typeof window !== 'undefined' && !!(window.crypto as any);
const HAS_SUBTLE_CRYPTO = HAS_CRYPTO && !!(window.crypto.subtle as any);
const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
export function bufferToString(buffer: Uint8Array) {
const state = [];
for (let i = 0; i < buffer.byteLength; i += 1) {
const index = buffer[i] % CHARSET.length;
state.push(CHARSET[index]);
}
return state.join('');
}
export function urlSafe(buffer: Uint8Array): string {
const encoded = base64.fromByteArray(new Uint8Array(buffer));
return encoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}
// adapted from source: http://stackoverflow.com/a/11058858
// this is used in place of TextEncode as the api is not yet
// well supported: https://caniuse.com/#search=TextEncoder
export function textEncodeLite(str: string) {
const buf = new ArrayBuffer(str.length);
const bufView = new Uint8Array(buf);
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i);
}
return bufView;
}
export interface Crypto {
/**
* Generate a random string
*/
generateRandom(size: number): string;
/**
* Compute the SHA256 of a given code.
* This is useful when using PKCE.
*/
deriveChallenge(code: string): Promise<string>;
}
/**
* The default implementation of the `Crypto` interface.
* This uses the capabilities of the browser.
*/
export class DefaultCrypto implements Crypto {
generateRandom(size: number) {
const buffer = new Uint8Array(size);
if (HAS_CRYPTO) {
window.crypto.getRandomValues(buffer);
} else {
// fall back to Math.random() if nothing else is available
for (let i = 0; i < size; i += 1) {
buffer[i] = (Math.random() * CHARSET.length) | 0;
}
}
return bufferToString(buffer);
}
deriveChallenge(code: string): Promise<string> {
if (code.length < 43 || code.length > 128) {
return Promise.reject(new AppAuthError('Invalid code length.'));
}
if (!HAS_SUBTLE_CRYPTO) {
return Promise.reject(new AppAuthError('window.crypto.subtle is unavailable.'));
}
return new Promise((resolve, reject) => {
crypto.subtle.digest('SHA-256', textEncodeLite(code)).then(
(buffer) => {
return resolve(urlSafe(new Uint8Array(buffer)));
},
(error) => reject(error),
);
});
}
}

View File

@@ -0,0 +1,24 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents the AppAuthError type.
*/
export class AppAuthError {
constructor(
public message: string,
public extras?: any,
) {}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/* Global flags that control the behavior of App Auth JS. */
/* Logging turned on ? */
export const IS_LOG = true;
/* Profiling turned on ? */
export const IS_PROFILE = false;

View File

@@ -1,18 +1,17 @@
export {
BaseTokenRequestHandler,
BasicQueryStringUtils,
FetchRequestor,
LocalStorageBackend,
RedirectRequestHandler,
RevokeTokenRequest,
} from '@openid/appauth';
export { AuthorizationRequest } from '@openid/appauth/built/authorization_request';
export { AuthorizationNotifier } from '@openid/appauth/built/authorization_request_handler';
export { AuthorizationServiceConfiguration } from '@openid/appauth/built/authorization_service_configuration';
export {
GRANT_TYPE_AUTHORIZATION_CODE,
GRANT_TYPE_REFRESH_TOKEN,
TokenRequest,
} from '@openid/appauth/built/token_request';
export { TokenResponse } from '@openid/appauth/built/token_response';
export type { LocationLike, StringMap } from '@openid/appauth/built/types';
export * from './authorization_request.js';
export * from './authorization_request_handler.js';
export * from './authorization_response.js';
export * from './authorization_service_configuration.js';
export * from './crypto_utils.js';
export * from './errors.js';
export * from './flags.js';
export * from './logger.js';
export * from './query_string_utils.js';
export * from './redirect_based_handler.js';
export * from './revoke_token_request.js';
export * from './storage.js';
export * from './token_request.js';
export * from './token_request_handler.js';
export * from './token_response.js';
export type * from './types.js';
export * from './xhr.js';

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import { IS_LOG, IS_PROFILE } from './flags.js';
export function log(message: string, ...args: any[]) {
if (IS_LOG) {
const length = args ? args.length : 0;
if (length > 0) {
console.log(message, ...args);
} else {
console.log(message);
}
}
}
// check to see if native support for profiling is available.
const NATIVE_PROFILE_SUPPORT = typeof window !== 'undefined' && !!window.performance && !!console.profile;
/**
* A decorator that can profile a function.
*/
export function profile(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
if (IS_PROFILE) {
return performProfile(target, propertyKey, descriptor);
} else {
// return as-is
return descriptor;
}
}
function performProfile(target: any, propertyKey: string, descriptor: PropertyDescriptor): PropertyDescriptor {
const originalCallable = descriptor.value;
// name must exist
let name = originalCallable.name;
if (!name) {
name = 'anonymous function';
}
if (NATIVE_PROFILE_SUPPORT) {
descriptor.value = function (args: any[]) {
console.profile(name);
const startTime = window.performance.now();
const result = originalCallable.call(this || window, ...args);
const duration = window.performance.now() - startTime;
console.log(`${name} took ${duration} ms`);
console.profileEnd();
return result;
};
} else {
descriptor.value = function (args: any[]) {
log(`Profile start ${name}`);
const start = Date.now();
const result = originalCallable.call(this || window, ...args);
const duration = Date.now() - start;
log(`Profile end ${name} took ${duration} ms.`);
return result;
};
}
return descriptor;
}

View File

@@ -0,0 +1,64 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { LocationLike, StringMap } from './types.js';
/**
* Query String Utilities.
*/
export interface QueryStringUtils {
stringify(input: StringMap): string;
parse(query: LocationLike, useHash?: boolean): StringMap;
parseQueryString(query: string): StringMap;
}
export class BasicQueryStringUtils implements QueryStringUtils {
parse(input: LocationLike, useHash?: boolean) {
if (useHash) {
return this.parseQueryString(input.hash);
} else {
return this.parseQueryString(input.search);
}
}
parseQueryString(query: string): StringMap {
const result: StringMap = {};
// if anything starts with ?, # or & remove it
query = query.trim().replace(/^(\?|#|&)/, '');
const params = query.split('&');
for (let i = 0; i < params.length; i += 1) {
const param = params[i]; // looks something like a=b
const parts = param.split('=');
if (parts.length >= 2) {
const key = decodeURIComponent(parts.shift()!);
const value = parts.length > 0 ? parts.join('=') : null;
if (value) {
result[key] = decodeURIComponent(value);
}
}
}
return result;
}
stringify(input: StringMap) {
const encoded: string[] = [];
for (const key in input) {
if (Object.prototype.hasOwnProperty.call(input, key) && input[key]) {
encoded.push(`${encodeURIComponent(key)}=${encodeURIComponent(input[key])}`);
}
}
return encoded.join('&');
}
}

View File

@@ -0,0 +1,146 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AuthorizationRequest } from './authorization_request.js';
import type { AuthorizationRequestResponse } from './authorization_request_handler.js';
import { AuthorizationRequestHandler } from './authorization_request_handler.js';
import { AuthorizationError, AuthorizationResponse } from './authorization_response.js';
import type { AuthorizationServiceConfiguration } from './authorization_service_configuration.js';
import type { Crypto } from './crypto_utils.js';
import { DefaultCrypto } from './crypto_utils.js';
import { log } from './logger.js';
import { BasicQueryStringUtils } from './query_string_utils.js';
import type { StorageBackend } from './storage.js';
import { LocalStorageBackend } from './storage.js';
import type { LocationLike } from './types.js';
/** key for authorization request. */
const authorizationRequestKey = (handle: string) => {
return `${handle}_appauth_authorization_request`;
};
/** key for authorization service configuration */
const authorizationServiceConfigurationKey = (handle: string) => {
return `${handle}_appauth_authorization_service_configuration`;
};
/** key in local storage which represents the current authorization request. */
const AUTHORIZATION_REQUEST_HANDLE_KEY = 'appauth_current_authorization_request';
/**
* Represents an AuthorizationRequestHandler which uses a standard
* redirect based code flow.
*/
export class RedirectRequestHandler extends AuthorizationRequestHandler {
constructor(
// use the provided storage backend
// or initialize local storage with the default storage backend which
// uses window.localStorage
public storageBackend: StorageBackend = new LocalStorageBackend(),
utils = new BasicQueryStringUtils(),
public locationLike: LocationLike = window.location,
crypto: Crypto = new DefaultCrypto(),
) {
super(utils, crypto);
}
performAuthorizationRequest(configuration: AuthorizationServiceConfiguration, request: AuthorizationRequest) {
const handle = this.crypto.generateRandom(10);
// before you make request, persist all request related data in local storage.
const persisted = Promise.all([
this.storageBackend.setItem(AUTHORIZATION_REQUEST_HANDLE_KEY, handle),
// Calling toJson() adds in the code & challenge when possible
request
.toJson()
.then((result) => this.storageBackend.setItem(authorizationRequestKey(handle), JSON.stringify(result))),
this.storageBackend.setItem(authorizationServiceConfigurationKey(handle), JSON.stringify(configuration.toJson())),
]);
persisted.then(() => {
// make the redirect request
const url = this.buildRequestUrl(configuration, request);
log('Making a request to ', request, url);
this.locationLike.assign(url);
});
}
/**
* Attempts to introspect the contents of storage backend and completes the
* request.
*/
protected completeAuthorizationRequest(): Promise<AuthorizationRequestResponse | null> {
// TODO(rahulrav@): handle authorization errors.
return this.storageBackend.getItem(AUTHORIZATION_REQUEST_HANDLE_KEY).then((handle) => {
if (handle) {
// we have a pending request.
// fetch authorization request, and check state
return (
this.storageBackend
.getItem(authorizationRequestKey(handle))
// requires a corresponding instance of result
// TODO(rahulrav@): check for inconsitent state here
.then((result) => JSON.parse(result!))
.then((json) => new AuthorizationRequest(json))
.then((request) => {
// check redirect_uri and state
const currentUri = `${this.locationLike.origin}${this.locationLike.pathname}`;
const queryParams = this.utils.parse(this.locationLike, true /* use hash */);
const state: string | undefined = queryParams['state'];
const code: string | undefined = queryParams['code'];
const error: string | undefined = queryParams['error'];
log('Potential authorization request ', currentUri, queryParams, state, code, error);
const shouldNotify = state === request.state;
let authorizationResponse: AuthorizationResponse | null = null;
let authorizationError: AuthorizationError | null = null;
if (shouldNotify) {
if (error) {
// get additional optional info.
const errorUri = queryParams['error_uri'];
const errorDescription = queryParams['error_description'];
authorizationError = new AuthorizationError({
error: error,
error_description: errorDescription,
error_uri: errorUri,
state: state,
});
} else {
authorizationResponse = new AuthorizationResponse({ code: code, state: state });
}
// cleanup state
return Promise.all([
this.storageBackend.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY),
this.storageBackend.removeItem(authorizationRequestKey(handle)),
this.storageBackend.removeItem(authorizationServiceConfigurationKey(handle)),
]).then(() => {
log('Delivering authorization response');
return {
request: request,
response: authorizationResponse,
error: authorizationError,
} as AuthorizationRequestResponse;
});
} else {
log('Mismatched request (state and request_uri) dont match.');
return Promise.resolve(null);
}
})
);
} else {
return null;
}
});
}
}

View File

@@ -0,0 +1,73 @@
/* eslint-disable local-rules/umb-class-prefix */
import type { StringMap } from './types.js';
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Supported token types
*/
export type TokenTypeHint = 'refresh_token' | 'access_token';
/**
* Represents the Token Request as JSON.
*/
export interface RevokeTokenRequestJson {
token: string;
token_type_hint?: TokenTypeHint;
client_id?: string;
client_secret?: string;
}
/**
* Represents a revoke token request.
* For more information look at:
* https://tools.ietf.org/html/rfc7009#section-2.1
*/
export class RevokeTokenRequest {
token: string;
tokenTypeHint: TokenTypeHint | undefined;
clientId: string | undefined;
clientSecret: string | undefined;
constructor(request: RevokeTokenRequestJson) {
this.token = request.token;
this.tokenTypeHint = request.token_type_hint;
this.clientId = request.client_id;
this.clientSecret = request.client_secret;
}
/**
* Serializes a TokenRequest to a JavaScript object.
*/
toJson(): RevokeTokenRequestJson {
const json: RevokeTokenRequestJson = { token: this.token };
if (this.tokenTypeHint) {
json['token_type_hint'] = this.tokenTypeHint;
}
if (this.clientId) {
json['client_id'] = this.clientId;
}
if (this.clientSecret) {
json['client_secret'] = this.clientSecret;
}
return json;
}
toStringMap(): StringMap {
const json = this.toJson();
// :(
return json as any;
}
}

View File

@@ -0,0 +1,101 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* A subset of the `Storage` interface which we need for the backends to work.
*
* Essentially removes the indexable properties and readonly properties from
* `Storage` in lib.dom.d.ts. This is so that a custom type can extend it for
* testing.
*/
export interface UnderlyingStorage {
readonly length: number;
clear(): void;
getItem(key: string): string | null;
removeItem(key: string): void;
setItem(key: string, data: string): void;
}
/**
* Asynchronous storage APIs. All methods return a `Promise`.
* All methods take the `DOMString`
* IDL type (as it is the lowest common denominator).
*/
export abstract class StorageBackend {
/**
* When passed a key `name`, will return that key's value.
*/
public abstract getItem(name: string): Promise<string | null>;
/**
* When passed a key `name`, will remove that key from the storage.
*/
public abstract removeItem(name: string): Promise<void>;
/**
* When invoked, will empty all keys out of the storage.
*/
public abstract clear(): Promise<void>;
/**
* The setItem() method of the `StorageBackend` interface,
* when passed a key name and value, will add that key to the storage,
* or update that key's value if it already exists.
*/
public abstract setItem(name: string, value: string): Promise<void>;
}
/**
* A `StorageBackend` backed by `localstorage`.
*/
export class LocalStorageBackend extends StorageBackend {
private storage: UnderlyingStorage;
constructor(storage?: UnderlyingStorage) {
super();
this.storage = storage || window.localStorage;
}
public getItem(name: string): Promise<string | null> {
return new Promise<string | null>((resolve, reject) => {
const value = this.storage.getItem(name);
if (value) {
resolve(value);
} else {
resolve(null);
}
});
}
public removeItem(name: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.storage.removeItem(name);
resolve();
});
}
public clear(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.storage.clear();
resolve();
});
}
public setItem(name: string, value: string): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.storage.setItem(name, value);
resolve();
});
}
}

View File

@@ -0,0 +1,99 @@
/* eslint-disable local-rules/umb-class-prefix */
/* eslint-disable local-rules/exported-string-constant-naming */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { StringMap } from './types.js';
export const GRANT_TYPE_AUTHORIZATION_CODE = 'authorization_code';
export const GRANT_TYPE_REFRESH_TOKEN = 'refresh_token';
/**
* Represents the Token Request as JSON.
*/
export interface TokenRequestJson {
grant_type: string;
code?: string;
refresh_token?: string;
redirect_uri: string;
client_id: string;
extras?: StringMap;
}
/**
* Represents an Access Token request.
* For more information look at:
* https://tools.ietf.org/html/rfc6749#section-4.1.3
*/
export class TokenRequest {
clientId: string;
redirectUri: string;
grantType: string;
code: string | undefined;
refreshToken: string | undefined;
extras: StringMap | undefined;
constructor(request: TokenRequestJson) {
this.clientId = request.client_id;
this.redirectUri = request.redirect_uri;
this.grantType = request.grant_type;
this.code = request.code;
this.refreshToken = request.refresh_token;
this.extras = request.extras;
}
/**
* Serializes a TokenRequest to a JavaScript object.
*/
toJson(): TokenRequestJson {
return {
grant_type: this.grantType,
code: this.code,
refresh_token: this.refreshToken,
redirect_uri: this.redirectUri,
client_id: this.clientId,
extras: this.extras,
};
}
toStringMap(): StringMap {
const map: StringMap = {
grant_type: this.grantType,
client_id: this.clientId,
redirect_uri: this.redirectUri,
};
if (this.code) {
map['code'] = this.code;
}
if (this.refreshToken) {
map['refresh_token'] = this.refreshToken;
}
// copy over extras
if (this.extras) {
for (const extra in this.extras) {
if (
Object.prototype.hasOwnProperty.call(this.extras, extra) &&
!Object.prototype.hasOwnProperty.call(map, extra)
) {
// check before inserting to requestMap
map[extra] = this.extras[extra];
}
}
}
return map;
}
}

View File

@@ -0,0 +1,87 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import type { AuthorizationServiceConfiguration } from './authorization_service_configuration.js';
import { AppAuthError } from './errors.js';
import { BasicQueryStringUtils } from './query_string_utils.js';
import type { QueryStringUtils } from './query_string_utils.js';
import type { RevokeTokenRequest } from './revoke_token_request.js';
import type { TokenRequest } from './token_request.js';
import { TokenError, TokenResponse } from './token_response.js';
import type { TokenErrorJson, TokenResponseJson } from './token_response.js';
import { FetchRequestor, type Requestor } from './xhr.js';
/**
* Represents an interface which can make a token request.
*/
export interface TokenRequestHandler {
/**
* Performs the token request, given the service configuration.
*/
performTokenRequest(configuration: AuthorizationServiceConfiguration, request: TokenRequest): Promise<TokenResponse>;
performRevokeTokenRequest(
configuration: AuthorizationServiceConfiguration,
request: RevokeTokenRequest,
): Promise<boolean>;
}
/**
* The default token request handler.
*/
export class BaseTokenRequestHandler implements TokenRequestHandler {
constructor(
public readonly requestor: Requestor = new FetchRequestor(),
public readonly utils: QueryStringUtils = new BasicQueryStringUtils(),
) {}
private isTokenResponse(response: TokenResponseJson | TokenErrorJson): response is TokenResponseJson {
return (response as TokenErrorJson).error === undefined;
}
performRevokeTokenRequest(
configuration: AuthorizationServiceConfiguration,
request: RevokeTokenRequest,
): Promise<boolean> {
const revokeTokenResponse = this.requestor.xhr<boolean>({
url: configuration.revocationEndpoint,
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: this.utils.stringify(request.toStringMap()),
});
return revokeTokenResponse.then((response) => {
return true;
});
}
performTokenRequest(configuration: AuthorizationServiceConfiguration, request: TokenRequest): Promise<TokenResponse> {
const tokenResponse = this.requestor.xhr<TokenResponseJson | TokenErrorJson>({
url: configuration.tokenEndpoint,
method: 'POST',
dataType: 'json', // adding implicit dataType
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: this.utils.stringify(request.toStringMap()),
});
return tokenResponse.then((response) => {
if (this.isTokenResponse(response)) {
return new TokenResponse(response);
} else {
return Promise.reject<TokenResponse>(new AppAuthError(response.error, new TokenError(response)));
}
});
}
}

View File

@@ -0,0 +1,137 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Represents the access token types.
* For more information see:
* https://tools.ietf.org/html/rfc6749#section-7.1
*/
export type TokenType = 'bearer' | 'mac';
/**
* Represents the TokenResponse as a JSON Object.
*/
export interface TokenResponseJson {
access_token: string;
token_type?: TokenType /* treating token type as optional, as its going to be inferred. */;
expires_in?: string /* lifetime in seconds. */;
refresh_token?: string;
scope?: string;
id_token?: string /* https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse */;
issued_at?: number /* when was it issued ? */;
}
/**
* Represents the possible error codes from the token endpoint.
* For more information look at:
* https://tools.ietf.org/html/rfc6749#section-5.2
*/
export type ErrorType =
| 'invalid_request'
| 'invalid_client'
| 'invalid_grant'
| 'unauthorized_client'
| 'unsupported_grant_type'
| 'invalid_scope';
/**
* Represents the TokenError as a JSON Object.
*/
export interface TokenErrorJson {
error: ErrorType;
error_description?: string;
error_uri?: string;
}
// constants
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds
/**
* Returns the instant of time in seconds.
*/
export const nowInSeconds = () => Math.round(new Date().getTime() / 1000);
/**
* Represents the Token Response type.
* For more information look at:
* https://tools.ietf.org/html/rfc6749#section-5.1
*/
export class TokenResponse {
accessToken: string;
tokenType: TokenType;
expiresIn: number | undefined;
refreshToken: string | undefined;
scope: string | undefined;
idToken: string | undefined;
issuedAt: number;
constructor(response: TokenResponseJson) {
this.accessToken = response.access_token;
this.tokenType = response.token_type || 'bearer';
if (response.expires_in) {
this.expiresIn = parseInt(response.expires_in, 10);
}
this.refreshToken = response.refresh_token;
this.scope = response.scope;
this.idToken = response.id_token;
this.issuedAt = response.issued_at || nowInSeconds();
}
toJson(): TokenResponseJson {
return {
access_token: this.accessToken,
id_token: this.idToken,
refresh_token: this.refreshToken,
scope: this.scope,
token_type: this.tokenType,
issued_at: this.issuedAt,
expires_in: this.expiresIn?.toString(),
};
}
isValid(buffer: number = AUTH_EXPIRY_BUFFER): boolean {
if (this.expiresIn) {
const now = nowInSeconds();
return now < this.issuedAt + this.expiresIn + buffer;
} else {
return true;
}
}
}
/**
* Represents the Token Error type.
* For more information look at:
* https://tools.ietf.org/html/rfc6749#section-5.2
*/
export class TokenError {
error: ErrorType;
errorDescription: string | undefined;
errorUri: string | undefined;
constructor(tokenError: TokenErrorJson) {
this.error = tokenError.error;
this.errorDescription = tokenError.error_description;
this.errorUri = tokenError.error_uri;
}
toJson(): TokenErrorJson {
return {
error: this.error,
error_description: this.errorDescription,
error_uri: this.errorUri,
};
}
}

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface StringMap {
[key: string]: string;
}
/**
* Represents a window.location like object.
*/
export interface LocationLike {
hash: string;
host: string;
origin: string;
hostname: string;
pathname: string;
port: string;
protocol: string;
search: string;
assign(url: string): void;
}
export interface XhrRequestInit extends RequestInit {
url: string;
dataType?: string;
data?: any;
}

View File

@@ -0,0 +1,78 @@
/* eslint-disable local-rules/umb-class-prefix */
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AppAuthError } from './errors.js';
import type { XhrRequestInit } from './types.js';
/**
* An class that abstracts away the ability to make an XMLHttpRequest.
*/
export abstract class Requestor {
abstract xhr<T>(settings: unknown): Promise<T>;
}
/**
* Uses fetch API to make Ajax requests
*/
export class FetchRequestor extends Requestor {
xhr<T>(settings: XhrRequestInit): Promise<T> {
if (!settings.url) {
return Promise.reject(new AppAuthError('A URL must be provided.'));
}
const url: URL = new URL(<string>settings.url);
const requestInit: RequestInit = {};
requestInit.method = settings.method;
requestInit.mode = 'cors';
if (settings.data) {
if (settings.method && settings.method.toUpperCase() === 'POST') {
requestInit.body = <string>settings.data;
} else {
const searchParams = new URLSearchParams(settings.data);
searchParams.forEach((value, key) => {
url.searchParams.append(key, value);
});
}
}
// Set the request headers
requestInit.headers = {};
if (settings.headers) {
requestInit.headers = settings.headers;
}
const isJsonDataType = settings.dataType && settings.dataType.toLowerCase() === 'json';
// Set 'Accept' header value for json requests (Taken from
// https://github.com/jquery/jquery/blob/e0d941156900a6bff7c098c8ea7290528e468cf8/src/ajax.js#L644
// )
if (isJsonDataType) {
(requestInit.headers as any).Accept = 'application/json, text/javascript, */*; q=0.01';
}
return fetch(url.toString(), requestInit).then((response) => {
if (response.status >= 200 && response.status < 300) {
const contentType = response.headers.get('content-type');
if (isJsonDataType || (contentType && contentType.indexOf('application/json') !== -1)) {
return response.json();
} else {
return response.text();
}
} else {
return Promise.reject(new AppAuthError(response.status.toString(), response.statusText));
}
});
}
}

View File

@@ -17,4 +17,5 @@ export {
switchMap,
filter,
startWith,
skip,
} from 'rxjs';

View File

@@ -1,10 +1,6 @@
import type { ManifestBase } from '../types/index.js';
import { isManifestBaseType } from '../type-guards/index.js';
import {
PackageResource,
OpenAPI,
type PackageManifestResponseModel,
} from '@umbraco-cms/backoffice/external/backend-api';
import { OpenAPI, ManifestResource, type ManifestResponseModel } from '@umbraco-cms/backoffice/external/backend-api';
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
@@ -26,7 +22,19 @@ export class UmbServerExtensionRegistrator extends UmbControllerBase {
* @remark Users must have the BACKOFFICE_ACCESS permission to access this method.
*/
public async registerAllExtensions() {
const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifest());
const { data: packages } = await tryExecuteAndNotify(this, ManifestResource.getManifestManifest());
if (packages) {
await this.#loadServerPackages(packages);
}
}
/**
* Registers all private extensions from the server.
* This is used to register all private extensions that are available to the user.
* @remark Users must have the BACKOFFICE_ACCESS permission to access this method.
*/
public async registerPrivateExtensions() {
const { data: packages } = await tryExecuteAndNotify(this, ManifestResource.getManifestManifestPrivate());
if (packages) {
await this.#loadServerPackages(packages);
}
@@ -38,13 +46,13 @@ export class UmbServerExtensionRegistrator extends UmbControllerBase {
* @remark Any user can access this method without any permissions.
*/
public async registerPublicExtensions() {
const { data: packages } = await tryExecuteAndNotify(this, PackageResource.getPackageManifestPublic());
const { data: packages } = await tryExecuteAndNotify(this, ManifestResource.getManifestManifestPublic());
if (packages) {
await this.#loadServerPackages(packages);
}
}
async #loadServerPackages(packages: PackageManifestResponseModel[]) {
async #loadServerPackages(packages: ManifestResponseModel[]) {
const extensions: ManifestBase[] = [];
packages.forEach((p) => {

View File

@@ -6,4 +6,4 @@ export * from './load-manifest-api.function.js';
export * from './load-manifest-element.function.js';
export * from './load-manifest-plain-css.function.js';
export * from './load-manifest-plain-js.function.js';
export * from './types.js';
export type * from './types.js';

View File

@@ -2,7 +2,7 @@ export * from './condition/index.js';
export * from './controller/index.js';
export * from './functions/index.js';
export * from './initializers/index.js';
export * from './models/index.js';
export type * from './models/index.js';
export * from './registry/extension.registry.js';
export * from './type-guards/index.js';
export * from './types/index.js';
export type * from './types/index.js';

View File

@@ -274,6 +274,22 @@ describe('UmbLocalizeController', () => {
});
});
describe('string', () => {
it('should replace words prefixed with a # with translated value', async () => {
const str = '#close';
const str2 = '#logout #close';
const str3 = '#logout #missing_translation_key #close';
expect(controller.string(str)).to.equal('Close');
expect(controller.string(str2)).to.equal('Log out Close');
expect(controller.string(str3)).to.equal('Log out #missing_translation_key Close');
});
it('should return the word with a # if the word is not found', async () => {
const str = '#missing_translation_key';
expect(controller.string(str)).to.equal('#missing_translation_key');
});
});
describe('host element', () => {
let element: UmbLocalizeControllerHostElement;

View File

@@ -159,4 +159,21 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
relativeTime(value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions): string {
return new Intl.RelativeTimeFormat(this.lang(), options).format(value, unit);
}
string(text: string): string {
// find all words starting with #
const regex = /#\w+/g;
const localizedText = text.replace(regex, (match: string) => {
const key = match.slice(1);
// TODO: find solution to pass dynamic string to term
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const localized = this.term(key);
// we didn't find a localized string, so we return the original string with the #
return localized === key ? match : localized;
});
return localizedText;
}
}

View File

@@ -90,11 +90,11 @@ switch (import.meta.env.VITE_UMBRACO_INSTALL_STATUS) {
switch (import.meta.env.VITE_UMBRACO_EXTENSION_MOCKS) {
case 'on':
handlers.push(manifestsHandlers.manifestDevelopmentHandler);
handlers.push(...manifestsHandlers.manifestDevelopmentHandlers);
break;
default:
handlers.push(manifestsHandlers.manifestEmptyHandler);
handlers.push(...manifestsHandlers.manifestEmptyHandlers);
}
export { handlers };

View File

@@ -20,7 +20,7 @@ import { handlers as configHandlers } from './handlers/config.handlers.js';
export const handlers = [
serverHandlers.serverRunningHandler,
serverHandlers.serverInformationHandler,
manifestsHandlers.manifestEmptyHandler,
...manifestsHandlers.manifestEmptyHandlers,
...auditLogHandlers,
...installHandlers,
...upgradeHandlers,

View File

@@ -3,107 +3,159 @@ const { rest } = window.MockServiceWorker;
import type { PackageManifestResponse } from '../../packages/packages/types.js';
import { umbracoPath } from '@umbraco-cms/backoffice/utils';
export const manifestDevelopmentHandler = rest.get(umbracoPath('/package/manifest'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PackageManifestResponse>([
{
name: 'My Package Name',
version: '1.0.0',
extensions: [
{
type: 'bundle',
alias: 'My.Package.Bundle',
name: 'My Package Bundle',
js: '/App_Plugins/custom-bundle-package/index.js',
},
],
},
{
name: 'Named Package',
version: '1.0.0',
extensions: [
{
type: 'section',
alias: 'My.Section.Custom',
name: 'Custom Section',
js: '/App_Plugins/section.js',
elementName: 'my-section-custom',
weight: 1,
meta: {
label: 'Custom',
pathname: 'my-custom',
export const manifestDevelopmentHandlers = [
rest.get(umbracoPath('/manifest/manifest/private'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PackageManifestResponse>([
{
name: 'My Package Name',
version: '1.0.0',
extensions: [
{
type: 'bundle',
alias: 'My.Package.Bundle',
name: 'My Package Bundle',
js: '/App_Plugins/custom-bundle-package/index.js',
},
},
{
type: 'propertyEditorUi',
alias: 'My.PropertyEditorUI.Custom',
name: 'My Custom Property Editor UI',
js: '/App_Plugins/property-editor.js',
elementName: 'my-property-editor-ui-custom',
meta: {
label: 'My Custom Property',
icon: 'document',
group: 'Common',
propertyEditorSchema: 'Umbraco.TextBox',
],
},
{
name: 'Named Package',
version: '1.0.0',
extensions: [
{
type: 'section',
alias: 'My.Section.Custom',
name: 'Custom Section',
js: '/App_Plugins/section.js',
elementName: 'my-section-custom',
weight: 1,
meta: {
label: 'Custom',
pathname: 'my-custom',
},
},
},
],
},
{
name: 'Package with an entry point',
extensions: [
{
type: 'entryPoint',
name: 'My Custom Entry Point',
alias: 'My.Entrypoint.Custom',
js: '/App_Plugins/custom-entrypoint.js',
},
],
},
{
name: 'Package with a view',
extensions: [
{
type: 'packageView',
alias: 'My.PackageView.Custom',
name: 'My Custom Package View',
js: '/App_Plugins/package-view.js',
meta: {
packageName: 'Package with a view',
{
type: 'propertyEditorUi',
alias: 'My.PropertyEditorUI.Custom',
name: 'My Custom Property Editor UI',
js: '/App_Plugins/property-editor.js',
elementName: 'my-property-editor-ui-custom',
meta: {
label: 'My Custom Property',
icon: 'document',
group: 'Common',
propertyEditorSchema: 'Umbraco.TextBox',
},
},
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.Google',
name: 'My Custom Google MFA Provider',
forProviderName: 'Google Authenticator',
},
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.SMS',
name: 'My Custom SMS MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
],
},
{
name: 'Package with an entry point',
extensions: [
{
type: 'entryPoint',
name: 'My Custom Entry Point',
alias: 'My.Entrypoint.Custom',
js: '/App_Plugins/custom-entrypoint.js',
},
},
],
},
]),
);
});
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.Google',
name: 'My Custom Google MFA Provider',
forProviderName: 'Google Authenticator',
},
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom.SMS',
name: 'My Custom SMS MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
{
name: 'Package with a view',
extensions: [
{
type: 'packageView',
alias: 'My.PackageView.Custom',
name: 'My Custom Package View',
js: '/App_Plugins/package-view.js',
meta: {
packageName: 'Package with a view',
},
},
],
},
{
name: 'My MFA Package',
extensions: [
{
type: 'mfaLoginProvider',
alias: 'My.MfaLoginProvider.Custom',
name: 'My Custom MFA Provider',
forProviderName: 'sms',
meta: {
label: 'Setup SMS Verification',
},
},
],
},
]),
);
}),
rest.get(umbracoPath('/manifest/manifest/public'), (_req, res, ctx) => {
return res(
ctx.status(200),
ctx.json<PackageManifestResponse>([
{
name: 'My Auth Package',
extensions: [
{
type: 'authProvider',
alias: 'My.AuthProvider.Google',
name: 'My Custom Auth Provider',
forProviderName: 'Umbraco.Google',
meta: {
label: 'Sign in with Google',
},
},
{
type: 'authProvider',
alias: 'My.AuthProvider.Github',
name: 'My Github Auth Provider',
forProviderName: 'Umbraco.Github',
meta: {
label: 'GitHub',
defaultView: {
look: 'primary',
icon: 'icon-github',
color: 'success',
},
},
},
],
},
]),
);
}),
];
export const manifestEmptyHandler = rest.get(umbracoPath('/package/manifest'), (_req, res, ctx) => {
return res(
// Respond with a 200 status code
ctx.status(200),
ctx.json<PackageManifestResponse>([]),
);
});
export const manifestEmptyHandlers = [
rest.get(umbracoPath('/manifest/manifest/private'), (_req, res, ctx) => {
return res(ctx.status(200), ctx.json<PackageManifestResponse>([]));
}),
rest.get(umbracoPath('/manifest/manifest/public'), (_req, res, ctx) => {
return res(ctx.status(200), ctx.json<PackageManifestResponse>([]));
}),
];

View File

@@ -21,7 +21,7 @@ export const manifests: Array<ManifestTypes> = [
name: 'Save Block Grid Area Type Workspace Action',
api: UmbSubmitWorkspaceAction,
meta: {
label: 'Submit',
label: '#general_submit',
look: 'primary',
color: 'positive',
},

View File

@@ -9,7 +9,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
js: () => import('./settings.element.js'),
weight: 1000,
meta: {
label: 'Settings',
label: '#general_settings',
pathname: 'settings',
icon: 'icon-settings',
},

View File

@@ -9,7 +9,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
js: () => import('./block-grid-type-workspace-view-settings.element.js'),
weight: 1000,
meta: {
label: 'Settings',
label: '#general_settings',
pathname: 'settings',
icon: 'icon-settings',
},
@@ -27,7 +27,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
js: () => import('./block-grid-type-workspace-view-areas.element.js'),
weight: 1000,
meta: {
label: 'Areas',
label: '#blockEditor_areas',
pathname: 'areas',
icon: 'icon-grid',
},
@@ -45,7 +45,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
js: () => import('./block-grid-type-workspace-view-advanced.element.js'),
weight: 1000,
meta: {
label: 'Advanced',
label: '#blockEditor_tabAreas',
pathname: 'advanced',
icon: 'icon-wrench',
},

View File

@@ -9,7 +9,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
js: () => import('./block-list-type-workspace-view.element.js'),
weight: 1000,
meta: {
label: 'Settings',
label: '#blockEditor_tabBlockSettings',
pathname: 'settings',
icon: 'icon-settings',
},

View File

@@ -9,7 +9,7 @@ export const workspaceViews: Array<ManifestWorkspaceView> = [
js: () => import('./block-rte-type-workspace-view.element.js'),
weight: 1000,
meta: {
label: 'Settings',
label: '#general_settings',
pathname: 'settings',
icon: 'icon-settings',
},

View File

@@ -12,7 +12,7 @@ export const manifests: Array<ManifestWorkspaceActions> = [
name: 'Save Block Type Workspace Action',
api: UmbSubmitWorkspaceAction,
meta: {
label: 'Submit',
label: '#general_submit',
look: 'primary',
color: 'positive',
},

View File

@@ -10,7 +10,7 @@ export const manifests: Array<ManifestTypes> = [
name: 'Save Block Type Workspace Action',
api: UmbSubmitWorkspaceAction,
meta: {
label: 'Submit',
label: '#general_submit',
look: 'primary',
color: 'positive',
},
@@ -38,7 +38,7 @@ export const manifests: Array<ManifestTypes> = [
js: () => import('./views/edit/block-workspace-view-edit.element.js'),
weight: 1000,
meta: {
label: 'Content',
label: '#general_content',
pathname: 'content',
icon: 'icon-document',
blockElementManagerName: 'content',
@@ -63,7 +63,7 @@ export const manifests: Array<ManifestTypes> = [
js: () => import('./views/edit/block-workspace-view-edit.element.js'),
weight: 1000,
meta: {
label: 'Settings',
label: '#general_settings',
pathname: 'settings',
icon: 'icon-settings',
blockElementManagerName: 'settings',

View File

@@ -89,6 +89,7 @@ export class UmbAuthFlow {
// state
readonly #configuration: AuthorizationServiceConfiguration;
readonly #redirectUri: string;
readonly #postLogoutRedirectUri: string;
readonly #clientId: string;
readonly #scope: string;
@@ -99,10 +100,12 @@ export class UmbAuthFlow {
constructor(
openIdConnectUrl: string,
redirectUri: string,
postLogoutRedirectUri: string,
clientId = 'umbraco-back-office',
scope = 'offline_access',
) {
this.#redirectUri = redirectUri;
this.#postLogoutRedirectUri = postLogoutRedirectUri;
this.#clientId = clientId;
this.#scope = scope;
@@ -187,15 +190,23 @@ export class UmbAuthFlow {
}
/**
* This method will make an authorization request to the server.
* Make an authorization request to the server using the specified identity provider.
* This method will redirect the user to the authorization endpoint of the server.
*
* @param username The username to use for the authorization request. It will be provided to the OpenID server as a hint.
* @param identityProvider The identity provider to use for the authorization request.
* @param usernameHint (Optional) The username to use for the authorization request. It will be provided to the OpenID server as a hint.
*/
makeAuthorizationRequest(username?: string): void {
makeAuthorizationRequest(identityProvider: string, usernameHint?: string): void {
const extras: StringMap = { prompt: 'consent', access_type: 'offline' };
if (username) {
extras['login_hint'] = username;
// If the identity provider is not 'Umbraco', we will add it to the extras.
if (identityProvider !== 'Umbraco') {
extras['identity_provider'] = identityProvider;
}
// If there is a username hint, we will add it to the extras.
if (usernameHint) {
extras['login_hint'] = usernameHint;
}
// create a request
@@ -275,7 +286,7 @@ export class UmbAuthFlow {
// which will redirect the user back to the client
// and the client will then try and log in again (if the user is not logged in)
// which will redirect the user to the login page
location.href = `${this.#configuration.endSessionEndpoint}?post_logout_redirect_uri=${this.#redirectUri}`;
location.href = `${this.#configuration.endSessionEndpoint}?post_logout_redirect_uri=${this.#postLogoutRedirectUri}`;
}
/**

View File

@@ -1,3 +1,4 @@
import { umbExtensionsRegistry } from '../extension-registry/index.js';
import { UmbAuthFlow } from './auth-flow.js';
import { UMB_AUTH_CONTEXT } from './auth.context.token.js';
import type { UmbOpenApiConfiguration } from './models/openApiConfiguration.js';
@@ -5,11 +6,15 @@ import { OpenAPI } from '@umbraco-cms/backoffice/external/backend-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbBooleanState } from '@umbraco-cms/backoffice/observable-api';
import { ReplaySubject, filter, switchMap } from '@umbraco-cms/backoffice/external/rxjs';
export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
#isAuthorized = new UmbBooleanState<boolean>(false);
readonly isAuthorized = this.#isAuthorized.asObservable();
#isInitialized = new ReplaySubject<boolean>(1);
readonly isInitialized = this.#isInitialized.asObservable().pipe(filter((isInitialized) => isInitialized));
#isBypassed = false;
#serverUrl;
#backofficePath;
@@ -21,14 +26,16 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
this.#serverUrl = serverUrl;
this.#backofficePath = backofficePath;
this.#authFlow = new UmbAuthFlow(serverUrl, this.#getRedirectUrl());
this.#authFlow = new UmbAuthFlow(serverUrl, this.getRedirectUrl(), this.getPostLogoutRedirectUrl());
}
/**
* Initiates the login flow.
* @param identityProvider The provider to use for login. Default is 'Umbraco'.
* @param usernameHint The username hint to use for login.
*/
makeAuthorizationRequest() {
return this.#authFlow.makeAuthorizationRequest();
makeAuthorizationRequest(identityProvider = 'Umbraco', usernameHint?: string) {
return this.#authFlow.makeAuthorizationRequest(identityProvider, usernameHint);
}
/**
@@ -88,6 +95,16 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
return this.#authFlow.clearTokenStorage();
}
/**
* Handles the case where the user has timed out, i.e. the token has expired.
* This will clear the token storage and set the user as unauthorized.
* @memberof UmbAuthContext
*/
timeOut() {
this.clearTokenStorage();
this.#isAuthorized.setValue(false);
}
/**
* Signs the user out by removing any tokens from the browser.
* @memberof UmbAuthContext
@@ -141,7 +158,19 @@ export class UmbAuthContext extends UmbContextBase<UmbAuthContext> {
};
}
#getRedirectUrl() {
setInitialized() {
this.#isInitialized.next(true);
}
getAuthProviders() {
return this.isInitialized.pipe(switchMap(() => umbExtensionsRegistry.byType('authProvider')));
}
getRedirectUrl() {
return `${window.location.origin}${this.#backofficePath}`;
}
getPostLogoutRedirectUrl() {
return `${window.location.origin}${this.#backofficePath.endsWith('/') ? this.#backofficePath : this.#backofficePath + '/'}logout`;
}
}

View File

@@ -0,0 +1,55 @@
import type { ManifestAuthProvider } from '../../extension-registry/models/index.js';
import type { UmbAuthProviderDefaultProps } from '../types.js';
import { UmbLitElement } from '../../lit-element/lit-element.element.js';
import { UmbTextStyles } from '../../style/index.js';
import { css, customElement, html, nothing, property } from '@umbraco-cms/backoffice/external/lit';
@customElement('umb-auth-provider-default')
export class UmbAuthProviderDefaultElement extends UmbLitElement implements UmbAuthProviderDefaultProps {
@property({ attribute: false })
manifest!: ManifestAuthProvider;
@property({ attribute: false })
onSubmit!: (providerName: string, loginHint?: string) => void;
connectedCallback(): void {
super.connectedCallback();
this.setAttribute('part', 'auth-provider-default');
}
render() {
return html`
<uui-button
type="button"
@click=${() => this.onSubmit(this.manifest.forProviderName)}
id="auth-provider-button"
.label=${this.manifest.meta?.label ?? this.manifest.forProviderName}
.look=${this.manifest.meta?.defaultView?.look ?? 'outline'}
.color=${this.manifest.meta?.defaultView?.color ?? 'default'}>
${this.manifest.meta?.defaultView?.icon
? html`<uui-icon .name=${this.manifest.meta?.defaultView?.icon}></uui-icon>`
: nothing}
${this.manifest.meta?.label ?? this.manifest.forProviderName}
</uui-button>
`;
}
static styles = [
UmbTextStyles,
css`
:host {
display: block;
}
#auth-provider-button {
width: 100%;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
'umb-auth-provider-default': UmbAuthProviderDefaultElement;
}
}

View File

@@ -0,0 +1 @@
export * from './auth-provider-default.element.js';

View File

@@ -1,3 +1,9 @@
import './components/index.js';
export * from './auth.context.js';
export * from './auth.context.token.js';
export * from './modals/index.js';
export * from './models/openApiConfiguration.js';
export * from './components/index.js';
export type * from './types.js';

View File

@@ -0,0 +1,5 @@
import type { ManifestTypes } from '../extension-registry/models/index.js';
import { manifests as modalManifests } from './modals/manifests.js';
import { manifests as providerManifests } from './providers/manifests.js';
export const manifests: Array<ManifestTypes> = [...modalManifests, ...providerManifests];

View File

@@ -0,0 +1 @@
export * from './umb-app-auth-modal.token.js';

View File

@@ -0,0 +1,10 @@
import type { ManifestModal } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestModal> = [
{
type: 'modal',
alias: 'Umb.Modal.AppAuth',
name: 'Umb App Auth Modal',
js: () => import('./umb-app-auth-modal.element.js'),
},
];

View File

@@ -0,0 +1,92 @@
import { UmbModalBaseElement } from '../../modal/index.js';
import type { UmbModalAppAuthConfig, UmbModalAppAuthValue } from './umb-app-auth-modal.token.js';
import { css, customElement, html } from '@umbraco-cms/backoffice/external/lit';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
@customElement('umb-app-auth-modal')
export class UmbAppAuthModalElement extends UmbModalBaseElement<UmbModalAppAuthConfig, UmbModalAppAuthValue> {
get props() {
return {
userLoginState: this.data?.userLoginState ?? 'loggingIn',
onSubmit: this.onSubmit,
};
}
get headline() {
return this.data?.userLoginState === 'timedOut'
? this.localize.term('login_instruction')
: this.localize.term(
[
'login_greeting0',
'login_greeting1',
'login_greeting2',
'login_greeting3',
'login_greeting4',
'login_greeting5',
'login_greeting6',
][new Date().getDay()],
);
}
render() {
return html`
<umb-body-layout id="login-layout">
<h1 id="greeting" slot="header">${this.headline}</h1>
${this.data?.userLoginState === 'timedOut'
? html`<p style="margin-top:0">${this.localize.term('login_timeout')}</p>`
: ''}
<umb-extension-slot
id="providers"
type="authProvider"
default-element="umb-auth-provider-default"
.props=${this.props}></umb-extension-slot>
</umb-body-layout>
`;
}
private onSubmit = (providerName: string) => {
this.value = { providerName };
this._submitModal();
};
static styles = [
UmbTextStyles,
css`
:host {
display: block;
--umb-body-layout-color-background: #fff;
}
#login-layout {
width: 380px;
max-width: 80vw;
min-height: 327px;
}
#greeting {
width: 100%;
color: var(--umb-login-header-color, var(--uui-color-interactive));
text-align: center;
font-weight: 400;
font-size: var(--umb-login-header-font-size, 2rem);
line-height: 1.2;
margin: 0;
}
#providers {
display: flex;
flex-direction: column;
gap: var(--uui-size-space-5);
}
`,
];
}
export default UmbAppAuthModalElement;
declare global {
interface HTMLElementTagNameMap {
'umb-app-auth-modal': UmbAppAuthModalElement;
}
}

View File

@@ -0,0 +1,26 @@
import { UmbModalToken } from '../../modal/token/index.js';
import type { UmbUserLoginState } from '../types.js';
export type UmbModalAppAuthConfig = {
userLoginState: UmbUserLoginState;
};
export type UmbModalAppAuthValue = {
/**
* The name of the provider that the user has selected to authenticate with.
* @required
*/
providerName?: string;
/**
* The login hint that the user has provided to the provider.
* @optional
*/
loginHint?: string;
};
export const UMB_MODAL_APP_AUTH = new UmbModalToken<UmbModalAppAuthConfig, UmbModalAppAuthValue>('Umb.Modal.AppAuth', {
modal: {
type: 'dialog',
},
});

View File

@@ -0,0 +1,18 @@
import type { ManifestAuthProvider } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestAuthProvider> = [
{
type: 'authProvider',
alias: 'Umb.AuthProviders.Umbraco',
name: 'Umbraco login provider',
forProviderName: 'Umbraco',
weight: 1000,
meta: {
label: 'Sign in with Umbraco',
defaultView: {
icon: 'icon-umbraco',
look: 'primary',
},
},
},
];

View File

@@ -0,0 +1,26 @@
import type { ManifestAuthProvider } from '../extension-registry/index.js';
/**
* User login state that can be used to determine the current state of the user.
* @example 'loggedIn'
*/
export type UmbUserLoginState = 'loggingIn' | 'loggedOut' | 'timedOut';
export interface UmbAuthProviderDefaultProps {
/**
* The manifest for the registered provider.
*/
manifest?: ManifestAuthProvider;
/**
* The current user login state.
*/
userLoginState?: UmbUserLoginState;
/**
* Callback that is called when the user selects a provider.
* @param providerName The name of the provider that the user selected.
* @param loginHint The login hint to use for login if available.
*/
onSubmit: (providerName: string, loginHint?: string) => void;
}

View File

@@ -1,9 +1,9 @@
import { expect } from '@open-wc/testing';
import type { ManifestCollectionView } from '../extension-registry/models/index.js';
import { umbExtensionsRegistry } from '../extension-registry/index.js';
import { UmbCollectionViewManager } from './collection-view.manager.js';
import { Observable } from '@umbraco-cms/backoffice/external/rxjs';
import { UmbControllerHostElementMixin } from '@umbraco-cms/backoffice/controller-api';
import type { ManifestCollectionView } from '@umbraco-cms/backoffice/extension-registry';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import { customElement } from '@umbraco-cms/backoffice/external/lit';
@customElement('test-my-controller-host')

View File

@@ -1,6 +1,6 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import type { CSSResultGroup } from '@umbraco-cms/backoffice/external/lit';
import { css, html, repeat, customElement, state, nothing } from '@umbraco-cms/backoffice/external/lit';
import { css, html, repeat, customElement, state, nothing, property } from '@umbraco-cms/backoffice/external/lit';
import type { UmbModalManagerContext, UmbModalContext } from '@umbraco-cms/backoffice/modal';
import { UMB_MODAL_MANAGER_CONTEXT, UmbModalElement } from '@umbraco-cms/backoffice/modal';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
@@ -13,6 +13,9 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
@state()
_modals: Array<UmbModalContext> = [];
@property({ reflect: true, attribute: 'fill-background' })
fillBackground = false;
private _modalManager?: UmbModalManagerContext;
constructor() {
@@ -35,6 +38,7 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
/** We cannot render the umb-modal element directly in the uui-modal-container because it wont get recognized by UUI.
* We therefore have a helper class which creates the uui-modal element and returns it. */
#createModalElements(modals: Array<UmbModalContext>) {
this.removeAttribute('fill-background');
const oldValue = this._modals;
this._modals = modals;
@@ -61,6 +65,15 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
modal.addEventListener('umb:destroy', this.#onCloseEnd.bind(this, modal.key));
this._modalElementMap.set(modal.key, modalElement);
// If any of the modals are fillBackground, set the fillBackground property to true
if (modal.backdropBackground) {
this.fillBackground = true;
this.shadowRoot
?.getElementById('container')
?.style.setProperty('--backdrop-background', modal.backdropBackground);
}
this.requestUpdate();
});
}
@@ -78,13 +91,13 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
render() {
return html`
<uui-modal-container>
<uui-modal-container id="container">
${this._modals.length > 0
? repeat(
this._modals,
(modal) => modal.key,
(modal) => this.#renderModal(modal.key),
)
)
: ''}
</uui-modal-container>
`;
@@ -97,6 +110,10 @@ export class UmbBackofficeModalContainerElement extends UmbLitElement {
position: absolute;
z-index: 1000;
}
:host([fill-background]) #container::after {
background: var(--backdrop-background);
}
`,
];
}

View File

@@ -134,7 +134,7 @@ export class UmbBodyLayoutElement extends LitElement {
css`
:host {
display: flex;
background-color: var(--uui-color-background);
background-color: var(--umb-body-layout-color-background, var(--uui-color-background));
width: 100%;
height: 100%;
flex-direction: column;

View File

@@ -11,7 +11,7 @@ export const contentTypeDesignEditorManifest: UmbBackofficeManifestKind = {
element: () => import('./content-type-design-editor.element.js'),
weight: 1000,
meta: {
label: 'Design',
label: '#general_design',
pathname: 'design',
icon: 'icon-document-dashed-line',
},

View File

@@ -11,11 +11,11 @@ export const manifest: UmbBackofficeManifestKind = {
type: 'entityAction',
kind: 'delete',
api: () => import('./delete.action.js'),
weight: 900,
weight: 1100,
forEntityTypes: [],
meta: {
icon: 'icon-trash',
label: 'Delete...',
label: '#actions_delete',
itemRepositoryAlias: '',
detailRepositoryAlias: '',
},

View File

@@ -15,7 +15,7 @@ export const manifest: UmbBackofficeManifestKind = {
forEntityTypes: [],
meta: {
icon: 'icon-documents',
label: 'Duplicate to...',
label: '#actions_copy',
itemRepositoryAlias: '',
duplicateRepositoryAlias: '',
pickerModal: '',

View File

@@ -2,4 +2,3 @@ export * from './duplicate/duplicate.action.js';
export * from './delete/delete.action.js';
export * from './move/move.action.js';
export * from './sort-children-of/sort-children-of.action.js';
export * from './trash/trash.action.js';

View File

@@ -15,7 +15,7 @@ export const manifest: UmbBackofficeManifestKind = {
forEntityTypes: [],
meta: {
icon: 'icon-enter',
label: 'Move to (TBD)...',
label: '#actions_move',
itemRepositoryAlias: '',
moveRepositoryAlias: '',
pickerModal: '',

View File

@@ -15,7 +15,7 @@ export const manifest: UmbBackofficeManifestKind = {
forEntityTypes: [],
meta: {
icon: 'icon-height',
label: 'Sort Children...',
label: '#actions_sort',
itemRepositoryAlias: '',
sortRepositoryAlias: '',
},

View File

@@ -1,34 +0,0 @@
import { UmbEntityActionBase } from '../../entity-action-base.js';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
//import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
export class UmbTrashEntityAction extends UmbEntityActionBase<any> {
constructor(host: UmbControllerHost, args: any) {
super(host, args);
}
async execute() {
console.log(`execute trash for: ${this.args.unique}`);
/*
if (!this.unique) throw new Error('Unique is not available');
if (!this.repository) return;
const { data } = await this.repository.requestItems([this.unique]);
if (data) {
const item = data[0];
await umbConfirmModal(this._host, {
headline: `Trash ${item.name}`,
content: 'Are you sure you want to move this item to the recycle bin?',
color: 'danger',
confirmLabel: 'Trash',
});
this.repository?.trash(this.unique);
}
*/
}
}
export default UmbTrashEntityAction;

Some files were not shown because too many files have changed in this diff Show More