init
This commit is contained in:
commit
9d735b1cf0
3 changed files with 327 additions and 0 deletions
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"deno.enable": true
|
||||||
|
}
|
3
README.md
Normal file
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Forgejo Oauth2 cli app
|
||||||
|
|
||||||
|
This is a simple command line application that allows you to create a new OAuth2 application in Forgejo.
|
321
app.ts
Normal file
321
app.ts
Normal file
|
@ -0,0 +1,321 @@
|
||||||
|
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<string>();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue