const CONFIG = { client_id: "3d463405-d098-4038-8b25-a44ca5b9ab9c", redirect_uri: "http://127.0.0.1:3000/", authorization_endpoint: "https://git.kjao.me/login/oauth/authorize", token_endpoint: "https://git.kjao.me/login/oauth/access_token", requested_scopes: "read:repository,write:repository" }; const STATES = { // Either the non-d auth: "auth", error: "error", loading: "loading", repoSelected: "repoSelected", repoCreated: "repoCreated", buttonOn: "enabled", featureOn: "fEnabled", confirmOn: "enabled", } var G_REPO_VALUE = "", G_CONFIRM_VALUE = "", G_CONFIRM = -1; // UTILITY // document.on = document.addEventListener; function sugar(obj) { obj.on = obj.addEventListener; obj.addClass = (x) => obj.classList.add(x); obj.delClass = (x) => obj.classList.remove(x); obj.hasClass = (x) => obj.classList.contains(x); obj.setClass = (x, y) => obj.classList.toggle(x, y); return obj; } function get(s) { let x = [...document.querySelectorAll(s)].map(sugar); return (x.length === 1) ? x[0] : x; } // Check if object is empty. function isEmpty(obj) { for (const prop in obj) { if (Object.hasOwn(obj, prop)) { return false; } } return true; } // Returns error and resolves promise if necessary. function getErr(f) { return (err) => { if (err instanceof Promise) { return err.then(f); } else { return err; } } } // Parse a query string into an object function parseQueryString(string) { if(string == "") { return {}; } var segments = string.split("&").map(s => s.split("=") ); var queryString = {}; segments.forEach(s => G_QUERYString[s[0]] = s[1]); return queryString; } // Fetch function for applet use case. async function jfetch(url, method, obj={}) { let request = { headers: { "Content-Type": "application/json", "Authorization": window.localStorage.getItem("token"), }, method: method, }; if (!isEmpty(obj)) request["body"] = JSON.stringify(obj); return fetch(url, request) .then((response) => { let contentType = response.headers.get("Content-Type"); let json; if (contentType && contentType.indexOf("application/json" !== -1)) { json = response.json(); } else { // Reprocess non-json into json. json = response.text().then((text) => { return { status: response.status, "message": text }; }); } if (!response.ok) { throw json; } return json; }); } // OAUTH // async function getToken() { let response; if (G_QUERY.code) { response = await jfetch("/api/token", "POST", { code: G_QUERY.code }); } else { response = await jfetch("/api/token", "PATCH", { code: window.localStorage.getItem("refresh_token") }); } window.localStorage.setItem("token", response.access_token); window.localStorage.setItem("token_expiry", (Date.now() + response.expires_in*1000).toString()); window.localStorage.setItem("refresh_token", response.refresh_token); } async function tryRefresh() { if (Date.now() < parseInt(window.localStorage.getItem("token_expiry"))) return; // Not expired. await getToken().catch((err) => { let json = err.json(); if (json.status == 401 && json.code === 0) { // Could not refresh, so refresh token expired. window.localStorage.clear(); alert(`Could not get new session, did you remove access from Gitea?`); window.location = "/"; } else { throw json; } }); } // BUTTON ONCLICK MIDDLEWARE // // Refresh token if necessary. function tryAuth(f) { return async (e) => { e.preventDefault(); if (!G_AUTH) return; await tryRefresh(); await f(e); } } // Adds loading classes for UI while awaiting. function doLoading(f) { return async (e) => { if (e.target.hasClass("loading")) return; e.target.addClass(STATES["loading"]); await f(e); e.target.delClass(STATES["loading"]); } } // BUTTON ONCLICK HELPERS // function setRepoError(s, err) { const input = get("#repoSelectInput"), error = get("#repoSelectError"), info = get("#repoInfo"); error.innerText = s; info.setClass(STATES["repoSelected"], !err); input.setClass(STATES["error"], err); } function setRepoActions(s) { get("#repoCreate").setClass(STATES["buttonOn"], !s); get("#repoDelete").setClass(STATES["buttonOn"], s); get("#repoSecret").setClass(STATES["buttonOn"], s); get("#repoFeatures").setClass(STATES["repoCreated"], s); } async function getConfirm() { get("#confirmOverlay").addClass(STATES["confirmOn"]); return new Promise((resolve, reject) => { const check = function() { if (G_CONFIRM == -1) { setTimeout(check, 100); } else { get("#confirmOverlay").delClass(STATES["confirmOn"]); resolve(G_CONFIRM); G_CONFIRM = -1; } } check(); }); } document.on("keydown", (e) => { if (e.keyCode == 27) get("#confirmOverlay").delClass(STATES["confirmOn"]); }); // BUTTON ONCLICK FUNCTIONS // get("#login").on("click", async (e) => { e.preventDefault(); if (G_AUTH) { window.localStorage.clear(); window.location = "/"; } else { // Build the authorization URL let url = CONFIG.authorization_endpoint + "?response_type=code" + "&client_id="+encodeURIComponent(CONFIG.client_id) + "&scope="+encodeURIComponent(CONFIG.requested_scopes) + "&redirect_uri="+encodeURIComponent(CONFIG.redirect_uri); // Redirect to the authorization server window.location = url; } }); get("#repoSelectInput").on("keydown", (e) => { if (e.keyCode === 13) { get("#repoSelectButton").click(); return; } let value = e.target.value; if (value !== G_REPO_VALUE) { get("#repoSelectError").innerText = ""; // Delete error text. get("#repoSelectInput").delClass(STATES["error"]); // Remove error class. get("#repoInfo").delClass(STATES["repoSelected"]); // Remove repoSelect class. setRepoActions(false); // Set actions to default. get(".feature").forEach((feature) => feature.delClass(STATES["featureOn"])); G_REPO_VALUE = value; } }); get("#repoSelectButton").on("click", tryAuth(doLoading(selectRepo))); async function selectRepo(e) { const input = get("#repoSelectInput"); let repo = input.value.trim(); input.value = repo; if (repo === "") { setRepoError("Field cannot be empty.", true); return; } await jfetch("/api/repo/" + repo, "GET") .then((json) => { setRepoError("OK", false); setRepoActions(json["exists"]); if (json["exists"]) { json["features"].forEach((feature) => { get(`.feature[data-tag=${feature}]`).addClass(STATES["featureOn"]); }); } get("#repoLinkInput").value = CONFIG["redirect_uri"] + repo; get("#repoInfo").addClass(STATES["repoSelected"]); }) .catch(getErr((err) => { console.log(err); setRepoError("Unable to find repository or you have insufficient permissions.", true); })); } get("#repoCreate").on("click", tryAuth(doLoading(createRepo))); async function createRepo(e) { const input = get("#repoSelectInput"); await jfetch("/api/repo/" + input.value, "POST") .then((json) => { setRepoActions(true); }) .catch(getErr((err) => { console.log(err); alert("An unexpected error occurred! Check console for more details."); })); } get("#repoDelete").on("click", tryAuth(doLoading(deleteRepo))); async function deleteRepo(e) { const input = get("#repoSelectInput"); if (!(await getConfirm())) return; await jfetch("/api/repo/" + input.value, "DELETE") .then((json) => { setRepoActions(false); }) .catch(getErr((err) => { console.log(err); alert("An unexpected error occurred! Check console for more details."); })); } get("#repoSecret").on("click", tryAuth(doLoading(regenSecret))); async function regenSecret(e) { const input = get("#repoSelectInput"); await jfetch("/api/repo/" + input.value, "PATCH", { secret: true, feature: "" }) .then((json) => { setRepoActions(true); }) .catch(getErr((err) => { console.log(err); alert("An unexpected error occurred! Check console for more details."); })); } get("#repoLinkButton").on("click", async (e) => { const input = get("#repoLinkInput"); input.select(); input.setSelectionRange(0, 99999); navigator.clipboard.writeText(input.value); }); get(".feature").forEach((el) => el.on("click", tryAuth(doLoading(toggleFeature)))); async function toggleFeature(e) { const input = get("#repoSelectInput"); let attr = e.target.getAttribute("data-tag"); if (e.target.hasClass(STATES["featureOn"]) && !(await getConfirm())) return; await jfetch("/api/repo/" + input.value, "PATCH", { secret: false, feature: attr }) .then((json) => { e.target.setClass(STATES["featureOn"]); }) .catch(getErr((err) => { console.log(err); alert("An unexpected error occurred! Check console for more details."); })); } get("#confirmInput").on("keydown", function(e) { if (e.keyCode === 13) { get("#confirmButtonYes").click(); return; } if (e.keyCode === 27) { get("#confirmButtonNo").click(); return; } let value = e.target.value; if (value !== G_CONFIRM_VALUE) { get("#confirmInput").delClass(STATES["error"]); G_REPO_VALUE = value; } }); get("#confirmButtonNo").on("click", (e) => { G_CONFIRM = false; get("#confirmInput").value = ""; }); get("#confirmButtonYes").on("click", (e) => { let confirmValue = get("#confirmInput").value.trim(); let repoValue = get("#repoSelectInput").value; if (confirmValue === repoValue) { G_CONFIRM = true; get("#confirmInput").value = ""; } else { get("#confirmInput").addClass(STATES["error"]); } }); async function init() { // Get query and delete URL parameters. if(G_QUERY.error) alert("Error returned from authorization server: "+q.error); // Check for auth errors. if(G_QUERY.code) { await getToken(); G_AUTH = true; window.history.replaceState({}, document.title, window.location.pathname); } get("body").setClass(STATES["auth"], G_AUTH); } var G_QUERY = parseQueryString(window.location.search.substring(1)); var G_AUTH = window.localStorage.getItem("token") !== null; init();