webdav token regeneration
This commit is contained in:
@@ -74,6 +74,51 @@ pub fn list_webdav_tokens(
|
|||||||
Ok(tokens)
|
Ok(tokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn regenerate_webdav_token(
|
||||||
|
conn: &mut PgPooledConnection,
|
||||||
|
token_id: Uuid,
|
||||||
|
user_id: Uuid,
|
||||||
|
tenant_id: Option<Uuid>,
|
||||||
|
) -> Result<IssuedWebdavToken, AppError> {
|
||||||
|
let mut query = webdav_tokens::table
|
||||||
|
.filter(webdav_tokens::id.eq(token_id))
|
||||||
|
.filter(webdav_tokens::user_id.eq(user_id))
|
||||||
|
.into_boxed();
|
||||||
|
|
||||||
|
if let Some(tenant) = tenant_id {
|
||||||
|
query = query.filter(webdav_tokens::tenant_id.eq(tenant));
|
||||||
|
}
|
||||||
|
|
||||||
|
let record = query
|
||||||
|
.first::<WebdavToken>(conn)
|
||||||
|
.optional()
|
||||||
|
.map_err(AppError::from)?
|
||||||
|
.ok_or_else(AppError::not_found)?;
|
||||||
|
|
||||||
|
if record.revoked_at.is_some() {
|
||||||
|
return Err(AppError::bad_request(
|
||||||
|
"cannot regenerate a revoked WebDAV token",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let raw_secret = generate_secret()?;
|
||||||
|
let token_prefix = raw_secret[..TOKEN_PREFIX_LENGTH].to_string();
|
||||||
|
let token_hash = hash_secret(&raw_secret)?;
|
||||||
|
|
||||||
|
let updated = diesel::update(webdav_tokens::table.find(record.id))
|
||||||
|
.set((
|
||||||
|
webdav_tokens::token_prefix.eq(&token_prefix),
|
||||||
|
webdav_tokens::token_hash.eq(&token_hash),
|
||||||
|
webdav_tokens::last_used_at.eq::<Option<NaiveDateTime>>(None),
|
||||||
|
))
|
||||||
|
.get_result::<WebdavToken>(conn)?;
|
||||||
|
|
||||||
|
Ok(IssuedWebdavToken {
|
||||||
|
token: raw_secret,
|
||||||
|
record: updated,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn find_active_token_by_secret(
|
pub fn find_active_token_by_secret(
|
||||||
conn: &mut PgPooledConnection,
|
conn: &mut PgPooledConnection,
|
||||||
user_id: Uuid,
|
user_id: Uuid,
|
||||||
|
|||||||
@@ -533,7 +533,10 @@ pub async fn list_tenants(
|
|||||||
.select(tenant_dsl::name)
|
.select(tenant_dsl::name)
|
||||||
.first(&mut tenant_conn)
|
.first(&mut tenant_conn)
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
tenants.push(TenantSnippet { id: tenant_id, name });
|
tenants.push(TenantSnippet {
|
||||||
|
id: tenant_id,
|
||||||
|
name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(TenantListResponse { tenants }))
|
Ok(Json(TenantListResponse { tenants }))
|
||||||
@@ -699,7 +702,10 @@ fn complete_login(
|
|||||||
.select(tenant_dsl::name)
|
.select(tenant_dsl::name)
|
||||||
.first(&mut tenant_conn)
|
.first(&mut tenant_conn)
|
||||||
.map_err(AppError::from)?;
|
.map_err(AppError::from)?;
|
||||||
tenants.push(TenantSnippet { id: tenant_id, name });
|
tenants.push(TenantSnippet {
|
||||||
|
id: tenant_id,
|
||||||
|
name,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Json(TenantSelectionResponse {
|
Ok(Json(TenantSelectionResponse {
|
||||||
|
|||||||
@@ -148,6 +148,10 @@ pub fn create_router(state: AppState) -> Router<()> {
|
|||||||
"/webdav-tokens",
|
"/webdav-tokens",
|
||||||
get(profile::list_webdav_tokens).post(profile::create_webdav_token),
|
get(profile::list_webdav_tokens).post(profile::create_webdav_token),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/webdav-tokens/:id/regenerate",
|
||||||
|
post(profile::regenerate_webdav_token),
|
||||||
|
)
|
||||||
.route("/webdav-tokens/:id", delete(profile::delete_webdav_token))
|
.route("/webdav-tokens/:id", delete(profile::delete_webdav_token))
|
||||||
.route("/passkeys", get(profile::list_passkeys))
|
.route("/passkeys", get(profile::list_passkeys))
|
||||||
.route("/passkeys/:id", delete(profile::delete_passkey));
|
.route("/passkeys/:id", delete(profile::delete_passkey));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::auth::{
|
|||||||
passkeys::PasskeySummary,
|
passkeys::PasskeySummary,
|
||||||
webdav_tokens::{
|
webdav_tokens::{
|
||||||
create_webdav_token as issue_token, list_webdav_tokens as load_tokens,
|
create_webdav_token as issue_token, list_webdav_tokens as load_tokens,
|
||||||
revoke_webdav_token as revoke_token,
|
regenerate_webdav_token as rotate_token, revoke_webdav_token as revoke_token,
|
||||||
},
|
},
|
||||||
TenantScopedConn,
|
TenantScopedConn,
|
||||||
};
|
};
|
||||||
@@ -134,6 +134,31 @@ pub async fn create_webdav_token(
|
|||||||
Ok((StatusCode::CREATED, Json(response)))
|
Ok((StatusCode::CREATED, Json(response)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
post,
|
||||||
|
path = "/api/profile/webdav-tokens/{id}/regenerate",
|
||||||
|
params(("id" = Uuid, Path, description = "WebDAV token ID")),
|
||||||
|
responses((status = 200, description = "WebDAV token regenerated", body = WebdavTokenCreatedResponse)),
|
||||||
|
tag = "Profile"
|
||||||
|
)]
|
||||||
|
pub async fn regenerate_webdav_token(
|
||||||
|
TenantScopedConn {
|
||||||
|
mut conn,
|
||||||
|
tenant_id,
|
||||||
|
user_id,
|
||||||
|
..
|
||||||
|
}: TenantScopedConn,
|
||||||
|
Path(token_id): Path<Uuid>,
|
||||||
|
) -> AppResult<Json<WebdavTokenCreatedResponse>> {
|
||||||
|
let issued = rotate_token(&mut conn, token_id, user_id, Some(tenant_id))?;
|
||||||
|
let response = WebdavTokenCreatedResponse {
|
||||||
|
token: issued.token,
|
||||||
|
token_info: webdav_token_to_response(issued.record),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Json(response))
|
||||||
|
}
|
||||||
|
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
delete,
|
delete,
|
||||||
path = "/api/profile/webdav-tokens/{id}",
|
path = "/api/profile/webdav-tokens/{id}",
|
||||||
@@ -208,6 +233,7 @@ fn parse_timestamp(value: &str) -> AppResult<NaiveDateTime> {
|
|||||||
paths(
|
paths(
|
||||||
crate::routes::profile::list_webdav_tokens,
|
crate::routes::profile::list_webdav_tokens,
|
||||||
crate::routes::profile::create_webdav_token,
|
crate::routes::profile::create_webdav_token,
|
||||||
|
crate::routes::profile::regenerate_webdav_token,
|
||||||
crate::routes::profile::delete_webdav_token,
|
crate::routes::profile::delete_webdav_token,
|
||||||
crate::routes::profile::list_passkeys,
|
crate::routes::profile::list_passkeys,
|
||||||
crate::routes::profile::delete_passkey
|
crate::routes::profile::delete_passkey
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ use crate::error::{AppError, AppResult};
|
|||||||
use crate::models::{Document, DocumentVersion, Folder, User};
|
use crate::models::{Document, DocumentVersion, Folder, User};
|
||||||
use crate::schema::{
|
use crate::schema::{
|
||||||
document_versions::dsl as document_versions_dsl, documents::dsl as documents_dsl,
|
document_versions::dsl as document_versions_dsl, documents::dsl as documents_dsl,
|
||||||
folders::dsl as folders_dsl, user_memberships::dsl as memberships_dsl,
|
folders::dsl as folders_dsl, user_memberships::dsl as memberships_dsl, users::dsl as users_dsl,
|
||||||
users::dsl as users_dsl,
|
|
||||||
};
|
};
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
use crate::tenants::{apply_tenant_guc, apply_user_guc, clear_user_guc};
|
use crate::tenants::{apply_tenant_guc, apply_user_guc, clear_user_guc};
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ pub fn clear_tenant_context(conn: &mut PgConnection) -> AppResult<()> {
|
|||||||
set_config('papercrate.tenant_id', '', false), \
|
set_config('papercrate.tenant_id', '', false), \
|
||||||
set_config('papercrate.user_id', '', false), \
|
set_config('papercrate.user_id', '', false), \
|
||||||
set_config('papercrate.refresh_token_hash', '', false), \
|
set_config('papercrate.refresh_token_hash', '', false), \
|
||||||
set_config('papercrate.webdav_token_prefix', '', false)"
|
set_config('papercrate.webdav_token_prefix', '', false)",
|
||||||
)
|
)
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.map(|_| ())
|
.map(|_| ())
|
||||||
|
|||||||
@@ -90,7 +90,9 @@ impl JobHandler for IndexDocumentTextJob {
|
|||||||
|
|
||||||
let state_clone = state.clone();
|
let state_clone = state.clone();
|
||||||
let tenant_id = job.tenant_id;
|
let tenant_id = job.tenant_id;
|
||||||
let context = match task::spawn_blocking(move || load_context(state_clone, tenant_id, payload)).await {
|
let context =
|
||||||
|
match task::spawn_blocking(move || load_context(state_clone, tenant_id, payload)).await
|
||||||
|
{
|
||||||
Ok(Ok(ctx)) => ctx,
|
Ok(Ok(ctx)) => ctx,
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
warn!(job_id = %job.id, error = %err, "index job will retry");
|
warn!(job_id = %job.id, error = %err, "index job will retry");
|
||||||
|
|||||||
@@ -489,7 +489,11 @@ fn persist_ocr_metadata(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enqueue_index_job(state: &AppState, tenant_id: Uuid, payload: &OcrPayload) -> Result<(), String> {
|
fn enqueue_index_job(
|
||||||
|
state: &AppState,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
payload: &OcrPayload,
|
||||||
|
) -> Result<(), String> {
|
||||||
let mut conn = state
|
let mut conn = state
|
||||||
.db_for_tenant(tenant_id)
|
.db_for_tenant(tenant_id)
|
||||||
.map_err(|err| format!("{err:?}"))?;
|
.map_err(|err| format!("{err:?}"))?;
|
||||||
|
|||||||
@@ -129,7 +129,13 @@ impl JobHandler for GenerateThumbnailsJob {
|
|||||||
let version_id = initial.version.id;
|
let version_id = initial.version.id;
|
||||||
let tenant_id = initial.tenant_id;
|
let tenant_id = initial.tenant_id;
|
||||||
match task::spawn_blocking(move || {
|
match task::spawn_blocking(move || {
|
||||||
persist_document_page_count(state_clone, tenant_id, document_id, version_id, page_count)
|
persist_document_page_count(
|
||||||
|
state_clone,
|
||||||
|
tenant_id,
|
||||||
|
document_id,
|
||||||
|
version_id,
|
||||||
|
page_count,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,8 +37,7 @@ use tower::util::ServiceExt;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations");
|
||||||
const RESET_DATABASE_SQL: &str =
|
const RESET_DATABASE_SQL: &str = "DROP SCHEMA IF EXISTS tenant CASCADE;\n\
|
||||||
"DROP SCHEMA IF EXISTS tenant CASCADE;\n\
|
|
||||||
DROP SCHEMA IF EXISTS shared CASCADE;\n\
|
DROP SCHEMA IF EXISTS shared CASCADE;\n\
|
||||||
DROP SCHEMA IF EXISTS public CASCADE;\n\
|
DROP SCHEMA IF EXISTS public CASCADE;\n\
|
||||||
CREATE SCHEMA public;\n\
|
CREATE SCHEMA public;\n\
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ async fn webdav_token_api_crud() -> Result<()> {
|
|||||||
assert_eq!(created.info.label.as_deref(), Some("dav"));
|
assert_eq!(created.info.label.as_deref(), Some("dav"));
|
||||||
assert!(created.info.last_used_at.is_none());
|
assert!(created.info.last_used_at.is_none());
|
||||||
|
|
||||||
|
let regenerate_response = app
|
||||||
|
.post_json(
|
||||||
|
&format!("/api/profile/webdav-tokens/{token_id}/regenerate"),
|
||||||
|
&json!({}),
|
||||||
|
Some(&access_token),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(regenerate_response.status(), StatusCode::OK);
|
||||||
|
let regenerate_body = body_to_vec(regenerate_response.into_body()).await?;
|
||||||
|
let regenerated: CreateTokenResponse = serde_json::from_slice(®enerate_body)?;
|
||||||
|
assert_eq!(regenerated.info.id, token_id);
|
||||||
|
assert_ne!(regenerated.token, created.token);
|
||||||
|
assert!(regenerated.info.last_used_at.is_none());
|
||||||
|
|
||||||
let list_response = app
|
let list_response = app
|
||||||
.get("/api/profile/webdav-tokens", Some(&access_token))
|
.get("/api/profile/webdav-tokens", Some(&access_token))
|
||||||
.await?;
|
.await?;
|
||||||
@@ -106,9 +120,10 @@ async fn webdav_basic_auth_uses_tokens() -> Result<()> {
|
|||||||
let token_id = created.info.id;
|
let token_id = created.info.id;
|
||||||
|
|
||||||
let router = webdav::create_router().with_state(app.state.clone());
|
let router = webdav::create_router().with_state(app.state.clone());
|
||||||
|
let original_secret = created.token.clone();
|
||||||
let auth_header = format!(
|
let auth_header = format!(
|
||||||
"Basic {}",
|
"Basic {}",
|
||||||
BASE64.encode(format!("{}:{}", username, created.token))
|
BASE64.encode(format!("{}:{}", username, original_secret))
|
||||||
);
|
);
|
||||||
|
|
||||||
let propfind = Method::from_bytes(b"PROPFIND")?;
|
let propfind = Method::from_bytes(b"PROPFIND")?;
|
||||||
@@ -131,6 +146,57 @@ async fn webdav_basic_auth_uses_tokens() -> Result<()> {
|
|||||||
.await?;
|
.await?;
|
||||||
assert!(used.is_some());
|
assert!(used.is_some());
|
||||||
|
|
||||||
|
let regenerate_response = app
|
||||||
|
.post_json(
|
||||||
|
&format!("/api/profile/webdav-tokens/{token_id}/regenerate"),
|
||||||
|
&json!({}),
|
||||||
|
Some(&access_token),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(regenerate_response.status(), StatusCode::OK);
|
||||||
|
let regenerate_body = body_to_vec(regenerate_response.into_body()).await?;
|
||||||
|
let regenerated: CreateTokenResponse = serde_json::from_slice(®enerate_body)?;
|
||||||
|
assert_ne!(regenerated.token, original_secret);
|
||||||
|
|
||||||
|
let unused_after_regen = app
|
||||||
|
.with_conn(move |conn| {
|
||||||
|
let record = webdav_tokens::table
|
||||||
|
.find(token_id)
|
||||||
|
.first::<WebdavToken>(conn)?;
|
||||||
|
Ok::<_, anyhow::Error>(record.last_used_at)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
assert!(unused_after_regen.is_none());
|
||||||
|
|
||||||
|
let old_secret_request = Request::builder()
|
||||||
|
.method(propfind.clone())
|
||||||
|
.uri("/")
|
||||||
|
.header(
|
||||||
|
header::AUTHORIZATION,
|
||||||
|
format!(
|
||||||
|
"Basic {}",
|
||||||
|
BASE64.encode(format!("{}:{}", username, original_secret))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.header("depth", "0")
|
||||||
|
.body(Body::empty())?;
|
||||||
|
let old_secret_response = router.clone().oneshot(old_secret_request).await?;
|
||||||
|
assert_eq!(old_secret_response.status(), StatusCode::UNAUTHORIZED);
|
||||||
|
|
||||||
|
let new_secret_header = format!(
|
||||||
|
"Basic {}",
|
||||||
|
BASE64.encode(format!("{}:{}", username, regenerated.token))
|
||||||
|
);
|
||||||
|
|
||||||
|
let success_request = Request::builder()
|
||||||
|
.method(propfind.clone())
|
||||||
|
.uri("/")
|
||||||
|
.header(header::AUTHORIZATION, new_secret_header.clone())
|
||||||
|
.header("depth", "0")
|
||||||
|
.body(Body::empty())?;
|
||||||
|
let response = router.clone().oneshot(success_request).await?;
|
||||||
|
assert_eq!(response.status(), StatusCode::MULTI_STATUS);
|
||||||
|
|
||||||
let delete_response = app
|
let delete_response = app
|
||||||
.delete(
|
.delete(
|
||||||
&format!("/api/profile/webdav-tokens/{token_id}"),
|
&format!("/api/profile/webdav-tokens/{token_id}"),
|
||||||
@@ -142,7 +208,7 @@ async fn webdav_basic_auth_uses_tokens() -> Result<()> {
|
|||||||
let failure_request = Request::builder()
|
let failure_request = Request::builder()
|
||||||
.method(propfind)
|
.method(propfind)
|
||||||
.uri("/")
|
.uri("/")
|
||||||
.header(header::AUTHORIZATION, auth_header)
|
.header(header::AUTHORIZATION, new_secret_header)
|
||||||
.header("depth", "0")
|
.header("depth", "0")
|
||||||
.body(Body::empty())?;
|
.body(Body::empty())?;
|
||||||
let response = router.oneshot(failure_request).await?;
|
let response = router.oneshot(failure_request).await?;
|
||||||
|
|||||||
@@ -322,6 +322,7 @@ const AppLayout = () => {
|
|||||||
const [webdavTokensLoading, setWebdavTokensLoading] = useState(false);
|
const [webdavTokensLoading, setWebdavTokensLoading] = useState(false);
|
||||||
const [creatingWebdavToken, setCreatingWebdavToken] = useState(false);
|
const [creatingWebdavToken, setCreatingWebdavToken] = useState(false);
|
||||||
const [deletingWebdavTokenId, setDeletingWebdavTokenId] = useState(null);
|
const [deletingWebdavTokenId, setDeletingWebdavTokenId] = useState(null);
|
||||||
|
const [regeneratingWebdavTokenId, setRegeneratingWebdavTokenId] = useState(null);
|
||||||
const [webdavTokenSecret, setWebdavTokenSecret] = useState(null);
|
const [webdavTokenSecret, setWebdavTokenSecret] = useState(null);
|
||||||
const [searchQuery, setSearchQuery] = useState('');
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
const [activeTagFilters, setActiveTagFilters] = useState([]);
|
const [activeTagFilters, setActiveTagFilters] = useState([]);
|
||||||
@@ -1597,6 +1598,47 @@ const AppLayout = () => {
|
|||||||
[refreshWebdavTokens, notifyApiError, setStatusMessage],
|
[refreshWebdavTokens, notifyApiError, setStatusMessage],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const regenerateWebdavToken = useCallback(
|
||||||
|
async (tokenId) => {
|
||||||
|
if (!tokenId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setRegeneratingWebdavTokenId(tokenId);
|
||||||
|
try {
|
||||||
|
const { data } = await api.post(`/profile/webdav-tokens/${tokenId}/regenerate`);
|
||||||
|
if (data?.token_info) {
|
||||||
|
setWebdavTokens((previous) => {
|
||||||
|
let found = false;
|
||||||
|
const next = previous.map((entry) => {
|
||||||
|
if (entry.id === data.token_info.id) {
|
||||||
|
found = true;
|
||||||
|
return data.token_info;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
});
|
||||||
|
if (!found) {
|
||||||
|
return [data.token_info, ...previous];
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await refreshWebdavTokens();
|
||||||
|
}
|
||||||
|
if (data?.token) {
|
||||||
|
setWebdavTokenSecret(data.token);
|
||||||
|
}
|
||||||
|
setStatusMessage('WebDAV token regenerated.', 'success');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
notifyApiError(error, 'Failed to regenerate WebDAV token.');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
setRegeneratingWebdavTokenId(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[notifyApiError, refreshWebdavTokens, setStatusMessage],
|
||||||
|
);
|
||||||
|
|
||||||
const dismissCreatedWebdavToken = useCallback(() => {
|
const dismissCreatedWebdavToken = useCallback(() => {
|
||||||
setWebdavTokenSecret(null);
|
setWebdavTokenSecret(null);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -5040,9 +5082,11 @@ const AppLayout = () => {
|
|||||||
webdavTokensLoading,
|
webdavTokensLoading,
|
||||||
creatingWebdavToken,
|
creatingWebdavToken,
|
||||||
deletingWebdavTokenId,
|
deletingWebdavTokenId,
|
||||||
|
regeneratingWebdavTokenId,
|
||||||
refreshWebdavTokens,
|
refreshWebdavTokens,
|
||||||
createWebdavToken,
|
createWebdavToken,
|
||||||
deleteWebdavToken,
|
deleteWebdavToken,
|
||||||
|
regenerateWebdavToken,
|
||||||
webdavTokenSecret,
|
webdavTokenSecret,
|
||||||
dismissCreatedWebdavToken,
|
dismissCreatedWebdavToken,
|
||||||
passkeys,
|
passkeys,
|
||||||
@@ -5098,9 +5142,11 @@ const AppLayout = () => {
|
|||||||
webdavTokensLoading,
|
webdavTokensLoading,
|
||||||
creatingWebdavToken,
|
creatingWebdavToken,
|
||||||
deletingWebdavTokenId,
|
deletingWebdavTokenId,
|
||||||
|
regeneratingWebdavTokenId,
|
||||||
refreshWebdavTokens,
|
refreshWebdavTokens,
|
||||||
createWebdavToken,
|
createWebdavToken,
|
||||||
deleteWebdavToken,
|
deleteWebdavToken,
|
||||||
|
regenerateWebdavToken,
|
||||||
webdavTokenSecret,
|
webdavTokenSecret,
|
||||||
dismissCreatedWebdavToken,
|
dismissCreatedWebdavToken,
|
||||||
passkeys,
|
passkeys,
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ const SettingsRoute = () => {
|
|||||||
webdavTokensLoading,
|
webdavTokensLoading,
|
||||||
creatingWebdavToken,
|
creatingWebdavToken,
|
||||||
deletingWebdavTokenId,
|
deletingWebdavTokenId,
|
||||||
|
regeneratingWebdavTokenId,
|
||||||
refreshWebdavTokens,
|
refreshWebdavTokens,
|
||||||
createWebdavToken,
|
createWebdavToken,
|
||||||
deleteWebdavToken,
|
deleteWebdavToken,
|
||||||
|
regenerateWebdavToken,
|
||||||
webdavTokenSecret,
|
webdavTokenSecret,
|
||||||
dismissCreatedWebdavToken,
|
dismissCreatedWebdavToken,
|
||||||
passkeys,
|
passkeys,
|
||||||
@@ -58,9 +60,11 @@ const SettingsRoute = () => {
|
|||||||
loading={webdavTokensLoading}
|
loading={webdavTokensLoading}
|
||||||
creating={creatingWebdavToken}
|
creating={creatingWebdavToken}
|
||||||
deletingId={deletingWebdavTokenId}
|
deletingId={deletingWebdavTokenId}
|
||||||
|
regeneratingId={regeneratingWebdavTokenId}
|
||||||
onRefresh={refreshWebdavTokens}
|
onRefresh={refreshWebdavTokens}
|
||||||
onCreate={createWebdavToken}
|
onCreate={createWebdavToken}
|
||||||
onDelete={deleteWebdavToken}
|
onDelete={deleteWebdavToken}
|
||||||
|
onRegenerate={regenerateWebdavToken}
|
||||||
createdToken={webdavTokenSecret}
|
createdToken={webdavTokenSecret}
|
||||||
onDismissCreatedToken={dismissCreatedWebdavToken}
|
onDismissCreatedToken={dismissCreatedWebdavToken}
|
||||||
passkeys={passkeys}
|
passkeys={passkeys}
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ const SettingsModal = ({
|
|||||||
loading = false,
|
loading = false,
|
||||||
creating = false,
|
creating = false,
|
||||||
deletingId = null,
|
deletingId = null,
|
||||||
|
regeneratingId = null,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onCreate,
|
onCreate,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onRegenerate,
|
||||||
createdToken = null,
|
createdToken = null,
|
||||||
onDismissCreatedToken,
|
onDismissCreatedToken,
|
||||||
passkeys = [],
|
passkeys = [],
|
||||||
@@ -147,6 +149,16 @@ const SettingsModal = ({
|
|||||||
[newTokenExpires, newTokenLabel, onCreate],
|
[newTokenExpires, newTokenLabel, onCreate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleRegenerateToken = useCallback(
|
||||||
|
async (token) => {
|
||||||
|
if (!token?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onRegenerate?.(token.id);
|
||||||
|
},
|
||||||
|
[onRegenerate],
|
||||||
|
);
|
||||||
|
|
||||||
const renderWebdavSection = useMemo(() => {
|
const renderWebdavSection = useMemo(() => {
|
||||||
const hasTokens = Array.isArray(tokens) && tokens.length > 0;
|
const hasTokens = Array.isArray(tokens) && tokens.length > 0;
|
||||||
|
|
||||||
@@ -240,14 +252,28 @@ const SettingsModal = ({
|
|||||||
{isRevoked ? (
|
{isRevoked ? (
|
||||||
<span className="settings-status">Revoked</span>
|
<span className="settings-status">Revoked</span>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="secondary"
|
||||||
|
onClick={() => handleRegenerateToken(token)}
|
||||||
|
disabled={
|
||||||
|
regeneratingId === token.id || deletingId === token.id
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{regeneratingId === token.id ? 'Regenerating…' : 'Regenerate'}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="danger"
|
className="danger"
|
||||||
onClick={() => onDelete?.(token.id)}
|
onClick={() => onDelete?.(token.id)}
|
||||||
disabled={deletingId === token.id}
|
disabled={
|
||||||
|
deletingId === token.id || regeneratingId === token.id
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{deletingId === token.id ? 'Revoking…' : 'Revoke'}
|
{deletingId === token.id ? 'Revoking…' : 'Revoke'}
|
||||||
</button>
|
</button>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -264,12 +290,14 @@ const SettingsModal = ({
|
|||||||
createdToken,
|
createdToken,
|
||||||
creating,
|
creating,
|
||||||
deletingId,
|
deletingId,
|
||||||
|
regeneratingId,
|
||||||
newTokenLabel,
|
newTokenLabel,
|
||||||
newTokenExpires,
|
newTokenExpires,
|
||||||
formError,
|
formError,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
handleCopyToken,
|
handleCopyToken,
|
||||||
handleCreateToken,
|
handleCreateToken,
|
||||||
|
handleRegenerateToken,
|
||||||
handleRefresh,
|
handleRefresh,
|
||||||
onDelete,
|
onDelete,
|
||||||
handleDismissSecret,
|
handleDismissSecret,
|
||||||
|
|||||||
Reference in New Issue
Block a user