parent
ba69ac02bc
commit
d5c665be8e
@ -288,13 +288,25 @@ export class App extends LiteElement {
|
||||
case "join":
|
||||
return html`<btrix-join
|
||||
class="w-full md:bg-gray-100 flex items-center justify-center"
|
||||
@navigate="${this.onNavigateTo}"
|
||||
@logged-in="${this.onLoggedIn}"
|
||||
.authState="${this.authService.authState}"
|
||||
token="${this.viewState.params.token}"
|
||||
email="${this.viewState.params.email}"
|
||||
></btrix-join>`;
|
||||
|
||||
case "acceptInvite":
|
||||
return html`<btrix-accept-invite
|
||||
class="w-full md:bg-gray-100 flex items-center justify-center"
|
||||
@navigate="${this.onNavigateTo}"
|
||||
@logged-in="${this.onLoggedIn}"
|
||||
@notify="${this.onNotify}"
|
||||
.authState="${this.authService.authState}"
|
||||
token="${this.viewState.params.token}"
|
||||
email="${this.viewState.params.email}"
|
||||
></btrix-accept-invite>`;
|
||||
|
||||
case "login":
|
||||
case "loginWithRedirect":
|
||||
case "forgotPassword":
|
||||
return html`<btrix-log-in
|
||||
class="w-full md:bg-gray-100 flex items-center justify-center"
|
||||
@ -302,6 +314,7 @@ export class App extends LiteElement {
|
||||
@logged-in=${this.onLoggedIn}
|
||||
.authState=${this.authService.authState}
|
||||
.viewState=${this.viewState}
|
||||
redirectUrl=${this.viewState.params.redirectUrl}
|
||||
></btrix-log-in>`;
|
||||
|
||||
case "resetPassword":
|
||||
@ -413,7 +426,7 @@ export class App extends LiteElement {
|
||||
});
|
||||
|
||||
if (!detail.api) {
|
||||
this.navigate(DASHBOARD_ROUTE);
|
||||
this.navigate(detail.redirectUrl || DASHBOARD_ROUTE);
|
||||
}
|
||||
|
||||
if (detail.firstLogin) {
|
||||
|
129
frontend/src/pages/accept-invite.ts
Normal file
129
frontend/src/pages/accept-invite.ts
Normal file
@ -0,0 +1,129 @@
|
||||
import { state, property } from "lit/decorators.js";
|
||||
import { msg, localized } from "@lit/localize";
|
||||
|
||||
import LiteElement, { html } from "../utils/LiteElement";
|
||||
import type { AuthState, LoggedInEvent } from "../utils/AuthService";
|
||||
import AuthService from "../utils/AuthService";
|
||||
import { DASHBOARD_ROUTE } from "../routes";
|
||||
|
||||
@localized()
|
||||
export class AcceptInvite extends LiteElement {
|
||||
@property({ type: Object })
|
||||
authState?: AuthState;
|
||||
|
||||
@property({ type: String })
|
||||
token?: string;
|
||||
|
||||
@property({ type: String })
|
||||
email?: string;
|
||||
|
||||
@state()
|
||||
serverError?: string;
|
||||
|
||||
connectedCallback(): void {
|
||||
if (this.token && this.email) {
|
||||
super.connectedCallback();
|
||||
} else {
|
||||
throw new Error("Missing email or token");
|
||||
}
|
||||
}
|
||||
|
||||
private get isLoggedIn(): boolean {
|
||||
return Boolean(
|
||||
this.authState && this.email && this.authState.username === this.email
|
||||
);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (!this.isLoggedIn) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("notify", {
|
||||
detail: {
|
||||
message: msg("Log in to continue."),
|
||||
type: "success",
|
||||
icon: "check2-circle",
|
||||
duration: 10000,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.navTo(
|
||||
`/log-in?redirectUrl=${encodeURIComponent(
|
||||
`${window.location.pathname}${window.location.search}`
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let serverError;
|
||||
|
||||
if (this.serverError) {
|
||||
serverError = html`
|
||||
<div class="mb-5">
|
||||
<btrix-alert id="formError" type="danger"
|
||||
>${this.serverError}</btrix-alert
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<article class="w-full max-w-sm grid gap-5">
|
||||
${serverError}
|
||||
|
||||
<main class="md:bg-white md:shadow-xl md:rounded-lg md:px-12 md:py-12">
|
||||
<h1 class="text-3xl text-center font-semibold mb-5">
|
||||
${msg("Join archive")}
|
||||
</h1>
|
||||
|
||||
<!-- TODO archive details -->
|
||||
|
||||
<div class="text-center">
|
||||
<sl-button type="primary" @click=${this.onAccept}
|
||||
>${msg("Accept invitation")}</sl-button
|
||||
>
|
||||
</div>
|
||||
</main>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
private async onAccept() {
|
||||
if (!this.authState || !this.isLoggedIn) {
|
||||
// TODO handle error
|
||||
this.serverError = msg("Something unexpected went wrong");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiFetch(
|
||||
`/archives/invite-accept/${this.token}`,
|
||||
this.authState,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("notify", {
|
||||
detail: {
|
||||
// TODO archive details
|
||||
message: msg("You've joined the archive."),
|
||||
type: "success",
|
||||
icon: "check2-circle",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
this.navTo(DASHBOARD_ROUTE);
|
||||
} catch (err: any) {
|
||||
if (err.isApiError && err.message === "Invalid Invite Code") {
|
||||
this.serverError = msg("This invitation is not valid.");
|
||||
} else {
|
||||
this.serverError = msg("Something unexpected went wrong");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -36,12 +36,12 @@ export class Archives extends LiteElement {
|
||||
return html`<div class="grid gap-4">
|
||||
<h1 class="text-xl font-bold">${msg("Archives")}</h1>
|
||||
|
||||
<ul class="border rounded-lg grid gap-6 overflow-hidden">
|
||||
<ul class="border rounded-lg overflow-hidden">
|
||||
${this.archiveList.map(
|
||||
(archive) =>
|
||||
(archive, i) =>
|
||||
html`
|
||||
<li
|
||||
class="p-3 md:p-6 hover:bg-gray-50"
|
||||
class="p-3 md:p-6 hover:bg-gray-50${i > 0 ? " border-t" : ""}"
|
||||
role="button"
|
||||
@click=${this.makeOnArchiveClick(archive)}
|
||||
>
|
||||
|
@ -26,3 +26,8 @@ import(/* webpackChunkName: "users-invite" */ "./users-invite").then(
|
||||
customElements.define("btrix-users-invite", UsersInvite);
|
||||
}
|
||||
);
|
||||
import(/* webpackChunkName: "accept-invite" */ "./accept-invite").then(
|
||||
({ AcceptInvite }) => {
|
||||
customElements.define("btrix-accept-invite", AcceptInvite);
|
||||
}
|
||||
);
|
||||
|
@ -1,99 +1,12 @@
|
||||
import { state, property } from "lit/decorators.js";
|
||||
import { msg, localized, str } from "@lit/localize";
|
||||
import { createMachine, interpret, assign } from "@xstate/fsm";
|
||||
import { msg, localized } from "@lit/localize";
|
||||
|
||||
import { DASHBOARD_ROUTE } from "../routes";
|
||||
import type { AuthState } from "../utils/AuthService";
|
||||
import LiteElement, { html } from "../utils/LiteElement";
|
||||
import type { LoggedInEvent } from "../utils/AuthService";
|
||||
import AuthService from "../utils/AuthService";
|
||||
|
||||
type JoinContext = {
|
||||
serverError?: string;
|
||||
};
|
||||
type JoinErrorEvent = {
|
||||
type: "ERROR";
|
||||
detail: {
|
||||
serverError?: JoinContext["serverError"];
|
||||
};
|
||||
};
|
||||
type JoinEvent =
|
||||
| { type: "SUBMIT_SIGN_UP" }
|
||||
| { type: "ACCEPT_INVITE" }
|
||||
| { type: "SIGN_UP_SUCCESS" }
|
||||
| JoinErrorEvent;
|
||||
type JoinTypestate =
|
||||
| {
|
||||
value: "initial";
|
||||
context: JoinContext;
|
||||
}
|
||||
| {
|
||||
value: "submittingForm";
|
||||
context: JoinContext;
|
||||
}
|
||||
| {
|
||||
value: "acceptInvite";
|
||||
context: JoinContext;
|
||||
}
|
||||
| {
|
||||
value: "acceptingInvite";
|
||||
context: JoinContext;
|
||||
};
|
||||
|
||||
const initialContext = {};
|
||||
|
||||
const machine = createMachine<JoinContext, JoinEvent, JoinTypestate>(
|
||||
{
|
||||
id: "join",
|
||||
initial: "initial",
|
||||
context: initialContext,
|
||||
states: {
|
||||
["initial"]: {
|
||||
on: {
|
||||
SUBMIT_SIGN_UP: "submittingForm",
|
||||
},
|
||||
},
|
||||
["submittingForm"]: {
|
||||
on: {
|
||||
SIGN_UP_SUCCESS: "acceptInvite",
|
||||
ERROR: {
|
||||
target: "initial",
|
||||
actions: "setError",
|
||||
},
|
||||
},
|
||||
},
|
||||
["acceptInvite"]: {
|
||||
on: {
|
||||
ACCEPT_INVITE: "acceptingInvite",
|
||||
},
|
||||
},
|
||||
["acceptingInvite"]: {
|
||||
on: {
|
||||
ERROR: {
|
||||
target: "acceptInvite",
|
||||
actions: "setError",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
actions: {
|
||||
setError: assign((context, event) => ({
|
||||
...context,
|
||||
...(event as JoinErrorEvent).detail,
|
||||
})),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@localized()
|
||||
export class Join extends LiteElement {
|
||||
private joinStateService = interpret(machine);
|
||||
|
||||
@property({ type: Object })
|
||||
authState?: AuthState;
|
||||
|
||||
@property({ type: String })
|
||||
token?: string;
|
||||
|
||||
@ -101,7 +14,7 @@ export class Join extends LiteElement {
|
||||
email?: string;
|
||||
|
||||
@state()
|
||||
private joinState = machine.initialState;
|
||||
serverError?: string;
|
||||
|
||||
connectedCallback(): void {
|
||||
if (this.token && this.email) {
|
||||
@ -111,151 +24,33 @@ export class Join extends LiteElement {
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
// Enable state machine
|
||||
this.joinStateService.subscribe((state) => {
|
||||
this.joinState = state;
|
||||
});
|
||||
|
||||
this.joinStateService.start();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.joinStateService.stop();
|
||||
}
|
||||
|
||||
render() {
|
||||
const isSignUp =
|
||||
this.joinState.value === "initial" ||
|
||||
this.joinState.value === "submittingForm";
|
||||
const isAcceptInvite =
|
||||
this.joinState.value === "acceptInvite" ||
|
||||
this.joinState.value === "acceptingInvite";
|
||||
|
||||
let content;
|
||||
|
||||
if (isSignUp) {
|
||||
content = this.renderSignUp();
|
||||
} else if (isAcceptInvite) {
|
||||
content = this.renderAccept();
|
||||
}
|
||||
|
||||
return html`
|
||||
<article class="w-full max-w-sm grid gap-5">
|
||||
<h1 class="text-3xl font-semibold mb-3">${msg("Join archive")}</h1>
|
||||
|
||||
<!-- TODO invitation details -->
|
||||
|
||||
<div class="flex items-center text-sm font-medium">
|
||||
<div
|
||||
class="flex-0 mx-3 ${isSignUp ? "text-primary" : "text-blue-400"}"
|
||||
>
|
||||
1. Create account
|
||||
</div>
|
||||
<hr
|
||||
class="flex-1 mx-3 ${isSignUp
|
||||
? "border-gray-400"
|
||||
: "border-blue-400"}"
|
||||
/>
|
||||
<div
|
||||
class="flex-0 mx-3 ${isSignUp ? "text-gray-400" : "text-primary"}"
|
||||
>
|
||||
2. Accept invite
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<main class="md:bg-white md:shadow-xl md:rounded-lg md:px-12 md:py-12">
|
||||
${content}
|
||||
<h1 class="text-3xl font-semibold mb-5">
|
||||
${msg("Join Browsertrix Cloud")}
|
||||
</h1>
|
||||
|
||||
<!-- TODO archive details if available -->
|
||||
|
||||
<btrix-sign-up-form
|
||||
email=${this.email!}
|
||||
inviteToken=${this.token!}
|
||||
@authenticated=${this.onAuthenticated}
|
||||
></btrix-sign-up-form>
|
||||
</main>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderSignUp() {
|
||||
return html`
|
||||
<btrix-sign-up-form
|
||||
email=${this.email!}
|
||||
inviteToken=${this.token!}
|
||||
@submit=${this.onSignUp}
|
||||
@error=${() => this.joinStateService.send("ERROR")}
|
||||
@authenticated=${this.onAuthenticated}
|
||||
></btrix-sign-up-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderAccept() {
|
||||
let serverError;
|
||||
|
||||
if (this.joinState.context.serverError) {
|
||||
serverError = html`
|
||||
<div class="mb-5">
|
||||
<btrix-alert id="formError" type="danger"
|
||||
>${this.joinState.context.serverError}</btrix-alert
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
${serverError}
|
||||
|
||||
<div class="text-center">
|
||||
<sl-button type="primary" @click=${this.onAccept}
|
||||
>Accept invitation</sl-button
|
||||
>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private onSignUp() {
|
||||
this.joinStateService.send("SUBMIT_SIGN_UP");
|
||||
}
|
||||
|
||||
private onAuthenticated(event: LoggedInEvent) {
|
||||
this.joinStateService.send("SIGN_UP_SUCCESS");
|
||||
|
||||
this.dispatchEvent(
|
||||
AuthService.createLoggedInEvent({
|
||||
...event.detail,
|
||||
api: true,
|
||||
// TODO separate logic for confirmation message
|
||||
// firstLogin: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async onAccept() {
|
||||
this.joinStateService.send("ACCEPT_INVITE");
|
||||
|
||||
if (!this.authState) {
|
||||
this.joinStateService.send({
|
||||
type: "ERROR",
|
||||
detail: {
|
||||
serverError: msg("Something unexpected went wrong"),
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.apiFetch(`/invite/accept/${this.token}`, this.authState);
|
||||
|
||||
this.navTo(DASHBOARD_ROUTE);
|
||||
} catch (err: any) {
|
||||
if (err.isApiError && err.message === "Invalid Invite Code") {
|
||||
this.joinStateService.send({
|
||||
type: "ERROR",
|
||||
detail: {
|
||||
serverError: msg("This invitation is not valid."),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
this.joinStateService.send({
|
||||
type: "ERROR",
|
||||
detail: {
|
||||
serverError: msg("Something unexpected went wrong"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import { createMachine, interpret, assign } from "@xstate/fsm";
|
||||
|
||||
import type { ViewState } from "../utils/APIRouter";
|
||||
import LiteElement, { html } from "../utils/LiteElement";
|
||||
import type { LoggedInEventDetail } from "../utils/AuthService";
|
||||
import AuthService from "../utils/AuthService";
|
||||
import { DASHBOARD_ROUTE } from "../routes";
|
||||
|
||||
type FormContext = {
|
||||
successMessage?: string;
|
||||
@ -126,6 +128,9 @@ export class LogInPage extends LiteElement {
|
||||
@property({ type: Object })
|
||||
viewState!: ViewState;
|
||||
|
||||
@property({ type: String })
|
||||
redirectUrl: string = DASHBOARD_ROUTE;
|
||||
|
||||
private formStateService = interpret(machine);
|
||||
|
||||
@state()
|
||||
@ -308,6 +313,8 @@ export class LogInPage extends LiteElement {
|
||||
try {
|
||||
const data = await AuthService.login({ email: username, password });
|
||||
|
||||
(data as LoggedInEventDetail).redirectUrl = this.redirectUrl;
|
||||
|
||||
this.dispatchEvent(AuthService.createLoggedInEvent(data));
|
||||
|
||||
// no state update here, since "logged-in" event
|
||||
|
@ -2,8 +2,10 @@ export const ROUTES = {
|
||||
home: "/",
|
||||
join: "/join/:token?email",
|
||||
signUp: "/sign-up",
|
||||
acceptInvite: "/invite/accept/:token?email",
|
||||
verify: "/verify?token",
|
||||
login: "/log-in",
|
||||
loginWithRedirect: "/log-in?redirectUrl",
|
||||
forgotPassword: "/log-in/forgot-password",
|
||||
resetPassword: "/reset-password?token",
|
||||
myAccount: "/my-account",
|
||||
|
@ -14,9 +14,10 @@ type Session = {
|
||||
|
||||
export type AuthState = (Auth & Session) | null;
|
||||
|
||||
type LoggedInEventDetail = Auth & {
|
||||
export type LoggedInEventDetail = Auth & {
|
||||
api?: boolean;
|
||||
firstLogin?: boolean;
|
||||
redirectUrl?: string;
|
||||
};
|
||||
|
||||
export interface LoggedInEvent<T = LoggedInEventDetail> extends CustomEvent {
|
||||
|
Loading…
Reference in New Issue
Block a user