diff --git a/public/git-login/index.html b/public/git-login/index.html new file mode 100644 index 0000000..b19978c --- /dev/null +++ b/public/git-login/index.html @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + GitLab Account Setup | Software CaRD + + + +

Software CaRD

+

GitLab Account Setup

+
+ + +

+
+ + + + diff --git a/public/git-login/main.js b/public/git-login/main.js new file mode 100644 index 0000000..fd4b8f8 --- /dev/null +++ b/public/git-login/main.js @@ -0,0 +1,280 @@ +import * as User from "/modules/user.js" + +window.onload = async function () { + // setup site when user is already logged in + const savedToken = User.getApiToken(); + if (savedToken) { + document.getElementById("token-input").value = savedToken; + const alreadyKnownText = document.getElementById("already-known"); + const name = User.getName(); + const username = User.getUsername(); + // TODO: This feels like a good use case for a web component... + alreadyKnownText.innerHTML = `You are already authenticated as ${name} (${username}).`; + } + + // token save button onclick + var saveButton = document.getElementById("token-save-button"); + saveButton.onclick = async function () { + var tokenInput = document.getElementById("token-input"); + const token = tokenInput.value.trim(); + if (token) { + if (token === savedToken) { + window.location = "../"; + return; + } + const platform_name = User.getGitPlatformName(); + const platform = User.getGitPlatform(); + if (!platform) { + console.debug("No platform saved."); + return; + } + const headers = { "Content-Type": "application/json" }; + if (token.startsWith("glpat")) { + headers["PRIVATE-TOKEN"] = token; + } else { + headers["Authorization"] = `Bearer ${token}`; + } + + const response = await fetch(platform.apiUrl + "/user", { headers }); + + if (!response.ok) { + alert("Could not authenticate"); + location.reload(); + return; + } + + const userData = await response.json(); + + if (platform.host == "gitlab") { + localStorage.setItem("gitlab-username", userData["username"]); + localStorage.setItem("gitlab-name", userData["name"]); + localStorage.setItem("gitlab-api-token", token); + User.setUser(platform_name, token, userData["username"], userData["name"]); + } else { + localStorage.setItem("gitlab-username", userData["login"]); + localStorage.setItem("gitlab-name", userData["name"]); + localStorage.setItem("gitlab-api-token", token); + User.setUser(platform_name, token, userData["login"], userData["name"]); + } + + window.location = "../"; + return; + } + }; + + // Show the authorization UI and update button caption & token link + async function onPlatformSelected(key) { + const select = document.getElementById("platform-select"); + if (key && select.value != key) { + select.value = key; + } + const platform = User.getGitPlatform(key); + const authUI = document.getElementById("auth-ui"); + const oauthBtn = document.getElementById("oauth-button"); + const tokenA = document.getElementById("token-link"); + + // Hide everything if nothing valid is selected + if (!platform) { + authUI.classList.add("hidden"); + console.log("hidden"); + } else { + console.log("show"); + User.setGitPlatform(key); + // show ui + authUI.classList.remove("hidden"); + // Update button caption + oauthBtn.textContent = `Connect to ${platform.shortUrl}`; + // Update token link + if (platform.host == "github") { // github + tokenA.href = `${platform.baseUrl}/settings/personal-access-tokens` + // diable oauth for GitHub for now + document.getElementById("oauth-button").disabled = true; + } else { // gitlab + tokenA.href = `${platform.baseUrl}/-/user_settings/personal_access_tokens`; + document.getElementById("oauth-button").disabled = false; + } + } + } + + const REDIRECT_URI = location.origin + location.pathname; + const SCOPE_GL = "read_api"; + const SCOPE_GH = "read:user user:email repo"; + + // PKCE + const b64url = ab => btoa(String.fromCharCode(...new Uint8Array(ab))) + .replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""); + async function sha256(input) { + const data = new TextEncoder().encode(input); + return await crypto.subtle.digest("SHA-256", data); + } + function randUrlSafe(len=64){ + const b = crypto.getRandomValues(new Uint8Array(len)); + return b64url(b).slice(0, len); + } + + function buildUrl(base, path, params) { + const u = new URL(path, base); + u.search = params.toString(); + return u.href; + } + + // Start OAuth + async function startLoginWithOAuth() { + const platform = User.getGitPlatform(); + if (!platform) {return;} + const base_url = platform.baseUrl; + const client_id = platform.clientId; + const host = platform.host; + + if (!base_url || !client_id) throw new Error("baseUrl and clientId are required."); + + if (host == "github") { + const state = randUrlSafe(24); + const code_verifier = randUrlSafe(96); + const code_challenge = b64url(await sha256(code_verifier)); + + sessionStorage.setItem(`pkce_${state}`, JSON.stringify({ + code_verifier, client_id, base_url, REDIRECT_URI + })); + + const params = new URLSearchParams({ + client_id: client_id, + redirect_uri: REDIRECT_URI, + response_type: "code", + scope: SCOPE_GH, + state, + code_challenge, + code_challenge_method: "S256", + }); + + const auth_url = new URL("/login/oauth/authorize", base_url); + auth_url.search = params.toString(); + console.debug("Authorize URL:", auth_url); + location.assign(auth_url.toString()); + + } else if (host == "gitlab") { + const state = randUrlSafe(24); + const code_verifier = randUrlSafe(96); + const code_challenge = b64url(await sha256(code_verifier)); + + sessionStorage.setItem(`pkce_${state}`, JSON.stringify({ + code_verifier, client_id, base_url, REDIRECT_URI + })); + + const params = new URLSearchParams({ + client_id: client_id, + redirect_uri: REDIRECT_URI, + response_type: "code", + scope: SCOPE_GL, + state, + code_challenge, + code_challenge_method: "S256", + }); + + const auth_url = buildUrl(base_url, "/oauth/authorize", params); + console.debug("Authorize URL:", auth_url); + location.assign(auth_url); + } + } + + // OAuth Callback + async function handleCallback() { + const url = new URL(location.href); + const code = url.searchParams.get("code"); + const state = url.searchParams.get("state"); + + const savedState = JSON.parse(sessionStorage.getItem(`pkce_${state}`)); + if (!savedState) return; + const codeVerifier = savedState["code_verifier"]; + + console.debug("Callback:", { savedState, origin: location.origin, state, storageKeys: Object.keys(sessionStorage), code, url, codeVerifier }); + + if (!code) return; + + console.debug("Got code", code) + + const platform = User.getGitPlatform(); + if (!platform) { + console.debug("No Git Platform saved. Callback invalid."); + location.reload(); + return; + } + + const base_url = platform.baseUrl + const client_id = platform.clientId + + console.debug("Getting token from", platform.label) + + const body = new URLSearchParams({ + client_id: client_id, + grant_type: "authorization_code", + code, + redirect_uri: REDIRECT_URI, + code_verifier: codeVerifier, + }); + let tokenUrl = `${base_url}/oauth/token`; + if (platform.host == "github") { + // tokenUrl = "/cgi-bin/github-token.py"; + // tokenUrl = "https://github.com/login/oauth/access_token" + throw new Error("GitHub Oauth callbacks are not supported"); + } + console.debug("Fetching from ", tokenUrl); + const resp = await fetch(tokenUrl, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json" }, + body + }); + + // Check response + const raw = await resp.text(); + console.debug("Token raw response:", raw || "(empty)"); + if (!resp.ok) { + throw new Error(`Git Host Error ${resp.status}: ${raw || "(empty body)"}`); + } + if (!raw) { + throw new Error("Git Host returned empty body"); + } + + // Get token from response + let token; + try { + token = JSON.parse(raw); + } catch (e) { + throw new Error("Token response is not valid JSON: " + raw); + } + + // clean up url + history.replaceState({}, "", REDIRECT_URI); + + // save token + var tokenInput = document.getElementById("token-input"); + var saveButton = document.getElementById("token-save-button"); + console.debug("Token received: ", token.access_token) + tokenInput.value = token.access_token; + saveButton.onclick(); + } + + // Connect OAuth button + document.getElementById("oauth-button").onclick = () => startLoginWithOAuth(); + + // build platform selection + const select = document.getElementById("platform-select"); + for (const [key, entry] of Object.entries(User.PLATFORMS)) { + const opt = document.createElement("option"); + opt.value = key; + opt.textContent = entry.label; + select.appendChild(opt); + } + // call once to enforce the default hidden state + onPlatformSelected(User.getGitPlatformName() || select.value || null); + // update when the user changes the selection + select.addEventListener("change", (e) => { + onPlatformSelected(e.target.value || null); + }); + + handleCallback().catch(err => { + alert("Error:\n" + (err?.message || err)); + location.reload(); + return; + }); +}; diff --git a/public/gitlab-setup/index.html b/public/gitlab-setup/index.html deleted file mode 100644 index 6e568f1..0000000 --- a/public/gitlab-setup/index.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - GitLab Account Setup | Software CaRD - - - -

Software CaRD

-

GitLab Account Setup

-

- Please click here to create a personal access token. - Copy it, paste it in the box below, and click "Save". - The token should start with glpat-. -

-

- The token will be saved in the browser. - Do not use this feature on a shared computer account. -

-

-

- - - -

- - - diff --git a/public/gitlab-setup/main.js b/public/gitlab-setup/main.js deleted file mode 100644 index 9a2f7d7..0000000 --- a/public/gitlab-setup/main.js +++ /dev/null @@ -1,47 +0,0 @@ -window.onload = async function () { - const savedToken = localStorage.getItem("gitlab-api-token"); - if (savedToken) { - document.getElementById("token-input").value = savedToken; - const alreadyKnownText = document.getElementById("already-known"); - const name = localStorage.getItem("gitlab-name"); - const username = localStorage.getItem("gitlab-username"); - // TODO: This feels like a good use case for a web component... - alreadyKnownText.innerHTML = `You are already authenticated as ${name} (${username}).`; - } - - var saveButton = document.getElementById("token-save-button"); - saveButton.onclick = async function () { - var tokenInput = document.getElementById("token-input"); - const token = tokenInput.value.trim(); - if (token) { - if (token === savedToken) { - window.location = "../"; - return; - } - - if (!token.startsWith("glpat")) { - alert("Token is invalid"); - location.reload(); - return; - } - - const response = await fetch("https://codebase.helmholtz.cloud/api/v4/user", { - headers: { "Content-Type": "application/json", "PRIVATE-TOKEN": token } - }); - - if (!response.ok) { - alert("Could not authenticate"); - location.reload(); - return; - } - - const userData = await response.json(); - localStorage.setItem("gitlab-username", userData["username"]); - localStorage.setItem("gitlab-name", userData["name"]); - localStorage.setItem("gitlab-api-token", token); - - window.location = "../"; - return; - } - }; -}; diff --git a/public/index.html b/public/index.html index 11e3a09..c4da6c5 100644 --- a/public/index.html +++ b/public/index.html @@ -19,7 +19,7 @@

Welcome

You can do the following:

    -
  1. Set up the connection to your GitLab account.
  2. +
  3. Set up the connection to your GitLab account.
  4. Load an example CI pipeline.
  5. Load the diff --git a/public/main.js b/public/main.js index d52c67c..d54c9f4 100644 --- a/public/main.js +++ b/public/main.js @@ -1,10 +1,11 @@ import { deleteAllPipelines } from "./modules/storage.js"; +import * as User from "/modules/user.js" window.onload = async function () { - const gitLabName = localStorage.getItem("gitlab-name"); - if (gitLabName) { + const username = User.getName() || User.getUsername(); + if (username) { const welcomeUsernameSpan = document.getElementById("welcome-username"); - welcomeUsernameSpan.innerText = ", " + gitLabName; + welcomeUsernameSpan.innerText = ", " + username; } const clearAllDataButton = document.getElementById("clear-all-data-button"); diff --git a/public/modules/user.js b/public/modules/user.js new file mode 100644 index 0000000..fec04c6 --- /dev/null +++ b/public/modules/user.js @@ -0,0 +1,68 @@ +export const PLATFORMS = { + jugit: { + label: "Jugit GitLab", + host: "gitlab", + baseUrl: "https://jugit.fz-juelich.de", + shortUrl: "jugit.fz-juelich.de", + apiUrl: "https://jugit.fz-juelich.de/api/v4", + clientId: "7a61209bc6348b8f53820b16a4a86e012ce64965fbf4581f86c22b455e3b5488", + }, + helmholtz: { + label: "Helmholtz GitLab", + host: "gitlab", + baseUrl: "https://codebase.helmholtz.cloud", + shortUrl: "codebase.helmholtz.cloud", + apiUrl: "https://codebase.helmholtz.cloud/api/v4", + clientId: "24722afbaa0d7c09566902879811c6552afa6a0bbd2cc421ab3e89af4faa2ed8", + }, + gitlab: { + label: "Global GitLab", + host: "gitlab", + baseUrl: "https://gitlab.com", + shortUrl: "gitlab.com", + apiUrl: "https://gitlab.com/api/v4", + clientId: "1133e9cee188c31bd68c9d0e8531774a4aae9d2458e13d83e67991213f868007", + }, + github: { + label: "GitHub", + host: "github", + baseUrl: "https://github.com", + shortUrl: "github.com", + apiUrl: "https://api.github.com", + clientId: "Ov23liQZskHsZ2MKgYhB", + }, + }; + +export function setUser(gitPlatform, apiToken, username, name) { + for (const [key, val] of [ + ["username", username], + ["name", name], + ["api-token", apiToken], + ["git-platform", gitPlatform], + ]) { + if (val == null) { + localStorage.removeItem(key) + } else { + localStorage.setItem(key, String(val)); + } + } +} + +export function setGitPlatform(platform_name) { + localStorage.setItem("git-platform-name", String(platform_name)); +} + +export function getUsername() { return localStorage.getItem("username"); } +export function getName() { return localStorage.getItem("name"); } +export function getApiToken() { return localStorage.getItem("api-token"); } +export function getGitPlatformName() { return localStorage.getItem("git-platform-name"); } + +export function getGitPlatform(platform = getGitPlatformName()) { + const key = (platform ?? "").toLowerCase().trim(); + if (!key) return null; + + const entry = PLATFORMS[key]; + if (!entry) return null; + + return entry; +} \ No newline at end of file diff --git a/public/style.css b/public/style.css index 368f3ea..a5e83fc 100644 --- a/public/style.css +++ b/public/style.css @@ -94,3 +94,7 @@ opacity: 1; .error{ color: red; } + +.hidden { + display: none; +}