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