parent
04fbe6fc4d
commit
58eba70c68
@ -9,6 +9,7 @@
|
|||||||
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
"@formatjs/intl-getcanonicallocales": "^1.8.0",
|
||||||
"@lit/localize": "^0.11.1",
|
"@lit/localize": "^0.11.1",
|
||||||
"@shoelace-style/shoelace": "^2.0.0-beta.61",
|
"@shoelace-style/shoelace": "^2.0.0-beta.61",
|
||||||
|
"@xstate/fsm": "^1.6.2",
|
||||||
"axios": "^0.22.0",
|
"axios": "^0.22.0",
|
||||||
"color": "^4.0.1",
|
"color": "^4.0.1",
|
||||||
"lit": "^2.0.0",
|
"lit": "^2.0.0",
|
||||||
|
|||||||
341
frontend/src/components/account-settings.ts
Normal file
341
frontend/src/components/account-settings.ts
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
import { state, query } from "lit/decorators.js";
|
||||||
|
import { msg, localized } from "@lit/localize";
|
||||||
|
import { createMachine, interpret, assign } from "@xstate/fsm";
|
||||||
|
|
||||||
|
import type { AuthState } from "../types/auth";
|
||||||
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
|
import { needLogin } from "../utils/auth";
|
||||||
|
|
||||||
|
type FormContext = {
|
||||||
|
successMessage?: string;
|
||||||
|
serverError?: string;
|
||||||
|
fieldErrors: { [fieldName: string]: string };
|
||||||
|
};
|
||||||
|
type FormSuccessEvent = {
|
||||||
|
type: "SUCCESS";
|
||||||
|
detail: {
|
||||||
|
successMessage?: FormContext["successMessage"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
type FormErrorEvent = {
|
||||||
|
type: "ERROR";
|
||||||
|
detail: {
|
||||||
|
serverError?: FormContext["serverError"];
|
||||||
|
fieldErrors?: FormContext["fieldErrors"];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
type FormEvent =
|
||||||
|
| { type: "EDIT" }
|
||||||
|
| { type: "CANCEL" }
|
||||||
|
| { type: "SUBMIT" }
|
||||||
|
| FormSuccessEvent
|
||||||
|
| FormErrorEvent;
|
||||||
|
|
||||||
|
type FormTypestate =
|
||||||
|
| {
|
||||||
|
value: "readOnly";
|
||||||
|
context: FormContext;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: "editingForm";
|
||||||
|
context: FormContext;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
value: "submittingForm";
|
||||||
|
context: FormContext;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialContext = {
|
||||||
|
fieldErrors: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const machine = createMachine<FormContext, FormEvent, FormTypestate>(
|
||||||
|
{
|
||||||
|
id: "changePasswordForm",
|
||||||
|
initial: "readOnly",
|
||||||
|
context: initialContext,
|
||||||
|
states: {
|
||||||
|
["readOnly"]: {
|
||||||
|
on: {
|
||||||
|
EDIT: {
|
||||||
|
target: "editingForm",
|
||||||
|
actions: "reset",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
["editingForm"]: {
|
||||||
|
on: { CANCEL: "readOnly", SUBMIT: "submittingForm" },
|
||||||
|
},
|
||||||
|
["submittingForm"]: {
|
||||||
|
on: {
|
||||||
|
SUCCESS: {
|
||||||
|
target: "readOnly",
|
||||||
|
actions: "setSucessMessage",
|
||||||
|
},
|
||||||
|
ERROR: {
|
||||||
|
target: "editingForm",
|
||||||
|
actions: "setError",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: {
|
||||||
|
reset: assign(() => initialContext),
|
||||||
|
setSucessMessage: assign((context, event) => ({
|
||||||
|
...context,
|
||||||
|
...(event as FormSuccessEvent).detail,
|
||||||
|
})),
|
||||||
|
setError: assign((context, event) => ({
|
||||||
|
...context,
|
||||||
|
...(event as FormErrorEvent).detail,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
@needLogin
|
||||||
|
@localized()
|
||||||
|
export class AccountSettings extends LiteElement {
|
||||||
|
authState?: AuthState;
|
||||||
|
|
||||||
|
private _stateService = interpret(machine);
|
||||||
|
|
||||||
|
@state()
|
||||||
|
private formState = machine.initialState;
|
||||||
|
|
||||||
|
@query("#newPassword")
|
||||||
|
private newPasswordInput?: HTMLInputElement;
|
||||||
|
|
||||||
|
@query("#confirmNewPassword")
|
||||||
|
private confirmNewPasswordInput?: HTMLInputElement;
|
||||||
|
|
||||||
|
firstUpdated() {
|
||||||
|
this._stateService.subscribe((state) => {
|
||||||
|
this.formState = state;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._stateService.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this._stateService.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPasswordMatch() {
|
||||||
|
const newPassword = this.newPasswordInput!.value;
|
||||||
|
const confirmNewPassword = this.confirmNewPasswordInput!.value;
|
||||||
|
|
||||||
|
if (newPassword === confirmNewPassword) {
|
||||||
|
this.confirmNewPasswordInput!.setCustomValidity("");
|
||||||
|
} else {
|
||||||
|
this.confirmNewPasswordInput!.setCustomValidity(
|
||||||
|
msg("Passwords don't match")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const showForm =
|
||||||
|
this.formState.value === "editingForm" ||
|
||||||
|
this.formState.value === "submittingForm";
|
||||||
|
let successMessage;
|
||||||
|
|
||||||
|
if (this.formState.context.successMessage) {
|
||||||
|
successMessage = html`
|
||||||
|
<div>
|
||||||
|
<bt-alert type="success"
|
||||||
|
>${this.formState.context.successMessage}</bt-alert
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`<div class="grid gap-4">
|
||||||
|
<h1 class="text-xl font-bold">${msg("Account settings")}</h1>
|
||||||
|
|
||||||
|
${successMessage}
|
||||||
|
|
||||||
|
<section class="p-4 md:p-8 border rounded-lg grid gap-6">
|
||||||
|
<div>
|
||||||
|
<div class="mb-1 text-gray-500">Email</div>
|
||||||
|
<div>${this.authState!.username}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${showForm
|
||||||
|
? this.renderChangePasswordForm()
|
||||||
|
: html`
|
||||||
|
<div>
|
||||||
|
<sl-button
|
||||||
|
type="primary"
|
||||||
|
outline
|
||||||
|
@click=${() => this._stateService.send("EDIT")}
|
||||||
|
>${msg("Change password")}</sl-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</section>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderChangePasswordForm() {
|
||||||
|
const passwordFieldError = this.formState.context.fieldErrors.password;
|
||||||
|
let formError;
|
||||||
|
|
||||||
|
if (this.formState.context.serverError) {
|
||||||
|
formError = html`
|
||||||
|
<div class="mb-5">
|
||||||
|
<bt-alert id="formError" type="danger"
|
||||||
|
>${this.formState.context.serverError}</bt-alert
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html` <div class="max-w-sm">
|
||||||
|
<h3 class="font-bold mb-3">${msg("Change password")}</h3>
|
||||||
|
<sl-form @sl-submit="${this.onSubmit}" aria-describedby="formError">
|
||||||
|
<div class="mb-5">
|
||||||
|
<sl-input
|
||||||
|
id="password"
|
||||||
|
class="${passwordFieldError ? "text-danger" : ""}"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
label="${msg("Current password")}"
|
||||||
|
aria-describedby="passwordError"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
</sl-input>
|
||||||
|
${passwordFieldError
|
||||||
|
? html`<div id="passwordError" class="text-danger" role="alert">
|
||||||
|
${passwordFieldError}
|
||||||
|
</div>`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<sl-input
|
||||||
|
id="newPassword"
|
||||||
|
name="newPassword"
|
||||||
|
type="password"
|
||||||
|
label="${msg("New password")}"
|
||||||
|
required
|
||||||
|
@sl-blur=${this.checkPasswordMatch}
|
||||||
|
>
|
||||||
|
</sl-input>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<sl-input
|
||||||
|
id="confirmNewPassword"
|
||||||
|
name="confirmNewPassword"
|
||||||
|
type="password"
|
||||||
|
label="${msg("Confirm new password")}"
|
||||||
|
required
|
||||||
|
@sl-blur=${this.checkPasswordMatch}
|
||||||
|
>
|
||||||
|
</sl-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${formError}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<sl-button
|
||||||
|
type="primary"
|
||||||
|
?loading=${this.formState.value === "submittingForm"}
|
||||||
|
submit
|
||||||
|
>${msg("Update password")}</sl-button
|
||||||
|
>
|
||||||
|
<sl-button
|
||||||
|
type="text"
|
||||||
|
@click=${() => this._stateService.send("CANCEL")}
|
||||||
|
>${msg("Cancel")}</sl-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</sl-form>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onSubmit(event: { detail: { formData: FormData } }) {
|
||||||
|
if (!this.authState) return;
|
||||||
|
|
||||||
|
this._stateService.send("SUBMIT");
|
||||||
|
|
||||||
|
const { formData } = event.detail;
|
||||||
|
let nextAuthState: AuthState = null;
|
||||||
|
|
||||||
|
// Validate current password by generating token
|
||||||
|
try {
|
||||||
|
// TODO consolidate with log-in method
|
||||||
|
const resp = await fetch("/api/auth/jwt/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: "password",
|
||||||
|
username: this.authState.username,
|
||||||
|
password: formData.get("password") as string,
|
||||||
|
}).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (data.token_type === "bearer" && data.access_token) {
|
||||||
|
const detail = {
|
||||||
|
api: true,
|
||||||
|
auth: `Bearer ${data.access_token}`,
|
||||||
|
username: this.authState.username,
|
||||||
|
};
|
||||||
|
this.dispatchEvent(new CustomEvent("logged-in", { detail }));
|
||||||
|
|
||||||
|
nextAuthState = {
|
||||||
|
username: detail.username,
|
||||||
|
headers: {
|
||||||
|
Authorization: detail.auth,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextAuthState) {
|
||||||
|
this._stateService.send({
|
||||||
|
type: "ERROR",
|
||||||
|
detail: {
|
||||||
|
fieldErrors: {
|
||||||
|
password: msg("Wrong password"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
password: formData.get("newPassword"),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.apiFetch("/users/me", nextAuthState, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
|
||||||
|
this._stateService.send({
|
||||||
|
type: "SUCCESS",
|
||||||
|
detail: {
|
||||||
|
successMessage: "Successfully updated password",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
this._stateService.send({
|
||||||
|
type: "ERROR",
|
||||||
|
detail: {
|
||||||
|
serverError: msg("Something went wrong changing password"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
frontend/src/components/alert.ts
Normal file
52
frontend/src/components/alert.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { LitElement, html, css } from "lit";
|
||||||
|
import { property } from "lit/decorators.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alert used inline, e.g. for form server errors
|
||||||
|
*
|
||||||
|
* Usage example:
|
||||||
|
* ```ts
|
||||||
|
* <input aria-describedby="error_message" />
|
||||||
|
* <bt-alert id="error_message>${errorMessage}</bt-alert>
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class Alert extends LitElement {
|
||||||
|
@property({ type: String })
|
||||||
|
type: "success" | "warning" | "danger" | "info" = "info";
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host > div {
|
||||||
|
padding: var(--sl-spacing-x-small) var(--sl-spacing-small);
|
||||||
|
border-radius: var(--sl-border-radius-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
background-color: var(--sl-color-success-50);
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: var(--sl-color-warning-50);
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger {
|
||||||
|
background-color: var(--sl-color-danger-50);
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
background-color: var(--sl-color-sky-50);
|
||||||
|
color: var(--sl-color-sky-600);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
console.log("id:", this.id);
|
||||||
|
return html`
|
||||||
|
<div class="${this.type}" role="alert">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { state } from "lit/decorators.js";
|
import { state } from "lit/decorators.js";
|
||||||
import { msg, updateWhenLocaleChanges } from "@lit/localize";
|
import { msg, localized } from "@lit/localize";
|
||||||
|
|
||||||
import "./shoelace";
|
import "./shoelace";
|
||||||
import { LocalePicker } from "./components/locale-picker";
|
import { LocalePicker } from "./components/locale-picker";
|
||||||
|
import { Alert } from "./components/alert";
|
||||||
|
import { AccountSettings } from "./components/account-settings";
|
||||||
import { LogInPage } from "./pages/log-in";
|
import { LogInPage } from "./pages/log-in";
|
||||||
import { MyAccountPage } from "./pages/my-account";
|
import { MyAccountPage } from "./pages/my-account";
|
||||||
import { ArchivePage } from "./pages/archive-info";
|
import { ArchivePage } from "./pages/archive-info";
|
||||||
@ -18,11 +20,12 @@ const ROUTES = {
|
|||||||
home: "/",
|
home: "/",
|
||||||
login: "/log-in",
|
login: "/log-in",
|
||||||
myAccount: "/my-account",
|
myAccount: "/my-account",
|
||||||
|
accountSettings: "/account/settings",
|
||||||
"archive-info": "/archive/:aid",
|
"archive-info": "/archive/:aid",
|
||||||
"archive-info-tab": "/archive/:aid/:tab",
|
"archive-info-tab": "/archive/:aid/:tab",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// ===========================================================================
|
@localized()
|
||||||
export class App extends LiteElement {
|
export class App extends LiteElement {
|
||||||
router: APIRouter;
|
router: APIRouter;
|
||||||
|
|
||||||
@ -39,11 +42,6 @@ export class App extends LiteElement {
|
|||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
// Note we use updateWhenLocaleChanges here so that we're always up to date with
|
|
||||||
// the active locale (the result of getLocale()) when the locale changes via a
|
|
||||||
// history navigation.
|
|
||||||
updateWhenLocaleChanges(this);
|
|
||||||
|
|
||||||
const authState = window.localStorage.getItem("authState");
|
const authState = window.localStorage.getItem("authState");
|
||||||
if (authState) {
|
if (authState) {
|
||||||
this.authState = JSON.parse(authState);
|
this.authState = JSON.parse(authState);
|
||||||
@ -96,7 +94,7 @@ export class App extends LiteElement {
|
|||||||
${this.renderNavBar()}
|
${this.renderNavBar()}
|
||||||
<main class="relative flex-auto flex">${this.renderPage()}</main>
|
<main class="relative flex-auto flex">${this.renderPage()}</main>
|
||||||
<footer class="flex justify-center p-4 border-t">
|
<footer class="flex justify-center p-4 border-t">
|
||||||
<locale-picker></locale-picker>
|
<bt-locale-picker></bt-locale-picker>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -122,7 +120,11 @@ export class App extends LiteElement {
|
|||||||
></span>
|
></span>
|
||||||
</div>
|
</div>
|
||||||
<sl-menu>
|
<sl-menu>
|
||||||
<sl-menu-item>Your account</sl-menu-item>
|
<sl-menu-item
|
||||||
|
@click=${() => this.navigate(ROUTES.accountSettings)}
|
||||||
|
>
|
||||||
|
${msg("Your account")}
|
||||||
|
</sl-menu-item>
|
||||||
<sl-menu-item @click="${this.onLogOut}"
|
<sl-menu-item @click="${this.onLogOut}"
|
||||||
>${msg("Log Out")}</sl-menu-item
|
>${msg("Log Out")}</sl-menu-item
|
||||||
>
|
>
|
||||||
@ -155,7 +157,7 @@ export class App extends LiteElement {
|
|||||||
${navLink({ href: "/users", label: "Users" })}
|
${navLink({ href: "/users", label: "Users" })}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
${template}
|
<div class="p-4 md:p-8 flex-1">${template}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -187,6 +189,14 @@ export class App extends LiteElement {
|
|||||||
.authState="${this.authState}"
|
.authState="${this.authState}"
|
||||||
></my-account>`);
|
></my-account>`);
|
||||||
|
|
||||||
|
case "accountSettings":
|
||||||
|
return appLayout(html`<btrix-account-settings
|
||||||
|
class="w-full"
|
||||||
|
@navigate="${this.onNavigateTo}"
|
||||||
|
@need-login="${this.onNeedLogin}"
|
||||||
|
.authState="${this.authState}"
|
||||||
|
></btrix-account-settings>`);
|
||||||
|
|
||||||
case "archive-info":
|
case "archive-info":
|
||||||
case "archive-info-tab":
|
case "archive-info-tab":
|
||||||
return appLayout(html`<btrix-archive
|
return appLayout(html`<btrix-archive
|
||||||
@ -208,13 +218,19 @@ export class App extends LiteElement {
|
|||||||
this.navigate("/");
|
this.navigate("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoggedIn(event: CustomEvent<{ auth: string; username: string }>) {
|
onLoggedIn(
|
||||||
|
event: CustomEvent<{ api?: boolean; auth: string; username: string }>
|
||||||
|
) {
|
||||||
|
const { detail } = event;
|
||||||
this.authState = {
|
this.authState = {
|
||||||
username: event.detail.username,
|
username: detail.username,
|
||||||
headers: { Authorization: event.detail.auth },
|
headers: { Authorization: detail.auth },
|
||||||
};
|
};
|
||||||
window.localStorage.setItem("authState", JSON.stringify(this.authState));
|
window.localStorage.setItem("authState", JSON.stringify(this.authState));
|
||||||
this.navigate(ROUTES.myAccount);
|
|
||||||
|
if (!detail.api) {
|
||||||
|
this.navigate(ROUTES.myAccount);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onNeedLogin(event?: CustomEvent<{ api: boolean }>) {
|
onNeedLogin(event?: CustomEvent<{ api: boolean }>) {
|
||||||
@ -236,9 +252,11 @@ export class App extends LiteElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("locale-picker", LocalePicker);
|
customElements.define("bt-alert", Alert);
|
||||||
|
customElements.define("bt-locale-picker", LocalePicker);
|
||||||
customElements.define("browsertrix-app", App);
|
customElements.define("browsertrix-app", App);
|
||||||
customElements.define("log-in", LogInPage);
|
customElements.define("log-in", LogInPage);
|
||||||
customElements.define("my-account", MyAccountPage);
|
customElements.define("my-account", MyAccountPage);
|
||||||
customElements.define("btrix-archive", ArchivePage);
|
customElements.define("btrix-archive", ArchivePage);
|
||||||
customElements.define("btrix-archive-configs", ArchiveConfigsPage);
|
customElements.define("btrix-archive-configs", ArchiveConfigsPage);
|
||||||
|
customElements.define("btrix-account-settings", AccountSettings);
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { state, property } from "lit/decorators.js";
|
import { state, property } from "lit/decorators.js";
|
||||||
|
import { msg, localized } from "@lit/localize";
|
||||||
|
|
||||||
import LiteElement, { html } from "../utils/LiteElement";
|
import LiteElement, { html } from "../utils/LiteElement";
|
||||||
import type { Auth } from "../types/auth";
|
import type { Auth } from "../types/auth";
|
||||||
|
|
||||||
|
@localized()
|
||||||
export class LogInPage extends LiteElement {
|
export class LogInPage extends LiteElement {
|
||||||
@state()
|
@state()
|
||||||
isLoggingIn: boolean = false;
|
isLoggingIn: boolean = false;
|
||||||
@ -10,16 +13,25 @@ export class LogInPage extends LiteElement {
|
|||||||
loginError?: string;
|
loginError?: string;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let formError;
|
||||||
|
|
||||||
|
if (this.loginError) {
|
||||||
|
formError = html`
|
||||||
|
<div class="mb-5">
|
||||||
|
<bt-alert id="formError" type="danger">${this.loginError}</bt-alert>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="md:bg-white md:shadow-2xl md:rounded-lg md:px-12 md:py-12">
|
<div class="md:bg-white md:shadow-2xl md:rounded-lg md:px-12 md:py-12">
|
||||||
<div class="max-w-md">
|
<div class="max-w-md">
|
||||||
<sl-form @sl-submit="${this.onSubmit}">
|
<sl-form @sl-submit="${this.onSubmit}" aria-describedby="formError">
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<sl-input
|
<sl-input
|
||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
label="Username"
|
label="${msg("Username")}"
|
||||||
placeholder="Username"
|
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</sl-input>
|
</sl-input>
|
||||||
@ -29,22 +41,22 @@ export class LogInPage extends LiteElement {
|
|||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
label="Password"
|
label="${msg("Password")}"
|
||||||
placeholder="Password"
|
|
||||||
required
|
required
|
||||||
>
|
>
|
||||||
</sl-input>
|
</sl-input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
${formError}
|
||||||
|
|
||||||
<sl-button
|
<sl-button
|
||||||
class="w-full"
|
class="w-full"
|
||||||
type="primary"
|
type="primary"
|
||||||
?loading=${this.isLoggingIn}
|
?loading=${this.isLoggingIn}
|
||||||
submit
|
submit
|
||||||
>Log in</sl-button
|
>${msg("Log in")}</sl-button
|
||||||
>
|
>
|
||||||
</sl-form>
|
</sl-form>
|
||||||
|
|
||||||
<div id="login-error" class="text-red-600">${this.loginError}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@ -72,7 +84,7 @@ export class LogInPage extends LiteElement {
|
|||||||
});
|
});
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
this.isLoggingIn = false;
|
this.isLoggingIn = false;
|
||||||
this.loginError = "Sorry, invalid credentials";
|
this.loginError = msg("Sorry, invalid username or password");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,11 @@ const primaryColor = Color(PRIMARY_COLOR);
|
|||||||
|
|
||||||
const theme = css`
|
const theme = css`
|
||||||
:root {
|
:root {
|
||||||
|
/* contextual variables */
|
||||||
--primary: ${unsafeCSS(PRIMARY_COLOR)};
|
--primary: ${unsafeCSS(PRIMARY_COLOR)};
|
||||||
|
--success: var(--sl-color-success-600);
|
||||||
|
--warning: var(--sl-color-warning-600);
|
||||||
|
--danger: var(--sl-color-danger-600);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Theme Tokens
|
* Theme Tokens
|
||||||
|
|||||||
@ -25,8 +25,20 @@ export default class LiteElement extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async apiFetch(path: string, auth: Auth) {
|
async apiFetch(
|
||||||
const resp = await fetch("/api" + path, { headers: auth.headers });
|
path: string,
|
||||||
|
auth: Auth,
|
||||||
|
options?: { method?: string; headers?: any; body?: any }
|
||||||
|
) {
|
||||||
|
const { headers, ...opts } = options || {};
|
||||||
|
const resp = await fetch("/api" + path, {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...headers,
|
||||||
|
...auth.headers,
|
||||||
|
},
|
||||||
|
...opts,
|
||||||
|
});
|
||||||
|
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
if (resp.status === 401) {
|
if (resp.status === 401) {
|
||||||
|
|||||||
@ -24,6 +24,9 @@ function makeTheme() {
|
|||||||
colors: {
|
colors: {
|
||||||
...colors.map(makeColorPalette),
|
...colors.map(makeColorPalette),
|
||||||
primary: `var(--primary)`,
|
primary: `var(--primary)`,
|
||||||
|
success: `var(--success)`,
|
||||||
|
warning: `var(--warning)`,
|
||||||
|
danger: `var(--danger)`,
|
||||||
},
|
},
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: `var(--sl-font-sans)`,
|
sans: `var(--sl-font-sans)`,
|
||||||
|
|||||||
@ -1041,6 +1041,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d"
|
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d"
|
||||||
integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==
|
integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==
|
||||||
|
|
||||||
|
"@xstate/fsm@^1.6.2":
|
||||||
|
version "1.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@xstate/fsm/-/fsm-1.6.2.tgz#177a6920ba0d7d8522585641adc42ba59ecd9e36"
|
||||||
|
integrity sha512-vOfiFVQu9mQceA8oJ3PcA4vwhtyo/j/mbVDVIlHDOh3iuiTqMnp805zZ3QsouRdO2Ie3B7n3jMw8BntI74fZxg==
|
||||||
|
|
||||||
"@xtuc/ieee754@^1.2.0":
|
"@xtuc/ieee754@^1.2.0":
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user