mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-02-08 21:30:08 +00:00
migration improvements
This commit is contained in:
@@ -1,61 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT did, handle, email, created_at, email_verified, deactivated_at, invites_disabled\n FROM users\n WHERE did > $1\n AND ($2::text IS NULL OR email ILIKE $2)\n AND ($3::text IS NULL OR handle ILIKE $3)\n ORDER BY did ASC\n LIMIT $4",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "did",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "email_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "deactivated_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "invites_disabled",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "29520eea3a5f2fe13fabc503808ca19247adeb9095dd6766e148f9d8eaa6d589"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT account_type::text = 'delegated' as \"is_delegated!\" FROM users WHERE did = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "is_delegated!",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "40d42ed61a77074b298539e492d8fb6493174a7c49324e6f4f20b68bc30e95f4"
|
||||
}
|
||||
@@ -905,6 +905,7 @@ pub struct RecoverPasskeyAccountResult {
|
||||
pub struct MigrationReactivationInput {
|
||||
pub did: Did,
|
||||
pub new_handle: Handle,
|
||||
pub new_email: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -2699,12 +2699,22 @@ impl UserRepository for PostgresUserRepository {
|
||||
return Err(tranquil_db_traits::MigrationReactivationError::NotDeactivated);
|
||||
}
|
||||
|
||||
let update_result: Result<_, sqlx::Error> =
|
||||
let update_result: Result<_, sqlx::Error> = if let Some(ref new_email) = input.new_email {
|
||||
sqlx::query(
|
||||
"UPDATE users SET handle = $1, email = $2, email_verified = false WHERE id = $3",
|
||||
)
|
||||
.bind(input.new_handle.as_str())
|
||||
.bind(new_email)
|
||||
.bind(account_id)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
} else {
|
||||
sqlx::query("UPDATE users SET handle = $1 WHERE id = $2")
|
||||
.bind(input.new_handle.as_str())
|
||||
.bind(account_id)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
.await
|
||||
};
|
||||
|
||||
if let Err(e) = update_result {
|
||||
if let Some(db_err) = e.as_database_error()
|
||||
|
||||
@@ -410,6 +410,7 @@ pub async fn create_account(
|
||||
let reactivate_input = tranquil_db_traits::MigrationReactivationInput {
|
||||
did: Did::new_unchecked(&did),
|
||||
new_handle: Handle::new_unchecked(&handle),
|
||||
new_email: email.clone(),
|
||||
};
|
||||
match state
|
||||
.user_repo
|
||||
@@ -477,6 +478,29 @@ pub async fn create_account(
|
||||
error!("Error creating session: {:?}", e);
|
||||
return ApiError::InternalError(None).into_response();
|
||||
}
|
||||
let hostname =
|
||||
std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let verification_required = if let Some(ref user_email) = email {
|
||||
let token =
|
||||
crate::auth::verification_token::generate_migration_token(&did, user_email);
|
||||
let formatted_token =
|
||||
crate::auth::verification_token::format_token_for_display(&token);
|
||||
if let Err(e) = crate::comms::comms_repo::enqueue_migration_verification(
|
||||
state.user_repo.as_ref(),
|
||||
state.infra_repo.as_ref(),
|
||||
reactivated.user_id,
|
||||
user_email,
|
||||
&formatted_token,
|
||||
&hostname,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to enqueue migration verification email: {:?}", e);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
return (
|
||||
axum::http::StatusCode::OK,
|
||||
Json(CreateAccountOutput {
|
||||
@@ -485,7 +509,7 @@ pub async fn create_account(
|
||||
did_doc: state.did_resolver.resolve_did_document(&did).await,
|
||||
access_jwt: access_meta.token,
|
||||
refresh_jwt: refresh_meta.token,
|
||||
verification_required: false,
|
||||
verification_required,
|
||||
verification_channel: "email".to_string(),
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -151,6 +151,8 @@ pub async fn submit_plc_operation(
|
||||
}
|
||||
}
|
||||
let _ = state.cache.delete(&format!("handle:{}", user.handle)).await;
|
||||
let _ = state.cache.delete(&format!("plc:doc:{}", did)).await;
|
||||
let _ = state.cache.delete(&format!("plc:data:{}", did)).await;
|
||||
if state.did_resolver.refresh_did(did).await.is_none() {
|
||||
warn!(did = %did, "Failed to refresh DID cache after PLC update");
|
||||
}
|
||||
|
||||
@@ -425,6 +425,11 @@ pub async fn activate_account(
|
||||
if let Some(ref h) = handle {
|
||||
let _ = state.cache.delete(&format!("handle:{}", h)).await;
|
||||
}
|
||||
let _ = state.cache.delete(&format!("plc:doc:{}", did)).await;
|
||||
let _ = state.cache.delete(&format!("plc:data:{}", did)).await;
|
||||
if state.did_resolver.refresh_did(did.as_str()).await.is_none() {
|
||||
warn!("[MIGRATION] activateAccount: Failed to refresh DID cache for {}", did);
|
||||
}
|
||||
info!(
|
||||
"[MIGRATION] activateAccount: Sequencing account event (active=true) for did={}",
|
||||
did
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
pub fn test_function() -> &'static str {
|
||||
"NEW_FILE_TEST_67890"
|
||||
}
|
||||
@@ -8,60 +8,137 @@ export interface BlobMigrationResult {
|
||||
sourceUnreachable: boolean;
|
||||
}
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
const RETRY_DELAYS = [1000, 2000, 4000];
|
||||
|
||||
const sleep = (ms: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
const safeProgress = (
|
||||
onProgress: (update: Partial<MigrationProgress>) => void,
|
||||
update: Partial<MigrationProgress>,
|
||||
): void => {
|
||||
try {
|
||||
onProgress(update);
|
||||
} catch (e) {
|
||||
console.warn("[blob-migration] Progress callback failed:", e);
|
||||
}
|
||||
};
|
||||
|
||||
interface MigrateBlobResult {
|
||||
cid: string;
|
||||
success: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const migrateSingleBlob = async (
|
||||
cid: string,
|
||||
userDid: string,
|
||||
sourceClient: AtprotoClient,
|
||||
localClient: AtprotoClient,
|
||||
attempt = 0,
|
||||
): Promise<MigrateBlobResult> => {
|
||||
try {
|
||||
console.log(
|
||||
`[blob-migration] Fetching blob ${cid} from source (attempt ${attempt + 1})`,
|
||||
);
|
||||
const { data: blobData, contentType } = await sourceClient
|
||||
.getBlobWithContentType(userDid, cid);
|
||||
console.log(
|
||||
`[blob-migration] Got blob ${cid}, size: ${blobData.byteLength}, type: ${contentType}`,
|
||||
);
|
||||
|
||||
console.log(`[blob-migration] Uploading blob ${cid} to local PDS...`);
|
||||
const uploadResult = await localClient.uploadBlob(blobData, contentType);
|
||||
console.log(
|
||||
`[blob-migration] Upload response for ${cid}:`,
|
||||
JSON.stringify(uploadResult),
|
||||
);
|
||||
|
||||
return { cid, success: true };
|
||||
} catch (e) {
|
||||
const errorMessage = (e as Error).message || String(e);
|
||||
console.error(
|
||||
`[blob-migration] Failed to migrate blob ${cid} (attempt ${attempt + 1}):`,
|
||||
errorMessage,
|
||||
);
|
||||
|
||||
const isRetryable = attempt < MAX_RETRIES - 1 &&
|
||||
!errorMessage.includes("404") &&
|
||||
!errorMessage.includes("not found") &&
|
||||
!errorMessage.includes("BlobNotFound");
|
||||
|
||||
if (isRetryable) {
|
||||
const delay = RETRY_DELAYS[attempt] ?? 4000;
|
||||
console.log(`[blob-migration] Retrying ${cid} in ${delay}ms...`);
|
||||
await sleep(delay);
|
||||
return migrateSingleBlob(
|
||||
cid,
|
||||
userDid,
|
||||
sourceClient,
|
||||
localClient,
|
||||
attempt + 1,
|
||||
);
|
||||
}
|
||||
|
||||
return { cid, success: false, error: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const collectMissingBlobs = async (
|
||||
localClient: AtprotoClient,
|
||||
): Promise<string[]> => {
|
||||
const allBlobs: string[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
||||
do {
|
||||
const { blobs, cursor: nextCursor } = await localClient.listMissingBlobs(
|
||||
cursor,
|
||||
500,
|
||||
);
|
||||
console.log(
|
||||
`[blob-migration] listMissingBlobs returned ${blobs.length} blobs, cursor: ${nextCursor}`,
|
||||
);
|
||||
allBlobs.push(...blobs.map((blob) => blob.cid));
|
||||
cursor = nextCursor;
|
||||
} while (cursor);
|
||||
|
||||
return allBlobs;
|
||||
};
|
||||
|
||||
export async function migrateBlobs(
|
||||
localClient: AtprotoClient,
|
||||
sourceClient: AtprotoClient | null,
|
||||
userDid: string,
|
||||
onProgress: (update: Partial<MigrationProgress>) => void,
|
||||
): Promise<BlobMigrationResult> {
|
||||
const missingBlobs: string[] = [];
|
||||
let cursor: string | undefined;
|
||||
|
||||
console.log("[blob-migration] Starting blob migration for", userDid);
|
||||
console.log(
|
||||
"[blob-migration] Source client:",
|
||||
sourceClient ? `available (baseUrl: ${sourceClient.getBaseUrl()})` : "NOT AVAILABLE",
|
||||
);
|
||||
console.log(
|
||||
"[blob-migration] Local client baseUrl:",
|
||||
localClient.getBaseUrl(),
|
||||
);
|
||||
console.log("[blob-migration] Local client baseUrl:", localClient.getBaseUrl());
|
||||
console.log(
|
||||
"[blob-migration] Local client has access token:",
|
||||
localClient.getAccessToken() ? "yes" : "NO",
|
||||
);
|
||||
|
||||
onProgress({ currentOperation: "Checking for missing blobs..." });
|
||||
safeProgress(onProgress, { currentOperation: "Checking for missing blobs..." });
|
||||
|
||||
do {
|
||||
const { blobs, cursor: nextCursor } = await localClient.listMissingBlobs(
|
||||
cursor,
|
||||
100,
|
||||
);
|
||||
console.log(
|
||||
"[blob-migration] listMissingBlobs returned",
|
||||
blobs.length,
|
||||
"blobs, cursor:",
|
||||
nextCursor,
|
||||
);
|
||||
missingBlobs.push(...blobs.map((blob) => blob.cid));
|
||||
cursor = nextCursor;
|
||||
} while (cursor);
|
||||
const missingBlobs = await collectMissingBlobs(localClient);
|
||||
|
||||
console.log("[blob-migration] Total missing blobs:", missingBlobs.length);
|
||||
onProgress({ blobsTotal: missingBlobs.length });
|
||||
safeProgress(onProgress, { blobsTotal: missingBlobs.length });
|
||||
|
||||
if (missingBlobs.length === 0) {
|
||||
console.log("[blob-migration] No blobs to migrate");
|
||||
onProgress({ currentOperation: "No blobs to migrate" });
|
||||
safeProgress(onProgress, { currentOperation: "No blobs to migrate" });
|
||||
return { migrated: 0, failed: [], total: 0, sourceUnreachable: false };
|
||||
}
|
||||
|
||||
if (!sourceClient) {
|
||||
console.warn(
|
||||
"[blob-migration] No source client available, cannot fetch blobs",
|
||||
);
|
||||
onProgress({
|
||||
console.warn("[blob-migration] No source client available, cannot fetch blobs");
|
||||
safeProgress(onProgress, {
|
||||
currentOperation:
|
||||
`${missingBlobs.length} media files missing. No source PDS URL available - your old server may have shut down. Posts will work, but some images/media may be unavailable.`,
|
||||
});
|
||||
@@ -73,100 +150,54 @@ export async function migrateBlobs(
|
||||
};
|
||||
}
|
||||
|
||||
onProgress({ currentOperation: `Migrating ${missingBlobs.length} blobs...` });
|
||||
safeProgress(onProgress, {
|
||||
currentOperation: `Migrating ${missingBlobs.length} blobs...`,
|
||||
});
|
||||
|
||||
let migrated = 0;
|
||||
const failed: string[] = [];
|
||||
let sourceUnreachable = false;
|
||||
const results = await missingBlobs.reduce<
|
||||
Promise<{ migrated: number; failed: string[] }>
|
||||
>(
|
||||
async (accPromise, cid, index) => {
|
||||
const acc = await accPromise;
|
||||
|
||||
for (const cid of missingBlobs) {
|
||||
if (sourceUnreachable) {
|
||||
failed.push(cid);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
onProgress({
|
||||
currentOperation: `Migrating blob ${
|
||||
migrated + 1
|
||||
}/${missingBlobs.length}...`,
|
||||
safeProgress(onProgress, {
|
||||
currentOperation: `Migrating blob ${index + 1}/${missingBlobs.length}...`,
|
||||
blobsMigrated: acc.migrated,
|
||||
});
|
||||
|
||||
console.log("[blob-migration] Fetching blob", cid, "from source");
|
||||
const { data: blobData, contentType } = await sourceClient
|
||||
.getBlobWithContentType(userDid, cid);
|
||||
console.log(
|
||||
"[blob-migration] Got blob",
|
||||
const result = await migrateSingleBlob(
|
||||
cid,
|
||||
"size:",
|
||||
blobData.byteLength,
|
||||
"contentType:",
|
||||
contentType,
|
||||
);
|
||||
console.log("[blob-migration] Uploading blob", cid, "to local PDS...");
|
||||
const uploadResult = await localClient.uploadBlob(blobData, contentType);
|
||||
console.log(
|
||||
"[blob-migration] Upload response for",
|
||||
cid,
|
||||
":",
|
||||
JSON.stringify(uploadResult),
|
||||
);
|
||||
migrated++;
|
||||
onProgress({ blobsMigrated: migrated });
|
||||
} catch (e) {
|
||||
const errorMessage = (e as Error).message || String(e);
|
||||
console.error(
|
||||
"[blob-migration] Failed to migrate blob",
|
||||
cid,
|
||||
":",
|
||||
errorMessage,
|
||||
userDid,
|
||||
sourceClient,
|
||||
localClient,
|
||||
);
|
||||
|
||||
const isNetworkError = errorMessage.includes("fetch") ||
|
||||
errorMessage.includes("network") ||
|
||||
errorMessage.includes("CORS") ||
|
||||
errorMessage.includes("Failed to fetch") ||
|
||||
errorMessage.includes("NetworkError") ||
|
||||
errorMessage.includes("blocked by CORS");
|
||||
return result.success
|
||||
? { migrated: acc.migrated + 1, failed: acc.failed }
|
||||
: { migrated: acc.migrated, failed: [...acc.failed, cid] };
|
||||
},
|
||||
Promise.resolve({ migrated: 0, failed: [] as string[] }),
|
||||
);
|
||||
|
||||
if (isNetworkError) {
|
||||
sourceUnreachable = true;
|
||||
console.warn(
|
||||
"[blob-migration] Source appears unreachable (likely CORS or network issue), skipping remaining blobs",
|
||||
);
|
||||
const remaining = missingBlobs.length - migrated - 1;
|
||||
if (migrated > 0) {
|
||||
onProgress({
|
||||
currentOperation:
|
||||
`Source PDS unreachable (browser security restriction). ${migrated} media files migrated successfully. ${
|
||||
remaining + 1
|
||||
} could not be fetched - these may need to be re-uploaded.`,
|
||||
});
|
||||
} else {
|
||||
onProgress({
|
||||
currentOperation:
|
||||
`Cannot reach source PDS (browser security restriction). This commonly happens when the old server has shut down or doesn't allow cross-origin requests. Your posts will work, but ${missingBlobs.length} media files couldn't be recovered.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
failed.push(cid);
|
||||
}
|
||||
}
|
||||
const { migrated, failed } = results;
|
||||
|
||||
if (migrated === missingBlobs.length) {
|
||||
onProgress({
|
||||
currentOperation: `All ${migrated} blobs migrated successfully`,
|
||||
});
|
||||
} else if (migrated > 0) {
|
||||
onProgress({
|
||||
currentOperation:
|
||||
`${migrated}/${missingBlobs.length} blobs migrated. ${failed.length} failed.`,
|
||||
});
|
||||
} else {
|
||||
onProgress({
|
||||
currentOperation: `Could not migrate blobs (${failed.length} missing)`,
|
||||
});
|
||||
}
|
||||
safeProgress(onProgress, { blobsMigrated: migrated });
|
||||
|
||||
return { migrated, failed, total: missingBlobs.length, sourceUnreachable };
|
||||
const statusMessage = migrated === missingBlobs.length
|
||||
? `All ${migrated} blobs migrated successfully`
|
||||
: migrated > 0
|
||||
? `${migrated}/${missingBlobs.length} blobs migrated. ${failed.length} failed.`
|
||||
: `Could not migrate blobs (${failed.length} missing)`;
|
||||
|
||||
safeProgress(onProgress, { currentOperation: statusMessage });
|
||||
|
||||
console.log(`[blob-migration] Complete: ${migrated} migrated, ${failed.length} failed`);
|
||||
failed.length > 0 && console.log("[blob-migration] Failed CIDs:", failed);
|
||||
|
||||
return {
|
||||
migrated,
|
||||
failed,
|
||||
total: missingBlobs.length,
|
||||
sourceUnreachable: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ export function createInboundMigrationFlow() {
|
||||
authMethod: "password",
|
||||
passkeySetupToken: null,
|
||||
oauthCodeVerifier: null,
|
||||
localAccessToken: null,
|
||||
generatedAppPassword: null,
|
||||
generatedAppPasswordName: null,
|
||||
});
|
||||
@@ -276,6 +277,9 @@ export function createInboundMigrationFlow() {
|
||||
|
||||
if (postEmailSteps.includes(targetStep)) {
|
||||
localClient = createLocalClient();
|
||||
if (state.localAccessToken) {
|
||||
localClient.setAccessToken(state.localAccessToken);
|
||||
}
|
||||
if (state.authMethod === "passkey" && state.passkeySetupToken) {
|
||||
setStep("passkey-setup");
|
||||
migrationLog(
|
||||
@@ -289,6 +293,9 @@ export function createInboundMigrationFlow() {
|
||||
}
|
||||
} else if (targetStep === "email-verify") {
|
||||
localClient = createLocalClient();
|
||||
if (state.localAccessToken) {
|
||||
localClient.setAccessToken(state.localAccessToken);
|
||||
}
|
||||
setStep("email-verify");
|
||||
migrationLog("handleOAuthCallback: Resuming at email-verify");
|
||||
} else {
|
||||
@@ -389,6 +396,7 @@ export function createInboundMigrationFlow() {
|
||||
state.passkeySetupToken = passkeySetup.setupToken;
|
||||
if (passkeySetup.accessJwt) {
|
||||
localClient.setAccessToken(passkeySetup.accessJwt);
|
||||
state.localAccessToken = passkeySetup.accessJwt;
|
||||
}
|
||||
} else {
|
||||
const accountParams = {
|
||||
@@ -408,6 +416,7 @@ export function createInboundMigrationFlow() {
|
||||
did: session.did,
|
||||
});
|
||||
localClient.setAccessToken(session.accessJwt);
|
||||
state.localAccessToken = session.accessJwt;
|
||||
}
|
||||
|
||||
setProgress({ currentOperation: "Exporting repository..." });
|
||||
@@ -599,10 +608,12 @@ export function createInboundMigrationFlow() {
|
||||
return true;
|
||||
}
|
||||
|
||||
await localClient.loginDeactivated(
|
||||
state.targetEmail,
|
||||
state.targetPassword,
|
||||
);
|
||||
if (!localClient.getAccessToken()) {
|
||||
await localClient.loginDeactivated(
|
||||
state.targetEmail,
|
||||
state.targetPassword,
|
||||
);
|
||||
}
|
||||
|
||||
if (!sourceClient) {
|
||||
setStep("source-handle");
|
||||
@@ -916,6 +927,7 @@ export function createInboundMigrationFlow() {
|
||||
state.targetHandle = stored.targetHandle;
|
||||
state.targetEmail = stored.targetEmail;
|
||||
state.authMethod = stored.authMethod ?? "password";
|
||||
state.localAccessToken = stored.localAccessToken ?? null;
|
||||
state.progress = {
|
||||
...createInitialProgress(),
|
||||
...stored.progress,
|
||||
|
||||
@@ -497,12 +497,14 @@ export function createOfflineInboundMigrationFlow() {
|
||||
const { verified } = await api.checkEmailVerified(state.targetEmail);
|
||||
if (!verified) return false;
|
||||
|
||||
const session = await api.createSession(
|
||||
state.targetEmail,
|
||||
state.targetPassword,
|
||||
);
|
||||
state.localAccessToken = session.accessJwt;
|
||||
state.localRefreshToken = session.refreshJwt;
|
||||
if (!state.localAccessToken) {
|
||||
const session = await api.createSession(
|
||||
state.targetEmail,
|
||||
state.targetPassword,
|
||||
);
|
||||
state.localAccessToken = session.accessJwt;
|
||||
state.localRefreshToken = session.refreshJwt;
|
||||
}
|
||||
saveOfflineState(state);
|
||||
|
||||
setStep("plc-signing");
|
||||
|
||||
@@ -22,6 +22,7 @@ export function saveMigrationState(state: MigrationState): void {
|
||||
targetEmail: state.targetEmail,
|
||||
authMethod: state.authMethod,
|
||||
passkeySetupToken: state.passkeySetupToken ?? undefined,
|
||||
localAccessToken: state.localAccessToken ?? undefined,
|
||||
progress: {
|
||||
repoExported: state.progress.repoExported,
|
||||
repoImported: state.progress.repoImported,
|
||||
|
||||
@@ -69,6 +69,7 @@ export interface InboundMigrationState {
|
||||
authMethod: AuthMethod;
|
||||
passkeySetupToken: string | null;
|
||||
oauthCodeVerifier: string | null;
|
||||
localAccessToken: string | null;
|
||||
generatedAppPassword: string | null;
|
||||
generatedAppPasswordName: string | null;
|
||||
needsReauth?: boolean;
|
||||
@@ -117,6 +118,7 @@ export interface StoredMigrationState {
|
||||
targetEmail: string;
|
||||
authMethod?: AuthMethod;
|
||||
passkeySetupToken?: string;
|
||||
localAccessToken?: string;
|
||||
progress: {
|
||||
repoExported: boolean;
|
||||
repoImported: boolean;
|
||||
|
||||
@@ -1065,6 +1065,12 @@
|
||||
"accessLevel": "Käyttöoikeustaso",
|
||||
"adding": "Lisätään...",
|
||||
"addControllerButton": "+ Lisää hallinnoija",
|
||||
"addControllerWarningTitle": "Tärkeää: Tämä muuttaa kirjautumistapasi",
|
||||
"addControllerWarningText": "Hallinnoijan lisääminen tarkoittaa, että vain hallinnoijatili voi kirjautua tähän tiliin OAuth:n kautta. Et voi enää kirjautua suoraan omilla tunnuksillasi kolmannen osapuolen sovellusten tai verkkokäyttöliittymän kautta.",
|
||||
"addControllerWarningBullet1": "Hallinnoija voi toimia puolestasi myöntämilläsi oikeuksilla",
|
||||
"addControllerWarningBullet2": "Sinun täytyy ensin kirjautua hallinnoijana ja sitten vaihtaa tähän tiliin",
|
||||
"addControllerWarningBullet3": "Voit poistaa hallinnoijan myöhemmin palauttaaksesi suoran kirjautumisoikeuden",
|
||||
"addControllerConfirm": "Ymmärrän, etten voi enää kirjautua suoraan",
|
||||
"controllerAdded": "Hallinnoija lisätty",
|
||||
"controllerRemoved": "Hallinnoija poistettu",
|
||||
"failedToAddController": "Hallinnoijan lisääminen epäonnistui",
|
||||
|
||||
@@ -1078,6 +1078,12 @@
|
||||
"adding": "追加中...",
|
||||
"accessLevel": "アクセスレベル",
|
||||
"addControllerButton": "+ コントローラーを追加",
|
||||
"addControllerWarningTitle": "重要: ログイン方法が変わります",
|
||||
"addControllerWarningText": "コントローラーを追加すると、OAuth経由でこのアカウントにログインできるのはコントローラーアカウントのみになります。サードパーティアプリやWebインターフェースから自分の認証情報で直接ログインすることはできなくなります。",
|
||||
"addControllerWarningBullet1": "コントローラーは付与した権限であなたの代わりに操作できるようになります",
|
||||
"addControllerWarningBullet2": "まずコントローラーとしてログインし、その後このアカウントに切り替える必要があります",
|
||||
"addControllerWarningBullet3": "後でコントローラーを削除すれば、直接ログインできるようになります",
|
||||
"addControllerConfirm": "直接ログインできなくなることを理解しました",
|
||||
"auditLogDesc": "すべての委任アクティビティを表示",
|
||||
"cannotAddControllers": "他のアカウントを管理しているため、コントローラーを追加できません。アカウントはコントローラーを持つか、他のアカウントを管理するかのいずれかのみ可能です。",
|
||||
"cannotControlAccounts": "このアカウントにはコントローラーがいるため、他のアカウントを管理できません。アカウントはコントローラーを持つか、他のアカウントを管理するかのいずれかのみ可能です。",
|
||||
|
||||
@@ -1078,6 +1078,12 @@
|
||||
"adding": "추가 중...",
|
||||
"accessLevel": "액세스 수준",
|
||||
"addControllerButton": "+ 컨트롤러 추가",
|
||||
"addControllerWarningTitle": "중요: 로그인 방식이 변경됩니다",
|
||||
"addControllerWarningText": "컨트롤러를 추가하면 OAuth를 통해 이 계정에 로그인할 수 있는 것은 컨트롤러 계정뿐입니다. 서드파티 앱이나 웹 인터페이스에서 본인 자격 증명으로 직접 로그인할 수 없게 됩니다.",
|
||||
"addControllerWarningBullet1": "컨트롤러는 부여한 권한으로 귀하를 대신하여 작업할 수 있습니다",
|
||||
"addControllerWarningBullet2": "먼저 컨트롤러로 로그인한 후 이 계정으로 전환해야 합니다",
|
||||
"addControllerWarningBullet3": "나중에 컨트롤러를 제거하면 직접 로그인 권한을 복구할 수 있습니다",
|
||||
"addControllerConfirm": "직접 로그인할 수 없게 되는 것을 이해합니다",
|
||||
"auditLogDesc": "모든 위임 활동 보기",
|
||||
"cannotAddControllers": "다른 계정을 관리하고 있어 컨트롤러를 추가할 수 없습니다. 계정은 컨트롤러를 가지거나 다른 계정을 관리할 수 있지만 둘 다는 불가능합니다.",
|
||||
"cannotControlAccounts": "이 계정에 컨트롤러가 있어 다른 계정을 관리할 수 없습니다. 계정은 컨트롤러를 가지거나 다른 계정을 관리할 수 있지만 둘 다는 불가능합니다.",
|
||||
|
||||
@@ -1078,6 +1078,12 @@
|
||||
"adding": "Lägger till...",
|
||||
"accessLevel": "Åtkomstnivå",
|
||||
"addControllerButton": "+ Lägg till kontrollant",
|
||||
"addControllerWarningTitle": "Viktigt: Detta ändrar hur du loggar in",
|
||||
"addControllerWarningText": "Att lägga till en kontrollant innebär att endast kontrollantskontot kommer att kunna logga in på detta konto via OAuth. Du kommer inte längre att kunna logga in direkt med dina egna uppgifter via tredjepartsappar eller webbgränssnittet.",
|
||||
"addControllerWarningBullet1": "Kontrollanten kommer att kunna agera för dig med de behörigheter du beviljar",
|
||||
"addControllerWarningBullet2": "Du måste först logga in som kontrollant och sedan byta till detta konto",
|
||||
"addControllerWarningBullet3": "Du kan ta bort kontrollanten senare för att återfå direkt inloggning",
|
||||
"addControllerConfirm": "Jag förstår att jag inte längre kommer att kunna logga in direkt",
|
||||
"auditLogDesc": "Visa all delegeringsaktivitet",
|
||||
"cannotAddControllers": "Du kan inte lägga till kontrollanter eftersom detta konto kontrollerar andra konton. Ett konto kan antingen ha kontrollanter eller kontrollera andra konton, men inte båda.",
|
||||
"cannotControlAccounts": "Du kan inte kontrollera andra konton eftersom detta konto har kontrollanter. Ett konto kan antingen ha kontrollanter eller kontrollera andra konton, men inte båda.",
|
||||
|
||||
@@ -1079,6 +1079,12 @@
|
||||
"adding": "添加中...",
|
||||
"accessLevel": "访问级别",
|
||||
"addControllerButton": "+ 添加控制者",
|
||||
"addControllerWarningTitle": "重要提示:这将改变您的登录方式",
|
||||
"addControllerWarningText": "添加控制者意味着只有控制者账户才能通过 OAuth 登录此账户。您将无法再使用自己的凭据通过第三方应用或网页界面直接登录。",
|
||||
"addControllerWarningBullet1": "控制者将能够以您授予的权限代表您进行操作",
|
||||
"addControllerWarningBullet2": "您需要先以控制者身份登录,然后切换到此账户",
|
||||
"addControllerWarningBullet3": "您可以稍后移除控制者以恢复直接登录权限",
|
||||
"addControllerConfirm": "我理解我将无法再直接登录",
|
||||
"auditLogDesc": "查看所有委托活动",
|
||||
"cannotAddControllers": "因为此账户正在控制其他账户,所以无法添加控制者。账户只能拥有控制者或控制其他账户,不能同时两者兼备。",
|
||||
"cannotControlAccounts": "因为此账户有控制者,所以无法控制其他账户。账户只能拥有控制者或控制其他账户,不能同时两者兼备。",
|
||||
|
||||
Reference in New Issue
Block a user