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