1
0
Fork 0
forked from 2sys/shoutdotdev

implement API key deletion

This commit is contained in:
Brent Schroeter 2025-04-04 14:22:44 -07:00
parent bef9cc4cca
commit 588bf33d6e
2 changed files with 113 additions and 3 deletions

View file

@ -19,7 +19,7 @@ use crate::{
guards,
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_TEAMS},
projects::{Project, DEFAULT_PROJECT_NAME},
schema::{team_memberships, teams},
schema::{api_keys, team_memberships, teams},
settings::Settings,
team_memberships::TeamMembership,
teams::Team,
@ -31,6 +31,7 @@ pub fn new_router() -> Router<AppState> {
.route("/teams", get(teams_page))
.route("/teams/{team_id}", get(team_page))
.route("/teams/{team_id}/new-api-key", post(post_new_api_key))
.route("/teams/{team_id}/remove-api-key", post(remove_api_key))
.route("/new-team", get(new_team_page))
.route("/new-team", post(post_new_team))
}
@ -96,11 +97,61 @@ async fn post_new_api_key(
Ok(Redirect::to(&format!(
"{}/en/teams/{}/projects",
base_path,
team.id.hyphenated()
team.id.simple()
))
.into_response())
}
#[derive(Deserialize)]
struct RemoveApiKeyForm {
csrf_token: String,
key_id: Uuid,
}
async fn remove_api_key(
State(Settings { base_path, .. }): State<Settings>,
DbConn(db_conn): DbConn,
Path(team_id): Path<Uuid>,
CurrentUser(current_user): CurrentUser,
Form(form): Form<RemoveApiKeyForm>,
) -> Result<impl IntoResponse, AppError> {
guards::require_valid_csrf_token(&form.csrf_token, &current_user, &db_conn).await?;
let team = guards::require_team_membership(&current_user, &team_id, &db_conn).await?;
let n_deleted = {
let team_id = team.id;
db_conn
.interact::<_, Result<usize, AppError>>(move |conn| {
diesel::delete(
api_keys::table
.filter(ApiKey::with_team(&team_id))
.filter(ApiKey::with_id(&form.key_id)),
)
.execute(conn)
.context("failed to delete API key from database")
.map_err(Into::into)
})
.await
.unwrap()?
};
assert!(
n_deleted < 2,
"there should never be more than 1 API key with the same ID"
);
if n_deleted == 0 {
Err(AppError::NotFoundError(
"no API key with that ID and team found".to_owned(),
))
} else {
Ok(Redirect::to(&format!(
"{}/en/teams/{}/projects",
base_path,
team.id.simple()
))
.into_response())
}
}
async fn new_team_page(
State(Settings { base_path, .. }): State<Settings>,
State(navbar_template): State<NavbarBuilder>,

View file

@ -98,7 +98,66 @@
>
Copy
</button>
<button class="btn btn-outline-light" type="button">Delete</button>
<button
class="btn btn-outline-light"
type="button"
data-bs-toggle="modal"
data-bs-target="#confirm-delete-key-modal-{{ key.id }}"
>
Delete
</button>
</div>
<div
class="modal fade"
id="confirm-delete-key-modal-{{ key.id }}"
tabindex="-1"
aria-labelledby="confirm-delete-key-label-{{ key.id }}"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1
class="modal-title fs-5"
id="confirm-delete-key-label-{{ key.id }}"
>
Confirm
</h1>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Cancel"
></button>
</div>
<div class="modal-body">
Are you sure you want to delete the key
<code>{{ key.id|compact_uuid|redact }}</code>?
{% if let Some(last_used_at) = key.last_used_at %}
It was last used: {{ last_used_at.format("%Y-%m-%d") }}.
{% else %}
It has never been used.
{% endif %}
</div>
<div class="modal-footer">
<form
method="post"
action="{{ breadcrumbs.join("../remove-api-key") }}"
>
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="key_id" value="{{ key.id }}">
<button
type="button"
class="btn btn-secondary"
data-bs-dismiss="modal"
>
Cancel
</button>
<button type="submit" class="btn btn-danger">Delete</button>
</form>
</div>
</div>
</div>
</div>
</td>
</tr>