Published
Edited
Aug 30, 2022
1 fork
Importers
12 stars
Insert cell
Insert cell
Insert cell
Insert cell
createLogin = () => {
// When no-one is logged in we want don't want the cell to resolve, so we return a promise
// We want that promise to be resolved next time we get a value
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);

// On a new page refresh the currentUser is unkown and we have to listen to the auth state change to discover
// the initial state. This article explains it well
// https://medium.com/firebase-developers/why-is-my-currentuser-null-in-firebase-auth-4701791f74f0
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;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
user
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
import { firebase } with { FIREBASE_CONFIG as firebaseConfig } from "@tomlarkworthy/firebase"
Insert cell
Insert cell
import { firebase as userFirebase } with { TOKEN_FIREBASE_CONFIG as firebaseConfig } from "@tomlarkworthy/firebase"
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colors = ({
dark: "#4A44C4",
dark_darker: "#3933A3",
dark_darkest: "#2B277C",
light: "#FDF7E6",
light_darker: "#FBF0D1",
light_darkest: "#F9E8B8",
alt_light: "#9DE2BF",
alt_light_darker: "#75D6A5",
alt_light_darkest: "#4ECB8B",
alt_dark: "#E78AAE",
alt_darker: "#DE5E90",
alt_darkest: "#D53472",
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
container = (inner) => {
return view`<div class='es-frame'>${style()}${['...', inner()]}</div>`
}
Insert cell
screen
Insert cell
Insert cell
container(() => htl.html`<div style="width: 400px">
<span class="e-btns">
${button({
action: "login",
label: "Comment signin",
icon: chatIcon
})}${button({
action: "copy",
label: "Copy",
icon: copyIcon
})}
</span>
<p class="e-info" style="font-size: 14px;">write a comment containing code <i>'dasdasdasdas'</i> in a comment<br>
⚠️ comment not found (try again?)</p>
</span>`)
Insert cell
content = ({
labels: {
copy: "Copy to clipboard",
login: "Login with comment",
logout: () =>
`Logout <b>${userFirebase
.auth()
.currentUser.uid.replace("observablehq|", '')}</b>`,
verify: "Login"
},
icons: {
copy: copyIcon,
login: chatIcon
}
})
Insert cell
expandContent = (val) => typeof val === 'function' ? val() : val;
Insert cell
viewof testScreen = screen({
info: "Please copy code",
actions: ["login", "verify"],
toggles: [
{
code: "profile_relmeauth",
label: html`[optional] scan for teams?`,
value: true,
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. (related [relmeauth](https://observablehq.com/@endpointservices/auth#observable_features))`}
</details>`
}
]
})
Insert cell
testScreen
Insert cell
screen = ({ info, actions = [], toggles = [] } = {}) =>
container(
() => view`<div>
<span class="e-btns">
${[
"actions",
actions.map(action =>
button({
action,
label: expandContent(content.labels[action]),
icon: content.icons[action]
})
)
]}
</span>
${info ? html`<p class="e-info" style="font-size: 14px;">${info}</p>` : ''}
${[
'...',
Object.fromEntries(
toggles.map(toggle => {
return [
toggle.code,
view`<div>
<div>${[
'...',
stopPropagation(
Inputs.toggle({ label: toggle.label, value: toggle.value })
)
]}</div>
<div>${toggle.caption}</div>
</div>`
];
})
)
]}
</span>`
)
Insert cell
stopPropagation = _view => {
_view.addEventListener('input', evt => {
if (evt?.detail?.user === undefined) evt.stopPropagation();
});
return view`<span>${['...', _view]}`;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
prepare = async publicCode => {
if (!publicCode) throw new Error("public code required");
const response = await fetch(`${prepare_backend.href}?code=${publicCode}`);
if (response.status !== 200)
throw new Error(`Err ${response.status}, ${await response.text()}`);
return await response.text();
}
Insert cell
prepareOK = suite.test("prepare returns 200", async () => {
const code = randomId(16);
expect(await prepare(code)).toBe("OK");
})
Insert cell
prepareCodeResuse = suite.test("prepare codes cannot be reused", async (done) => {
try {
const code = randomId(16);
await prepare(code);
await prepare(code);
} catch (err) {
done() // Test will only resolve it error thrown
}
})
Insert cell
Insert cell
Insert cell
Insert cell
html`<img width=300px src="${await FileAttachment(
"ezgif.com-gif-maker.webp"
).url()}"></img>`
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
teamScan = suite.test("team scan runs", async () => {
expect(await findObservablehqAccounts("tomlarkworthy")).toContain(
"tomlarkworthy"
);
})
Insert cell
verifyWithoutPrepare = suite.test(
"verify 401 when not prepared",
async (done) => {
try {
await verify({
notebookURL:
"https://observablehq.com/@endpointservices/login-with-comment",
privateCode: randomId()
});
} catch (err) {
expect(err.message).toBe("No code prepared");
done();
}
}
)
Insert cell
verifyWithoutComment = suite.test(
"verify 401 for missing comment",
async (done) => {
const privateCode = randomId();
const publicCode = await hash(privateCode);
await prepare(publicCode);
let result = null;
try {
result = await verify({
notebookURL:
"https://observablehq.com/@endpointservices/login-with-comment",
privateCode: privateCode
});
} catch (err) {
expect(err.message).toBe(
"Comment code not found, try again in a moment?"
);
done();
}
throw new Error(result);
}
)
Insert cell
Insert cell
function checkIsURL(arg, name) {
try {
return new URL(arg).toString()
} catch (err) {
throw new Error(`${name || 'arg'} is not a URL`)
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more