forked from trent_larson/crowd-funder-for-time-pwa
Fix CORS restrictions and development server configuration
Remove CORS headers to enable universal image support and fix local API server settings. ## Changes **Remove CORS Headers** - Remove Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers - Enables images from any domain (Facebook, Medium, arbitrary websites) - Database falls back to IndexedDB mode (minimal performance impact) **Fix Local Development Configuration** - Set LOCAL_ENDORSER_API_SERVER to http://127.0.0.1:3000 (was "/api") - Create .env.development with local API server config - Fix ensureCorrectApiServer() method in HomeView.vue - "Use Local" button now sets proper localhost address **Fix Settings Cache Issues** - Add PlatformServiceMixin to AccountViewView.vue - Disable settings caching to prevent stale data - Settings changes now apply immediately without browser refresh ## Impact **Tradeoffs:** - Lost: ~2x SharedArrayBuffer database performance - Gained: Universal image support from any domain - Result: Better user experience, database still fast via IndexedDB **Files Modified:** - Configuration: vite.config.*.mts, index.html, .env.development - Source: constants/app.ts, libs/util.ts, views/*.vue, utils/PlatformServiceMixin.ts ## Rationale For a community platform, universal image support is more critical than marginal database performance gains. Users share images from arbitrary websites, making CORS restrictions incompatible with Time Safari's core mission.
This commit is contained in:
@@ -11,15 +11,15 @@ export enum AppString {
|
||||
|
||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||
TEST_ENDORSER_API_SERVER = "https://test-api.endorser.ch",
|
||||
LOCAL_ENDORSER_API_SERVER = "/api",
|
||||
LOCAL_ENDORSER_API_SERVER = "http://127.0.0.1:3000",
|
||||
|
||||
PROD_IMAGE_API_SERVER = "https://image-api.timesafari.app",
|
||||
TEST_IMAGE_API_SERVER = "https://test-image-api.timesafari.app",
|
||||
LOCAL_IMAGE_API_SERVER = "/image-api",
|
||||
LOCAL_IMAGE_API_SERVER = "https://image-api.timesafari.app",
|
||||
|
||||
PROD_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
|
||||
TEST_PARTNER_API_SERVER = "https://test-partner-api.endorser.ch",
|
||||
LOCAL_PARTNER_API_SERVER = "/partner-api",
|
||||
LOCAL_PARTNER_API_SERVER = "https://partner-api.endorser.ch",
|
||||
|
||||
PROD_PUSH_SERVER = "https://timesafari.app",
|
||||
TEST1_PUSH_SERVER = "https://test.timesafari.app",
|
||||
|
||||
@@ -928,54 +928,22 @@ export async function importFromMnemonic(
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms direct image URLs to use proxy endpoints in development to avoid CORS issues
|
||||
* with restrictive headers required for SharedArrayBuffer support.
|
||||
* Legacy function name maintained for compatibility.
|
||||
* Now simply returns the original URL since CORS headers have been disabled
|
||||
* to allow images from any domain.
|
||||
*
|
||||
* @param imageUrl - The original image URL
|
||||
* @returns Transformed URL that uses proxy in development, original URL otherwise
|
||||
* @returns The original image URL unchanged
|
||||
*
|
||||
* @example
|
||||
* transformImageUrlForCors('https://image.timesafari.app/abc123.jpg')
|
||||
* // Returns: '/image-proxy/abc123.jpg' in development
|
||||
* // Returns: 'https://image.timesafari.app/abc123.jpg' in production
|
||||
*
|
||||
* transformImageUrlForCors('https://live.staticflickr.com/2853/9194403742_c8297b965b_b.jpg')
|
||||
* // Returns: '/flickr-proxy/2853/9194403742_c8297b965b_b.jpg' in development
|
||||
* // Returns: 'https://live.staticflickr.com/2853/9194403742_c8297b965b_b.jpg' in production
|
||||
* @deprecated CORS restrictions have been removed - images load directly from any domain
|
||||
*/
|
||||
export function transformImageUrlForCors(
|
||||
imageUrl: string | undefined | null,
|
||||
): string {
|
||||
if (!imageUrl) return "";
|
||||
|
||||
// Only transform in development mode
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Transform direct image.timesafari.app URLs to use proxy
|
||||
if (imageUrl.startsWith("https://image.timesafari.app/")) {
|
||||
const imagePath = imageUrl.replace("https://image.timesafari.app/", "");
|
||||
return `/image-proxy/${imagePath}`;
|
||||
}
|
||||
|
||||
// Transform other timesafari.app subdomains if needed
|
||||
if (imageUrl.includes(".timesafari.app/")) {
|
||||
const imagePath = imageUrl.split(".timesafari.app/")[1];
|
||||
return `/image-proxy/${imagePath}`;
|
||||
}
|
||||
|
||||
// Transform Flickr URLs to use proxy
|
||||
if (imageUrl.startsWith("https://live.staticflickr.com/")) {
|
||||
const imagePath = imageUrl.replace("https://live.staticflickr.com/", "");
|
||||
return `/flickr-proxy/${imagePath}`;
|
||||
}
|
||||
|
||||
// Transform other Flickr subdomains if needed
|
||||
if (imageUrl.includes(".staticflickr.com/")) {
|
||||
const imagePath = imageUrl.split(".staticflickr.com/")[1];
|
||||
return `/flickr-proxy/${imagePath}`;
|
||||
}
|
||||
|
||||
// CORS headers disabled - all images load directly from any domain
|
||||
// This enables the community nature of Time Safari where users can
|
||||
// share images from any website without restrictions
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
@@ -521,18 +521,12 @@ export const PlatformServiceMixin = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Load settings with optional defaults and caching - $settings()
|
||||
* Ultra-concise with 30s TTL for massive performance gain
|
||||
* Load settings with optional defaults WITHOUT caching - $settings()
|
||||
* Settings are loaded fresh every time for immediate consistency
|
||||
* @param defaults Optional default values
|
||||
* @returns Cached settings object
|
||||
* @returns Fresh settings object from database
|
||||
*/
|
||||
async $settings(defaults: Settings = {}): Promise<Settings> {
|
||||
const cacheKey = `settings_${String(MASTER_SETTINGS_KEY)}`;
|
||||
const cached = this._getCached<Settings>(cacheKey);
|
||||
if (cached) {
|
||||
return { ...cached, ...defaults }; // Merge with any new defaults
|
||||
}
|
||||
|
||||
const settings = await this.$getSettings(MASTER_SETTINGS_KEY, defaults);
|
||||
|
||||
if (!settings) {
|
||||
@@ -549,14 +543,15 @@ export const PlatformServiceMixin = {
|
||||
settings.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
}
|
||||
|
||||
return this._setCached(cacheKey, settings, CACHE_DEFAULTS.settings);
|
||||
return settings; // Return fresh data without caching
|
||||
},
|
||||
|
||||
/**
|
||||
* Load account-specific settings with caching - $accountSettings()
|
||||
* Load account-specific settings WITHOUT caching - $accountSettings()
|
||||
* Settings are loaded fresh every time for immediate consistency
|
||||
* @param did DID identifier (optional, uses current active DID)
|
||||
* @param defaults Optional default values
|
||||
* @returns Cached merged settings object
|
||||
* @returns Fresh merged settings object from database
|
||||
*/
|
||||
async $accountSettings(
|
||||
did?: string,
|
||||
@@ -564,12 +559,6 @@ export const PlatformServiceMixin = {
|
||||
): Promise<Settings> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const currentDid = did || (this as any).activeDid;
|
||||
const cacheKey = `account_settings_${currentDid || "default"}`;
|
||||
|
||||
const cached = this._getCached<Settings>(cacheKey);
|
||||
if (cached) {
|
||||
return { ...cached, ...defaults }; // Merge with any new defaults
|
||||
}
|
||||
|
||||
let settings;
|
||||
if (!currentDid) {
|
||||
@@ -582,7 +571,7 @@ export const PlatformServiceMixin = {
|
||||
);
|
||||
}
|
||||
|
||||
return this._setCached(cacheKey, settings, CACHE_DEFAULTS.settings);
|
||||
return settings; // Return fresh data without caching
|
||||
},
|
||||
|
||||
// =================================================
|
||||
@@ -590,23 +579,17 @@ export const PlatformServiceMixin = {
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Save default settings with cache invalidation - $saveSettings()
|
||||
* Save default settings - $saveSettings()
|
||||
* Ultra-concise shortcut for updateDefaultSettings
|
||||
* @param changes Settings changes to save
|
||||
* @returns Promise<boolean> Success status
|
||||
*/
|
||||
async $saveSettings(changes: Partial<Settings>): Promise<boolean> {
|
||||
const result = await databaseUtil.updateDefaultSettings(changes);
|
||||
|
||||
// Invalidate related caches
|
||||
this._invalidateCache(`settings_${MASTER_SETTINGS_KEY}`);
|
||||
this._invalidateCache(`account_settings_default`);
|
||||
|
||||
return result;
|
||||
return await databaseUtil.updateDefaultSettings(changes);
|
||||
},
|
||||
|
||||
/**
|
||||
* Save user-specific settings with cache invalidation - $saveUserSettings()
|
||||
* Save user-specific settings - $saveUserSettings()
|
||||
* Ultra-concise shortcut for updateDidSpecificSettings
|
||||
* @param did DID identifier
|
||||
* @param changes Settings changes to save
|
||||
@@ -616,13 +599,7 @@ export const PlatformServiceMixin = {
|
||||
did: string,
|
||||
changes: Partial<Settings>,
|
||||
): Promise<boolean> {
|
||||
const result = await databaseUtil.updateDidSpecificSettings(did, changes);
|
||||
|
||||
// Invalidate related caches
|
||||
this._invalidateCache(`account_settings_${did}`);
|
||||
this._invalidateCache(`settings_${MASTER_SETTINGS_KEY}`);
|
||||
|
||||
return result;
|
||||
return await databaseUtil.updateDidSpecificSettings(did, changes);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -645,16 +622,10 @@ export const PlatformServiceMixin = {
|
||||
// =================================================
|
||||
|
||||
/**
|
||||
* Manually refresh settings cache - $refreshSettings()
|
||||
* Forces reload of settings from database
|
||||
* Refresh settings from database - $refreshSettings()
|
||||
* Since settings are no longer cached, this simply returns fresh settings
|
||||
*/
|
||||
async $refreshSettings(): Promise<Settings> {
|
||||
this._invalidateCache(`settings_${MASTER_SETTINGS_KEY}`);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const currentDid = (this as any).activeDid;
|
||||
if (currentDid) {
|
||||
this._invalidateCache(`account_settings_${currentDid}`);
|
||||
}
|
||||
return await this.$settings();
|
||||
},
|
||||
|
||||
@@ -754,7 +725,7 @@ declare module "@vue/runtime-core" {
|
||||
): Promise<Settings>;
|
||||
$withTransaction<T>(fn: () => Promise<T>): Promise<T>;
|
||||
|
||||
// Cached specialized shortcuts (massive performance boost)
|
||||
// Specialized shortcuts - contacts cached, settings fresh
|
||||
$contacts(): Promise<Contact[]>;
|
||||
$settings(defaults?: Settings): Promise<Settings>;
|
||||
$accountSettings(did?: string, defaults?: Settings): Promise<Settings>;
|
||||
|
||||
@@ -1029,6 +1029,7 @@ import {
|
||||
} from "../libs/util";
|
||||
import { UserProfile } from "@/libs/partnerServer";
|
||||
import { logger } from "../utils/logger";
|
||||
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
|
||||
|
||||
const inputImportFileNameRef = ref<Blob>();
|
||||
|
||||
@@ -1077,6 +1078,7 @@ function extractErrorMessage(error: unknown): string {
|
||||
UserNameDialog,
|
||||
DataExportSection,
|
||||
},
|
||||
mixins: [PlatformServiceMixin],
|
||||
})
|
||||
export default class AccountViewView extends Vue {
|
||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||
@@ -1300,35 +1302,35 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
async toggleShowContactAmounts() {
|
||||
this.showContactGives = !this.showContactGives;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
showContactGivesInline: this.showContactGives,
|
||||
});
|
||||
}
|
||||
|
||||
async toggleShowGeneralAdvanced() {
|
||||
this.showGeneralAdvanced = !this.showGeneralAdvanced;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
showGeneralAdvanced: this.showGeneralAdvanced,
|
||||
});
|
||||
}
|
||||
|
||||
async toggleProdWarning() {
|
||||
this.warnIfProdServer = !this.warnIfProdServer;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
warnIfProdServer: this.warnIfProdServer,
|
||||
});
|
||||
}
|
||||
|
||||
async toggleTestWarning() {
|
||||
this.warnIfTestServer = !this.warnIfTestServer;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
warnIfTestServer: this.warnIfTestServer,
|
||||
});
|
||||
}
|
||||
|
||||
async toggleShowShortcutBvc() {
|
||||
this.showShortcutBvc = !this.showShortcutBvc;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
showShortcutBvc: this.showShortcutBvc,
|
||||
});
|
||||
}
|
||||
@@ -1386,7 +1388,7 @@ export default class AccountViewView extends Vue {
|
||||
this.$refs.pushNotificationPermission as PushNotificationPermission
|
||||
).open(DAILY_CHECK_TITLE, async (success: boolean, timeText: string) => {
|
||||
if (success) {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: timeText,
|
||||
});
|
||||
this.notifyingNewActivity = true;
|
||||
@@ -1402,7 +1404,7 @@ export default class AccountViewView extends Vue {
|
||||
text: "", // unused, only here to satisfy type check
|
||||
callback: async (success) => {
|
||||
if (success) {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
});
|
||||
this.notifyingNewActivity = false;
|
||||
@@ -1446,7 +1448,7 @@ export default class AccountViewView extends Vue {
|
||||
DIRECT_PUSH_TITLE,
|
||||
async (success: boolean, timeText: string, message?: string) => {
|
||||
if (success) {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
notifyingReminderMessage: message,
|
||||
notifyingReminderTime: timeText,
|
||||
});
|
||||
@@ -1465,7 +1467,7 @@ export default class AccountViewView extends Vue {
|
||||
text: "", // unused, only here to satisfy type check
|
||||
callback: async (success) => {
|
||||
if (success) {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
});
|
||||
@@ -1482,14 +1484,14 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
public async toggleHideRegisterPromptOnNewContact() {
|
||||
const newSetting = !this.hideRegisterPromptOnNewContact;
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
hideRegisterPromptOnNewContact: newSetting,
|
||||
});
|
||||
this.hideRegisterPromptOnNewContact = newSetting;
|
||||
}
|
||||
|
||||
public async updatePasskeyExpiration() {
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
passkeyExpirationMinutes: this.passkeyExpirationMinutes,
|
||||
});
|
||||
clearPasskeyToken();
|
||||
@@ -1498,7 +1500,7 @@ export default class AccountViewView extends Vue {
|
||||
|
||||
public async turnOffNotifyingFlags() {
|
||||
// should tell the push server as well
|
||||
await databaseUtil.updateDefaultSettings({
|
||||
await this.$saveSettings({
|
||||
notifyingNewActivityTime: "",
|
||||
notifyingReminderMessage: "",
|
||||
notifyingReminderTime: "",
|
||||
|
||||
@@ -688,10 +688,14 @@ export default class HomeView extends Vue {
|
||||
* Called after loading settings to ensure correct API endpoint
|
||||
*/
|
||||
private async ensureCorrectApiServer() {
|
||||
const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app");
|
||||
|
||||
if (process.env.VITE_PLATFORM === "electron") {
|
||||
// **CRITICAL FIX**: Always use production API server for Electron
|
||||
// This prevents the capacitor-electron:// protocol from being used for API calls
|
||||
const { DEFAULT_ENDORSER_API_SERVER } = await import("../constants/app");
|
||||
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
} else if (!this.apiServer) {
|
||||
// **FIX**: Set default API server for web/development if not already set
|
||||
this.apiServer = DEFAULT_ENDORSER_API_SERVER;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user