mirror of
https://github.com/BetterSEQTA/BetterSEQTA-Plus.git
synced 2026-06-17 17:07:07 +00:00
add aden's requested changes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import { clearCloudPfpCache } from "@/seqta/utils/cloudPfpCache";
|
||||
import { clearLastUploadedSnapshot } from "@/seqta/utils/cloudSettingsSync";
|
||||
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
|
||||
|
||||
const REDIRECT_URI = "https://accounts.betterseqta.org/auth/bsplus/callback";
|
||||
@@ -205,6 +206,7 @@ class CloudAuthService {
|
||||
public async logout(): Promise<void> {
|
||||
const userId = this._state.user?.id;
|
||||
if (userId) await clearCloudPfpCache(userId);
|
||||
await clearLastUploadedSnapshot();
|
||||
await browser.storage.local.remove([
|
||||
STORAGE_KEYS.accessToken,
|
||||
STORAGE_KEYS.refreshToken,
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import {
|
||||
isKeyIncludedInCloudUploadPayload,
|
||||
migrateLegacyToPluginSettings,
|
||||
normalizeThemeIdForSync,
|
||||
resolveThemeIdForPostSyncDownload,
|
||||
} from "./cloudSettingsSync";
|
||||
|
||||
describe("migrateLegacyToPluginSettings", () => {
|
||||
it("maps animatedbk without overwriting existing plugin fields", () => {
|
||||
const result = migrateLegacyToPluginSettings({
|
||||
animatedbk: true,
|
||||
"plugin.animated-background.settings": { speed: 1.5 },
|
||||
});
|
||||
expect(result["plugin.animated-background.settings"]).toEqual({
|
||||
speed: 1.5,
|
||||
enabled: true,
|
||||
});
|
||||
expect(result).not.toHaveProperty("animatedbk");
|
||||
});
|
||||
|
||||
it("does not set enabled when legacy key is absent", () => {
|
||||
const result = migrateLegacyToPluginSettings({
|
||||
"plugin.animated-background.settings": { speed: 1.0 },
|
||||
});
|
||||
expect(result["plugin.animated-background.settings"]).toEqual({ speed: 1.0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("isKeyIncludedInCloudUploadPayload", () => {
|
||||
it("excludes auth and device cache prefixes", () => {
|
||||
expect(isKeyIncludedInCloudUploadPayload("bsplus_token")).toBe(false);
|
||||
expect(isKeyIncludedInCloudUploadPayload("plugin.global-search.storage.index")).toBe(
|
||||
false,
|
||||
);
|
||||
expect(isKeyIncludedInCloudUploadPayload("bsplus.analytics.v2.school.1")).toBe(false);
|
||||
});
|
||||
|
||||
it("includes core and plugin settings keys", () => {
|
||||
expect(isKeyIncludedInCloudUploadPayload("DarkMode")).toBe(true);
|
||||
expect(isKeyIncludedInCloudUploadPayload("plugin.profile-picture.settings")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveThemeIdForPostSyncDownload", () => {
|
||||
it("prefers top-level themeId over data.selectedTheme", () => {
|
||||
expect(
|
||||
resolveThemeIdForPostSyncDownload({
|
||||
themeId: " top-id ",
|
||||
data: { selectedTheme: "other-id" },
|
||||
}),
|
||||
).toBe("top-id");
|
||||
});
|
||||
|
||||
it("falls back to data.selectedTheme", () => {
|
||||
expect(
|
||||
resolveThemeIdForPostSyncDownload({
|
||||
data: { selectedTheme: "from-data" },
|
||||
}),
|
||||
).toBe("from-data");
|
||||
});
|
||||
|
||||
it("returns undefined when theme id is empty", () => {
|
||||
expect(
|
||||
resolveThemeIdForPostSyncDownload({
|
||||
data: { selectedTheme: " " },
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeThemeIdForSync", () => {
|
||||
it("normalizes envelope theme ids consistently", () => {
|
||||
expect(normalizeThemeIdForSync(" uuid ")).toBe("uuid");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
buildUploadPatch,
|
||||
CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
|
||||
diffSyncableStorage,
|
||||
normalizeStorageForSync,
|
||||
normalizeThemeIdForSync,
|
||||
} from "./cloudSettingsSync";
|
||||
|
||||
describe("normalizeStorageForSync", () => {
|
||||
it("strips omitted auth and client-only keys", () => {
|
||||
const normalized = normalizeStorageForSync({
|
||||
DarkMode: true,
|
||||
bsplus_token: "secret",
|
||||
bsplus_cloud_settings_known_remote_updated_at: "2026-01-01T00:00:00.000Z",
|
||||
"bsplus.analytics.v2.school.1": { cached: true },
|
||||
});
|
||||
expect(normalized).toEqual({ DarkMode: true });
|
||||
});
|
||||
|
||||
it("migrates legacy animatedbk to plugin settings", () => {
|
||||
const normalized = normalizeStorageForSync({ animatedbk: true });
|
||||
expect(normalized).toEqual({
|
||||
"plugin.animated-background.settings": { enabled: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffSyncableStorage", () => {
|
||||
it("returns empty object when maps are identical", () => {
|
||||
const map = { DarkMode: true, onoff: true };
|
||||
expect(diffSyncableStorage(map, { ...map })).toEqual({});
|
||||
});
|
||||
|
||||
it("includes only changed scalar keys", () => {
|
||||
const current = { DarkMode: false, onoff: true };
|
||||
const baseline = { DarkMode: true, onoff: true };
|
||||
expect(diffSyncableStorage(current, baseline)).toEqual({ DarkMode: false });
|
||||
});
|
||||
|
||||
it("includes whole plugin object when nested value changes", () => {
|
||||
const current = {
|
||||
"plugin.global-search.settings": { enabled: true, searchHotkey: "ctrl+k" },
|
||||
};
|
||||
const baseline = {
|
||||
"plugin.global-search.settings": { enabled: false, searchHotkey: "ctrl+k" },
|
||||
};
|
||||
expect(diffSyncableStorage(current, baseline)).toEqual({
|
||||
"plugin.global-search.settings": { enabled: true, searchHotkey: "ctrl+k" },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not emit keys removed locally (absent from current)", () => {
|
||||
const current = { DarkMode: true };
|
||||
const baseline = { DarkMode: true, onoff: true };
|
||||
expect(diffSyncableStorage(current, baseline)).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildUploadPatch", () => {
|
||||
it("returns null when current matches baseline", () => {
|
||||
const all = { DarkMode: true, selectedTheme: "" };
|
||||
const baseline = { DarkMode: true, selectedTheme: "" };
|
||||
expect(buildUploadPatch(all, baseline)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns sparse envelope when values differ", () => {
|
||||
const all = {
|
||||
DarkMode: false,
|
||||
selectedTheme: "theme-uuid",
|
||||
bsplus_token: "ignore-me",
|
||||
};
|
||||
const baseline = { DarkMode: true, selectedTheme: "theme-uuid" };
|
||||
const patch = buildUploadPatch(all, baseline);
|
||||
expect(patch).toEqual({
|
||||
schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
|
||||
themeId: "theme-uuid",
|
||||
data: { DarkMode: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeThemeIdForSync", () => {
|
||||
it("trims whitespace and returns empty for non-strings", () => {
|
||||
expect(normalizeThemeIdForSync(" abc ")).toBe("abc");
|
||||
expect(normalizeThemeIdForSync(undefined)).toBe("");
|
||||
expect(normalizeThemeIdForSync(" ")).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import browser from "webextension-polyfill";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
/** Matches the contract in docs/CLOUD_SETTINGS_SYNC_SERVER.md */
|
||||
export const CLOUD_SETTINGS_SYNC_SCHEMA_VERSION = 1;
|
||||
@@ -17,6 +18,13 @@ export const BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY =
|
||||
export const BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY =
|
||||
"bsplus_pending_theme_ensure_after_cloud";
|
||||
|
||||
/**
|
||||
* Client-only: normalized syncable storage last acked by a successful PUT.
|
||||
* Never uploaded; used to compute sparse upload patches.
|
||||
*/
|
||||
export const BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY =
|
||||
"bsplus_cloud_settings_last_uploaded_snapshot";
|
||||
|
||||
/**
|
||||
* Never uploaded to the cloud backup (OAuth and legacy keys).
|
||||
* IndexedDB (e.g. Global Search’s `betterseqta-index` database) is not part of
|
||||
@@ -48,6 +56,7 @@ export const SENSITIVE_DEVICE_STORAGE_KEY_PREFIXES = [
|
||||
|
||||
const CLIENT_ONLY_CLOUD_KEYS_EXACT = [
|
||||
BSPLUS_CLOUD_KNOWN_REMOTE_UPDATED_AT_KEY,
|
||||
BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY,
|
||||
"bsplus_lastCloudPoll",
|
||||
BSPLUS_PENDING_THEME_ENSURE_AFTER_CLOUD_KEY,
|
||||
] as const;
|
||||
@@ -118,30 +127,79 @@ export function normalizeThemeIdForSync(raw: unknown): string {
|
||||
return raw.trim();
|
||||
}
|
||||
|
||||
export function buildUploadPayload(all: Record<string, unknown>): {
|
||||
schemaVersion: number;
|
||||
themeId: string;
|
||||
data: Record<string, unknown>;
|
||||
} {
|
||||
/** Filter omit lists and migrate legacy keys → full syncable map for diff/export. */
|
||||
export function normalizeStorageForSync(all: Record<string, unknown>): Record<string, unknown> {
|
||||
const filtered: Record<string, unknown> = {};
|
||||
for (const [k, v] of Object.entries(all)) {
|
||||
if (shouldOmitKeyFromCloudPayload(k)) continue;
|
||||
filtered[k] = v;
|
||||
}
|
||||
const data = migrateLegacyToPluginSettings(filtered);
|
||||
const themeId = normalizeThemeIdForSync(all.selectedTheme);
|
||||
return migrateLegacyToPluginSettings(filtered);
|
||||
}
|
||||
|
||||
/** Keys in `current` whose values differ from `baseline` (sparse PUT body). */
|
||||
export function diffSyncableStorage(
|
||||
current: Record<string, unknown>,
|
||||
baseline: Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
const patch: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(current)) {
|
||||
if (!isEqual(value, baseline[key])) {
|
||||
patch[key] = value;
|
||||
}
|
||||
}
|
||||
return patch;
|
||||
}
|
||||
|
||||
export type CloudSettingsUploadEnvelope = {
|
||||
schemaVersion: number;
|
||||
themeId: string;
|
||||
data: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/** Sparse upload envelope, or null when nothing changed vs baseline. */
|
||||
export function buildUploadPatch(
|
||||
all: Record<string, unknown>,
|
||||
baseline: Record<string, unknown>,
|
||||
): CloudSettingsUploadEnvelope | null {
|
||||
const normalized = normalizeStorageForSync(all);
|
||||
const data = diffSyncableStorage(normalized, baseline);
|
||||
if (Object.keys(data).length === 0) return null;
|
||||
return {
|
||||
schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
|
||||
themeId,
|
||||
themeId: normalizeThemeIdForSync(all.selectedTheme),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getSnapshotForUpload(): Promise<{
|
||||
schemaVersion: number;
|
||||
themeId: string;
|
||||
data: Record<string, unknown>;
|
||||
}> {
|
||||
/** Full normalized snapshot (dev export / debugging). */
|
||||
export function buildUploadPayload(all: Record<string, unknown>): CloudSettingsUploadEnvelope {
|
||||
const data = normalizeStorageForSync(all);
|
||||
return {
|
||||
schemaVersion: CLOUD_SETTINGS_SYNC_SCHEMA_VERSION,
|
||||
themeId: normalizeThemeIdForSync(all.selectedTheme),
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getLastUploadedSnapshot(): Promise<Record<string, unknown> | null> {
|
||||
const stored = await browser.storage.local.get(BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY);
|
||||
const snapshot = stored[BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY];
|
||||
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return null;
|
||||
return snapshot as Record<string, unknown>;
|
||||
}
|
||||
|
||||
export async function saveLastUploadedSnapshot(
|
||||
snapshot: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
await browser.storage.local.set({ [BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY]: snapshot });
|
||||
}
|
||||
|
||||
export async function clearLastUploadedSnapshot(): Promise<void> {
|
||||
await browser.storage.local.remove(BSPLUS_CLOUD_LAST_UPLOADED_SNAPSHOT_KEY);
|
||||
}
|
||||
|
||||
export async function getSnapshotForUpload(): Promise<CloudSettingsUploadEnvelope> {
|
||||
const all = await browser.storage.local.get();
|
||||
return buildUploadPayload(all as Record<string, unknown>);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ function mergePluginSettingsDefaults(
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes any missing cloud-syncable keys so uploads contain a full schema.
|
||||
* Writes any missing cloud-syncable keys locally for consistent diffing.
|
||||
* Never overwrites existing values. Missing plugin settings respect legacy keys.
|
||||
*/
|
||||
export async function ensureSyncableStorageDefaults(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user