shoGambler/templates/login.html
2024-10-20 17:12:11 +01:00

170 lines
7.7 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log in with Google</title>
</head>
<body>
<h1 id="text">Log in with Google</h1>
<button id="authorize">Authorize</button>
<button id="delete">Delete every last byte of my data</button>
<p>By logging in, you agree to our <a href="/privacy">Privacy Policy</a> and <a href="/tos">Terms of Service</a></p>
<script>
// Configuration
const clientId = '165295772598-ua5imd4pduoapsh0bnu96lul2j6bhsul.apps.googleusercontent.com';
const redirectUri = 'https://sho.ailur.dev/login';
const authorizationEndpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
const tokenEndpoint = 'https://oauth2.googleapis.com/token';
const userinfoEndpoint = 'https://www.googleapis.com/oauth2/v1/userinfo?alt=json';
// Generate a random code verifier
function generateCodeVerifier() {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
const length = 128;
return Array.from(crypto.getRandomValues(new Uint8Array(length)))
.map((x) => charset[x % charset.length])
.join("");
}
// Create a code challenge from the code verifier using SHA-256
async function createCodeChallenge(codeVerifier) {
const buffer = new TextEncoder().encode(codeVerifier);
const hashArrayBuffer = await crypto.subtle.digest('SHA-256', buffer);
return btoa(String.fromCharCode(...new Uint8Array(hashArrayBuffer)))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
// Authorization function with PKCE
document.getElementById('authorize').addEventListener('click', () => {
const codeVerifier = generateCodeVerifier();
localStorage.setItem('codeVerifier', codeVerifier); // Store code verifier
createCodeChallenge(codeVerifier)
.then((codeChallenge) => {
window.location.href = `${authorizationEndpoint}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&code_challenge=${codeChallenge}&code_challenge_method=S256&scope=https://www.googleapis.com/auth/youtube.readonly%20https://www.googleapis.com/auth/userinfo.profile`;
})
.catch((error) => {
console.error('Error generating code challenge:', error);
});
})
// Delete every last byte of user data
document.getElementById('delete').addEventListener('click', async () => {
document.getElementById("text").innerText = "Deleting every last byte of your data..."
let response = await fetch("/api/delete", {
method: "POST",
headers: {
"Authorization": localStorage.getItem("accessToken")
}
})
if (response.status !== 200) {
document.getElementById("text").innerText = "Theres probably an outage or something. Try again later. Can't say I didn't try."
} else {
localStorage.clear()
document.getElementById("text").innerText = "Deleted every last byte of your data, even from memory (well, when the garbage collector kicks in)."
}
})
// Parse the authorization code from the URL
function parseCodeFromUrl() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('code');
}
// Exchange authorization code for access token
async function exchangeCodeForToken(code) {
const codeVerifier = localStorage.getItem('codeVerifier'); // Retrieve code verifier
const formData = new URLSearchParams();
formData.append('client_id', String(clientId));
formData.append('code', String(code));
formData.append('redirect_uri', String(redirectUri));
formData.append('grant_type', 'authorization_code');
formData.append('code_verifier', String(codeVerifier));
// Google, for some godforsaken reason, wants client_secret during a PKCE flow.
// What the hell? That's not remotely compatible with the PKCE RFC.
// I hope to god that Google is still doing PKCE properly even though they're asking for client_secret.
// #### you, Google, the hate is absolutely justified.
formData.append('client_secret', String('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'))
let response = await fetch(tokenEndpoint, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: formData
});
if (response.status !== 200) {
console.error('Failed to exchange code for token:', response.status, await response.text());
return;
}
const data = await response.json();
console.log('Successfully exchanged code for token:', data);
const accessToken = data['access_token'];
// Request userinfo with ACCESS TOKEN in bearer format
// For some other godforsaken reason, they want the access token, not the id token.
// #### you, Google.
// THAT'S NOT HOW USERINFO WORKS, GOOGLE. YOU IDIOT!
fetch(userinfoEndpoint, {
headers: {
"Authorization": "Bearer " + accessToken
}
})
.then((response) => {
async function doStuff() {
if (response.status === 200) {
const userinfoData = await response.json();
console.log("Userinfo:", userinfoData);
console.log(accessToken)
console.log("User:", userinfoData["name"]);
console.log("Sub:", userinfoData["id"]);
localStorage.removeItem("codeVerifier")
document.getElementById("text").innerText = "Authenticated, " + userinfoData.name + ", now logging into the server..."
await fetch("/api/authorize", {
method: "POST",
body: JSON.stringify({
"idToken": data['id_token'],
"accessToken": accessToken,
}),
}).then(response => {
response.json().then(data => {
if (response.status === 200) {
localStorage.setItem("accessToken", data["sessionToken"])
localStorage.setItem("user", userinfoData["name"])
localStorage.setItem("oauthToken", accessToken)
localStorage.setItem("sub", userinfoData["id"])
window.location.href = "/"
} else {
document.getElementById("text").innerText = "Authentication failed"
}
})
});
} else {
document.getElementById("text").innerText = "Authentication failed"
}
}
doStuff()
});
}
// Main function to handle OAuth2 flow
async function main() {
if (localStorage.getItem("user") !== null) {
document.getElementById("text").innerText = "Welcome back, " + localStorage.getItem("user") + ". Aren't you already logged in?"
}
const code = parseCodeFromUrl();
if (code) {
await exchangeCodeForToken(code);
}
}
// Call the main function on page load
main();
</script>
</body>
</html>