Skip to content

Commit 9db0143

Browse files
committed
add JWT signing and redirect for login flow
1 parent 2405456 commit 9db0143

File tree

11 files changed

+432
-235
lines changed

11 files changed

+432
-235
lines changed

apps/login/next.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const nextConfig: NextConfig = {
55
async headers() {
66
return [
77
{
8-
source: "/api/request",
8+
source: "/api/:path*",
99
headers: [
1010
{
1111
key: "Access-Control-Allow-Origin",

apps/login/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"next": "15.1.6",
1818
"react": "19.0.0",
1919
"react-dom": "19.0.0",
20+
"server-only": "^0.0.1",
2021
"thirdweb": "workspace:*"
2122
},
2223
"devDependencies": {

apps/login/public/twl.js

Lines changed: 135 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,69 +2,76 @@
22
(function () {
33
const globalSetup = getSetup();
44

5-
const USER_ADDRESS_KEY = "tw.login:userAddress";
6-
const SESSION_KEY_ADDRESS_KEY = "tw.login:sessionKeyAddress";
5+
const JWT_KEY = "tw.login:jwt";
76
const CODE_KEY = "tw.login:code";
87

9-
function main() {
10-
// check if redirected first, this sets up the logged in state if it was from redirect
11-
const params = parseURLHash(new URL(window.location));
12-
if (params && params.code === localStorage.getItem(CODE_KEY)) {
13-
// reset the URL hash
14-
window.location.hash = "";
15-
// reset the code
16-
localStorage.setItem(CODE_KEY, params.code);
17-
// write the userAddress to local storage
18-
localStorage.setItem(USER_ADDRESS_KEY, params.userAddress);
19-
// write the sessionKeyAddress to local storage
20-
localStorage.setItem(SESSION_KEY_ADDRESS_KEY, params.sessionKeyAddress);
21-
}
8+
// check if redirected first, this sets up the logged in state if it was from redirect
9+
const result = parseURL(new URL(window.location));
10+
if (
11+
result &&
12+
result.length === 2 &&
13+
result[1] === localStorage.getItem(CODE_KEY)
14+
) {
15+
// reset the URL
16+
window.location.hash = "";
17+
window.location.search = "";
2218

23-
const userAddress = localStorage.getItem(USER_ADDRESS_KEY);
24-
const sessionKeyAddress = localStorage.getItem(SESSION_KEY_ADDRESS_KEY);
19+
// write the jwt to local storage
20+
localStorage.setItem(JWT_KEY, result[0]);
21+
}
2522

26-
if (userAddress && sessionKeyAddress) {
27-
// handle logged in state
28-
handleIsLoggedIn();
29-
} else {
30-
// handle not logged in state
31-
handleNotLoggedIn();
32-
}
23+
// always reset the code
24+
localStorage.removeItem(CODE_KEY);
25+
26+
const jwt = localStorage.getItem(JWT_KEY);
27+
28+
if (jwt) {
29+
// handle logged in state
30+
handleIsLoggedIn();
31+
} else {
32+
// handle not logged in state
33+
handleNotLoggedIn();
3334
}
3435

3536
function handleIsLoggedIn() {
36-
console.log("handleIsLoggedIn");
37-
3837
window.thirdweb = {
3938
isLoggedIn: true,
40-
getAddress: () => getAddress(),
39+
getUser: async () => {
40+
const res = await fetch(`${globalSetup.baseUrl}/api/user`, {
41+
headers: {
42+
Authorization: `Bearer ${localStorage.getItem(JWT_KEY)}`,
43+
},
44+
});
45+
return res.json();
46+
},
4147
logout: () => {
42-
window.localStorage.removeItem(USER_ADDRESS_KEY);
43-
window.localStorage.removeItem(SESSION_KEY_ADDRESS_KEY);
48+
window.localStorage.removeItem(JWT_KEY);
4449
window.location.reload();
4550
},
4651
};
52+
53+
renderFloatingBubble(true);
4754
}
4855

4956
function handleNotLoggedIn() {
5057
window.thirdweb = { login: onLogin, isLoggedIn: false };
58+
renderFloatingBubble(false);
5159
}
5260

5361
function onLogin() {
54-
const code = window.crypto.getRandomValues(new Uint8Array(4)).join("");
62+
const code = window.crypto.getRandomValues(new Uint8Array(16)).join("");
5563
localStorage.setItem(CODE_KEY, code);
5664
// redirect to the login page
5765
const redirect = new URL(globalSetup.baseUrl);
5866
redirect.searchParams.set("code", code);
5967
redirect.searchParams.set("clientId", globalSetup.clientId);
60-
redirect.searchParams.set("redirect", window.location.href);
68+
redirect.searchParams.set(
69+
"redirect",
70+
window.location.origin + window.location.pathname,
71+
);
6172
window.location.href = redirect.href;
6273
}
6374

64-
function getAddress() {
65-
return localStorage.getItem(USER_ADDRESS_KEY);
66-
}
67-
6875
// utils
6976
function getSetup() {
7077
const el = document.currentScript;
@@ -74,52 +81,114 @@
7481
const baseUrl = new URL(el.src).origin;
7582
const dataset = el.dataset;
7683
const clientId = dataset.clientId;
84+
const theme = dataset.theme || "dark";
7785
if (!clientId) {
7886
throw new Error("Missing client-id");
7987
}
80-
return { clientId, baseUrl };
88+
return { clientId, baseUrl, theme };
8189
}
8290

8391
/**
8492
* @param {URL} url
85-
* @returns null | { [key: string]: string }
93+
* @returns null | [string, string]
8694
*/
87-
function parseURLHash(url) {
88-
if (!url.hash) {
89-
return null;
90-
}
95+
function parseURL(url) {
9196
try {
92-
return decodeHash(url.hash);
97+
const hash = url.hash.startsWith("#") ? url.hash.slice(1) : url.hash;
98+
const code = url.searchParams.get("code");
99+
if (!hash || !code) {
100+
return null;
101+
}
102+
return [hash, code];
93103
} catch {
94104
// if this fails, invalid data -> return null
95105
return null;
96106
}
97107
}
98108

99-
/**
100-
* Decodes a URL hash string to extract the three keys.
101-
*
102-
* @param {string} hash - A string like "#eyJrZXkxIjoiVmFsdWU..."
103-
* @returns {{ userAddress: string, sessionKeyAddress: string, code: string }} An object with the three keys
104-
*/
105-
function decodeHash(hash) {
106-
// Remove the "#" prefix, if present.
107-
const base64Data = hash.startsWith("#") ? hash.slice(1) : hash;
108-
109-
// Decode the Base64 string, then parse the JSON.
110-
const jsonString = atob(base64Data);
111-
const data = JSON.parse(jsonString);
112-
113-
// data should have the shape { userAddress, sessionKeyAddress, code }.
114-
if (
115-
"userAddress" in data &&
116-
"sessionKeyAddress" in data &&
117-
"code" in data
118-
) {
119-
return data;
120-
}
121-
return null;
109+
async function renderFloatingBubble(loggedIn) {
110+
const el = document.createElement("div");
111+
el.id = "tw-floating-bubble";
112+
el.style.position = "fixed";
113+
el.style.bottom = "24px";
114+
el.style.right = "24px";
115+
el.style.zIndex = "1000";
116+
el.style.width = "138px";
117+
el.style.height = "40px";
118+
el.style.backgroundColor =
119+
globalSetup.theme === "dark" ? "#131418" : "#ffffff";
120+
el.style.color = globalSetup.theme === "dark" ? "white" : "black";
121+
el.style.borderRadius = "8px";
122+
el.style.placeItems = "center";
123+
el.style.fontSize = loggedIn ? "12px" : "12px";
124+
el.style.cursor = "pointer";
125+
el.style.overflow = "hidden";
126+
el.style.boxShadow = "1px 1px 10px rgba(0, 0, 0, 0.5)";
127+
el.style.display = "flex";
128+
el.style.alignItems = "center";
129+
el.style.justifyContent = "space-around";
130+
el.style.fontFamily = "sans-serif";
131+
el.style.gap = "8px";
132+
el.style.padding = "0px 8px";
133+
el.onclick = () => {
134+
if (loggedIn) {
135+
window.thirdweb.logout();
136+
} else {
137+
window.thirdweb.login();
138+
}
139+
};
140+
el.innerHTML = loggedIn ? await renderBlobbie() : renderThirdwebLogo();
141+
document.body.appendChild(el);
142+
}
143+
144+
function renderThirdwebLogo() {
145+
const el = document.createElement("img");
146+
el.src =
147+
"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5MDMiIGhlaWdodD0iNTQ5IiB2aWV3Qm94PSIwIDAgOTAzIDU0OSIgZmlsbD0ibm9uZSI+PHBhdGggZD0iTTQ3NS4wNjEuOTI2YTM5LjQ1IDM5LjQ1IDAgMCAxIDE1LjI5NyA2LjgxIDM1Ljg2IDM1Ljg2IDAgMCAxIDExLjI5OCAxNS40OHEyMS43MjIgNTQuMzQ3IDQzLjQ0MyAxMDguNjI3YTM5MzM3IDM5MzM3IDAgMCAwIDQ4LjMxNSAxMjAuNzU4YzguNDM1IDIxLjEwOCAxNi44MjcgNDIuMjM4IDI1LjM5MyA2My4zMDJhMzQuOTcgMzQuOTcgMCAwIDEgMCAyNy4yMzlsLTczLjU5OSAxODMuNjAxYy00Ljg3MyAxMi4xNTItMTMuNzAyIDE5LjcwNi0yNi42ODIgMjEuNjc3LTE2LjE0OSAyLjQzMS0zMC41OTMtNS41NC0zNi40MjgtMTkuNzA3LTEzLjgxMS0zMy45NjEtMjcuMzE2LTY4LjAzMS00MC45My0xMDIuMDhsLTY5LjIyOC0xNzMuMmExNDI0NjMgMTQyNDYzIDAgMCAwLTYxLjU4MS0xNTQuMTVjLTYuODE3LTE3LjA1Ny0xMy40MTctMzQuMjAxLTIwLjU0MS01MS4xMjdhMzQuMiAzNC4yIDAgMCAxIC45OTQtMjguOTQyIDM0LjEgMzQuMSAwIDAgMSA5LjQzOS0xMS40MjcgMzQuMDUgMzQuMDUgMCAwIDEgMTMuMzg2LTYuMzM2Yy41OS0uMTMxIDEuMTU5LS4zNSAxLjcyNy0uNTI1eiIgZmlsbD0idXJsKCNhKSIvPjxwYXRoIGQ9Ik0xODguMjM5LjkyNmMxLjgxMy41MDQgMy42MjcuOTQxIDUuMzk3IDEuNTU1YTMyLjMgMzIuMyAwIDAgMSAxMi4zNzIgNy4xOSAzMi40IDMyLjQgMCAwIDEgOC4wNiAxMS44MzhBMzQxODEgMzQxODEgMCAwIDEgMjYxLjI3IDEzOS4zMXEyMy43NzUgNTkuNDI3IDQ3LjU1MSAxMTguODc1IDExLjYyNSAyOS4xNDUgMjMuMzYgNTguMjY2YTMzLjg1IDMzLjg1IDAgMCAxIDAgMjYuMjc2cS0xMy4xMTIgMzIuNzEzLTI2LjIyMyA2NS40MDRsLTQ4LjA3NiAxMTkuNjg1YTMzLjggMzMuOCAwIDAgMS0xMi4zNTcgMTUuMjkzIDMzLjcgMzMuNyAwIDAgMS0xOC43NiA1LjgxNSAzNC4zNSAzNC4zNSAwIDAgMS0xOS45MDktNS45MzMgMzQuNDUgMzQuNDUgMCAwIDEtMTIuODctMTYuMzM1Yy0xNC44MTYtMzcuNTk2LTI5LjkzOC03NS4wODMtNDQuOTI5LTExMi42MTNhNDEyMjkgNDEyMjkgMCAwIDAtNjAuNTc1LTE1Mi4zMzIgNDk1MDIwIDQ5NTAyMCAwIDAgMS02MS45NTEtMTU0Ljk2QzE4LjcwNyA4Ny4xOTcgMTAuOTUgNjcuNjAxIDMuMDE3IDQ4LjA2OUEzNC4xIDM0LjEgMCAwIDEgMi41NSAyMi41IDM0LjAyIDM0LjAyIDAgMCAxIDE5Ljk3NSAzLjgxNmE2NSA2NSAwIDAgMSA4LjkzNy0yLjg5eiIgZmlsbD0idXJsKCNiKSIvPjxwYXRoIGQ9Ik05MDIuODIgMzM0Ljg1OGMtNC4zNyAxNC43OC0xMC45MjYgMjguNzI4LTE2LjQ5OCA0Mi45ODNhMzE5NjkgMzE5NjkgMCAwIDEtNDguNjQ0IDEyMS41OWMtMy43MzcgOS4yODQtNy41MzkgMTguNTQ2LTExLjE2NyAyNy44NzRhMzQuMyAzNC4zIDAgMCAxLTEyLjY0MSAxNS42NzEgMzQuMTkgMzQuMTkgMCAwIDEtMzguNDQ4LS4xMTkgMzQuMyAzNC4zIDAgMCAxLTEyLjU0NS0xNS43NDlsLTcwLjc1OC0xNzYuOTY2LTYxLjE4Ny0xNTIuODU4YTQwMjk2OCA0MDI5NjggMCAwIDEtNTkuODc2LTE0OS43MDUgMzMuODUgMzMuODUgMCAwIDEgMS4yMTEtMjguNTc3IDMzLjggMzMuOCAwIDAgMSA5LjM5Ny0xMS4yMjMgMzMuNyAzMy43IDAgMCAxIDEzLjI1NS02LjE4MmMuNjU2LS4xNTMgMS4yODktLjM3MiAxLjk0NS0uNTdoMTU5LjY5OGMuNTAyLjE1NC45ODMuMzUxIDEuNDg2LjQ2YTM0IDM0IDAgMCAxIDE1LjE2MyA3LjQ2OCAzNC4xIDM0LjEgMCAwIDEgOS43MDUgMTMuODYgMjQ5MzIgMjQ5MzIgMCAwIDAgMjcuOTI3IDY5Ljg3IDMxNTMzMyAzMTUzMzMgMCAwIDEgNzMuMTQgMTgyLjk2NmM2LjQwMyAxNi4wNzIgMTMuNTI3IDMxLjg1OSAxOC43NzIgNDguMzY5eiIgZmlsbD0idXJsKCNjKSIvPjxkZWZzPjxsaW5lYXJHcmFkaWVudCBpZD0iYSIgeDE9IjI2Mi4zMjgiIHkxPSI0NS40MTkiIHgyPSI2NDcuMDIzIiB5Mj0iNDI5LjM0NCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRjAwQTgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2MjAwQzYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYiIgeDE9Ii0yNC4zNDIiIHkxPSI0NS4yODgiIHgyPSIzNjAuMjY1IiB5Mj0iNDI5LjEyNSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxzdG9wIHN0b3AtY29sb3I9IiNGRjAwQTgiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM2MjAwQzYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCBpZD0iYyIgeDE9IjU0My43NjIiIHkxPSI0NS4yOCIgeDI9IjkyOC4zOTIiIHkyPSI0MjkuMTQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIj48c3RvcCBzdG9wLWNvbG9yPSIjRkYwMEE4Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjNjIwMEM2Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PC9zdmc+";
148+
el.style.height = "16px";
149+
el.style.objectFit = "contain";
150+
el.style.flexShrink = "0";
151+
el.style.marginLeft = "-4px";
152+
return `${el.outerHTML} <span>Login</span><span></span>`;
122153
}
123154

124-
main();
155+
async function renderBlobbie() {
156+
const address = (await window.thirdweb.getUser()).address;
157+
158+
function hexToNumber(hex) {
159+
if (typeof hex !== "string")
160+
throw new Error(`hex string expected, got ${typeof hex}`);
161+
return hex === "" ? _0n : BigInt(`0x${hex}`);
162+
}
163+
164+
const COLOR_OPTIONS = [
165+
["#fca5a5", "#b91c1c"],
166+
["#fdba74", "#c2410c"],
167+
["#fcd34d", "#b45309"],
168+
["#fde047", "#a16207"],
169+
["#a3e635", "#4d7c0f"],
170+
["#86efac", "#15803d"],
171+
["#67e8f9", "#0e7490"],
172+
["#7dd3fc", "#0369a1"],
173+
["#93c5fd", "#1d4ed8"],
174+
["#a5b4fc", "#4338ca"],
175+
["#c4b5fd", "#6d28d9"],
176+
["#d8b4fe", "#7e22ce"],
177+
["#f0abfc", "#a21caf"],
178+
["#f9a8d4", "#be185d"],
179+
["#fda4af", "#be123c"],
180+
];
181+
const colors =
182+
COLOR_OPTIONS[
183+
Number(hexToNumber(address.slice(2, 4))) % COLOR_OPTIONS.length
184+
];
185+
const el = document.createElement("div");
186+
el.style.backgroundImage = `radial-gradient(ellipse at left bottom, ${colors[0]}, ${colors[1]})`;
187+
el.style.width = "24px";
188+
el.style.height = "24px";
189+
el.style.borderRadius = "50%";
190+
el.style.flexShrink = "0";
191+
192+
return `${el.outerHTML}<span>${address.slice(0, 6)}...${address.slice(-4)}</span><span></span>`;
193+
}
125194
})();

apps/login/public/twl.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)