Skip to content

Commit

Permalink
fix(login): remove iframe for captcha
Browse files Browse the repository at this point in the history
  • Loading branch information
Vexcited committed Feb 12, 2025
1 parent 109baff commit 3d9a0bf
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 162 deletions.
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri = { version = "2", features = ["webview-data-url"] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
#[tauri::command]
fn navigate(mut webview_window: tauri::WebviewWindow, url: String) {
_ = webview_window.navigate(tauri::Url::parse(&url).unwrap());
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
Expand All @@ -6,6 +11,7 @@ pub fn run() {
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_internal_api::init())
.invoke_handler(tauri::generate_handler![navigate])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
136 changes: 23 additions & 113 deletions src/components/arkose.tsx
Original file line number Diff line number Diff line change
@@ -1,115 +1,25 @@
import { onMount, createSignal, type Component, onCleanup, Show } from "solid-js";
import { Portal } from "solid-js/web";
import { message } from "@tauri-apps/plugin-dialog";

const Arkose: Component<{
key: string
dataExchange: string
onVerify: (token?: string) => void
}> = (props) => {
const html = () => `
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=0" />
<style>
html, body {
height: 100%;
width: 100%;
overflow: hidden;
position: fixed;
margin: 0;
padding: 0;
}
</style>
<script crossorigin="anonymous" data-callback="setupEnforcement" onerror="jsLoadError()" src="https://client-api.arkoselabs.com/v2/api.js" async defer></script>
<script>
function jsLoadError () {
window.top.postMessage(JSON.stringify({ type: "js:error" }), "*");
}
function setupEnforcement (arkoseEnforcement) {
arkoseEnforcement.setConfig({
selector: '#challenge',
publicKey: ${JSON.stringify(props.key)},
mode: 'inline',
data: { blob: ${JSON.stringify(props.dataExchange)} },
language: '',
isSDK: true,
accessibilitySettings: {
lockFocusToModal: true
},
noSuppress: false,
onCompleted (response) {
window.top.postMessage(JSON.stringify({ type: "completed", response }), "*");
},
onHide () {
window.top.postMessage(JSON.stringify({ type: "hide" }), "*");
},
onShow () {
window.top.postMessage(JSON.stringify({ type: "show" }), "*");
},
onError (response) {
window.top.postMessage(JSON.stringify({ type: "error", response }), "*");
},
onFailed (response) {
window.top.postMessage(JSON.stringify({ type: "failed", response }), "*");
}
});
}
</script>
</head>
<body id="challenge"></body>
</html>
`.trim();

const url = () => "data:text/html;base64," + btoa(html());
const [show, setShow] = createSignal(true);

const handleWindowMessage = (e: MessageEvent) => {
const event = e as MessageEvent;
const data = JSON.parse(event.data);

switch (data.type) {
case "show":
setShow(true);
break;
case "hide":
setShow(false);
break;
case "error":
case "failed":
case "js:error":
message("Uh oh, something went wrong, please try again. If the issue persists, please open an issue on GitHub.", {
kind: "error"
});
break;
case "completed":
props.onVerify(data.response.token);
setShow(false);
break;
}
export const createArkoseURL = (key: string, dataExchange: string) => {
const href = window.location.origin + window.location.pathname;

const html = `<html><head><meta name="viewport" content="width=device-width, initial-scale=1,maximum-scale=1,user-scalable=0"><style>html,body{display:flex;justify-content:center;align-items:center;background:#000;height:100%;width:100%;overflow:hidden;position:fixed;margin:0;padding:0;color:#fff}.spin{transition: opacity .175s; animation: spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}</style>`
+ `<script crossorigin="anonymous" data-callback="setup" src="https://client-api.arkoselabs.com/v2/api.js" async defer></script>`
+ `<script>
function setup(enforcement){
enforcement.setConfig({
selector:'#challenge',
publicKey:${JSON.stringify(key)},
mode:'inline',
data:{blob:${JSON.stringify(dataExchange)}},
isSDK:true,
accessibilitySettings:{lockFocusToModal:true},
onCompleted({token}){
location.href = \`${href}?arkoseToken=\${token}\`
},
onShow(){
document.querySelector('.spin').style.opacity = 0
}

onMount(() => {
window.addEventListener("message", handleWindowMessage);
});

onCleanup(() => {
window.removeEventListener("message", handleWindowMessage);
});

return (
<Portal>
<Show when={show()}>
<div class="fixed inset-0 z-50 bg-black bg-opacity-75 flex items-center justify-center">
<iframe
src={url()}
class="h-[450px] w-[400px]"
/>
</div>
</Show>
</Portal>
);
})
}</script>`
+ `</head><body id="challenge"><svg class="spin" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 4V2A10 10 0 0 0 2 12h2a8 8 0 0 1 8-8"/></svg></body></html>`;
return "data:text/html;base64," + btoa(html);
};

export default Arkose;
104 changes: 56 additions & 48 deletions src/views/login.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Show, type Component } from "solid-js";
import { useNavigate } from "@solidjs/router";
import { createEffect, on, Show, type Component } from "solid-js";
import { useNavigate, useSearchParams } from "@solidjs/router";
import { createStore } from "solid-js/store";
import { v4 as uuidv4 } from "uuid";

Expand All @@ -9,26 +9,27 @@ import { vonage_verify_otp } from "~/api/requests/auth/vonage/verify";
import { grant_firebase } from "~/api/requests/auth/token";
import { BEREAL_ARKOSE_PUBLIC_KEY } from "~/api/constants";

import Arkose from "~/components/arkose";
import { createArkoseURL } from "~/components/arkose";
import auth from "~/stores/auth";
import { DEMO_ACCESS_TOKEN, DEMO_PHONE_NUMBER, DEMO_REFRESH_TOKEN } from "~/utils/demo";
import MdiChevronLeft from '~icons/mdi/chevron-left'
import { postVonageDataExchange } from "~/api/requests/auth/vonage/data-exchange";
import Otp from "~/components/otp";
import MdiLoading from '~icons/mdi/loading'
import { BeRealError } from "~/api/models/errors";
import { invoke } from "@tauri-apps/api/core";

const LoginView: Component = () => {
const navigate = useNavigate();
const [params, setParams] = useSearchParams<{ arkoseToken: string }>()

const [state, setState] = createStore({
step: "phone" as ("phone" | "otp"),
deviceID: uuidv4(),
waitingOnArkose: false,
deviceID: localStorage.getItem("login__deviceID") || uuidv4(),
error: null as string | null,
loading: false,

phoneNumber: "",
arkoseToken: "",
arkoseDataExchange: "",
phoneNumber: localStorage.getItem("login__phoneNumber") || "",
requestID: "",
otp: "",
})
Expand All @@ -37,40 +38,49 @@ const LoginView: Component = () => {
if (!state.phoneNumber) return;

// Make sure there's no whitespace in the phone number.
const phoneNumber = state.phoneNumber.split(" ").join("");
const phoneNumber = state.phoneNumber.replace(/ /g, "");

try {
setState("loading", true);

// Captcha is always needed, except if we're trying
// to authenticate on the demonstration account.
if (!state.arkoseToken && phoneNumber !== DEMO_PHONE_NUMBER) {
const dataExchange = await postVonageDataExchange({
deviceID: state.deviceID,
phoneNumber,
})

setState({
waitingOnArkose: true,
arkoseDataExchange: dataExchange,
});

return;
}

if (state.step === "phone") {
let requestID: string;

if (phoneNumber === DEMO_PHONE_NUMBER) {
requestID = "demo";
}
else {
if (!params.arkoseToken) {
const dataExchange = await postVonageDataExchange({
deviceID: state.deviceID,
phoneNumber
});

// Save the state in `localStorage` to remember it
// when we come back from the Arkose challenge.
localStorage.setItem("login__phoneNumber", phoneNumber);
localStorage.setItem("login__deviceID", state.deviceID);

const url = createArkoseURL(BEREAL_ARKOSE_PUBLIC_KEY, dataExchange);
// For some odd reason, we can't use `location.href = url` here
// so we're doing it through a Tauri command instead.
return invoke("navigate", { url });
}

// We can safely remove the temporary state from localStorage.
localStorage.removeItem("login__phoneNumber");
localStorage.removeItem("login__deviceID");

const token = params.arkoseToken;
// One time use, we don't need it anymore.
setParams({ arkoseToken: void 0 });

requestID = await vonage_request_code({
deviceID: state.deviceID,
phoneNumber,
tokens: [{
identifier: VonageRequestCodeTokenIdentifier.ARKOSE,
token: state.arkoseToken,
token,
}]
});
}
Expand Down Expand Up @@ -109,23 +119,33 @@ const LoginView: Component = () => {
navigate("/feed");
}
}
catch (e) {
// TODO: show error in UI
console.error(e);
catch (error) {
if (error instanceof BeRealError) {
setState("error", error.message);
}
else {
setState("error", "An error occurred while authenticating, please try again later.");
}
}
finally {
setState("loading", false);
}
};

// If we're coming back from the Arkose challenge, we need to
// re-run the authentication process.
createEffect(on(() => params.arkoseToken, (token) => {
if (token)
runAuthentication();
}));

return (
<main class="h-screen flex flex-col px-4 pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)]">
<header class="shrink-0 flex items-center relative w-full h-8 pt-6">
<Show when={state.step === "otp"}>
<button type="button"
onClick={() => {
setState({
arkoseToken: "",
step: "phone",
otp: "",
});
Expand All @@ -152,24 +172,6 @@ const LoginView: Component = () => {
}}
>
<Show when={state.step === "phone"}>
<Show when={state.waitingOnArkose}>
<Arkose
key={BEREAL_ARKOSE_PUBLIC_KEY}
dataExchange={state.arkoseDataExchange}
onVerify={(token) => {
if (token) {
setState({
loading: false,
arkoseToken: token,
waitingOnArkose: false,
});

runAuthentication();
}
}}
/>
</Show>

<input
class="w-full max-w-280px mx-auto rounded-2xl py-3 px-4 text-white bg-white/10 text-2xl font-600 tracking-wide outline-none placeholder:text-white/40 focus:(outline outline-white outline-offset-2)"
type="tel"
Expand All @@ -185,6 +187,12 @@ const LoginView: Component = () => {
By continuing, you agree that StayReal is not affiliated with BeReal and that you are using this service at your own risk.
</p>

<Show when={state.error}>
<p class="mt-8 text-sm text-center px-4 text-red">
{state.error}
</p>
</Show>

<button type="submit" disabled={state.loading || !state.phoneNumber}
class="text-black bg-white rounded-2xl w-full py-3 mt-auto focus:(outline outline-white outline-offset-2) disabled:opacity-30 transition-all"
>
Expand Down

0 comments on commit 3d9a0bf

Please sign in to comment.