allow changing of the image API server URL

This commit is contained in:
2026-10-10 07:41:07 -06:00
parent adffef6a50
commit 7af00aff6f
10 changed files with 131 additions and 45 deletions
+6 -6
View File
@@ -854,19 +854,19 @@ export default class ImageMethodDialog extends Vue {
formData.append("image", this.blob, this.fileName || "photo.jpg");
formData.append("claimType", this.claimType);
try {
const imageServer =
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
if (
window.location.hostname === "localhost" &&
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
!imageServer.includes("localhost")
) {
logger.log(
"Using shared image API server, so only users on that server can play with images.",
);
}
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
const response = await axios.post(imageServer + "/image", formData, {
headers,
});
this.uploading = false;
this.close();
+6 -6
View File
@@ -556,19 +556,19 @@ export default class PhotoDialog extends Vue {
formData.append("image", this.blob, this.fileName || "photo.jpg");
formData.append("claimType", this.claimType);
try {
const imageServer =
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
if (
window.location.hostname === "localhost" &&
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
!imageServer.includes("localhost")
) {
logger.log(
"Using shared image API server, so only users on that server can play with images.",
);
}
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
const response = await axios.post(imageServer + "/image", formData, {
headers,
});
this.uploading = false;
this.close();
+16
View File
@@ -1401,6 +1401,22 @@ export const NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR = {
message: "There was an error deleting the image.",
};
// Used in: AccountViewView.vue, GiftedDetailsView.vue, NewEditProjectView.vue
// (deleteImage - the image server returned 404). The server looks an image up
// by URL and uploader, so this also happens when the image was uploaded to a
// different image server than the one in settings.
export const NOTIFY_IMAGE_DELETE_NOT_FOUND = {
title: "Image Not Deleted",
message:
"The image server in your settings has no record of that image, so it was not removed from storage. It may already be gone, or it may have been uploaded through a different image server.",
};
// Used in: GiftedDetailsView.vue (deleteImage method - image deletion error)
export const NOTIFY_IMAGE_DELETE_ERROR = {
title: "Image Deletion Error",
message: "There was an error deleting the image from the image server.",
};
// Used in: NewEditProjectView.vue (validateLocation method - invalid location error)
export const NOTIFY_PROJECT_INVALID_LOCATION = {
title: "Invalid Location",
+7
View File
@@ -287,6 +287,13 @@ const MIGRATIONS = [
ALTER TABLE settings ADD COLUMN smsNotificationsEnabled BOOLEAN DEFAULT FALSE;
`,
},
{
name: "011_add_imageServer_to_settings",
sql: `
-- Image API server URL; blank means DEFAULT_IMAGE_API_SERVER
ALTER TABLE settings ADD COLUMN imageServer TEXT;
`,
},
];
/**
+1 -1
View File
@@ -38,7 +38,7 @@ export type Settings = {
hasBackedUpSeed?: boolean; // tracks whether the user has backed up their seed phrase
hideRegisterPromptOnNewContact?: boolean;
isRegistered?: boolean;
// imageServer?: string; // if we want to allow modification then we should make image functionality optional -- or at least customizable
imageServer?: string; // image API server URL
lastName?: string; // deprecated - put all names in firstName
lastAckedOfferToUserJwtId?: string; // the last JWT ID for offer-to-user that they've acknowledged seeing
+3
View File
@@ -37,6 +37,7 @@ export interface AccountSettings {
notifyingReminderMessage?: string;
notifyingReminderTime?: string;
starredPlanHandleIds?: string[];
imageServer?: string;
reminderFastRolloverForTesting?: boolean;
partnerApiServer?: string;
profileImageUrl?: string;
@@ -102,6 +103,8 @@ export interface SettingsState {
activeDid: string;
apiServer: string;
apiServerInput: string;
imageServer: string;
imageServerInput: string;
partnerApiServer: string;
partnerApiServerInput: string;
webPushServer: string;
+68 -12
View File
@@ -638,6 +638,7 @@
color="white"
aria-hidden="true"
></font-awesome>
<span class="text-white">Unsavedaroonosity</span>
</button>
<div class="mt-2" role="group" aria-label="Quick server selection">
<button
@@ -735,6 +736,7 @@
class="fa-fw"
color="white"
></font-awesome>
<span class="text-white">Unsaved</span>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -781,6 +783,7 @@
class="fa-fw"
color="white"
></font-awesome>
<span class="text-white">Unsaved</span>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -813,11 +816,49 @@
{{ DEFAULT_PARTNER_API_SERVER }}
</span>
<div class="mt-2">
<span class="text-slate-500 text-sm font-bold">Image Server URL</span>
&nbsp;
<span class="text-sm">{{ DEFAULT_IMAGE_API_SERVER }}</span>
<h2 class="text-slate-500 text-sm font-bold mb-2">Image Server URL</h2>
<div class="px-3 py-4">
<input
v-model="imageServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
/>
<button
v-if="imageServerInput != imageServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSaveImageServer()"
>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
<span class="text-white">Unsaved</span>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="imageServerInput = AppConstants.PROD_IMAGE_API_SERVER"
>
Use Prod
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="imageServerInput = AppConstants.TEST_IMAGE_API_SERVER"
>
Use Test
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@click="imageServerInput = AppConstants.LOCAL_IMAGE_API_SERVER"
>
Use Local
</button>
</div>
<span v-if="!imageServerInput" class="px-4 text-sm">
When that setting is blank, this app will use the default image server
URL:
{{ DEFAULT_IMAGE_API_SERVER }}
</span>
<div class="mt-2">
<span class="text-slate-500 text-sm font-bold"
@@ -1025,6 +1066,7 @@ import { logger } from "../utils/logger";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers, TIMEOUTS } from "@/utils/notify";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { NOTIFY_IMAGE_DELETE_NOT_FOUND } from "@/constants/notifications";
import { showSeedPhraseReminder } from "@/utils/seedPhraseReminder";
import { AccountSettings, isApiError } from "@/interfaces/accountView";
import {
@@ -1122,6 +1164,8 @@ export default class AccountViewView extends Vue {
searchBox: { name: string; bbox: BoundingBox } | null = null;
partnerApiServer: string = DEFAULT_PARTNER_API_SERVER;
partnerApiServerInput: string = DEFAULT_PARTNER_API_SERVER;
imageServer: string = DEFAULT_IMAGE_API_SERVER;
imageServerInput: string = DEFAULT_IMAGE_API_SERVER;
passkeyExpirationDescription: string = "";
passkeyExpirationMinutes: number = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
previousPasskeyExpirationMinutes: number = DEFAULT_PASSKEY_EXPIRATION_MINUTES;
@@ -1354,6 +1398,8 @@ export default class AccountViewView extends Vue {
this.partnerApiServer = settings.partnerApiServer || this.partnerApiServer;
this.partnerApiServerInput =
settings.partnerApiServer || this.partnerApiServerInput;
this.imageServer = settings.imageServer || this.imageServer;
this.imageServerInput = settings.imageServer || this.imageServerInput;
this.profileImageUrl = settings.profileImageUrl;
this.showContactGives = !!settings.showContactGivesInline;
this.passkeyExpirationMinutes =
@@ -2435,7 +2481,7 @@ export default class AccountViewView extends Vue {
const imageResp = await fetchImageRateLimits(
this.axios,
did,
this.DEFAULT_IMAGE_API_SERVER,
this.imageServer,
);
if (imageResp && imageResp.status === 200) {
@@ -2470,7 +2516,7 @@ export default class AccountViewView extends Vue {
error: error instanceof Error ? error.message : String(error),
did: did,
apiServer: this.apiServer,
imageServer: this.DEFAULT_IMAGE_API_SERVER,
imageServer: this.imageServer,
partnerApiServer: this.partnerApiServer,
errorCode: axiosError?.response?.data?.error?.code,
errorMessage: axiosError?.response?.data?.error?.message,
@@ -2553,6 +2599,17 @@ export default class AccountViewView extends Vue {
});
}
async onClickSaveImageServer(): Promise<void> {
await this.$saveSettings({
imageServer: this.imageServerInput,
});
this.imageServer = this.imageServerInput;
await this.$saveUserSettings(this.activeDid, {
imageServer: this.imageServer,
});
}
async onClickSavePushServer(): Promise<void> {
await this.$saveSettings({
webPushServer: this.webPushServerInput,
@@ -2583,15 +2640,14 @@ export default class AccountViewView extends Vue {
async deleteImage(): Promise<void> {
try {
// Extract the image ID from the full URL
const imageId = this.profileImageUrl?.split("/").pop();
if (!imageId) {
if (!this.profileImageUrl) {
this.notify.error("Invalid image URL");
return;
}
// the image server looks the image up by its full URL
const response = await this.axios.delete(
this.apiServer + "/api/image/" + imageId,
this.imageServer + "/image/" + encodeURIComponent(this.profileImageUrl),
{ headers: await getHeaders(this.activeDid) },
);
if (response.status === 204) {
@@ -2607,12 +2663,12 @@ export default class AccountViewView extends Vue {
}
} catch (error) {
if (isApiError(error) && error.response?.status === 404) {
// it already doesn't exist so we won't say anything to the user
// Clear the local reference since the image is gone
// still drop it from the profile, which is what the user asked for
this.profileImageUrl = "";
await this.$saveSettings({
profileImageUrl: "",
});
this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG);
} else {
this.notify.error(
ACCOUNT_VIEW_CONSTANTS.ERRORS.IMAGE_DELETE_ERROR,
+9 -4
View File
@@ -271,6 +271,8 @@ import {
NOTIFY_GIFTED_DETAILS_CREATE_GIVE_ERROR,
NOTIFY_GIFTED_DETAILS_GIFT_RECORDED,
NOTIFY_GIFT_ERROR_NO_DESCRIPTION,
NOTIFY_IMAGE_DELETE_ERROR,
NOTIFY_IMAGE_DELETE_NOT_FOUND,
} from "@/constants/notifications";
@Component({
@@ -563,30 +565,33 @@ export default class GiftedDetails extends Vue {
}
try {
const headers = await getHeaders(this.activeDid);
const imageServer =
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
if (
window.location.hostname === "localhost" &&
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
!imageServer.includes("localhost")
) {
logger.log(
"Using shared image API server, so only users on that server can play with images.",
);
}
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER + "/image/" + encodeURIComponent(imageUrl),
imageServer + "/image/" + encodeURIComponent(imageUrl),
{ headers },
);
if (response.status === 204) {
// don't bother with a notification
} else {
logger.error("Problem deleting image:", response);
this.notify.error(NOTIFY_IMAGE_DELETE_ERROR.message, TIMEOUTS.LONG);
}
} catch (error) {
logger.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any)?.response?.status === 404) {
logger.log("Image was already deleted:", error);
this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG);
} else {
logger.error("Failed to delete image from server:", error);
this.notify.error(NOTIFY_IMAGE_DELETE_ERROR.message, TIMEOUTS.LONG);
}
}
}
+9 -10
View File
@@ -335,13 +335,14 @@ import {
NotificationIface,
} from "../constants/app";
import { PlatformServiceMixin } from "../utils/PlatformServiceMixin";
import { createNotifyHelpers } from "../utils/notify";
import { createNotifyHelpers, TIMEOUTS } from "../utils/notify";
import {
NOTIFY_PROJECT_ACCOUNT_LOADING_ERROR,
NOTIFY_PROJECT_RETRIEVAL_ERROR,
NOTIFY_PROJECT_DELETE_IMAGE_CONFIRM,
NOTIFY_PROJECT_DELETE_IMAGE_ERROR,
NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR,
NOTIFY_IMAGE_DELETE_NOT_FOUND,
NOTIFY_PROJECT_INVALID_LOCATION,
NOTIFY_PROJECT_INVALID_START_DATE,
NOTIFY_PROJECT_INVALID_END_DATE,
@@ -617,9 +618,11 @@ export default class NewEditProjectView extends Vue {
}
try {
const headers = (await getHeaders(this.activeDid)) as AxiosRequestHeaders;
const imageServer =
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
if (
window.location.hostname === "localhost" &&
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
!imageServer.includes("localhost")
) {
logger.log(
"Using shared image API server, so only users on that server can play with images.",
@@ -627,9 +630,7 @@ export default class NewEditProjectView extends Vue {
}
const response = await this.axios.delete(
DEFAULT_IMAGE_API_SERVER +
"/image/" +
encodeURIComponent(this.imageUrl),
imageServer + "/image/" + encodeURIComponent(this.imageUrl),
{ headers },
);
if (response.status === 204) {
@@ -645,12 +646,10 @@ export default class NewEditProjectView extends Vue {
} catch (error) {
logger.error("Error deleting image:", error);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((error as any).response.status === 404) {
logger.log("The image was already deleted:", error);
if ((error as any)?.response?.status === 404) {
// still drop it from the project, which is what the user asked for
this.imageUrl = "";
// it already doesn't exist so we won't say anything to the user
this.notify.error(NOTIFY_IMAGE_DELETE_NOT_FOUND.message, TIMEOUTS.LONG);
} else {
this.notify.error(NOTIFY_PROJECT_DELETE_IMAGE_GENERAL_ERROR.message);
}
+6 -6
View File
@@ -338,19 +338,19 @@ export default class SharedPhotoView extends Vue {
);
formData.append("claimType", imageType);
const imageServer =
(await this.$accountSettings()).imageServer || DEFAULT_IMAGE_API_SERVER;
if (
window.location.hostname === "localhost" &&
!DEFAULT_IMAGE_API_SERVER.includes("localhost")
!imageServer.includes("localhost")
) {
logger.log(
"Using shared image API server, so only users on that server can play with images.",
);
}
const response = await axios.post(
DEFAULT_IMAGE_API_SERVER + "/image",
formData,
{ headers },
);
const response = await axios.post(imageServer + "/image", formData, {
headers,
});
if (response?.data?.url) {
this.imageBlob = undefined;
this.imageFileName = undefined;