247 lines
7.1 KiB
JavaScript
247 lines
7.1 KiB
JavaScript
import PocketBase, { isTokenExpired, LocalAuthStore } from "pocketbase";
|
|
|
|
const LOGIN_PATH = "#/login";
|
|
|
|
const currentPath = window.location.pathname.endsWith("/")
|
|
? window.location.pathname.substring(0, window.location.pathname.length - 1)
|
|
: window.location.pathname;
|
|
|
|
window.app = window.app || {};
|
|
window.app.pb = new PocketBase(
|
|
import.meta.env.PB_BACKEND_URL,
|
|
// concatenate the path in case hosted under subpath alongside other apps
|
|
new LocalAuthStore("__pb_superusers__" + currentPath),
|
|
);
|
|
|
|
// add UI specific header to all requests
|
|
app.pb.beforeSend = function(url, options) {
|
|
options.headers["x-request-source"] = "pbui";
|
|
return { url, options };
|
|
};
|
|
|
|
app.store.superuser = app.pb.authStore.record;
|
|
app.pb.authStore.onChange((_, record) => {
|
|
if (!record && window.location.hash != LOGIN_PATH) {
|
|
app.modals.close();
|
|
window.location.hash = LOGIN_PATH;
|
|
}
|
|
|
|
app.store.superuser = record;
|
|
});
|
|
|
|
// refresh the token in the background
|
|
if (app.pb.authStore.isValid) {
|
|
app.pb.collection(app.pb.authStore.record?.collectionName || "_superusers")
|
|
.authRefresh()
|
|
.catch((err) => {
|
|
console.warn("Failed to refresh the existing auth token:", err);
|
|
|
|
// clear the store only on invalidated/expired token
|
|
const status = err?.status << 0;
|
|
if (status == 401 || status == 403) {
|
|
app.utils.rememberPath();
|
|
app.pb.cancelAllRequests();
|
|
app.pb.authStore.clear();
|
|
}
|
|
});
|
|
}
|
|
|
|
// load initial store data
|
|
app.pb.authStore.onChange((_, record) => {
|
|
if (record?.id) {
|
|
app.store.loadCollections();
|
|
app.store.loadSettings();
|
|
app.store.loadOAuth2Providers();
|
|
}
|
|
});
|
|
|
|
// Modify the default RecordService to fire global events on record
|
|
// create, update, delete without relying on the realtime service
|
|
// -------------------------------------------------------------------
|
|
|
|
const originalRecordService = app.pb.collection;
|
|
app.pb.collection = function(idOrName) {
|
|
const service = originalRecordService.call(this, idOrName);
|
|
|
|
bindRecordServiceEvents(service);
|
|
|
|
return service;
|
|
};
|
|
|
|
function bindRecordServiceEvents(service) {
|
|
if (service.__customUIEvents) {
|
|
return;
|
|
}
|
|
|
|
service.__customUIEvents = true;
|
|
|
|
const originalCreate = service.create;
|
|
service.create = function() {
|
|
return originalCreate.apply(service, arguments).then((r) => {
|
|
setTimeout(() => {
|
|
document.dispatchEvent(new CustomEvent("record:create", { detail: r }));
|
|
document.dispatchEvent(new CustomEvent("record:save", { detail: r }));
|
|
}, 0);
|
|
return r;
|
|
});
|
|
};
|
|
|
|
const originalUpdate = service.update;
|
|
service.update = function() {
|
|
return originalUpdate.apply(service, arguments).then((r) => {
|
|
setTimeout(() => {
|
|
document.dispatchEvent(new CustomEvent("record:update", { detail: r }));
|
|
document.dispatchEvent(new CustomEvent("record:save", { detail: r }));
|
|
}, 0);
|
|
return r;
|
|
});
|
|
};
|
|
|
|
const originalDelete = service.delete;
|
|
service.delete = function() {
|
|
return originalDelete.apply(service, arguments).then((r) => {
|
|
const minimalRecord = {
|
|
id: arguments[0],
|
|
collectionId: service.collectionIdOrName,
|
|
collectionName: service.collectionIdOrName,
|
|
};
|
|
setTimeout(() => {
|
|
document.dispatchEvent(new CustomEvent("record:delete", { detail: minimalRecord }));
|
|
}, 0);
|
|
|
|
return r;
|
|
});
|
|
};
|
|
}
|
|
|
|
// File token helpers
|
|
// -------------------------------------------------------------------
|
|
|
|
const LAST_FILE_TOKEN_KEY = "pbLastFileToken";
|
|
let isFileTokenLoading = false;
|
|
let fileTokenPromises = [];
|
|
|
|
// clear stored token on logout
|
|
app.pb.authStore.onChange((_, record) => {
|
|
if (!record?.id) {
|
|
window.localStorage.removeItem(LAST_FILE_TOKEN_KEY);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Return a superuser file token.
|
|
* Optionally you can provide a collection identifier, to avoid unnecessery
|
|
* calls in case the collection doesn't have protected files.
|
|
*
|
|
* @param {String} optCollectionIdORName
|
|
* @return {Promise<string>}
|
|
*/
|
|
window.app.getFileToken = async function(optCollectionIdORName = "") {
|
|
// check if the collection needs a file token
|
|
const collection = optCollectionIdORName
|
|
&& app.store.collections?.find((c) => c.id == optCollectionIdORName || c.name == optCollectionIdORName);
|
|
if (collection) {
|
|
const hasProtectedFile = collection.fields?.find((f) => f.type == "file" && f.protected);
|
|
if (!hasProtectedFile) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
let token = window.localStorage.getItem(LAST_FILE_TOKEN_KEY);
|
|
|
|
if (!token || isTokenExpired(token, 60)) {
|
|
token = await fetchFileToken();
|
|
}
|
|
|
|
return token;
|
|
};
|
|
|
|
async function fetchFileToken() {
|
|
return new Promise(async (resolve, reject) => {
|
|
fileTokenPromises.push({ resolve, reject });
|
|
|
|
if (isFileTokenLoading) {
|
|
return;
|
|
}
|
|
|
|
isFileTokenLoading = true;
|
|
|
|
try {
|
|
const token = await app.pb.files.getToken();
|
|
|
|
window.localStorage.setItem(LAST_FILE_TOKEN_KEY, token);
|
|
|
|
fileTokenPromises.forEach((p) => p.resolve(token));
|
|
} catch (err) {
|
|
fileTokenPromises.forEach((p) => p.reject(err));
|
|
}
|
|
|
|
isFileTokenLoading = false;
|
|
fileTokenPromises = [];
|
|
});
|
|
}
|
|
|
|
// Generic API error handler
|
|
// -------------------------------------------------------------------
|
|
|
|
/**
|
|
* Helper to parse a response error and to show an optional toast message.
|
|
* In case of 401 it clears the auth store and redirects to the home page.
|
|
* In case of 403 it redirects to the home or login page.
|
|
*
|
|
* Example:
|
|
*
|
|
* ```js
|
|
* try {
|
|
* await app.pb.collection("example").getFullList()
|
|
* } catch (err) {
|
|
* if (!err?.isAbort) {
|
|
* app.checkApiError(err)
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @param {Error} err
|
|
* @param {boolean} showToast
|
|
*/
|
|
window.app.checkApiError = function(err, showToast = true) {
|
|
if (!err || !(err instanceof Error) || err.isAbort) {
|
|
console.warn("checkApiError - unexpected error type:", err);
|
|
return;
|
|
}
|
|
|
|
const statusCode = err?.status << 0;
|
|
const response = err?.response || {};
|
|
|
|
// add toast error notification
|
|
let msg = showToast && (response.message || err.message || "Something went wrong!");
|
|
if (msg) {
|
|
app.toasts.error(msg);
|
|
}
|
|
|
|
// unknown client-side error
|
|
if (statusCode == 0) {
|
|
console.log(err);
|
|
}
|
|
|
|
// populate form field errors
|
|
if (!app.utils.isEmpty(response.data)) {
|
|
app.store.errors = response.data;
|
|
}
|
|
|
|
// unauthorized
|
|
if (statusCode === 401 && window.location.hash != LOGIN_PATH) {
|
|
app.utils.rememberPath();
|
|
app.pb.cancelAllRequests();
|
|
return app.pb.authStore.clear();
|
|
}
|
|
|
|
// forbidden
|
|
if (statusCode === 403) {
|
|
app.pb.cancelAllRequests();
|
|
if (window.location.hash != LOGIN_PATH) {
|
|
window.location.hash = LOGIN_PATH;
|
|
}
|
|
}
|
|
};
|