refactor: all
This commit is contained in:
parent
9d735b1cf0
commit
5a09d5b070
7 changed files with 482 additions and 310 deletions
36
api/base-api.ts
Normal file
36
api/base-api.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
export class ApiError extends Error {
|
||||||
|
constructor(public readonly statusCode: number, message: string) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ApiError";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BaseApi {
|
||||||
|
constructor(protected readonly baseUrl: string) {}
|
||||||
|
|
||||||
|
protected async request<T>(
|
||||||
|
endpoint: string,
|
||||||
|
options: {
|
||||||
|
method: string,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
body?: string
|
||||||
|
}
|
||||||
|
): Promise<T> {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
const res = await fetch(url, options);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
// Log the error response for debugging
|
||||||
|
console.error(`Failed with ${res.status} ${res.statusText}`);
|
||||||
|
const errorText = await res.json();
|
||||||
|
console.error("Response:", errorText);
|
||||||
|
|
||||||
|
throw new ApiError(res.status, errorText.message);
|
||||||
|
}
|
||||||
|
if (res.status === 204) {
|
||||||
|
return undefined as T; // No content: ;
|
||||||
|
}
|
||||||
|
const data = await res.json();
|
||||||
|
return data as T;
|
||||||
|
}
|
||||||
|
}
|
82
api/oauth2-api.ts
Normal file
82
api/oauth2-api.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { BaseApi } from "./base-api.ts";
|
||||||
|
|
||||||
|
export type Oauth2Application = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
redirect_uris: string[],
|
||||||
|
client_id: string,
|
||||||
|
client_secret: string,
|
||||||
|
confidential_client: boolean,
|
||||||
|
/**
|
||||||
|
* @format date-time
|
||||||
|
*/
|
||||||
|
created: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class OAuth2Api extends BaseApi {
|
||||||
|
private token: string;
|
||||||
|
|
||||||
|
constructor(baseUrl: string, token: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to update token if needed later
|
||||||
|
updateToken(token: string): void {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOauth2Application(params: {
|
||||||
|
name: string,
|
||||||
|
redirect_uris: string[],
|
||||||
|
confidential_client?: boolean,
|
||||||
|
}): Promise<Oauth2Application | undefined> {
|
||||||
|
return await this.request<Oauth2Application>(`/api/v1/user/applications/oauth2`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${this.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async getOauth2Applications(): Promise<Oauth2Application[] | undefined> {
|
||||||
|
return await this.request<Oauth2Application[]>(`/api/v1/user/applications/oauth2`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${this.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOauth2Application(id: number): Promise<void> {
|
||||||
|
if (!this.token) {
|
||||||
|
throw new Error("Authentication required. Please login first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.request<void>(`/api/v1/user/applications/oauth2/${id}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${this.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOauth2Application(id: number, params: {
|
||||||
|
name: string,
|
||||||
|
redirect_uris: string[],
|
||||||
|
confidential_client?: boolean,
|
||||||
|
}): Promise<Oauth2Application | undefined> {
|
||||||
|
return await this.request<Oauth2Application>(`/api/v1/user/applications/oauth2/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Bearer ${this.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
51
api/token-api.ts
Normal file
51
api/token-api.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { BaseApi } from "./base-api.ts";
|
||||||
|
|
||||||
|
export type Token = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
scopes: string[],
|
||||||
|
sha1: string,
|
||||||
|
token_last_eight: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokenApi extends BaseApi {
|
||||||
|
constructor(baseUrl: string, private tokenName: string) {
|
||||||
|
super(baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTokens(username: string, secret: string): Promise<Token[] | undefined> {
|
||||||
|
return await this.request<Token[]>(`/api/v1/users/${username}/tokens`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${btoa(`${username}:${secret}`)}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteToken(username: string, secret: string, tokenId: number): Promise<boolean> {
|
||||||
|
const result = await this.request(`/api/v1/users/${username}/tokens/${tokenId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${btoa(`${username}:${secret}`)}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return result !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async createToken(username: string, secret: string): Promise<Token | undefined> {
|
||||||
|
return await this.request<Token>(`/api/v1/users/${username}/tokens`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Authorization": `Basic ${btoa(`${username}:${secret}`)}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: this.tokenName,
|
||||||
|
scopes: ["write:user", "read:user"],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
528
app.ts
528
app.ts
|
@ -1,319 +1,229 @@
|
||||||
import { AsyncLocalStorage } from "node:async_hooks";
|
import { Command } from "jsr:@cliffy/command@1.0.0-rc.7";
|
||||||
import { Select } from "jsr:@cliffy/prompt@1.0.0-rc.7";
|
import { Secret } from "jsr:@cliffy/prompt@1.0.0-rc.7";
|
||||||
import { promptSecret, parseArgs } from "jsr:@std/cli"
|
import { homedir } from "node:os";
|
||||||
|
import { TokenApi } from "./api/token-api.ts";
|
||||||
const tokenStorage = new AsyncLocalStorage<string>();
|
import { OAuth2Api } from "./api/oauth2-api.ts";
|
||||||
|
import { CredentialManager } from "./util/credential-manager.ts";
|
||||||
function printHelp() {
|
import { prettyOauth2Application, saveSecretKeys } from "./util/helpers.ts";
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Constants that were previously in types.ts
|
||||||
const URL_BASE = "https://git.monoid.top";
|
const URL_BASE = "https://git.monoid.top";
|
||||||
|
const TOKEN_NAME = "oauth2cli";
|
||||||
type Token = {
|
const CREDENTIALS_FILE = `${homedir()}/.oauth2cli-forgejo`;
|
||||||
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() {
|
async function main() {
|
||||||
const args = parseArgs(Deno.args);
|
const tokenApi = new TokenApi(URL_BASE, TOKEN_NAME);
|
||||||
|
const credentialManager = new CredentialManager(CREDENTIALS_FILE, TOKEN_NAME);
|
||||||
|
|
||||||
if (args.help) {
|
try {
|
||||||
printHelp();
|
await new Command()
|
||||||
return;
|
.name("forgejo-oauth2cli")
|
||||||
}
|
.version("0.1.0")
|
||||||
|
.description("Interactive client credentials generator for Forgejo.")
|
||||||
if (args.version) {
|
.command("login", "Authenticate with Forgejo interactively")
|
||||||
console.log("forgejo oauth2cli v0.1.0");
|
.action(async() => {
|
||||||
return;
|
const username = await prompt("Enter your username: ");
|
||||||
}
|
if (!username) {
|
||||||
|
console.error("Username is required.");
|
||||||
// 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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const secret = await Secret.prompt("Enter your secret: ");
|
||||||
|
if (!secret) {
|
||||||
|
console.error("Secret is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for existing tokens
|
||||||
|
const tokens = await tokenApi.listTokens(username, secret);
|
||||||
|
if (!tokens) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete existing token if found
|
||||||
|
const existingToken = tokens.find(t => t.name === TOKEN_NAME);
|
||||||
|
if (existingToken) {
|
||||||
|
console.log("Existing token found, replacing it...");
|
||||||
|
await tokenApi.deleteToken(username, secret, existingToken.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new token
|
||||||
|
console.log("Creating new token...");
|
||||||
|
const token = await tokenApi.createToken(username, secret);
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store credentials
|
||||||
|
await credentialManager.storeCredentials(username, token.sha1);
|
||||||
|
console.log("Login successful! Credentials stored securely.");
|
||||||
|
})
|
||||||
|
.command("logout", "Delete token and credentials")
|
||||||
|
.action(async () => {
|
||||||
|
const username = await prompt("Enter your username: ");
|
||||||
|
if (!username) {
|
||||||
|
console.error("Username is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = await Secret.prompt("Enter your secret: ");
|
||||||
|
if (!secret) {
|
||||||
|
console.error("Secret is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and delete token
|
||||||
|
const tokens = await tokenApi.listTokens(username, secret);
|
||||||
|
if (!tokens) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingToken = tokens.find(t => t.name === TOKEN_NAME);
|
||||||
|
if (existingToken) {
|
||||||
|
await tokenApi.deleteToken(username, secret, existingToken.id);
|
||||||
|
console.log("Token deleted from Forgejo.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear stored credentials
|
||||||
|
await credentialManager.clearCredentials(username);
|
||||||
|
console.log("Logout successful! Credentials removed.");
|
||||||
|
})
|
||||||
|
.command("list, ls", "List all OAuth2 applications")
|
||||||
|
.action(async () => {
|
||||||
|
const credentials = await credentialManager.getCredentials();
|
||||||
|
if (!credentials) return;
|
||||||
|
|
||||||
|
// Create OAuth2Api with token
|
||||||
|
const oauth2Api = new OAuth2Api(URL_BASE, credentials.token);
|
||||||
|
const apps = await oauth2Api.getOauth2Applications();
|
||||||
|
if (!apps) return;
|
||||||
|
|
||||||
|
if (apps.length === 0) {
|
||||||
|
console.log("No OAuth2 applications found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("OAuth2 applications:");
|
||||||
|
console.log(apps.map(prettyOauth2Application).join("\n\n"));
|
||||||
|
})
|
||||||
|
.command("create", "Create new OAuth2 application")
|
||||||
|
.action(async () => {
|
||||||
|
const credentials = await credentialManager.getCredentials();
|
||||||
|
if (!credentials) return;
|
||||||
|
|
||||||
|
// Create OAuth2Api with token
|
||||||
|
const oauth2Api = new OAuth2Api(URL_BASE, credentials.token);
|
||||||
|
|
||||||
|
const name = await prompt("Enter the name of the application: ");
|
||||||
|
if (!name) {
|
||||||
|
console.error("Name is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUris = await prompt("Enter the redirect URIs (comma separated): ");
|
||||||
|
if (!redirectUris) {
|
||||||
|
console.error("Redirect URIs are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await oauth2Api.createOauth2Application({
|
||||||
|
name,
|
||||||
|
redirect_uris: redirectUris.split(",").map(uri => uri.trim()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
|
console.log("OAuth2 application created successfully!");
|
||||||
|
console.log(prettyOauth2Application(app));
|
||||||
|
|
||||||
|
// Save secret keys
|
||||||
|
const path = await prompt("Enter the path to save the OAuth2 secret keys: ");
|
||||||
|
if (path) {
|
||||||
|
await saveSecretKeys(path, app.client_id, app.client_secret);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.command("delete", "Delete OAuth2 application")
|
||||||
|
.action(async () => {
|
||||||
|
const credentials = await credentialManager.getCredentials();
|
||||||
|
if (!credentials) return;
|
||||||
|
|
||||||
|
// Create OAuth2Api with token
|
||||||
|
const oauth2Api = new OAuth2Api(URL_BASE, credentials.token);
|
||||||
|
|
||||||
|
const id = await prompt("Enter the ID of the application to delete: ");
|
||||||
|
if (!id) {
|
||||||
|
console.error("ID is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = parseInt(id);
|
||||||
|
if (isNaN(appId)) {
|
||||||
|
console.error("ID must be a number.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = await oauth2Api.deleteOauth2Application(appId);
|
||||||
|
if (success) {
|
||||||
|
console.log("OAuth2 application deleted successfully!");
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.command("update", "Update OAuth2 application")
|
||||||
|
.action(async () => {
|
||||||
|
const credentials = await credentialManager.getCredentials();
|
||||||
|
if (!credentials) return;
|
||||||
|
|
||||||
|
// Create OAuth2Api with token
|
||||||
|
const oauth2Api = new OAuth2Api(URL_BASE, credentials.token);
|
||||||
|
|
||||||
|
const id = await prompt("Enter the ID of the application to update: ");
|
||||||
|
if (!id) {
|
||||||
|
console.error("ID is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appId = parseInt(id);
|
||||||
|
if (isNaN(appId)) {
|
||||||
|
console.error("ID must be a number.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = await prompt("Enter the new name of the application: ");
|
||||||
|
if (!name) {
|
||||||
|
console.error("Name is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectUris = await prompt("Enter the new redirect URIs (comma separated): ");
|
||||||
|
if (!redirectUris) {
|
||||||
|
console.error("Redirect URIs are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = await oauth2Api.updateOauth2Application(appId, {
|
||||||
|
name,
|
||||||
|
redirect_uris: redirectUris.split(",").map(uri => uri.trim()),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
|
console.log("OAuth2 application updated successfully!");
|
||||||
|
console.log(prettyOauth2Application(app));
|
||||||
|
|
||||||
|
// Save secret keys
|
||||||
|
const path = await prompt("Enter the path to save the OAuth2 secret keys: ");
|
||||||
|
if (path) {
|
||||||
|
await saveSecretKeys(path, app.client_id, app.client_secret);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.parse(Deno.args);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
console.error("An error occurred:", error.message);
|
||||||
}
|
}
|
||||||
})
|
else {
|
||||||
|
console.error("An unexpected error occurred:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (import.meta.main) {
|
if (import.meta.main) {
|
||||||
|
|
24
types.ts
Normal file
24
types.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
export const URL_BASE = "https://git.monoid.top";
|
||||||
|
export const TOKEN_NAME = "oauth2cli";
|
||||||
|
export const CREDENTIALS_FILE = `${Deno.env.get("HOME") || ""}/.oauth2cli-forgejo`;
|
||||||
|
|
||||||
|
export type Token = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
scopes: string[],
|
||||||
|
sha1: string,
|
||||||
|
token_last_eight: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Oauth2Application = {
|
||||||
|
id: number,
|
||||||
|
name: string,
|
||||||
|
redirect_uris: string[],
|
||||||
|
client_id: string,
|
||||||
|
client_secret: string,
|
||||||
|
confidential_client: boolean,
|
||||||
|
/**
|
||||||
|
* @format date-time
|
||||||
|
*/
|
||||||
|
created: string,
|
||||||
|
}
|
53
util/credential-manager.ts
Normal file
53
util/credential-manager.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { Entry } from "npm:@napi-rs/keyring";
|
||||||
|
|
||||||
|
export class CredentialManager {
|
||||||
|
constructor(
|
||||||
|
private credentialsFile: string,
|
||||||
|
private tokenName: string
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async storeCredentials(username: string, token: string): Promise<void> {
|
||||||
|
// Store username
|
||||||
|
await Deno.writeTextFile(this.credentialsFile, username);
|
||||||
|
|
||||||
|
// Store token in keyring
|
||||||
|
const entry = new Entry(this.tokenName, username);
|
||||||
|
if (entry.getPassword()) {
|
||||||
|
entry.deletePassword();
|
||||||
|
}
|
||||||
|
entry.setPassword(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCredentials(): Promise<{ username: string, token: string } | undefined> {
|
||||||
|
try {
|
||||||
|
const username = await Deno.readTextFile(this.credentialsFile);
|
||||||
|
const entry = new Entry(this.tokenName, username);
|
||||||
|
const token = entry.getPassword();
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.error("No token found in keyring. Please login first.");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { username, token };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("No credentials found. Please login first.");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearCredentials(username: string): Promise<void> {
|
||||||
|
// Remove username file
|
||||||
|
try {
|
||||||
|
await Deno.remove(this.credentialsFile);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to remove credentials file:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove token from keyring
|
||||||
|
const entry = new Entry(this.tokenName, username);
|
||||||
|
if (entry.getPassword()) {
|
||||||
|
entry.deletePassword();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
util/helpers.ts
Normal file
16
util/helpers.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import { Oauth2Application } from "../api/oauth2-api.ts";
|
||||||
|
|
||||||
|
export 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()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveSecretKeys(path: string, clientId: string, clientSecret: string): Promise<void> {
|
||||||
|
const secretKey = `FORGEJO_CLIENT_KEY=${clientId}\nFORGEJO_SECRET_KEY=${clientSecret}`;
|
||||||
|
await Deno.writeTextFile(path, secretKey, { append: true, create: true });
|
||||||
|
console.log("OAuth2 secret keys saved successfully!");
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue