add: tool to delete all accounts that except allow listed

This commit is contained in:
Charles
2025-09-13 23:12:56 -07:00
commit 65459610d8
5 changed files with 2202 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

2012
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

17
Cargo.toml Normal file
View File

@@ -0,0 +1,17 @@
[package]
name = "gitea-tools"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0.99"
clap = { version = "4.5.47", features = ["derive"] }
dialoguer = "0.12.0"
env_logger = "0.11.8"
lazy_static = "1.5.0"
log = "0.4.28"
reqwest = { version = "0.12.23", features=["json"]}
serde = { version = "1.0.220", features = ["derive"] }
serde_json = "1.0.144"
tokio = { version = "1.47.1", features = ["full"] }
tokio-task-pool = { version = "0.1.5", features = ["log"] }

72
src/lib.rs Normal file
View File

@@ -0,0 +1,72 @@
use reqwest::header::CONTENT_TYPE;
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize, Debug)]
pub struct GetTokenResp {
pub name: String,
pub sha1: String,
}
#[derive(Deserialize, Debug)]
pub struct Email {
pub username: String,
pub email: String,
pub verified: bool
}
pub async fn get_token(
client: &reqwest::Client,
router: &Router,
username: &str,
password: &str,
otp: Option<&str>,
) -> Result<GetTokenResp, anyhow::Error> {
let mut req = client
.post(router.create_token(username))
.basic_auth(username, Some(password))
.json(&json!({
"name": "gitea-tools",
"scopes": ["write:admin", "read:admin"],
}))
.header(CONTENT_TYPE, "application/json");
if let Some(otp) = otp {
req = req.header("X-Gitea-OTP", otp);
}
let resp = req.send().await?;
let resp = resp.json::<GetTokenResp>().await?;
Ok(resp)
}
pub async fn list_emails(client: &reqwest::Client, router: &Router) -> Result<Vec<Email>, anyhow::Error> {
Ok(client.get(&router.list_emails()).send().await?.json::<Vec<Email>>().await?)
}
pub async fn purge_user(client: &reqwest::Client, router: &Router, username: &str) -> Result<(), anyhow::Error> {
client.delete(router.delete_user(username) + "?purge=true").send().await?;
Ok(())
}
#[derive(Clone, Debug)]
pub struct Router {
base: String
}
impl Router {
pub fn new(base: String) -> Self {
Self { base }
}
fn create_token(&self, username: &str) -> String {
format!("{}/api/v1/users/{username}/tokens", self.base)
}
fn list_emails(&self) -> String {
format!("{}/api/v1/admin/emails", self.base)
}
fn delete_user(&self, username: &str) -> String {
format!("{}/api/v1/admin/users/{username}", self.base)
}
}

100
src/main.rs Normal file
View File

@@ -0,0 +1,100 @@
use std::collections::HashSet;
use clap::{Parser, arg};
use dialoguer::{Input, Password};
use gitea_tools::{Router, list_emails, purge_user};
use lazy_static::lazy_static;
use log::info;
use reqwest::{ClientBuilder, header};
use tokio_task_pool::Pool;
lazy_static! {
static ref ALLOW_LIST: HashSet<&'static str> = {
let mut m = HashSet::new();
m.insert("charles@tipsy.codes");
m.insert("gitea@local.domain");
m
};
}
#[derive(Parser, Debug)]
struct Args {
#[arg(short, long)]
username: Option<String>,
#[arg(short, long)]
password: Option<String>,
#[arg(short, long)]
token: Option<String>,
#[arg(short, long)]
gitea_address: String,
}
#[tokio::main]
async fn main() {
env_logger::init();
let args = Args::parse();
let router = Router::new(args.gitea_address);
let token: String;
if let Some(t) = args.token {
token = t;
} else {
let client = reqwest::Client::new();
let username: String;
if let Some(u) = args.username {
username = u;
} else {
username = Input::new()
.with_prompt("Gitea username")
.interact_text()
.unwrap();
}
let password: String;
if let Some(p) = args.password {
password = p;
} else {
password = Password::new()
.with_prompt("Gitea password (no echo)")
.interact()
.unwrap();
}
token = ::gitea_tools::get_token(&client, &router, &username, &password, None)
.await
.unwrap()
.sha1;
}
let mut headers = header::HeaderMap::new();
let mut auth_value = header::HeaderValue::from_str(&format!("token {token}")).unwrap();
auth_value.set_sensitive(true);
headers.insert(header::AUTHORIZATION, auth_value);
let client = ClientBuilder::new()
.default_headers(headers)
.build()
.unwrap();
// Get a list of active emails
let pool = Pool::bounded(10);
loop {
let emails = list_emails(&client, &router).await.unwrap();
if emails.len() == ALLOW_LIST.len() {
break;
}
for email in emails {
if ALLOW_LIST.contains(&email.email as &str) {
continue;
}
let username = email.username;
let client = client.clone();
let router = router.clone();
pool.spawn(async move {
info!("Deleting {}", username);
purge_user(&client, &router, &username).await.unwrap();
info!("Deleted {}!", username);
}).await.unwrap();
}
}
// Do we need to wait for pool to finish?
}