parent
							
								
									04fbe6fc4d
								
							
						
					
					
						commit
						58eba70c68
					
				| @ -9,6 +9,7 @@ | ||||
|     "@formatjs/intl-getcanonicallocales": "^1.8.0", | ||||
|     "@lit/localize": "^0.11.1", | ||||
|     "@shoelace-style/shoelace": "^2.0.0-beta.61", | ||||
|     "@xstate/fsm": "^1.6.2", | ||||
|     "axios": "^0.22.0", | ||||
|     "color": "^4.0.1", | ||||
|     "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 { state } from "lit/decorators.js"; | ||||
| import { msg, updateWhenLocaleChanges } from "@lit/localize"; | ||||
| import { msg, localized } from "@lit/localize"; | ||||
| 
 | ||||
| import "./shoelace"; | ||||
| import { LocalePicker } from "./components/locale-picker"; | ||||
| import { Alert } from "./components/alert"; | ||||
| import { AccountSettings } from "./components/account-settings"; | ||||
| import { LogInPage } from "./pages/log-in"; | ||||
| import { MyAccountPage } from "./pages/my-account"; | ||||
| import { ArchivePage } from "./pages/archive-info"; | ||||
| @ -18,11 +20,12 @@ const ROUTES = { | ||||
|   home: "/", | ||||
|   login: "/log-in", | ||||
|   myAccount: "/my-account", | ||||
|   accountSettings: "/account/settings", | ||||
|   "archive-info": "/archive/:aid", | ||||
|   "archive-info-tab": "/archive/:aid/:tab", | ||||
| } as const; | ||||
| 
 | ||||
| // ===========================================================================
 | ||||
| @localized() | ||||
| export class App extends LiteElement { | ||||
|   router: APIRouter; | ||||
| 
 | ||||
| @ -39,11 +42,6 @@ export class App extends LiteElement { | ||||
|   constructor() { | ||||
|     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"); | ||||
|     if (authState) { | ||||
|       this.authState = JSON.parse(authState); | ||||
| @ -96,7 +94,7 @@ export class App extends LiteElement { | ||||
|         ${this.renderNavBar()} | ||||
|         <main class="relative flex-auto flex">${this.renderPage()}</main> | ||||
|         <footer class="flex justify-center p-4 border-t"> | ||||
|           <locale-picker></locale-picker> | ||||
|           <bt-locale-picker></bt-locale-picker> | ||||
|         </footer> | ||||
|       </div> | ||||
|     `;
 | ||||
| @ -122,7 +120,11 @@ export class App extends LiteElement { | ||||
|                   ></span> | ||||
|                 </div> | ||||
|                 <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}" | ||||
|                     >${msg("Log Out")}</sl-menu-item | ||||
|                   > | ||||
| @ -155,7 +157,7 @@ export class App extends LiteElement { | ||||
|             ${navLink({ href: "/users", label: "Users" })} | ||||
|           </ul> | ||||
|         </nav> | ||||
|         ${template} | ||||
|         <div class="p-4 md:p-8 flex-1">${template}</div> | ||||
|       </div> | ||||
|     `;
 | ||||
| 
 | ||||
| @ -187,6 +189,14 @@ export class App extends LiteElement { | ||||
|           .authState="${this.authState}" | ||||
|         ></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-tab": | ||||
|         return appLayout(html`<btrix-archive
 | ||||
| @ -208,13 +218,19 @@ export class App extends LiteElement { | ||||
|     this.navigate("/"); | ||||
|   } | ||||
| 
 | ||||
|   onLoggedIn(event: CustomEvent<{ auth: string; username: string }>) { | ||||
|   onLoggedIn( | ||||
|     event: CustomEvent<{ api?: boolean; auth: string; username: string }> | ||||
|   ) { | ||||
|     const { detail } = event; | ||||
|     this.authState = { | ||||
|       username: event.detail.username, | ||||
|       headers: { Authorization: event.detail.auth }, | ||||
|       username: detail.username, | ||||
|       headers: { Authorization: detail.auth }, | ||||
|     }; | ||||
|     window.localStorage.setItem("authState", JSON.stringify(this.authState)); | ||||
|     this.navigate(ROUTES.myAccount); | ||||
| 
 | ||||
|     if (!detail.api) { | ||||
|       this.navigate(ROUTES.myAccount); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   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("log-in", LogInPage); | ||||
| customElements.define("my-account", MyAccountPage); | ||||
| customElements.define("btrix-archive", ArchivePage); | ||||
| customElements.define("btrix-archive-configs", ArchiveConfigsPage); | ||||
| customElements.define("btrix-account-settings", AccountSettings); | ||||
|  | ||||
| @ -1,7 +1,10 @@ | ||||
| import { state, property } from "lit/decorators.js"; | ||||
| import { msg, localized } from "@lit/localize"; | ||||
| 
 | ||||
| import LiteElement, { html } from "../utils/LiteElement"; | ||||
| import type { Auth } from "../types/auth"; | ||||
| 
 | ||||
| @localized() | ||||
| export class LogInPage extends LiteElement { | ||||
|   @state() | ||||
|   isLoggingIn: boolean = false; | ||||
| @ -10,16 +13,25 @@ export class LogInPage extends LiteElement { | ||||
|   loginError?: string; | ||||
| 
 | ||||
|   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` | ||||
|       <div class="md:bg-white md:shadow-2xl md:rounded-lg md:px-12 md:py-12"> | ||||
|         <div class="max-w-md"> | ||||
|           <sl-form @sl-submit="${this.onSubmit}"> | ||||
|           <sl-form @sl-submit="${this.onSubmit}" aria-describedby="formError"> | ||||
|             <div class="mb-5"> | ||||
|               <sl-input | ||||
|                 id="username" | ||||
|                 name="username" | ||||
|                 label="Username" | ||||
|                 placeholder="Username" | ||||
|                 label="${msg("Username")}" | ||||
|                 required | ||||
|               > | ||||
|               </sl-input> | ||||
| @ -29,22 +41,22 @@ export class LogInPage extends LiteElement { | ||||
|                 id="password" | ||||
|                 name="password" | ||||
|                 type="password" | ||||
|                 label="Password" | ||||
|                 placeholder="Password" | ||||
|                 label="${msg("Password")}" | ||||
|                 required | ||||
|               > | ||||
|               </sl-input> | ||||
|             </div> | ||||
| 
 | ||||
|             ${formError} | ||||
| 
 | ||||
|             <sl-button | ||||
|               class="w-full" | ||||
|               type="primary" | ||||
|               ?loading=${this.isLoggingIn} | ||||
|               submit | ||||
|               >Log in</sl-button | ||||
|               >${msg("Log in")}</sl-button | ||||
|             > | ||||
|           </sl-form> | ||||
| 
 | ||||
|           <div id="login-error" class="text-red-600">${this.loginError}</div> | ||||
|         </div> | ||||
|       </div> | ||||
|     `;
 | ||||
| @ -72,7 +84,7 @@ export class LogInPage extends LiteElement { | ||||
|     }); | ||||
|     if (resp.status !== 200) { | ||||
|       this.isLoggingIn = false; | ||||
|       this.loginError = "Sorry, invalid credentials"; | ||||
|       this.loginError = msg("Sorry, invalid username or password"); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -14,7 +14,11 @@ const primaryColor = Color(PRIMARY_COLOR); | ||||
| 
 | ||||
| const theme = css` | ||||
|   :root { | ||||
|     /* contextual variables */ | ||||
|     --primary: ${unsafeCSS(PRIMARY_COLOR)}; | ||||
|     --success: var(--sl-color-success-600); | ||||
|     --warning: var(--sl-color-warning-600); | ||||
|     --danger: var(--sl-color-danger-600); | ||||
| 
 | ||||
|     /* | ||||
|      * Theme Tokens | ||||
|  | ||||
| @ -25,8 +25,20 @@ export default class LiteElement extends LitElement { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   async apiFetch(path: string, auth: Auth) { | ||||
|     const resp = await fetch("/api" + path, { headers: auth.headers }); | ||||
|   async apiFetch( | ||||
|     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 === 401) { | ||||
|  | ||||
| @ -24,6 +24,9 @@ function makeTheme() { | ||||
|     colors: { | ||||
|       ...colors.map(makeColorPalette), | ||||
|       primary: `var(--primary)`, | ||||
|       success: `var(--success)`, | ||||
|       warning: `var(--warning)`, | ||||
|       danger: `var(--danger)`, | ||||
|     }, | ||||
|     fontFamily: { | ||||
|       sans: `var(--sl-font-sans)`, | ||||
|  | ||||
| @ -1041,6 +1041,11 @@ | ||||
|   resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d" | ||||
|   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": | ||||
|   version "1.2.0" | ||||
|   resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user