createLogin = () => {
let firstResolve;
const updateResult = () => {
const newValue = userFirebase.auth().currentUser;
if (firstResolve) {
firstResolve(newValue);
}
if (!newValue) {
if (!firstResolve)
userUi.value = new Promise((resolve) => (firstResolve = resolve));
else userUi.value = undefined;
} else {
userUi.value = newValue;
}
userUi.dispatchEvent(
new CustomEvent("input", {
bubbles: true,
detail: {
user: userUi.value || null
}
})
);
};
const userUi = html`<span>${viewroutine(async function* () {
let response;
let err = "";
const actionWas = (action) => response && response.actions.includes(action);
if (!mutable authStateKnown) {
let ready = null;
const isReady = new Promise((resolve) => (ready = resolve));
userFirebase.auth().onAuthStateChanged(ready);
await isReady;
mutable authStateKnown = true;
}
await new Promise((r) => r()); // micro tick so userUi initializes
while (true) {
try {
// update overall view state
updateResult();
if (!userFirebase.auth().currentUser) {
const loginUi = screen({
actions: ["login"]
});
// We need to see if someone logs in via a side channel
const unsubscribe = userFirebase.auth().onAuthStateChanged((user) => {
if (user)
loginUi.dispatchEvent(new Event("input", { bubbles: true }));
});
response = yield* ask(loginUi);
unsubscribe();
} else {
const logoutUi = screen({
actions: ["logout"]
});
// We need to see if someone logout ivia a side channel
const unsubscribe = userFirebase.auth().onAuthStateChanged((user) => {
if (!user)
logoutUi.dispatchEvent(new Event("input", { bubbles: true }));
});
response = yield* ask(logoutUi);
unsubscribe();
}
if (actionWas("logout")) {
console.log("Logging out");
yield screen({
info: md`Logging out...`
});
await userFirebase.auth().signOut();
}
if (actionWas("login")) {
console.log("login");
const privateCode = randomId(64);
const publicCode = await hash(privateCode);
yield screen({
info: md`Preparing...`
});
await prepare(publicCode);
let relmeauth = false;
while (!userFirebase.auth().currentUser) {
while (!actionWas("verify")) {
console.log("prompt verify");
response = yield* ask(
screen({
info: md`1. ${err}Add comment containing **${publicCode}** to this notebook using the cell burger menu to the left.
2. Click login to complete login.
<img width=300px src="${await FileAttachment(
"ezgif.com-gif-maker.webp"
).url()}"></img>
\n⚠️ Logging in discloses your [Observable](https://observablehq.com/) username to the notebook author.
${actionWas("copy") ? "\ncopied to clipboard" : ""}`,
actions: ["copy", "verify"],
toggles: [
{
code: "profile_relmeauth",
label: html`[optional] scan for teams?`,
value: relmeauth,
caption: html`<details class='e-info' style="display: inline-block;font-size: 14px;"><summary>how does scanning work?</summary>
${md`From your Observablehq profile URL we look for weblinks to team profile URLs, and if those profile URLs also weblink to your profile URL we consider you to have admin access for that team (see [relmeauth](https://observablehq.com/@endpointservices/auth#observable_features))`}
</details>`
}
]
})
);
if (actionWas("copy")) {
navigator.clipboard.writeText(
"public auth code: " + publicCode
);
}
relmeauth = response.profile_relmeauth === true;
}
console.log("Relmeauth scan?", relmeauth);
response = undefined;
console.log("verify");
yield screen({
info: md`Checking...`
});
try {
const verification = await verify({
notebookURL: html`<a href=""/>`.href.split("?")[0],
privateCode,
relmeauth
});
if (verification.access_token) {
await userFirebase
.auth()
.signInWithCustomToken(verification.access_token);
} else {
throw new Error("no token returned");
}
} catch (error) {
err = `<mark>⚠️ ${error.message}</mark>\n\n`;
}
}
}
} catch (err) {
yield* ask(
screen({
info: md`Uexpected error: ${err.message}`,
actions: ["ok"]
})
);
}
}
})}`;
return userUi;
}