mirror of
https://github.com/encounter/decomp.dev.git
synced 2026-07-10 03:18:48 -07:00
Add manage section to repos with admin permissions
This commit is contained in:
Generated
+1
@@ -1028,6 +1028,7 @@ dependencies = [
|
||||
"octocrab",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"time",
|
||||
"tower-sessions",
|
||||
"tracing",
|
||||
|
||||
@@ -8,11 +8,12 @@ publish = false
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
base64 = "0.22"
|
||||
rand = "0.9"
|
||||
decomp-dev-core = { path = "../core" }
|
||||
octocrab.workspace = true
|
||||
time.workspace = true
|
||||
rand = "0.9"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
time.workspace = true
|
||||
tower-sessions.workspace = true
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
url.workspace = true
|
||||
+66
-11
@@ -6,7 +6,10 @@ use axum::{
|
||||
};
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
|
||||
use decomp_dev_core::{AppError, config::GitHubConfig};
|
||||
use octocrab::{Octocrab, models::Author};
|
||||
use octocrab::{
|
||||
Octocrab,
|
||||
models::{Author, Permissions, Repository, RepositoryId},
|
||||
};
|
||||
use rand::{TryRngCore, rngs::OsRng};
|
||||
use time::{Duration, UtcDateTime};
|
||||
use tower_sessions::Session;
|
||||
@@ -42,6 +45,41 @@ pub type Profile = Author;
|
||||
pub struct CurrentUser {
|
||||
pub oauth: StoredOAuth,
|
||||
pub profile: Profile,
|
||||
#[serde(default)]
|
||||
pub repos: Vec<CurrentUserRepo>,
|
||||
}
|
||||
|
||||
impl CurrentUser {
|
||||
pub fn permissions_for_repo(&self, id: u64) -> Permissions {
|
||||
self.repos
|
||||
.iter()
|
||||
.find(|r| r.id.into_inner() == id)
|
||||
.map(|r| r.permissions.clone())
|
||||
.unwrap_or_else(default_permissions)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CurrentUserRepo {
|
||||
pub id: RepositoryId,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub permissions: Permissions,
|
||||
}
|
||||
|
||||
fn default_permissions() -> Permissions {
|
||||
serde_json::from_str::<Permissions>(r#"{"push":false,"pull":false}"#).unwrap()
|
||||
}
|
||||
|
||||
impl From<Repository> for CurrentUserRepo {
|
||||
fn from(repo: Repository) -> Self {
|
||||
Self {
|
||||
id: repo.id,
|
||||
owner: repo.owner.map(|o| o.login).unwrap_or_default(),
|
||||
repo: repo.name,
|
||||
permissions: repo.permissions.unwrap_or_else(default_permissions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
@@ -182,13 +220,29 @@ async fn fetch_access_token(config: &GitHubConfig, code: &str) -> Result<Current
|
||||
let oauth = StoredOAuth::from(oauth);
|
||||
let client = Octocrab::builder().oauth(oauth.clone().into()).build()?;
|
||||
let profile = client.current().user().await.context("Failed to fetch current user")?;
|
||||
tracing::info!("Logged in as @{}", profile.login);
|
||||
Ok(CurrentUser { oauth, profile })
|
||||
let repos = client
|
||||
.all_pages(
|
||||
client
|
||||
.current()
|
||||
.list_repos_for_authenticated_user()
|
||||
.per_page(100)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to fetch current user repositories")?,
|
||||
)
|
||||
.await
|
||||
.context("Failed to fetch current user repositories")?
|
||||
.into_iter()
|
||||
.map(CurrentUserRepo::from)
|
||||
.collect::<Vec<_>>();
|
||||
tracing::info!("Logged in as @{} ({} repos)", profile.login, repos.len());
|
||||
Ok(CurrentUser { oauth, profile, repos })
|
||||
}
|
||||
|
||||
async fn refresh_access_token(
|
||||
config: &GitHubConfig,
|
||||
refresh_token: &str,
|
||||
prev_auth: &CurrentUser,
|
||||
) -> Result<CurrentUser, anyhow::Error> {
|
||||
let Some(oauth_config) = &config.oauth else {
|
||||
tracing::warn!("No GitHub OAuth config found");
|
||||
@@ -210,7 +264,7 @@ async fn refresh_access_token(
|
||||
let client = Octocrab::builder().oauth(oauth.clone().into()).build()?;
|
||||
let profile = client.current().user().await.context("Failed to fetch current user")?;
|
||||
tracing::info!("Refreshed token for @{}", profile.login);
|
||||
Ok(CurrentUser { oauth, profile })
|
||||
Ok(CurrentUser { oauth, profile, repos: prev_auth.repos.clone() })
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for CurrentUser
|
||||
@@ -256,13 +310,14 @@ where
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
let current_user = match refresh_access_token(&config, refresh_token).await {
|
||||
Ok(current_user) => current_user,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to refresh access token: {:?}", e);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let current_user =
|
||||
match refresh_access_token(&config, refresh_token, &user).await {
|
||||
Ok(current_user) => current_user,
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to refresh access token: {:?}", e);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
if let Err(e) = session.insert(CURRENT_USER, current_user.clone()).await {
|
||||
tracing::error!("Failed to insert user into session: {}", e);
|
||||
}
|
||||
|
||||
@@ -938,6 +938,29 @@ impl Database {
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_project_settings(
|
||||
&self,
|
||||
project_id: u64,
|
||||
enable_pr_comments: bool,
|
||||
default_version: Option<String>,
|
||||
) -> Result<()> {
|
||||
let mut conn = self.pool.acquire().await?;
|
||||
let project_id_db = project_id as i64;
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE projects
|
||||
SET enable_pr_comments = ?, default_version = ?
|
||||
WHERE id = ?
|
||||
"#,
|
||||
enable_pr_comments,
|
||||
default_version,
|
||||
project_id_db,
|
||||
)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
|
||||
@@ -57,10 +57,17 @@ pub fn footer(start: Instant, current_user: Option<&CurrentUser>) -> Markup {
|
||||
"Logged in as "
|
||||
a href=(user.profile.html_url) { "@" (user.profile.login) }
|
||||
}
|
||||
" | "
|
||||
form action="/logout" method="post" style="display: inline" {
|
||||
input type="submit" class="button outline secondary" value="Logout";
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
span class="section" {
|
||||
small class="muted" {
|
||||
a href="/login" { "Login" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ pub fn build_router() -> Router<AppState> {
|
||||
.route("/og.png", get(decomp_dev_images::get_og))
|
||||
.route("/", get(project::get_projects))
|
||||
.route("/{owner}/{repo}", get(report::get_report))
|
||||
.route("/{owner}/{repo}", post(report::save_project))
|
||||
.route("/{owner}/{repo}/{version}", get(report::get_report))
|
||||
.route("/{owner}/{repo}/{version}/{commit}", get(report::get_report))
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ use std::{borrow::Cow, iter, time::Instant};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::{
|
||||
Json,
|
||||
Form, Json,
|
||||
extract::{Path, Query, State},
|
||||
http::{HeaderMap, StatusCode, Uri, header},
|
||||
response::{IntoResponse, Response},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use decomp_dev_auth::CurrentUser;
|
||||
use decomp_dev_core::{
|
||||
@@ -783,9 +783,87 @@ async fn render_template(
|
||||
noscript {
|
||||
img #treemap src=(image_url) alt="Progress graph";
|
||||
}
|
||||
@if current_user.as_ref().is_some_and(|u| u.permissions_for_repo(project_info.project.id).admin) {
|
||||
(manage_form(project_info))
|
||||
}
|
||||
}
|
||||
}
|
||||
(footer(start, current_user.as_ref()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn manage_form(project_info: &ProjectInfo) -> Markup {
|
||||
let project_base_path =
|
||||
format!("/{}/{}", project_info.project.owner, project_info.project.repo);
|
||||
let default_version = project_info.default_version();
|
||||
html! {
|
||||
h6 class="report-header" { "Manage" }
|
||||
form action=(project_base_path) method="post" {
|
||||
fieldset {
|
||||
label {
|
||||
"Default version"
|
||||
select name="default_version" {
|
||||
@for version in &project_info.report_versions {
|
||||
@if default_version == Some(version.as_str()) {
|
||||
option value=(version) selected { (version) }
|
||||
} @else {
|
||||
option value=(version) { (version) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
label {
|
||||
@if project_info.project.enable_pr_comments {
|
||||
input name="enable_pr_comments" type="checkbox" role="switch" checked;
|
||||
} @else {
|
||||
input name="enable_pr_comments" type="checkbox" role="switch";
|
||||
}
|
||||
"Enable PR comments"
|
||||
}
|
||||
}
|
||||
button type="submit" { "Save" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn form_bool<'de, D>(deserializer: D) -> Result<bool, D::Error>
|
||||
where D: serde::Deserializer<'de> {
|
||||
match <&str>::deserialize(deserializer)? {
|
||||
"on" => Ok(true),
|
||||
"off" => Ok(false),
|
||||
other => Err(serde::de::Error::unknown_variant(other, &["on", "off"])),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ProjectForm {
|
||||
#[serde(default, deserialize_with = "form_bool")]
|
||||
pub enable_pr_comments: bool,
|
||||
pub default_version: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn save_project(
|
||||
Path(params): Path<ReportParams>,
|
||||
State(state): State<AppState>,
|
||||
current_user: CurrentUser,
|
||||
Form(form): Form<ProjectForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
let Some(project_info) = state.db.get_project_info(¶ms.owner, ¶ms.repo, None).await?
|
||||
else {
|
||||
return Err(AppError::Status(StatusCode::NOT_FOUND));
|
||||
};
|
||||
if !current_user.permissions_for_repo(project_info.project.id).admin {
|
||||
return Err(AppError::Status(StatusCode::FORBIDDEN));
|
||||
}
|
||||
state
|
||||
.db
|
||||
.update_project_settings(
|
||||
project_info.project.id,
|
||||
form.enable_pr_comments,
|
||||
form.default_version,
|
||||
)
|
||||
.await?;
|
||||
let redirect_url = format!("/{}/{}", params.owner, params.repo);
|
||||
Ok(Redirect::to(&redirect_url).into_response())
|
||||
}
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ $breakpoints: (
|
||||
|
||||
// Forms
|
||||
"forms/basics": true,
|
||||
"forms/checkbox-radio-switch": false,
|
||||
"forms/checkbox-radio-switch": true,
|
||||
"forms/input-color": false,
|
||||
"forms/input-date": false,
|
||||
"forms/input-file": false,
|
||||
|
||||
Reference in New Issue
Block a user