import { AsyncLocalStorage } from "node:async_hooks"; import { Select } from "jsr:@cliffy/prompt@1.0.0-rc.7"; import { promptSecret, parseArgs } from "jsr:@std/cli" const tokenStorage = new AsyncLocalStorage(); function printHelp() { const helpText = `forgejo oauth2cli. interactive client credentials generator. Usage: app.ts [options] Options: -h, --help Show this help message -v, --version Show version information`; console.log(helpText); } const URL_BASE = "https://git.monoid.top"; type Token = { id: number, name: string, scopes: string[], sha1: string, token_last_eight: string, } async function listTokens(username: string, secret: string) { const res = await fetch(`${URL_BASE}/api/v1/users/${username}/tokens`, { method: "GET", headers: { "Authorization": `Basic ${btoa(`${username}:${secret}`)}`, "Content-Type": "application/json", }, }); if (!res.ok) { console.error("Failed to list tokens:", res.statusText); console.error("Response:", await res.text()); return; } const data = await res.json(); if (data.error) { console.error("Error listing tokens:", data.error); return; } return data as Token[]; } async function deleteToken(username: string, secret: string, tokenId: number) { const res = await fetch(`${URL_BASE}/api/v1/users/${username}/tokens/${tokenId}`, { method: "DELETE", headers: { "Authorization": `Basic ${btoa(`${username}:${secret}`)}`, "Content-Type": "application/json", }, }); if (!res.ok) { console.error("Failed to delete token:", res.statusText); console.error("Response:", await res.text()); return; } } async function getToken(username: string, secret: string) { // Basic auth // TODO: two factor auth and webauthn support const res = await fetch(`${URL_BASE}/api/v1/users/${username}/tokens`, { method: "POST", headers: { "Authorization": `Basic ${btoa(`${username}:${secret}`)}`, "Content-Type": "application/json", }, body: JSON.stringify({ name: "oauth2cli", scopes: ["write:user", "read:user"], }), }); if (!res.ok) { console.error("Failed to create token:", res.statusText); console.error("Response:", await res.text()); return; } const data = await res.json(); if (data.error) { console.error("Error creating token:", data.error); return; } return data as Token; } type Oauth2Application = { id: number, name: string, redirect_uris: string[], client_id: string, client_secret: string, confidential_client: boolean, /** * @format date-time */ created: string, } function prettyOauth2Application(app: Oauth2Application): string { return (`ID: ${app.id} Name: ${app.name} Redirect URIs: ${app.redirect_uris.join(", ")} Client ID: ${app.client_id} Confidential Client: ${app.confidential_client ? "Yes" : "No"} Created: ${new Date(app.created).toLocaleString()}`); } async function createOauth2Application({ confidential_client = false, name, redirect_uris, }: { confidential_client?: boolean, name: string, redirect_uris: string[], }) { const token = tokenStorage.getStore(); if (!token) { throw new Error("Token not found in storage."); } const res = await fetch(`${URL_BASE}/api/v1/user/applications/oauth2`, { method: "POST", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ confidential_client, name, redirect_uris, }), }); if (!res.ok) { console.error("Failed to create oauth2 application:", res.statusText); console.error("Response:", await res.text()); return; } const data = await res.json(); if (data.error) { console.error("Error creating oauth2 application:", data.error); return; } return data as Oauth2Application; } async function getOauth2Application() { const token = tokenStorage.getStore(); if (!token) { throw new Error("Token not found in storage."); } const res = await fetch(`${URL_BASE}/api/v1/user/applications/oauth2`, { method: "GET", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!res.ok) { console.error("Failed to get oauth2 application:", res.statusText); console.error("Response:", await res.text()); return; } const data = await res.json(); if (data.error) { console.error("Error getting oauth2 application:", data.error); return; } return data as Oauth2Application[]; } async function deleteOauth2Application(id: number) { const token = tokenStorage.getStore(); if (!token) { throw new Error("Token not found in storage."); } const res = await fetch(`${URL_BASE}/api/v1/user/applications/oauth2/${id}`, { method: "DELETE", headers: { "Authorization": `Bearer ${token}`, "Content-Type": "application/json", }, }); if (!res.ok) { console.error("Failed to delete oauth2 application:", res.statusText); console.error("Response:", await res.text()); return; } } async function main() { const args = parseArgs(Deno.args); if (args.help) { printHelp(); return; } if (args.version) { console.log("forgejo oauth2cli v0.1.0"); return; } // TODO: libsecret support (Linux), keychain support (macOS), wincred support (Windows) const username = await prompt("Enter your username: "); if (!username) { console.error("Username is required."); return; } const secret = await promptSecret("Enter your secret: "); if (!secret) { console.error("Secret is required."); return; } // check already created tokens const tokens = await listTokens(username, secret); if (!tokens) { console.error("Failed to list tokens."); return; } console.log("Tokens:", tokens.map(t => t.name)); if (tokens.map(t=> t.name).includes("oauth2cli")) { console.log("Token already created, deleting..."); const token = tokens.find(t => t.name === "oauth2cli"); if (!token) { console.error("Failed to find token."); return; } await deleteToken(username, secret, token.id); } console.log("Creating new token..."); // create new token const token = await getToken(username, secret); if (!token) { console.error("Failed to get token."); return; } console.log("Token created successfully!"); await tokenStorage.run(token.sha1 ,async () => { while (true) { const opt = await Select.prompt({ message: "Select an option", options: [ { name: "Create new oauth2 application", value: "create" }, { name: "Get existing oauth2 application", value: "get" }, { name: "Delete oauth2 application", value: "delete" }, { name: "List oauth2 applications", value: "list" }, { name: "Exit", value: "exit" }, ], }); switch (opt) { case "create": { const name = await prompt("Enter the name of the application: "); if (!name) { console.error("Name is required."); break; } const redirect_uris = await prompt("Enter the redirect uris (comma separated): "); if (!redirect_uris) { console.error("Redirect uris are required."); break; } const app = await createOauth2Application({ name, redirect_uris: redirect_uris.split(","), }); if (!app) { console.error("Failed to create oauth2 application."); break; } console.log("Oauth2 application created successfully!", app); break; } case "get": { const app = await getOauth2Application(); if (!app) { console.error("Failed to get oauth2 application."); break; } console.log("Oauth2 applications:", app); break; } case "delete": { const id = await prompt("Enter the id of the application to delete: "); if (!id) { console.error("Id is required."); break; } const appId = parseInt(id); if (isNaN(appId)) { console.error("Id must be a number."); break; } await deleteOauth2Application(appId); console.log("Oauth2 application deleted successfully!"); break; } case "list": { const app = await getOauth2Application(); if (!app) { console.error("Failed to get oauth2 application."); break; } console.log("Oauth2 applications:", app.map(prettyOauth2Application).join("\n\n")); break; } case "exit": default: return; } } }) } if (import.meta.main) { await main(); }