use crate::permissions::Perm;
use crate::*;
use crate::{db, get_user_id_strict, get_user_login_strict, Config, Repo, StatusCode};
use axum::{
debug_handler,
extract::{ConnectInfo, Form, State},
response::{IntoResponse, Redirect, Response},
routing::{get, post},
Json, Router,
};
use axum_extra::extract::SignedCookieJar;
use diesel::dsl::sql;
use diesel::sql_types::Bool;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl};
use diesel_async::scoped_futures::ScopedFutureExt;
use std::collections::BTreeMap;
use std::net::SocketAddr;
use thrussh_keys::PublicKeyBase64;
pub fn router() -> Router<Config> {
Router::new()
.route("/", get(settings))
.route("/ssh", get(ssh).post(ssh_post))
.route("/ssh/delete", post(ssh_delete))
.route("/ssh/add", post(ssh_add))
.route("/repo/delete", post(delete_repo))
.route("/repo/add", post(create_repo))
.route("/delete", post(delete))
.fallback(crate::fallback)
}
#[derive(Debug, Serialize)]
struct Settings<T> {
login: String,
token: Option<String>,
#[serde(flatten)]
content: T,
}
#[derive(Debug, Serialize)]
struct UserSettings {
repos: BTreeMap<String, Repo>,
is_owner: bool,
}
#[derive(Debug, Serialize)]
struct Ssh {
ssh_keys: BTreeMap<uuid::Uuid, String>,
}
#[debug_handler]
async fn settings(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
) -> Result<Response, crate::Error> {
use db::repositories::dsl as r;
let (uid, login) = crate::get_user_login_strict(&jar, &config).await?;
let mut db = config.db.get().await?;
let repos = r::repositories
.filter(r::owner.eq(uid))
.order_by(r::name)
.select(r::name)
.get_results::<String>(&mut db)
.await?;
debug!("repos = {:?}", repos);
let resp = Settings {
login,
token: token.authenticity_token().ok(),
content: UserSettings {
repos: repos
.into_iter()
.map(|r| (r.clone(), Repo { private: false }))
.collect(),
is_owner: true,
},
};
debug!("{:?}", resp);
Ok((token, Json(resp)).into_response())
}
#[debug_handler]
async fn ssh(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
) -> Result<Response, crate::Error> {
use db::publickeys::dsl as publickeys;
use db::users::dsl as u;
let uid = get_user_id_strict(&jar)?;
let mut db = config.db.get().await?;
let user = u::users
.select(u::login)
.filter(u::id.eq(uid))
.filter(u::email_is_invalid.is_null())
.get_result::<String>(&mut db)
.await?;
let ssh_keys = publickeys::publickeys
.filter(publickeys::user_id.eq(uid))
.order_by(publickeys::publickey)
.select((publickeys::id, publickeys::publickey))
.get_results::<(uuid::Uuid, String)>(&mut db)
.await?;
let resp = Settings {
login: user,
token: token.authenticity_token().ok(),
content: Ssh {
ssh_keys: ssh_keys.into_iter().collect(),
},
};
debug!("{:?}", resp);
Ok((token, Json(resp)).into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SshDelete {
delete: uuid::Uuid,
token: String,
}
#[debug_handler]
async fn ssh_delete(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
del: Form<SshDelete>,
) -> Result<Response, crate::Error> {
token.verify(&del.token)?;
use db::publickeys::dsl as publickeys;
let uid = get_user_id_strict(&jar)?;
diesel::delete(
publickeys::publickeys
.find(del.delete)
.filter(publickeys::user_id.eq(uid)),
)
.execute(&mut config.db.get().await?)
.await?;
Ok(Redirect::to("/settings/ssh").into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SshAdd {
key: String,
token: String,
}
use lazy_static::*;
lazy_static! {
static ref SSH_KEY: regex::Regex =
regex::Regex::new(r#"\s*((ssh-\S+)\s+(?P<key>.*)\s+(\S+))"#).unwrap();
}
#[debug_handler]
async fn ssh_add(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
add: Form<SshAdd>,
) -> Result<Response, crate::Error> {
token.verify(&add.token)?;
let uid = get_user_id_strict(&jar)?;
let bin = if let Some(cap) = SSH_KEY.captures(&add.key) {
let b = cap.name("key").map(|x| x.as_str()).unwrap_or(&add.key);
match thrussh_keys::parse_public_key_base64(b.trim()) {
Ok(thrussh_keys::key::PublicKey::RSA { key, .. }) => {
thrussh_keys::key::PublicKey::RSA {
key,
hash: thrussh_keys::key::SignatureHash::SHA2_512,
}
}
Ok(k) => k,
Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
}
} else {
return Ok(StatusCode::BAD_REQUEST.into_response());
};
use db::publickeys::dsl as publickeys;
diesel::insert_into(publickeys::publickeys)
.values((
publickeys::user_id.eq(uid),
publickeys::publickey.eq(&add.key),
publickeys::bin.eq(&bin.public_key_bytes()),
))
.on_conflict_do_nothing()
.execute(&mut config.db.get().await?)
.await?;
Ok(Redirect::to("/settings/ssh").into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SshPost {
id: uuid::Uuid,
key: String,
token: String,
}
#[debug_handler]
async fn ssh_post(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
post: Form<SshPost>,
) -> Result<Response, crate::Error> {
token.verify(&post.token)?;
let uid = get_user_id_strict(&jar)?;
let bin = if let Some(cap) = SSH_KEY.captures(&post.key) {
let b = cap.name("key").map(|x| x.as_str()).unwrap_or(&post.key);
match thrussh_keys::parse_public_key_base64(b.trim()) {
Ok(thrussh_keys::key::PublicKey::RSA { key, .. }) => {
thrussh_keys::key::PublicKey::RSA {
key,
hash: thrussh_keys::key::SignatureHash::SHA2_512,
}
}
Ok(k) => k,
Err(_) => return Ok(StatusCode::BAD_REQUEST.into_response()),
}
} else {
return Ok(StatusCode::BAD_REQUEST.into_response());
};
use db::publickeys::dsl as publickeys;
diesel::update(publickeys::publickeys.find(post.id))
.filter(publickeys::user_id.eq(uid))
.set((
publickeys::publickey.eq(&post.key),
publickeys::bin.eq(&bin.public_key_bytes()),
))
.execute(&mut config.db.get().await?)
.await?;
Ok(Redirect::to("/settings/ssh").into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateRepo {
name: String,
#[serde(default)]
private: bool,
token: String,
}
#[debug_handler]
async fn create_repo(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Form(create): Form<CreateRepo>,
) -> Result<Response, crate::Error> {
token.verify(&create.token)?;
use db::permissions::dsl as p;
use db::repositories::dsl as r;
use db::users::dsl as u;
debug!("create repo {:?}", create);
let mut db_ = config.db.get().await?;
let owner = get_user_id_strict(&jar)?;
db_.transaction(|mut txn| {
(async move {
let login = if let Some(login) = u::users
.find(owner)
.select(u::login)
.get_result::<String>(&mut txn)
.await
.optional()?
{
login
} else {
return Ok(StatusCode::FORBIDDEN.into_response());
};
let repo_id = diesel::insert_into(r::repositories)
.values((
r::owner.eq(owner),
r::name.eq(create.name),
r::creation_ip.eq(addr.to_string()),
))
.on_conflict_do_nothing()
.returning(r::id)
.get_result::<uuid::Uuid>(&mut txn)
.await?;
diesel::insert_into(p::permissions)
.values((
p::user_id.eq(owner),
p::repo_id.eq(repo_id),
p::start_date.eq(diesel::dsl::now),
p::perm.eq(Perm::create_owner().bits()),
))
.execute(&mut txn)
.await?;
if !create.private {
diesel::insert_into(p::permissions)
.values((
p::user_id.eq(uuid::Uuid::nil()),
p::repo_id.eq(repo_id),
p::start_date.eq(diesel::dsl::now),
p::perm.eq(Perm::create_public().bits()),
))
.execute(&mut txn)
.await?;
}
Ok(Redirect::to(&format!("/{}", login)).into_response())
})
.scope_boxed()
})
.await
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteRepo {
name: String,
token: String,
}
#[debug_handler]
async fn delete_repo(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Form(delete): Form<DeleteRepo>,
) -> Result<Response, crate::Error> {
token.verify(&delete.token)?;
let (owner, login) = get_user_login_strict(&jar, &config).await?;
use db::repositories::dsl as r;
if let Some(id) = diesel::delete(r::repositories)
.filter(r::owner.eq(owner))
.filter(r::name.eq(delete.name))
.returning(r::id)
.get_result(&mut config.db.get().await?)
.await.optional()?
{
config.repo_locks.remove(&id).await?;
}
Ok(Redirect::to(&format!("/{}", login)).into_response())
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DeleteAccount {
token: String,
password: String,
}
#[debug_handler]
async fn delete(
State(config): State<Config>,
jar: SignedCookieJar,
token: axum_csrf::CsrfToken,
Form(delete): Form<DeleteAccount>,
) -> Result<Response, crate::Error> {
use db::users::dsl as u;
token.verify(&delete.token)?;
let uid = get_user_id_strict(&jar)?;
let mut db = config.db.get().await?;
let n = diesel::delete(u::users.find(uid).filter(check_password!(&delete.password)))
.execute(&mut db)
.await
.unwrap();
if n == 0 {
use crate::db::profile_pics::dsl as pp;
use crate::db::publickeys::dsl as pk;
use crate::db::signingkeys::dsl as sk;
diesel::update(u::users.find(uid).filter(check_password!(&delete.password)))
.set(u::login.eq(sql("md5(concat(users.id::text, users.login))")))
.execute(&mut db)
.await?;
diesel::delete(pp::profile_pics.find(uid)).execute(&mut db).await?;
diesel::delete(pk::publickeys.filter(pk::user_id.eq(uid))).execute(&mut db).await?;
diesel::delete(sk::signingkeys.filter(sk::user_id.eq(uid))).execute(&mut db).await?;
}
Ok(Redirect::to("/").into_response())
}