Merge pull request #738 from umbraco/feature/login-app

Feature/login app
This commit is contained in:
Jacob Overgaard
2023-05-30 10:26:15 +02:00
committed by GitHub
11 changed files with 181 additions and 59 deletions

View File

@@ -1,5 +1,5 @@
{
"name": "umbraco-backoffice-auth",
"name": "@umbraco-cms/login",
"version": "0.0.0",
"license": "MIT",
"author": {
@@ -7,7 +7,6 @@
"email": "backoffice@umbraco.com"
},
"type": "module",
"main": "dist/main.js",
"module": "dist/main.js",
"files": [
"dist"
@@ -15,7 +14,7 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:production": "tsc && vite build --mode production",
"build:watch": "vite build --watch",
"preview": "vite preview"
}
}

View File

@@ -1,18 +1,14 @@
import { css, CSSResultGroup, html, LitElement, unsafeCSS } from 'lit';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement } from 'lit/decorators.js';
import logoImg from '/umbraco_logomark_white.svg';
import loginImg from '/login.jpeg';
@customElement('umb-auth-layout')
export class UmbAuthLayoutElement extends LitElement {
render() {
return html`
<div id="background"></div>
<div id="logo">
<img src="${logoImg}" alt="Umbraco" />
<img src="umbraco_logomark_white.svg" alt="Umbraco" />
</div>
<div id="container">
@@ -22,7 +18,7 @@ export class UmbAuthLayoutElement extends LitElement {
</div>
`;
}
static styles: CSSResultGroup = [
css`
#background {
@@ -31,7 +27,7 @@ export class UmbAuthLayoutElement extends LitElement {
background-position: 50%;
background-repeat: no-repeat;
background-size: cover;
background-image: url('${unsafeCSS(loginImg)}');
background-image: url('login.jpeg');
width: 100vw;
height: 100vh;
}

View File

@@ -0,0 +1,10 @@
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

@@ -0,0 +1,13 @@
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

@@ -0,0 +1,44 @@
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,4 +0,0 @@
export default function() {
sessionStorage.setItem('is-authenticated', 'true');
history.replaceState(null, '', 'section');
}

View File

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

View File

@@ -1,23 +1,51 @@
import { UUITextStyles } from '@umbraco-cms/backoffice/external/uui';
import { css, CSSResultGroup, html, LitElement } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { ifDefined } from 'lit/directives/if-defined.js';
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 {
@state()
private _loggingIn = false;
#authContext: IUmbAuthContext;
private _handleSubmit = (e: SubmitEvent) => {
@property({ type: String, attribute: 'return-url' })
returnUrl = '';
@property({ type: Boolean })
isLegacy = false;
@state()
private _loginState: UUIButtonState = undefined;
@state()
private _loginError = '';
@state()
private _isFormValid = false;
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;
const isValid = form.checkValidity();
if (!isValid) return;
this._isFormValid = form.checkValidity();
if (!this._isFormValid) return;
const formData = new FormData(form);
@@ -25,49 +53,44 @@ export default class UmbLoginElement extends LitElement {
const password = formData.get('password') as string;
const persist = formData.has('persist');
this._login(username, password, 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;
};
private async _login(username: string, password: string, persist: boolean) {
// TODO: Move login to new login app
this._loggingIn = true;
try {
this._loggingIn = false;
console.log('login');
} catch (error) {
console.log(error);
this._loggingIn = false;
}
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()];
}
private _greetings: Array<string> = [
'Happy super Sunday',
'Happy marvelous Monday',
'Happy tubular Tuesday',
'Happy wonderful Wednesday',
'Happy thunderous Thursday',
'Happy funky Friday',
'Happy Saturday',
];
@state()
private _greeting: string = this._greetings[new Date().getDay()];
render() {
return html`
<umb-auth-layout>
<div class="uui-text">
<h1 class="uui-h3">${this._greeting}</h1>
<h1 class="uui-h3">${this.#greeting}</h1>
<uui-form>
<form id="LoginForm" name="login" @submit="${this._handleSubmit}">
<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"
placeholder="Enter your email..."
label="Email"
required
required-message="Email is required"></uui-input>
</uui-form-layout-item>
@@ -77,21 +100,26 @@ export default class UmbLoginElement extends LitElement {
<uui-input-password
id="password"
name="password"
placeholder="Enter your password..."
label="Password"
required
required-message="Password is required"></uui-input-password>
</uui-form-layout-item>
<uui-form-layout-item>
<uui-checkbox name="persist" label="Remember me"> Remember me </uui-checkbox>
</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
?disabled=${!this._isFormValid}
type="submit"
label="Login"
look="primary"
color="positive"
state=${ifDefined(this._loggingIn ? 'waiting' : undefined)}></uui-button>
state=${this._loginState}></uui-button>
</form>
</uui-form>
</div>
@@ -99,6 +127,12 @@ export default class UmbLoginElement extends LitElement {
`;
}
#renderErrorMessage() {
if (!this._loginError || this._loginState !== 'failed') return nothing;
return html`<p class="text-danger">${this._loginError}</p>`;
}
static styles: CSSResultGroup = [
UUITextStyles,
css`
@@ -106,6 +140,9 @@ export default class UmbLoginElement extends LitElement {
#password {
width: 100%;
}
.text-danger {
color: var(--uui-color-danger-standalone);
}
`,
];
}

View File

@@ -0,0 +1,16 @@
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,7 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./types"
"declaration": false
},
"include": ["src/**/*.ts"],
"references": [

View File

@@ -9,7 +9,18 @@ export default defineConfig({
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/login',
emptyOutDir: true,
},
server: {
fs: {