Merge pull request #738 from umbraco/feature/login-app
Feature/login app
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
13
src/Umbraco.Web.UI.Client/apps/auth/src/auth.context.ts
Normal file
13
src/Umbraco.Web.UI.Client/apps/auth/src/auth.context.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
44
src/Umbraco.Web.UI.Client/apps/auth/src/auth.repository.ts
Normal file
44
src/Umbraco.Web.UI.Client/apps/auth/src/auth.repository.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export default function() {
|
||||
sessionStorage.setItem('is-authenticated', 'true');
|
||||
history.replaceState(null, '', 'section');
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
16
src/Umbraco.Web.UI.Client/apps/auth/src/types.ts
Normal file
16
src/Umbraco.Web.UI.Client/apps/auth/src/types.ts
Normal 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;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./types"
|
||||
"declaration": false
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"references": [
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user