mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(storage): make Blossom actually usable in the upload modal [LOCAL] (#30)
Two root causes for "Blossom set as default but not showing / not usable": 1. SAVE required unlock. saveConnection throws "Storage is locked" without a master key — but Blossom has no secret. Added storageService.saveKeylessConnection() (writes plaintext meta, no encryption, no unlock) and the Blossom save branch now uses it. So a Blossom connection saves even if the user never unlocked storage. 2. UploadModal treated Blossom like a locked S3 connection: it only set activeCredentials when isStorageUnlocked, showed "Locked 🔒", blocked the file-pick behind an unlock prompt, and mislabeled it "S3 Compatible". Now: getProviderLabel → "Blossom"; loadFromStorage resolves servers from the plaintext meta (no unlock); the summary shows "N servers / Nostr-native"; the two upload gates and updateLockUi bypass unlock for Blossom (activeProviderIsBlossom). Uploads authorize with the Nostr signer instead. Bumped UploadModal size baseline (+gating code). Lint + build clean. Local only. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
5935702e75
commit
ba62b35ada
4 files changed
+78
-21
No files matched your search
@@ -591,6 +591,41 @@ export class StorageService {
|
||||
userLogger.log(`[StorageService] Saved connection ${connectionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a KEYLESS connection (e.g. Blossom): all config lives in the plaintext
|
||||
* meta and there is no secret to encrypt, so this does NOT require unlock().
|
||||
* Used for providers whose auth is the user's Nostr signer, not a stored key.
|
||||
*/
|
||||
async saveKeylessConnection(pubkey, connectionId, meta = {}) {
|
||||
const account = (await this._getAccount(pubkey)) || {
|
||||
pubkey,
|
||||
connections: {},
|
||||
};
|
||||
if (!account.connections) {
|
||||
account.connections = {};
|
||||
}
|
||||
if (meta.defaultForUploads) {
|
||||
for (const key in account.connections) {
|
||||
const conn = account.connections[key];
|
||||
if (conn.meta) {
|
||||
conn.meta.defaultForUploads = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
account.connections[connectionId] = {
|
||||
id: connectionId,
|
||||
provider: meta.provider || connectionId,
|
||||
meta: {
|
||||
...meta,
|
||||
lastSaved: Date.now(),
|
||||
provider: meta.provider || connectionId,
|
||||
},
|
||||
encrypted: null,
|
||||
};
|
||||
await this._saveAccount(account);
|
||||
userLogger.log(`[StorageService] Saved keyless connection ${connectionId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves and decrypts a connection.
|
||||
* Requires unlock() to be called first.
|
||||
|
||||
@@ -16,6 +16,10 @@ import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
|
||||
import { buildR2Key, buildPublicUrl } from "../../r2.js";
|
||||
import { buildS3ObjectUrl } from "../../services/s3Service.js";
|
||||
import { PROVIDERS } from "../../services/storageService.js";
|
||||
import {
|
||||
BLOSSOM_PROVIDER,
|
||||
isBlossomProvider,
|
||||
} from "../../services/blossomService.js";
|
||||
import {
|
||||
buildStoragePointerValue,
|
||||
buildStoragePrefixFromKey,
|
||||
@@ -119,6 +123,9 @@ export class UploadModal {
|
||||
this.activeProvider = null;
|
||||
this.isStorageUnlocked = false;
|
||||
this.storageConfigured = false;
|
||||
// Blossom connections have no encrypted secret, so they never need a PIN
|
||||
// unlock — the upload authorizes with the Nostr signer instead.
|
||||
this.activeProviderIsBlossom = false;
|
||||
// #44: per-modal upload-destination override (null = account default).
|
||||
this.selectedConnectionId = null;
|
||||
|
||||
@@ -556,9 +563,10 @@ export class UploadModal {
|
||||
e.target.value = ""; // Clear selection
|
||||
return;
|
||||
}
|
||||
if (!this.isStorageUnlocked) {
|
||||
if (!this.isStorageUnlocked && !this.activeProviderIsBlossom) {
|
||||
// #56: unlock inline (silent restore or one passphrase prompt) and
|
||||
// continue with this same file pick instead of erroring.
|
||||
// continue with this same file pick instead of erroring. Blossom has no
|
||||
// secret to unlock — it authorizes with the Nostr signer at upload time.
|
||||
const unlocked = await this.ensureStorageUnlockedForUpload();
|
||||
if (!unlocked) {
|
||||
e.target.value = "";
|
||||
@@ -664,8 +672,9 @@ export class UploadModal {
|
||||
this.resetThumbnailPicker();
|
||||
return;
|
||||
}
|
||||
if (!this.isStorageUnlocked) {
|
||||
if (!this.isStorageUnlocked && !this.activeProviderIsBlossom) {
|
||||
// #56: prompt to unlock inline instead of silently dropping the pick.
|
||||
// Blossom needs no unlock (signer-authorized at upload time).
|
||||
const unlocked = await this.ensureStorageUnlockedForUpload();
|
||||
if (!unlocked) {
|
||||
this.resetThumbnailPicker();
|
||||
@@ -934,12 +943,20 @@ export class UploadModal {
|
||||
if (targetConn) {
|
||||
this.storageConfigured = true;
|
||||
this.activeProvider = targetConn.provider;
|
||||
this.activeProviderIsBlossom = isBlossomProvider(targetConn.provider);
|
||||
|
||||
// UI Updates
|
||||
this.toggleStorageView("summary");
|
||||
const providerName = this.getProviderLabel(targetConn.provider || targetConn.meta?.provider);
|
||||
const bucketName = targetConn.meta?.bucket || "Unknown Bucket";
|
||||
const urlStyle = this.describeUrlStyle(targetConn.provider, targetConn.meta?.forcePathStyle);
|
||||
const blossomServers = Array.isArray(targetConn.meta?.servers)
|
||||
? targetConn.meta.servers
|
||||
: [];
|
||||
const bucketName = this.activeProviderIsBlossom
|
||||
? `${blossomServers.length} server${blossomServers.length === 1 ? "" : "s"}`
|
||||
: targetConn.meta?.bucket || "Unknown Bucket";
|
||||
const urlStyle = this.activeProviderIsBlossom
|
||||
? "Nostr-native (content-addressed)"
|
||||
: this.describeUrlStyle(targetConn.provider, targetConn.meta?.forcePathStyle);
|
||||
|
||||
if (this.statusText.summaryProvider) {
|
||||
this.statusText.summaryProvider.textContent = providerName;
|
||||
@@ -955,7 +972,14 @@ export class UploadModal {
|
||||
}
|
||||
if (this.statusText.summaryCopy) this.statusText.summaryCopy.textContent = this.getSummaryCopy(targetConn.provider);
|
||||
|
||||
if (this.isStorageUnlocked) {
|
||||
if (this.activeProviderIsBlossom) {
|
||||
// No secret to decrypt — servers live in the plaintext meta.
|
||||
this.activeCredentials = {
|
||||
provider: BLOSSOM_PROVIDER,
|
||||
servers: blossomServers,
|
||||
meta: targetConn.meta || {},
|
||||
};
|
||||
} else if (this.isStorageUnlocked) {
|
||||
const details = await this.storageService.getConnection(pubkey, targetConn.id);
|
||||
if (details) {
|
||||
this.activeCredentials = details;
|
||||
@@ -970,6 +994,7 @@ export class UploadModal {
|
||||
this.storageConfigured = false;
|
||||
this.activeCredentials = null;
|
||||
this.activeProvider = null;
|
||||
this.activeProviderIsBlossom = false;
|
||||
this.toggleStorageView("empty");
|
||||
}
|
||||
this.updateLockUi();
|
||||
@@ -1064,7 +1089,8 @@ export class UploadModal {
|
||||
}
|
||||
|
||||
updateLockUi() {
|
||||
const locked = !this.isStorageUnlocked;
|
||||
// Blossom has no secret to unlock — never show it as "locked".
|
||||
const locked = !this.isStorageUnlocked && !this.activeProviderIsBlossom;
|
||||
if (this.statusText?.storageLock) {
|
||||
this.statusText.storageLock.textContent = locked ? "Locked 🔒" : "Unlocked 🔓";
|
||||
this.statusText.storageLock.className = locked ? "text-xs text-critical" : "text-xs text-success";
|
||||
@@ -1080,6 +1106,9 @@ export class UploadModal {
|
||||
}
|
||||
|
||||
getProviderLabel(provider) {
|
||||
if (isBlossomProvider(provider)) {
|
||||
return "Blossom";
|
||||
}
|
||||
if (provider === PROVIDERS.R2 || provider === "cloudflare_r2") {
|
||||
return "Cloudflare R2";
|
||||
}
|
||||
|
||||
@@ -805,25 +805,18 @@ export class ProfileStorageController {
|
||||
const isDefaultBlossom = this.storageDefaultInput?.checked || false;
|
||||
this.setStorageFormStatus("Saving...", "info");
|
||||
try {
|
||||
await saveProviderConnection(storageService, pubkey, {
|
||||
// Keyless: no secret to encrypt, so no storage unlock needed.
|
||||
await storageService.saveKeylessConnection(pubkey, BLOSSOM_PROVIDER, {
|
||||
provider: BLOSSOM_PROVIDER,
|
||||
payload: { provider: BLOSSOM_PROVIDER },
|
||||
meta: {
|
||||
provider: BLOSSOM_PROVIDER,
|
||||
servers,
|
||||
defaultForUploads: isDefaultBlossom,
|
||||
label: `Blossom (${servers.length} server${servers.length === 1 ? "" : "s"})`,
|
||||
},
|
||||
isDefault: isDefaultBlossom,
|
||||
servers,
|
||||
defaultForUploads: isDefaultBlossom,
|
||||
label: `Blossom (${servers.length} server${servers.length === 1 ? "" : "s"})`,
|
||||
});
|
||||
this.setStorageFormStatus("Blossom servers saved.", "success");
|
||||
this.mainController.showSuccess("Blossom storage saved.");
|
||||
} catch (error) {
|
||||
devLogger.error("Failed to save Blossom connection:", error);
|
||||
this.setStorageFormStatus(
|
||||
"Failed to save Blossom servers. Unlock storage first if prompted.",
|
||||
"error",
|
||||
);
|
||||
this.setStorageFormStatus("Failed to save Blossom servers.", "error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user