mirror of
https://github.com/mickael-kerjean/filestash
synced 2025-12-16 05:18:38 +01:00
153 lines
6.2 KiB
JavaScript
153 lines
6.2 KiB
JavaScript
import { createElement } from "../lib/skeleton/index.js";
|
|
import rxjs, { effect, stateMutation, applyMutation, preventDefault } from "../lib/rx.js";
|
|
import { qs } from "../lib/dom.js";
|
|
import { navigate, toHref } from "../lib/skeleton/router.js";
|
|
import { transition, zoomIn } from "../lib/animate.js";
|
|
import ajax from "../lib/ajax.js";
|
|
import { AjaxError } from "../lib/error.js";
|
|
import { basename } from "../lib/path.js";
|
|
import { loadCSS } from "../helpers/loader.js";
|
|
import { isDir } from "../pages/filespage/helper.js";
|
|
import assert from "../lib/assert.js";
|
|
import t from "../locales/index.js";
|
|
import notification from "../components/notification.js";
|
|
|
|
import ctrlError from "./ctrl_error.js";
|
|
|
|
export default function(render) {
|
|
const shareID = location.pathname.replace(toHref("/s/"), "");
|
|
const state$ = new rxjs.BehaviorSubject({ step: null });
|
|
const setState = (newState) => state$.next(newState);
|
|
|
|
effect(state$.asObservable().pipe(rxjs.mergeMap(({ step, ...state }) => {
|
|
if (step === null) return verify(render, { shareID, setState, body: null });
|
|
else if (step === "password") return ctrlPassword(render, { shareID, setState });
|
|
else if (step === "email") return ctrlEmail(render, { shareID, setState });
|
|
else if (step === "code") return ctrlEmailCodeVerification(render, { shareID, setState });
|
|
else if (step === "done") {
|
|
if (isDir(state["path"])) navigate(toHref(`/files/?share=${shareID}`));
|
|
else navigate(toHref(`/view/${encodeURIComponent(basename(state["path"]))}?share=${shareID}&nav=false`));
|
|
return rxjs.EMPTY;
|
|
}
|
|
else assert.fail(`unknown step: "${step}"`);
|
|
}), rxjs.catchError(ctrlError())));
|
|
}
|
|
|
|
function ctrlPassword(render, { shareID, setState }) {
|
|
const $page = createElement(`
|
|
<div class="component_container component_page_sharelogin">
|
|
<form>
|
|
<div class="input_group">
|
|
<input type="password" name="password" placeholder="${t("Password")}" class="component_input" autocomplete>
|
|
<button class="transparent">
|
|
<component-icon name="arrow_right"></component-icon>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`);
|
|
return ctrlAbstract(render, { shareID, setState, $page });
|
|
}
|
|
|
|
function ctrlEmail(render, { shareID, setState }) {
|
|
const $page = createElement(`
|
|
<div class="component_container component_page_sharelogin">
|
|
<form>
|
|
<div class="input_group">
|
|
<input type="email" name="email" placeholder="${t("Your email address")}" class="component_input" autocomplete>
|
|
<button class="transparent">
|
|
<component-icon name="arrow_right"></component-icon>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`);
|
|
return ctrlAbstract(render, { shareID, setState, $page });
|
|
}
|
|
|
|
function ctrlEmailCodeVerification(render, { shareID, setState }) {
|
|
const $page = createElement(`
|
|
<div class="component_container component_page_sharelogin">
|
|
<form>
|
|
<div class="input_group">
|
|
<input type="text" name="code" placeholder="${t("Code")}" class="component_input" autocomplete>
|
|
<button class="transparent">
|
|
<component-icon name="arrow_right"></component-icon>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
`);
|
|
return ctrlAbstract(render, { shareID, setState, $page });
|
|
}
|
|
|
|
function verify(_, { shareID, setState, body }) {
|
|
return ajax({
|
|
method: "POST",
|
|
url: `api/share/${shareID}/proof`,
|
|
responseType: "json",
|
|
body,
|
|
}).pipe(rxjs.mergeMap(({ responseJSON }) => {
|
|
const { key = "", path } = responseJSON.result;
|
|
if (key === "") setState({ step: "done", path });
|
|
else setState({ step: key });
|
|
|
|
return rxjs.of(!("error" in responseJSON.result));
|
|
}));
|
|
}
|
|
|
|
export async function init() {
|
|
return loadCSS(import.meta.url, "./ctrl_sharepage.css");
|
|
}
|
|
|
|
function ctrlAbstract(render, { shareID, setState, $page }) {
|
|
// feature: nice transition
|
|
render(transition($page, {
|
|
timeEnter: 250,
|
|
enter: zoomIn(1.2),
|
|
timeLeave: 0,
|
|
}));
|
|
|
|
effect(rxjs.fromEvent(qs($page, "form"), "submit").pipe(
|
|
preventDefault(),
|
|
// STEP1: loading spinner
|
|
rxjs.mapTo(["name", "loading"]),
|
|
applyMutation(qs($page, "component-icon"), "setAttribute"),
|
|
// STEP2: attempt to login
|
|
rxjs.map(() => ({ type: qs($page, "input").name, value: qs($page, "input").value })),
|
|
rxjs.switchMap((creds) => verify(render, { shareID, body: creds, setState }).pipe(rxjs.catchError((err) => {
|
|
if (err instanceof AjaxError) {
|
|
switch (err.code()) {
|
|
case "INTERNAL_SERVER_ERROR":
|
|
return rxjs.throwError(err);
|
|
case "FORBIDDEN":
|
|
return rxjs.of(false);
|
|
}
|
|
}
|
|
notification.error(err && err.message);
|
|
return rxjs.of(false);
|
|
}))),
|
|
// STEP3: update the UI when authentication fails, happy path is handle at the middleware
|
|
// level one layer above as the login ctrl has no idea what to show after login
|
|
rxjs.filter((ok) => !ok),
|
|
rxjs.mapTo(["name", "arrow_right"]), applyMutation(qs($page, "component-icon"), "setAttribute"),
|
|
rxjs.mapTo(""), stateMutation(qs($page, "input"), "value"),
|
|
rxjs.mapTo(["error"]), applyMutation(qs($page, ".input_group"), "classList", "add"),
|
|
rxjs.delay(300), applyMutation(qs($page, ".input_group"), "classList", "remove"),
|
|
rxjs.catchError(ctrlError(render)),
|
|
));
|
|
|
|
// feature: autofocus
|
|
effect(rxjs.of(null).pipe(
|
|
applyMutation(qs($page, "input"), "focus")
|
|
));
|
|
|
|
// feature: vertically center the form
|
|
effect(rxjs.fromEvent(window, "resize").pipe(
|
|
rxjs.startWith(null),
|
|
rxjs.map(() => ["margin-top", `${Math.floor(window.innerHeight / 3)}px`]),
|
|
applyMutation($page, "style", "setProperty")
|
|
));
|
|
|
|
return rxjs.EMPTY;
|
|
}
|