ezidam: apps: archive, and revoke refresh tokens associated with app
This commit is contained in:
parent
956f28f7e5
commit
e06bd31b4c
11 changed files with 159 additions and 2 deletions
|
|
@ -103,4 +103,8 @@ impl App {
|
||||||
) -> Result<Option<()>, Error> {
|
) -> Result<Option<()>, Error> {
|
||||||
Ok(DatabaseApps::new_secret(conn, self.id.as_ref(), secret.hash()).await?)
|
Ok(DatabaseApps::new_secret(conn, self.id.as_ref(), secret.hash()).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn archive(&self, conn: impl SqliteExecutor<'_>) -> Result<Option<()>, Error> {
|
||||||
|
Ok(DatabaseApps::archive(conn, self.id.as_ref()).await?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
crates/database/queries/apps/archive.sql
Normal file
5
crates/database/queries/apps/archive.sql
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
update apps
|
||||||
|
|
||||||
|
set is_archived = 1
|
||||||
|
|
||||||
|
where id is ?
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
update refresh_tokens
|
||||||
|
|
||||||
|
set revoked_at = CURRENT_TIMESTAMP
|
||||||
|
|
||||||
|
where app is ?
|
||||||
|
and revoked_at is null
|
||||||
|
|
@ -100,6 +100,16 @@
|
||||||
},
|
},
|
||||||
"query": "update apps\n\nset secret = ?\n\nwhere id is ?"
|
"query": "update apps\n\nset secret = ?\n\nwhere id is ?"
|
||||||
},
|
},
|
||||||
|
"2d562e7b19d7d8303a0e79d143d25fd68743ae30d1ec0b0ca5c7dfc367fdf357": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update apps\n\nset is_archived = 1\n\nwhere id is ?"
|
||||||
|
},
|
||||||
"37681902a5f5d87492812a525a6488e75d20c1c436a3ba2c5aa3f54da62fe861": {
|
"37681902a5f5d87492812a525a6488e75d20c1c436a3ba2c5aa3f54da62fe861": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
|
|
@ -508,6 +518,16 @@
|
||||||
},
|
},
|
||||||
"query": "update settings\n\nset url = ?\n\nwhere id is 0\n"
|
"query": "update settings\n\nset url = ?\n\nwhere id is 0\n"
|
||||||
},
|
},
|
||||||
|
"93b15a942a6c7db595990f00e14fde26d6d36b8c8de9935179d41f6c7c755978": {
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"nullable": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"query": "update refresh_tokens\n\nset revoked_at = CURRENT_TIMESTAMP\n\nwhere app is ?\n and revoked_at is null"
|
||||||
|
},
|
||||||
"9f1885c4786f73335b4d614f562bb7cad49c91bfe7f084d8c25c6c571673ab90": {
|
"9f1885c4786f73335b4d614f562bb7cad49c91bfe7f084d8c25c6c571673ab90": {
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [],
|
"columns": [],
|
||||||
|
|
|
||||||
|
|
@ -136,4 +136,13 @@ impl Apps {
|
||||||
|
|
||||||
Ok((query.rows_affected() == 1).then_some(()))
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn archive(conn: impl SqliteExecutor<'_>, id: &str) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult = sqlx::query_file!("queries/apps/archive.sql", id)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -96,4 +96,17 @@ impl RefreshTokens {
|
||||||
|
|
||||||
Ok((query.rows_affected() == 1).then_some(()))
|
Ok((query.rows_affected() == 1).then_some(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_all_for_app(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
app: &str,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
let query: SqliteQueryResult =
|
||||||
|
sqlx::query_file!("queries/refresh_tokens/revoke_all_for_app.sql", app)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
.map_err(handle_error)?;
|
||||||
|
|
||||||
|
Ok((query.rows_affected() >= 1).then_some(()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,8 @@ impl Icon {
|
||||||
"plus", Plus, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 5l0 14"></path><path d="M5 12l14 0"></path></svg>"#,
|
"plus", Plus, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-plus" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 5l0 14"></path><path d="M5 12l14 0"></path></svg>"#,
|
||||||
"moon", Moon, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-moon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path></svg>"#,
|
"moon", Moon, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-moon" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z"></path></svg>"#,
|
||||||
"sun", Sun, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-sun" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7"></path></svg>"#,
|
"sun", Sun, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-sun" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M12 12m-4 0a4 4 0 1 0 8 0a4 4 0 1 0 -8 0"></path><path d="M3 12h1m8 -9v1m8 8h1m-9 8v1m-6.4 -15.4l.7 .7m12.1 -.7l-.7 .7m0 11.4l.7 .7m-12.1 -.7l-.7 .7"></path></svg>"#,
|
||||||
"arrow-left", ArrowLeft, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-left" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l14 0"></path><path d="M5 12l6 6"></path><path d="M5 12l6 -6"></path></svg>"#
|
"arrow-left", ArrowLeft, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-left" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M5 12l14 0"></path><path d="M5 12l6 6"></path><path d="M5 12l6 -6"></path></svg>"#,
|
||||||
|
"alert-triangle-large", AlertTriangleLarge, r#"<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-lg icon-tabler icon-tabler-alert-triangle" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z"></path><path d="M12 9v4"></path><path d="M12 17h.01"></path></svg>"#
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +53,7 @@ pub fn icons_to_templates(tera: &mut Tera) {
|
||||||
Icon::Moon,
|
Icon::Moon,
|
||||||
Icon::Sun,
|
Icon::Sun,
|
||||||
Icon::ArrowLeft,
|
Icon::ArrowLeft,
|
||||||
|
Icon::AlertTriangleLarge,
|
||||||
];
|
];
|
||||||
|
|
||||||
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
// For each icon, it will output: ("icons/name", "<svg>...</svg>")
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ pub fn routes() -> Vec<Route> {
|
||||||
admin_apps_view,
|
admin_apps_view,
|
||||||
admin_apps_view_form,
|
admin_apps_view_form,
|
||||||
admin_apps_new_secret,
|
admin_apps_new_secret,
|
||||||
|
admin_apps_archive,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -166,3 +166,42 @@ pub async fn admin_apps_new_secret(
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromForm)]
|
||||||
|
pub struct ArchiveApp {
|
||||||
|
pub archive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/admin/apps/<id>/archive", data = "<form>")]
|
||||||
|
pub async fn admin_apps_archive(
|
||||||
|
mut db: Connection<Database>,
|
||||||
|
id: RocketAppID,
|
||||||
|
_admin: JwtAdmin,
|
||||||
|
form: Form<ArchiveApp>,
|
||||||
|
) -> Result<Flash<Redirect>> {
|
||||||
|
if !form.archive {
|
||||||
|
return Err(Error::bad_request("Not archiving"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut transaction = db.begin().await?;
|
||||||
|
|
||||||
|
// Get app
|
||||||
|
let app_id = &id.0;
|
||||||
|
let app = App::get_one_by_id(&mut transaction, app_id.as_ref())
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| Error::not_found(app_id.to_string()))?;
|
||||||
|
|
||||||
|
// Archive
|
||||||
|
app.archive(&mut transaction).await?;
|
||||||
|
|
||||||
|
// Revoke refresh tokens
|
||||||
|
refresh_tokens::RefreshToken::revoke_all_for_app(&mut transaction, app_id).await?;
|
||||||
|
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(Flash::new(
|
||||||
|
Redirect::to(uri!(admin_apps_list)),
|
||||||
|
FlashKind::Success,
|
||||||
|
format!("Application \"{}\" has been archived", app.label()),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,14 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-end">
|
<div class="card-footer text-end">
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<div class="d-flex">
|
||||||
|
|
||||||
|
<a class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#modal-archive">
|
||||||
|
Archive
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary ms-auto">Save</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
@ -157,6 +164,50 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Archive modal -->
|
||||||
|
<div class="modal modal-blur" tabindex="-1" id="modal-archive">
|
||||||
|
<div class="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
|
||||||
|
<div class="modal-status bg-danger"></div>
|
||||||
|
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
|
||||||
|
<div class="text-danger mb-2">
|
||||||
|
{% include "icons/alert-triangle-large" %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Do you want to archive {{ app.label }}?</h3>
|
||||||
|
<div class="text-muted">
|
||||||
|
Do you really want to archive this application? All users will be logged out.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<div class="w-100">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<a href="#" class="btn w-100" data-bs-dismiss="modal">Cancel</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col">
|
||||||
|
<form action="{{ app.id }}/archive" method="post">
|
||||||
|
<button type="submit" name="archive" value="true" class="btn btn-danger w-100">
|
||||||
|
Archive
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
||||||
{% block libs_js %}
|
{% block libs_js %}
|
||||||
|
|
|
||||||
|
|
@ -72,4 +72,11 @@ impl RefreshToken {
|
||||||
pub async fn use_token(&self, conn: impl SqliteExecutor<'_>) -> Result<Option<()>, Error> {
|
pub async fn use_token(&self, conn: impl SqliteExecutor<'_>) -> Result<Option<()>, Error> {
|
||||||
Ok(DatabaseRefreshTokens::use_token(conn, &self.token).await?)
|
Ok(DatabaseRefreshTokens::use_token(conn, &self.token).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn revoke_all_for_app(
|
||||||
|
conn: impl SqliteExecutor<'_>,
|
||||||
|
app: &AppID,
|
||||||
|
) -> Result<Option<()>, Error> {
|
||||||
|
Ok(DatabaseRefreshTokens::revoke_all_for_app(conn, app.as_ref()).await?)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue