mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Refactor UploadModal to use StorageService for secure R2 credentials
This commit is contained in:
1 parent
1db1f2add3
commit
cf9d1c08be
7 files changed
+193
-15
No files matched your search
@@ -122,7 +122,11 @@
|
||||
<div id="section-storage-settings" class="hidden mt-3 space-y-4 bg-surface-alt/50 p-4 rounded-lg border border-border/50">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-sm font-semibold text-text">Cloudflare R2 Credentials</h4>
|
||||
<span id="storage-status" class="text-xs text-muted">Stored locally</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<span id="storage-lock-status" class="text-xs text-muted">Locked 🔒</span>
|
||||
<button type="button" id="btn-storage-unlock" class="text-xs text-accent hover:underline hidden">Unlock</button>
|
||||
<span id="storage-status" class="text-xs text-muted hidden">Stored locally</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<p class="text-xs text-muted">
|
||||
|
||||
+20
-11
@@ -383,15 +383,18 @@ class R2Service {
|
||||
* @param {string} npub - The user's Nostr public key (npub).
|
||||
* @returns {Promise<object>} The bucket configuration entry and status.
|
||||
*/
|
||||
async ensureBucketConfigForNpub(npub) {
|
||||
if (!npub || !this.cloudflareSettings) {
|
||||
async ensureBucketConfigForNpub(npub, { credentials } = {}) {
|
||||
if (!npub) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountId = (this.cloudflareSettings.accountId || "").trim();
|
||||
const accessKeyId = (this.cloudflareSettings.accessKeyId || "").trim();
|
||||
const secretAccessKey = (this.cloudflareSettings.secretAccessKey || "").trim();
|
||||
const baseDomain = this.cloudflareSettings.baseDomain || "";
|
||||
// Prefer explicit credentials if provided, otherwise fallback to stored settings
|
||||
const settings = credentials || this.cloudflareSettings || {};
|
||||
|
||||
const accountId = (settings.accountId || "").trim();
|
||||
const accessKeyId = (settings.accessKeyId || "").trim();
|
||||
const secretAccessKey = (settings.secretAccessKey || "").trim();
|
||||
const baseDomain = settings.baseDomain || "";
|
||||
const corsOrigins = this.getCorsOrigins();
|
||||
|
||||
if (!accountId) {
|
||||
@@ -613,6 +616,7 @@ class R2Service {
|
||||
metadata = {},
|
||||
infoHash = "",
|
||||
settingsInput = null,
|
||||
explicitCredentials = null,
|
||||
publishVideoNote,
|
||||
onReset,
|
||||
forcedVideoKey = "",
|
||||
@@ -620,7 +624,9 @@ class R2Service {
|
||||
forcedTorrentKey = "",
|
||||
forcedTorrentUrl = "",
|
||||
} = {}) {
|
||||
if (settingsInput) {
|
||||
// If no explicit credentials, we might save settingsInput to legacy DB.
|
||||
// If explicitCredentials ARE provided, we skip saving and use them directly.
|
||||
if (!explicitCredentials && settingsInput) {
|
||||
const saved = await this.handleCloudflareSettingsSubmit(settingsInput, {
|
||||
quiet: true,
|
||||
});
|
||||
@@ -660,9 +666,10 @@ class R2Service {
|
||||
return false;
|
||||
}
|
||||
|
||||
const accountId = (this.cloudflareSettings?.accountId || "").trim();
|
||||
const accessKeyId = (this.cloudflareSettings?.accessKeyId || "").trim();
|
||||
const secretAccessKey = (this.cloudflareSettings?.secretAccessKey || "").trim();
|
||||
const effectiveSettings = explicitCredentials || this.cloudflareSettings || {};
|
||||
const accountId = (effectiveSettings.accountId || "").trim();
|
||||
const accessKeyId = (effectiveSettings.accessKeyId || "").trim();
|
||||
const secretAccessKey = (effectiveSettings.secretAccessKey || "").trim();
|
||||
|
||||
if (!accountId || !accessKeyId || !secretAccessKey) {
|
||||
this.setCloudflareUploadStatus(
|
||||
@@ -678,7 +685,9 @@ class R2Service {
|
||||
|
||||
let bucketResult = null;
|
||||
try {
|
||||
bucketResult = await this.ensureBucketConfigForNpub(npub);
|
||||
bucketResult = await this.ensureBucketConfigForNpub(npub, {
|
||||
credentials: explicitCredentials,
|
||||
});
|
||||
} catch (err) {
|
||||
userLogger.error("Failed to prepare R2 bucket:", err);
|
||||
this.setCloudflareUploadStatus(
|
||||
|
||||
@@ -279,6 +279,13 @@ export class StorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the storage is currently unlocked for the user.
|
||||
*/
|
||||
isUnlocked(pubkey) {
|
||||
return this.masterKeys.has(pubkey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a connection configuration.
|
||||
* Requires unlock() to be called first.
|
||||
|
||||
@@ -18,6 +18,7 @@ export default class ModalManager {
|
||||
ui = {},
|
||||
documentRef = typeof document !== "undefined" ? document : null,
|
||||
assets = {},
|
||||
services = {},
|
||||
} = {}) {
|
||||
this.app = app;
|
||||
this.ui = ui;
|
||||
@@ -25,6 +26,7 @@ export default class ModalManager {
|
||||
this.assets = {
|
||||
fallbackThumbnailSrc: assets.fallbackThumbnailSrc || null,
|
||||
};
|
||||
this.services = services;
|
||||
|
||||
this.uploadModal = null;
|
||||
this.uploadModalEvents = null;
|
||||
@@ -63,6 +65,7 @@ export default class ModalManager {
|
||||
services: {
|
||||
authService: app.authService,
|
||||
r2Service: app.r2Service,
|
||||
storageService: this.services.storageService,
|
||||
},
|
||||
utilities: {
|
||||
removeTrackingScripts,
|
||||
|
||||
@@ -405,6 +405,9 @@ export default class ApplicationBootstrap {
|
||||
assets: {
|
||||
fallbackThumbnailSrc: this.assets.fallbackThumbnailSrc,
|
||||
},
|
||||
services: {
|
||||
storageService,
|
||||
},
|
||||
});
|
||||
modalManager.initialize();
|
||||
this.modalManager = modalManager;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { createTorrentMetadata } from "../../utils/torrentHash.js";
|
||||
import { sanitizeBucketName } from "../../storage/r2-mgmt.js";
|
||||
import { buildR2Key, buildPublicUrl } from "../../r2.js";
|
||||
import { PROVIDERS } from "../../services/storageService.js";
|
||||
|
||||
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
|
||||
|
||||
@@ -25,6 +26,7 @@ export class UploadModal {
|
||||
constructor({
|
||||
authService,
|
||||
r2Service,
|
||||
storageService,
|
||||
publishVideoNote,
|
||||
removeTrackingScripts,
|
||||
setGlobalModalState,
|
||||
@@ -37,6 +39,7 @@ export class UploadModal {
|
||||
} = {}) {
|
||||
this.authService = authService || null;
|
||||
this.r2Service = r2Service || null;
|
||||
this.storageService = storageService || null;
|
||||
this.publishVideoNote =
|
||||
typeof publishVideoNote === "function" ? publishVideoNote : null;
|
||||
this.removeTrackingScripts =
|
||||
@@ -62,6 +65,8 @@ export class UploadModal {
|
||||
this.isVisible = false;
|
||||
this.activeSource = "upload"; // 'upload' | 'external'
|
||||
this.cloudflareSettings = this.r2Service?.getSettings?.() || null;
|
||||
this.isStorageUnlocked = false;
|
||||
this.activeConnectionId = null;
|
||||
|
||||
// UI References
|
||||
this.form = null;
|
||||
@@ -117,7 +122,21 @@ export class UploadModal {
|
||||
this.registerR2Subscriptions();
|
||||
|
||||
// Initial State
|
||||
await this.loadR2Settings();
|
||||
if (this.storageService) {
|
||||
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
|
||||
this.isStorageUnlocked = pubkey ? this.storageService.isUnlocked(pubkey) : false;
|
||||
if (this.isStorageUnlocked) {
|
||||
await this.loadFromStorage();
|
||||
} else {
|
||||
// Attempt legacy load if not unlocked (or maybe just show locked state)
|
||||
// For now we keep legacy behavior as fallback if storageService isn't used
|
||||
// But user requested provider agnostic.
|
||||
// We will update UI state in setSourceMode or similar
|
||||
}
|
||||
} else {
|
||||
await this.loadR2Settings();
|
||||
}
|
||||
this.updateLockUi();
|
||||
this.setSourceMode("upload");
|
||||
|
||||
return this.root;
|
||||
@@ -205,11 +224,13 @@ export class UploadModal {
|
||||
browseThumbnail: $("#btn-thumbnail-file"),
|
||||
r2HelpLink: $("#link-r2-help"),
|
||||
copyBucket: $("#btn-copy-bucket"),
|
||||
storageUnlock: $("#btn-storage-unlock"),
|
||||
};
|
||||
|
||||
// Status text
|
||||
this.statusText = {
|
||||
storage: $("#storage-status"),
|
||||
storageLock: $("#storage-lock-status"),
|
||||
uploadMain: $("#upload-status-text"),
|
||||
uploadPercent: $("#upload-percent-text"),
|
||||
};
|
||||
@@ -268,6 +289,11 @@ export class UploadModal {
|
||||
});
|
||||
}
|
||||
|
||||
// Storage Unlock
|
||||
if (this.toggles.storageUnlock) {
|
||||
this.toggles.storageUnlock.addEventListener("click", () => this.handleUnlock());
|
||||
}
|
||||
|
||||
// Automation
|
||||
this.setupDescriptionMirror();
|
||||
this.setupMutuallyExclusiveCheckboxes(this.toggles.nsfw, this.toggles.kids);
|
||||
@@ -430,6 +456,7 @@ export class UploadModal {
|
||||
// --- R2 Integration ---
|
||||
|
||||
async loadR2Settings() {
|
||||
// Fallback legacy load if storage service not used/available
|
||||
if (!this.r2Service?.loadSettings) return;
|
||||
const settings = await this.r2Service.loadSettings();
|
||||
this.cloudflareSettings = settings || {};
|
||||
@@ -438,6 +465,88 @@ export class UploadModal {
|
||||
return settings;
|
||||
}
|
||||
|
||||
async loadFromStorage() {
|
||||
if (!this.storageService) return;
|
||||
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
|
||||
if (!pubkey) return;
|
||||
|
||||
const connections = await this.storageService.listConnections(pubkey);
|
||||
const r2Conn = connections.find(c => c.provider === PROVIDERS.R2 || c.provider === "cloudflare_r2");
|
||||
|
||||
if (r2Conn) {
|
||||
try {
|
||||
const details = await this.storageService.getConnection(pubkey, r2Conn.id);
|
||||
if (details) {
|
||||
this.cloudflareSettings = {
|
||||
accountId: details.accountId,
|
||||
accessKeyId: details.accessKeyId,
|
||||
secretAccessKey: details.secretAccessKey,
|
||||
baseDomain: details.meta?.baseDomain || "",
|
||||
};
|
||||
this.fillSettingsForm(this.cloudflareSettings);
|
||||
this.updateStorageStatus(true);
|
||||
this.activeConnectionId = r2Conn.id;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load connection", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleUnlock() {
|
||||
if (!this.storageService) return;
|
||||
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
|
||||
if (!pubkey) return;
|
||||
|
||||
// We need a signer. Try window.nostr or authService
|
||||
let signer = window.nostr;
|
||||
if (!signer && this.authService?.signer) {
|
||||
signer = this.authService.signer;
|
||||
}
|
||||
|
||||
if (!signer) {
|
||||
alert("No signer available to unlock storage.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.toggles.storageUnlock.textContent = "Unlocking...";
|
||||
this.toggles.storageUnlock.disabled = true;
|
||||
await this.storageService.unlock(pubkey, { signer });
|
||||
this.isStorageUnlocked = true;
|
||||
this.updateLockUi();
|
||||
await this.loadFromStorage();
|
||||
} catch (err) {
|
||||
console.error("Unlock failed", err);
|
||||
alert("Failed to unlock storage: " + err.message);
|
||||
} finally {
|
||||
this.toggles.storageUnlock.textContent = "Unlock";
|
||||
this.toggles.storageUnlock.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateLockUi() {
|
||||
const locked = !this.isStorageUnlocked;
|
||||
if (this.statusText.storageLock) {
|
||||
this.statusText.storageLock.textContent = locked ? "Locked 🔒" : "Unlocked 🔓";
|
||||
this.statusText.storageLock.className = locked ? "text-xs text-critical" : "text-xs text-success";
|
||||
}
|
||||
if (this.toggles.storageUnlock) {
|
||||
if (locked) {
|
||||
this.toggles.storageUnlock.classList.remove("hidden");
|
||||
} else {
|
||||
this.toggles.storageUnlock.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
if (this.statusText.storage) {
|
||||
if (locked) {
|
||||
this.statusText.storage.classList.add("hidden");
|
||||
} else {
|
||||
this.statusText.storage.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hasValidR2Settings() {
|
||||
const s = this.cloudflareSettings;
|
||||
return Boolean(s?.accountId && s?.accessKeyId && s?.secretAccessKey && s?.baseDomain);
|
||||
@@ -531,7 +640,30 @@ export class UploadModal {
|
||||
|
||||
// 2. Save on Success
|
||||
btn.textContent = "Saving...";
|
||||
await this.r2Service.saveSettings(settings);
|
||||
|
||||
if (this.storageService && this.isStorageUnlocked) {
|
||||
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
|
||||
if (pubkey) {
|
||||
// Save to secure storage
|
||||
const connectionId = this.activeConnectionId || `r2-${Date.now()}`;
|
||||
await this.storageService.saveConnection(pubkey, connectionId, {
|
||||
provider: PROVIDERS.R2,
|
||||
accountId: settings.accountId,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
}, {
|
||||
provider: PROVIDERS.R2,
|
||||
label: "Default R2",
|
||||
baseDomain: settings.baseDomain,
|
||||
defaultForUploads: true
|
||||
});
|
||||
this.activeConnectionId = connectionId;
|
||||
}
|
||||
} else {
|
||||
// Legacy fallback
|
||||
await this.r2Service.saveSettings(settings);
|
||||
}
|
||||
|
||||
this.cloudflareSettings = settings;
|
||||
this.updateStorageStatus(true);
|
||||
|
||||
@@ -742,6 +874,12 @@ export class UploadModal {
|
||||
}
|
||||
|
||||
// 2. Upload
|
||||
let explicitCredentials = null;
|
||||
if (this.storageService && this.isStorageUnlocked && this.hasValidR2Settings()) {
|
||||
// If using secure storage, we pass the credentials explicitly so they aren't saved to legacy DB
|
||||
explicitCredentials = this.collectSettingsForm();
|
||||
}
|
||||
|
||||
await this.r2Service.uploadVideo({
|
||||
npub,
|
||||
file,
|
||||
@@ -749,7 +887,8 @@ export class UploadModal {
|
||||
torrentFile,
|
||||
metadata,
|
||||
infoHash: hasValidInfoHash ? normalizedInfoHash : "",
|
||||
settingsInput: this.collectSettingsForm(),
|
||||
settingsInput: explicitCredentials ? null : this.collectSettingsForm(),
|
||||
explicitCredentials,
|
||||
publishVideoNote: this.publishVideoNote,
|
||||
onReset: () => this.resetForm(),
|
||||
// Pass forced keys/URLs to ensure what's in the torrent matches where we upload
|
||||
@@ -828,6 +967,17 @@ export class UploadModal {
|
||||
this.setGlobalModalState("upload", true);
|
||||
this.isVisible = true;
|
||||
this.populateBucketName(); // Ensure bucket name is fresh on open
|
||||
|
||||
// Refresh lock state on open
|
||||
if (this.storageService) {
|
||||
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
|
||||
this.isStorageUnlocked = pubkey ? this.storageService.isUnlocked(pubkey) : false;
|
||||
this.updateLockUi();
|
||||
if (this.isStorageUnlocked) {
|
||||
this.loadFromStorage();
|
||||
}
|
||||
}
|
||||
|
||||
this.modalAccessibility?.activate({ triggerElement });
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ export function initUploadModal({
|
||||
const {
|
||||
authService = null,
|
||||
r2Service = null,
|
||||
storageService = null,
|
||||
} = services;
|
||||
|
||||
const {
|
||||
@@ -37,6 +38,7 @@ export function initUploadModal({
|
||||
new UploadModal({
|
||||
authService,
|
||||
r2Service,
|
||||
storageService,
|
||||
publishVideoNote,
|
||||
removeTrackingScripts: removeTrackingScriptsFn,
|
||||
setGlobalModalState: setGlobalModalStateFn,
|
||||
|
||||
Reference in new issue
Block a user