Files
pleno-vue/src/components/session/token/SessionUser.vue
T

1434 lines
53 KiB
Vue

<script>
import { authenticatedRequest } from "@/components/session/authenticatedRequest.vue";
import { computed, ref } from "vue";
import { parseError } from "@/components/request/HandleGlobalError.vue";
import { SuperUserObject } from "@/components/session/token/superUserObject.vue";
import { AdminUserObject } from "@/components/session/token/AdminUserObject.vue";
import { EditFieldForm } from "@/components/session/token/SessionUser/EditFieldForm.vue";
import { reCAPTCHA } from "@/components/session/token/SessionUser/reCAPTCHA/reCAPTCHA.vue";
import { Categories } from "@/components/session/token/SessionUser/Objects/Categories.vue";
import { Products } from "@/components/session/token/SessionUser/Objects/Products.vue";
import { ProductOptions } from "@/components/session/token/SessionUser/Objects/ProductOptions.vue";
import { Departments } from "@/components/session/token/SessionUser/Objects/Departments.vue";
import { DepartmentCategories } from "@/components/session/token/SessionUser/Objects/DepartmentCategories.vue";
import { EconomicDepartments } from "@/components/session/token/SessionUser/Objects/economicDepartments.vue";
import { EconomicProducts } from "@/components/session/token/SessionUser/Objects/economicProducts.vue";
import { Roles } from "@/components/session/token/SessionUser/Objects/Roles.vue";
import { Permissions } from "@/components/session/token/SessionUser/Objects/Permissions.vue";
import { CollectedOrderInvoices } from "@/components/session/token/SessionUser/Objects/CollectedOrderInvoices.vue";
import { DepartmentDailyReports } from "@/components/session/token/SessionUser/Objects/DepartmentDailyReports.vue";
import { DepartmentDailyReportComplaints } from "@/components/session/token/SessionUser/Objects/DepartmentDailyReportComplaints.vue";
import { Goals } from "@/components/session/token/SessionUser/Objects/Goals.vue";
import { Notifications } from "@/components/session/token/SessionUser/Objects/Notifications.vue";
import { Bookings } from "@/components/session/token/SessionUser/Objects/Bookings.vue";
import { Orders } from "@/components/session/token/SessionUser/Objects/Orders.vue";
import { Forms } from "@/components/session/token/SessionUser/Objects/Forms.vue";
import { Branding } from "@/components/session/token/SessionUser/Objects/Brandings.vue";
import { Vehicles } from "@/components/session/token/SessionUser/Objects/Vehicles.vue";
import { DepartmentNotificationSms } from "@/components/session/token/SessionUser/Objects/DepartmentNotificationSms.vue";
import { DepartmentTimeBookingsOpeningHours } from "@/components/session/token/SessionUser/Objects/DepartmentTimeBookingsOpeningHours.vue";
import { DepartmentTimeBookingsTypes } from "@/components/session/token/SessionUser/Objects/DepartmentTimeBookingsTypes.vue";
import { DepartmentTimeBookingsEntries } from "@/components/session/token/SessionUser/Objects/DepartmentTimeBookingsEntries.vue";
import { DepartmentVariables } from "@/components/session/token/SessionUser/Objects/DepartmentVariables.vue";
import { XLVaskVehicleTypes } from "@/components/session/token/SessionUser/Objects/XLVaskVehicleTypes.vue";
import { OpenAI } from "@/components/session/token/superUser/modules/openAI/OpenAI.vue";
import { DepartmentLanes } from "@/components/session/token/SessionUser/Objects/DepartmentLanes.vue";
import { DepartmentGates } from "@/components/session/token/SessionUser/Objects/DepartmentGates.vue";
import { DepartmentRelays } from "@/components/session/token/SessionUser/Objects/DepartmentRelays.vue";
import { OrderBookings } from "@/components/session/token/SessionUser/Objects/OrderBookings.vue";
import { ModuleActionLogs } from "@/components/session/token/SessionUser/Objects/ModuleActionLogs.vue";
import { SelfServeQuestions } from "@/components/session/token/SessionUser/Objects/SelfServeQuestions.vue";
import { SelfServeTasks } from "@/components/session/token/SessionUser/Objects/SelfServeTasks.vue";
import { SelfServeConditions } from "@/components/session/token/SessionUser/Objects/SelfServeConditions.vue";
import { SelfServeVehicleConditions } from "@/components/session/token/SessionUser/Objects/SelfServeVehicleConditions.vue";
import { SelfServeConditionRules } from "@/components/session/token/SessionUser/Objects/SelfServeConditionRules.vue";
import { SelfServeMachineTypes } from "@/components/session/token/SessionUser/Objects/SelfServeMachineTypes.vue";
import { ObjectsGlobal } from "@/components/session/token/SessionUser/Objects/ObjectsGlobal.vue";
import Swal from "sweetalert2";
import { SubuserGrants } from "@/components/session/token/SessionUser/Objects/SubuserGrants.vue";
import { Subusers } from "@/components/session/token/SessionUser/Objects/Subusers.vue";
import { configureReleaseRuntime } from "@/services/releaseTimeline.js";
import { fetchReleaseRuntime } from "@/services/releaseBootstrap.js";
import { pingApiServer } from "@/services/apiHealth.js";
import { normalizeSessionPayload } from "@/services/sessionPayload.js";
import {
isReleaseChannelApiAvailabilityError,
markReleaseChannelApiUnavailable,
reconcileSelectedReleaseChannel,
releaseChannelRuntimeRequestParams,
setReleaseChannelSwitchNoticePrincipal,
} from "@/services/releaseChannelAvailability.js";
import { clearCachedXlvaskUsageAmount } from "@/components/displays/department/pos/sync/xlvaskUsageAmountCache.js";
import { clearEdgeGatewayWorkspaceCache } from "@/services/edgeGateways.js";
import {
clearPeriodCache,
clearSelfWashCountsCache,
} from "@/views/dashboards/superUserDashboard/InvoicingBillingPeriod/imports/InvoicingBillingPeriodImportPaging.js";
const normalizePositiveInteger = (value) => {
const parsedValue = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(parsedValue) && parsedValue > 0 ? parsedValue : null;
};
const SUBUSER_MANAGEMENT_PERMISSIONS = {
list: {
customer: "list_own_subusers",
subuser: "SUBUSERS_LIST",
},
add: {
customer: "add_own_subusers",
subuser: "SUBUSERS_ADD",
},
edit: {
customer: "edit_own_subusers",
subuser: "SUBUSERS_EDIT",
},
delete: {
customer: "delete_own_subusers",
subuser: "SUBUSERS_DELETE",
},
};
const getStoredSessionSnapshot = () => {
if (typeof window === "undefined") {
return null;
}
try {
const token = window.localStorage.getItem("token");
if (token === null) {
return null;
}
return {
token,
isSubuser: window.localStorage.getItem("is_subuser") === "true",
selectedCustomerNumber: normalizePositiveInteger(window.localStorage.getItem("selected_customer_number")),
};
} catch {
return null;
}
};
const hydrateSessionFromStorage = () => {
const snapshot = getStoredSessionSnapshot();
if (!snapshot) {
return false;
}
SessionUser.token.value = snapshot.token;
SessionUser.authenticated.value = true;
SessionUser.isSubuser.value = snapshot.isSubuser;
SessionUser.subuser.selectedGrantCustomerNumber.value = snapshot.selectedCustomerNumber;
return true;
};
const clearPrivilegedSessionCaches = () => {
clearPeriodCache();
clearSelfWashCountsCache();
clearCachedXlvaskUsageAmount();
clearEdgeGatewayWorkspaceCache();
};
const clearStoredSession = () => {
clearPrivilegedSessionCaches();
if (typeof window === "undefined") {
return;
}
try {
window.localStorage.removeItem("token");
window.localStorage.removeItem("is_subuser");
window.localStorage.removeItem("selected_customer_number");
} catch {
// Storage can be unavailable in test or private browsing contexts.
}
};
const applyReleaseRuntimeConfig = (runtime) => {
const normalizedRuntime = runtime || {};
configureReleaseRuntime(normalizedRuntime);
reconcileSelectedReleaseChannel(normalizedRuntime);
const runtimeUrls =
normalizedRuntime.urls && typeof normalizedRuntime.urls === "object" ? normalizedRuntime.urls : {};
SessionUser.runtimeConfig.release.traceId.value = normalizedRuntime.trace_id || null;
SessionUser.runtimeConfig.release.channel.value = normalizedRuntime.channel || null;
SessionUser.runtimeConfig.release.availableChannels.value =
normalizedRuntime.available_channels || normalizedRuntime.availableChannels || [];
SessionUser.runtimeConfig.release.versions.value = normalizedRuntime.versions || {
frontend: null,
api: null,
bundle_id: null,
};
SessionUser.runtimeConfig.release.frontendBaseUrl.value =
normalizedRuntime.frontend_base_url || runtimeUrls.frontend_base_url || null;
SessionUser.runtimeConfig.release.apiBaseUrl.value =
normalizedRuntime.api_base_url || runtimeUrls.api_base_url || null;
SessionUser.runtimeConfig.release.availability.value = normalizedRuntime.availability || {
configured: true,
missing: [],
status: "ready",
explicit: false,
};
SessionUser.runtimeConfig.release.capturePolicy.value = normalizedRuntime.capture_policy || {
enabled: false,
capture_level: "metadata",
all_failure_metadata: true,
retention_days: 14,
};
};
export const refreshReleaseRuntime = async ({ throwOnError = false } = {}) => {
try {
const runtime = await fetchReleaseRuntime();
applyReleaseRuntimeConfig(runtime || {});
return runtime || {};
} catch (error) {
console.warn("Could not refresh release runtime", error);
if (throwOnError) {
throw error;
}
return null;
}
};
const resetSessionState = () => {
SessionUser.user.id.value = null;
SessionUser.user.customer_number.value = null;
SessionUser.user.display_name.value = null;
SessionUser.user.group_id.value = null;
SessionUser.user.email.value = null;
SessionUser.user.phone.number.value = null;
SessionUser.user.phone.country_code.value = null;
SessionUser.user.notifications.wash_certificate_email.value = null;
SessionUser.user.notifications.email_notifications_enabled.value = null;
SessionUser.user.notifications.sms_notifications_enabled.value = null;
SessionUser.user.notifications.superuser_new_customer_email_notifications_enabled.value = null;
SessionUser.user.created_at.value = null;
SessionUser.user.updated_at.value = null;
SessionUser.user.cached_at.value = null;
SessionUser.permissions.value = [];
SessionUser.token.value = null;
SessionUser.authenticated.value = false;
SessionUser.isSubuser.value = false;
SessionUser.initiated.value = false;
SessionUser.subuser.id.value = null;
SessionUser.subuser.username.value = null;
SessionUser.subuser.name.value = null;
SessionUser.subuser.email.value = null;
SessionUser.subuser.phone.country_code.value = null;
SessionUser.subuser.phone.number.value = null;
SessionUser.subuser.grants.value = [];
SessionUser.subuser.created_at.value = null;
SessionUser.subuser.updated_at.value = null;
SessionUser.subuser.suspended_at.value = null;
SessionUser.subuser.cached_at.value = null;
SessionUser.subuser.selectedGrantCustomerNumber.value = null;
if (typeof window !== "undefined") {
try {
window.localStorage.removeItem("selected_customer_number");
} catch {
// Storage is optional for the in-memory reset.
}
}
SessionUser.economicData.customerNumber.value = null;
SessionUser.economicData.name.value = null;
SessionUser.economicData.address.value = null;
SessionUser.economicData.zip.value = null;
SessionUser.economicData.city.value = null;
SessionUser.economicData.mobilePhone.value = null;
SessionUser.economicData.email.value = null;
SessionUser.economicData.cvr.value = null;
SessionUser.economicData.currency.value = null;
SessionUser.economicData.country.value = null;
SessionUser.economicData.cached_at.value = null;
SessionUser.runtimeConfig.economic.transactionDraftCustomerNumber.value = null;
SessionUser.runtimeConfig.economic.defaultDistributionDepartmentId.value = null;
SessionUser.runtimeConfig.release.traceId.value = null;
SessionUser.runtimeConfig.release.channel.value = null;
SessionUser.runtimeConfig.release.availableChannels.value = [];
SessionUser.runtimeConfig.release.versions.value = { frontend: null, api: null, bundle_id: null };
SessionUser.runtimeConfig.release.frontendBaseUrl.value = null;
SessionUser.runtimeConfig.release.apiBaseUrl.value = null;
SessionUser.runtimeConfig.release.availability.value = {
configured: true,
missing: [],
status: "ready",
explicit: false,
};
SessionUser.runtimeConfig.release.capturePolicy.value = {
enabled: false,
capture_level: "metadata",
all_failure_metadata: true,
retention_days: 14,
};
setReleaseChannelSwitchNoticePrincipal("");
configureReleaseRuntime({});
};
const reconcileSelectedSubuserGrant = (grants) => {
const grantList = Array.isArray(grants) ? grants : [];
const currentSelection = normalizePositiveInteger(SessionUser.subuser.selectedGrantCustomerNumber.value);
const validCustomerNumbers = grantList
.map((grant) => normalizePositiveInteger(grant?.billing_customer_number))
.filter((customerNumber) => customerNumber !== null);
if (currentSelection && validCustomerNumbers.includes(currentSelection)) {
SessionUser.subuser.selectGrant(currentSelection);
return;
}
if (validCustomerNumbers.length === 1) {
SessionUser.subuser.selectGrant(validCustomerNumbers[0]);
return;
}
SessionUser.subuser.selectGrant(null);
};
let sessionBootstrapPromise = null;
const extractErrorMessage = (error) => {
if (typeof error === "string") {
return error;
}
if (error?.response?.data?.data?.message) {
return error.response.data.data.message;
}
if (error?.response?.data?.message) {
return error.response.data.message;
}
if (typeof error?.response?.data === "string") {
return error.response.data;
}
if (error?.message) {
return error.message;
}
return "";
};
export const isInvalidSessionError = (error) => {
const status = Number.parseInt(String(error?.response?.status ?? ""), 10);
const message = extractErrorMessage(error).toLowerCase();
if (status === 401 || status === 419) {
return true;
}
if (![400, 403].includes(status)) {
return false;
}
return (
message.includes("invalid session") ||
message.includes("invalid token") ||
message.includes("unauthenticated") ||
message.includes("unauthorized")
);
};
const redirectToConnectivityIssue = () => {
if (typeof window === "undefined" || window.location.pathname === "/connectivity-issue") {
return false;
}
window.location.href = "/connectivity-issue";
return true;
};
export const handleSessionBootstrapFailure = async (error, { title, text } = {}) => {
if (isInvalidSessionError(error)) {
const wasSubuserSession = SessionUser.isSubuser.value || getStoredSessionSnapshot()?.isSubuser === true;
const pingResult = await pingApiServer();
if (!pingResult.ok) {
console.warn("Preserving stored session because API ping failed during invalid-session handling.", pingResult);
const redirected = redirectToConnectivityIssue();
return {
cleared: false,
apiReachable: false,
ping: pingResult,
redirectedTo: redirected ? "/connectivity-issue" : null,
};
}
forceClearSession();
if (wasSubuserSession) {
SessionUser.functions.redirectTo.pages.subuserLogin();
return { cleared: true, apiReachable: true, ping: pingResult, redirectedTo: "/login/driver" };
}
SessionUser.functions.redirectTo.pages.login();
return { cleared: true, apiReachable: true, ping: pingResult, redirectedTo: "/login" };
}
parseError(error, "auth");
console.error(error);
await Swal.fire({
title,
text,
icon: "error",
confirmButtonText: "Ok",
});
return { cleared: false, apiReachable: null, ping: null, redirectedTo: null };
};
export const refreshSessionData = async () => {
if (!hydrateSessionFromStorage()) {
resetSessionState();
return null;
}
if (SessionUser.isSubuser.value) {
return await getSubuserSessionData();
}
return await getSessionData();
};
/**
* Initiate the user session on app start
* @returns {Promise<void>}
*/
export const initiateOnAppStart = async ({ force = false } = {}) => {
if (sessionBootstrapPromise && !force) {
return await sessionBootstrapPromise;
}
const bootstrapPromise = (async () => {
if (!hydrateSessionFromStorage()) {
return null;
}
if (SessionUser.isSubuser.value) {
return await getSubuserSessionData();
}
return await getSessionData();
})();
sessionBootstrapPromise = bootstrapPromise;
try {
return await bootstrapPromise;
} finally {
if (sessionBootstrapPromise === bootstrapPromise) {
sessionBootstrapPromise = null;
}
}
};
/**
* Authenticate the user
* @param {string} customer_number
* @param {string} password
* @returns {Promise<void>}
*/
export const authenticateUser = async (customer_number, password) => {
return await authenticatedRequest("/auth/login", "POST", {
customer_number,
password,
})
.then((response) => {
clearPrivilegedSessionCaches();
SessionUser.token.value = response.data.token;
SessionUser.authenticated.value = true;
localStorage.setItem("token", response.data.token);
localStorage.removeItem("is_subuser");
// Reload the window to make sure the user's session is initiated
window.location.reload();
})
.catch((error) => {
parseError(error, "auth");
console.error(error);
});
};
/**
* Authenticate a subuser with password
* Supports three authentication methods:
* - phone_country_code + phone + password
* - subuser_id + password
* - username + password
* @param {Object} credentials - The authentication credentials
* @param {number} [credentials.phone_country_code] - Phone country code (1-3 digits)
* @param {number} [credentials.phone] - Phone number (4-15 digits)
* @param {number} [credentials.subuser_id] - Subuser ID
* @param {string} [credentials.username] - Username
* @param {string} credentials.password - Password (8-255 characters, uppercase, lowercase, and number)
* @returns {Promise<void>}
*/
export const authenticateSubuser = async (credentials) => {
const { phone_country_code, phone, subuser_id, username, password } = credentials;
// Build the request body based on provided credentials
let requestBody = { password };
if (phone_country_code !== undefined && phone !== undefined) {
requestBody.phone_country_code = phone_country_code;
requestBody.phone = phone;
} else if (subuser_id !== undefined) {
requestBody.subuser_id = subuser_id;
} else if (username !== undefined) {
requestBody.username = username;
} else {
throw new Error("Invalid credentials: must provide phone_country_code+phone, subuser_id, or username");
}
return await authenticatedRequest("/subusers/auth/password", "POST", requestBody)
.then((response) => {
clearPrivilegedSessionCaches();
SessionUser.token.value = response.data.data.session;
SessionUser.authenticated.value = true;
SessionUser.isSubuser.value = true;
console.warn("Subuser session initiated", response);
// Make sure the subuser's session is valid before setting the token and reloading
if (!response.data.data.session) {
throw new Error("No session token received for subuser authentication");
}
localStorage.setItem("token", response.data.data.session);
localStorage.setItem("is_subuser", "true");
// Reload the window to make sure the subuser's session is initiated
window.location.reload();
})
.catch((error) => {
parseError(error, "auth");
console.error(error);
throw error;
});
};
/**
* Get the subuser's session data from /subusers/me
* @returns {Promise<void>}
*/
export const getSubuserSessionData = async () => {
return await authenticatedRequest("/subusers/me", "GET")
.then(async (response) => {
const data = response.data.data || response.data;
SessionUser.subuser.id.value = data.id;
SessionUser.subuser.username.value = data.username;
SessionUser.subuser.name.value = data.name;
SessionUser.subuser.email.value = data.email;
SessionUser.subuser.phone.country_code.value = data.phone_country_code;
SessionUser.subuser.phone.number.value = data.phone;
SessionUser.subuser.grants.value = data.grants || [];
reconcileSelectedSubuserGrant(SessionUser.subuser.grants.value);
SessionUser.subuser.created_at.value = data.created_at;
SessionUser.subuser.updated_at.value = data.updated_at;
SessionUser.subuser.suspended_at.value = data.suspended_at;
SessionUser.subuser.cached_at.value = new Date();
setReleaseChannelSwitchNoticePrincipal(`subuser:${data.id || data.username || data.email || "unknown"}`);
// Build permissions from grants
const allPermissions = [];
if (data.grants && Array.isArray(data.grants)) {
data.grants.forEach((grant) => {
if (grant.permissions && Array.isArray(grant.permissions)) {
allPermissions.push(...grant.permissions);
}
});
}
SessionUser.permissions.value = [...new Set(allPermissions)];
await refreshReleaseRuntime();
SessionUser.initiated.value = true;
})
.catch(async (error) => {
if (isReleaseChannelApiAvailabilityError(error)) {
console.warn("Selected release channel API is unavailable during session bootstrap.", error);
markReleaseChannelApiUnavailable();
return;
}
return await handleSessionBootstrapFailure(error, {
title: "Fejl ved hentning af subbrugerdata",
text: "Der opstod en fejl ved hentning af dine brugerdata. Prøv at logge ind igen.",
});
});
};
/**
* Get the user's session data
* @returns {Promise<void>}
*/
export const getSessionData = async () => {
return await authenticatedRequest("/auth/session", "GET", releaseChannelRuntimeRequestParams())
.then((response) => {
const session = normalizeSessionPayload(response?.data?.data);
SessionUser.user.id.value = session.id;
SessionUser.user.customer_number.value = session.customer_number;
SessionUser.user.group_id.value = session.group_id;
SessionUser.user.email.value = session.email;
SessionUser.user.phone.number.value = session.phone.number;
SessionUser.user.phone.country_code.value = session.phone.country_code;
SessionUser.user.notifications.wash_certificate_email.value = session.notifications.wash_certificate_email;
SessionUser.user.notifications.email_notifications_enabled.value =
session.notifications.email_notifications_enabled;
SessionUser.user.notifications.sms_notifications_enabled.value = session.notifications.sms_notifications_enabled;
SessionUser.user.notifications.superuser_new_customer_email_notifications_enabled.value =
session.notifications.superuser_new_customer_email_notifications_enabled;
SessionUser.user.created_at.value = session.created_at;
SessionUser.user.updated_at.value = session.updated_at;
SessionUser.user.display_name.value = session.display_name;
setReleaseChannelSwitchNoticePrincipal(
`user:${session.id || session.customer_number || session.email || "unknown"}`
);
// Set the last cached time to now, this is used to determine if the user's data is outdated
SessionUser.user.cached_at.value = new Date();
SessionUser.permissions.value = session.permissions;
SessionUser.runtimeConfig.economic.transactionDraftCustomerNumber.value = normalizePositiveInteger(
session.runtime_config.economic.transaction_draft_customer_number
);
SessionUser.runtimeConfig.economic.defaultDistributionDepartmentId.value = normalizePositiveInteger(
session.runtime_config.economic.default_distribution_department_id
);
applyReleaseRuntimeConfig(session.runtime_config.release);
// E-conomic data is only fetched if the array isn't empty
if (session.economic_customer) {
SessionUser.economicData.customerNumber.value = session.economic_customer.customerNumber;
SessionUser.economicData.name.value = session.economic_customer.name;
// Set the display name to the e-conomic name if it's empty
if (SessionUser.user.display_name.value === null || SessionUser.user.display_name.value === "Unnamed") {
SessionUser.user.display_name.value = session.economic_customer.name;
}
SessionUser.economicData.address.value = session.economic_customer.address;
SessionUser.economicData.zip.value = session.economic_customer.zip;
SessionUser.economicData.city.value = session.economic_customer.city;
SessionUser.economicData.mobilePhone.value = session.economic_customer.mobilePhone;
SessionUser.economicData.email.value = session.economic_customer.email;
SessionUser.economicData.cvr.value = session.economic_customer.corporateIdentificationNumber;
SessionUser.economicData.currency.value = session.economic_customer.currency;
SessionUser.economicData.country.value = session.economic_customer.country;
SessionUser.economicData.cached_at.value = new Date();
}
SessionUser.initiated.value = true;
})
.catch(async (error) => {
if (isReleaseChannelApiAvailabilityError(error)) {
console.warn("Selected release channel API is unavailable during session bootstrap.", error);
markReleaseChannelApiUnavailable();
return;
}
return await handleSessionBootstrapFailure(error, {
title: "Fejl ved hentning af brugerdata",
text: "Der opstod en fejl ved hentning af dine brugerdata. Prøv at logge ind igen.",
});
});
};
const LOGOUT_REQUEST_TIMEOUT_MS = 2500;
const waitForLogoutRequest = async (request, timeoutMs = LOGOUT_REQUEST_TIMEOUT_MS) => {
const safeTimeoutMs = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : LOGOUT_REQUEST_TIMEOUT_MS;
let timeoutId = null;
let didTimeout = false;
const timeoutPromise = new Promise((resolve) => {
timeoutId = globalThis.setTimeout(() => {
didTimeout = true;
resolve({ timedOut: true });
}, safeTimeoutMs);
});
const requestPromise = Promise.resolve(request)
.catch((error) => {
if (!didTimeout) {
parseError(error, "auth");
console.error(error);
}
return { timedOut: false };
})
.finally(() => {
if (timeoutId !== null) {
globalThis.clearTimeout(timeoutId);
}
});
return await Promise.race([requestPromise, timeoutPromise]);
};
/**
* Destroy the user's session
* @returns {Promise<void>}
*/
export const destroySession = async ({ timeoutMs = LOGOUT_REQUEST_TIMEOUT_MS } = {}) => {
try {
if (localStorage.getItem("token")) {
await waitForLogoutRequest(authenticatedRequest("/auth/logout", "GET"), timeoutMs);
}
} finally {
clearStoredSession();
resetSessionState();
}
};
/**
* Force sign out the user
*/
export const forceClearSession = () => {
clearStoredSession();
resetSessionState();
};
/**
* Define the user session variables
*/
export const SessionUser = {
user: {
id: ref(null),
customer_number: ref(null),
display_name: ref(null),
group_id: ref(null),
email: ref(null),
phone: {
number: ref(null),
country_code: ref(null),
},
created_at: ref(null),
updated_at: ref(null),
cached_at: ref(null),
notifications: {
wash_certificate_email: ref(null),
email_notifications_enabled: ref(null),
sms_notifications_enabled: ref(null),
superuser_new_customer_email_notifications_enabled: ref(null),
setEmailNotificationsEnabled: (enabled) => {
return SessionUser.request("/account/notifications", "PUT", {
email_notifications_enabled: enabled,
})
.then(() => {
SessionUser.user.notifications.email_notifications_enabled.value = enabled;
})
.catch((error) => {
parseError(error, "user_notifications");
console.error(error);
});
},
setSmsNotificationsEnabled: (enabled) => {
return SessionUser.request("/account/notifications", "PUT", {
sms_notifications_enabled: enabled,
})
.then(() => {
SessionUser.user.notifications.sms_notifications_enabled.value = enabled;
})
.catch((error) => {
parseError(error, "user_notifications");
console.error(error);
});
},
setSuperuserNewCustomerEmailNotificationsEnabled: (enabled) => {
return SessionUser.request("/account/notifications", "PUT", {
superuser_new_customer_email_notifications_enabled: enabled,
})
.then(() => {
SessionUser.user.notifications.superuser_new_customer_email_notifications_enabled.value = enabled;
})
.catch((error) => {
parseError(error, "user_notifications");
console.error(error);
});
},
setWashCertificateEmail: (email) => {
return SessionUser.request("/account/notifications", "PUT", {
wash_certificate_email: email,
})
.then(() => {
SessionUser.user.notifications.wash_certificate_email.value = email;
})
.catch((error) => {
parseError(error, "user_notifications");
console.error(error);
});
},
},
},
/** Subuser profile data (populated when authenticated as subuser) */
subuser: {
id: ref(null),
username: ref(null),
name: ref(null),
email: ref(null),
phone: {
country_code: ref(null),
number: ref(null),
},
grants: ref([]),
created_at: ref(null),
updated_at: ref(null),
suspended_at: ref(null),
cached_at: ref(null),
/** Currently selected grant (billing_customer_number) for X-Customer-Number header */
selectedGrantCustomerNumber: ref(normalizePositiveInteger(localStorage.getItem("selected_customer_number"))),
/**
* Select a grant by billing_customer_number
* @param {number|null} customerNumber - The billing_customer_number to select, or null to clear
*/
selectGrant: (customerNumber) => {
clearEdgeGatewayWorkspaceCache();
const normalizedCustomerNumber = normalizePositiveInteger(customerNumber);
if (normalizedCustomerNumber === null) {
localStorage.removeItem("selected_customer_number");
SessionUser.subuser.selectedGrantCustomerNumber.value = null;
} else {
localStorage.setItem("selected_customer_number", normalizedCustomerNumber.toString());
SessionUser.subuser.selectedGrantCustomerNumber.value = normalizedCustomerNumber;
}
},
/**
* Get the currently selected grant object
* @returns {Object|null} The selected grant object or null
*/
getSelectedGrant: () => {
const selectedNum = SessionUser.subuser.selectedGrantCustomerNumber.value;
if (!selectedNum || !SessionUser.subuser.grants.value) return null;
return SessionUser.subuser.grants.value.find((g) => g.billing_customer_number === selectedNum) || null;
},
},
/** Whether the current session is a subuser session */
isSubuser: ref(false),
/** The user's permissions */
permissions: ref([]),
availableVirtualPermissions: ["has_superuser_token"],
runtimeConfig: {
economic: {
transactionDraftCustomerNumber: ref(null),
defaultDistributionDepartmentId: ref(null),
},
release: {
traceId: ref(null),
channel: ref(null),
availableChannels: ref([]),
versions: ref({ frontend: null, api: null }),
frontendBaseUrl: ref(null),
apiBaseUrl: ref(null),
availability: ref({
configured: true,
missing: [],
status: "ready",
}),
capturePolicy: ref({
enabled: false,
capture_level: "metadata",
all_failure_metadata: true,
retention_days: 14,
}),
},
},
virtualPermissions: computed(() => {
/** These permissions are NOT set by default for superusers, this should ONLY be used for superusers */
let virtualPermissions = [];
// Super user token
// Check if the user has a superuser token in the local storage
if (localStorage.getItem("superuser_token") !== null) {
// Validate the superuser token
if (localStorage.getItem("superuser_token") !== SessionUser.token.value) {
virtualPermissions.push("has_superuser_token");
}
}
return virtualPermissions;
}),
permissions_computed: computed(() => {
return [...SessionUser.permissions.value];
}),
hasPermission: (permission) => {
//console.log('Checking permission: ' + permission + ' - ' + hasPermission);
// Check if the permission is virtual
if (SessionUser.availableVirtualPermissions.values().toArray().includes(permission)) {
console.warn("Virtual permission detected: " + permission);
return SessionUser.virtualPermissions.value.values().toArray().includes(permission);
}
// For subusers, check permissions from the selected grant only
if (SessionUser.isSubuser.value) {
const selectedGrant = SessionUser.subuser.getSelectedGrant();
if (selectedGrant && selectedGrant.permissions) {
return selectedGrant.permissions.includes(permission);
}
return false;
}
return SessionUser.permissions.value.includes(permission) || SessionUser.permissions.value.includes("superuser");
},
/** Can access shortcuts */
canAccessAdmin: () => {
return SessionUser.hasPermission("admin") || SessionUser.hasPermission("superuser");
},
canAccessSuperUser: () => {
return SessionUser.hasPermission("superuser");
},
canAccessUser: () => {
return SessionUser.hasPermission("user");
},
canManageSubusers: (action) => {
const permissions = SUBUSER_MANAGEMENT_PERMISSIONS[action];
if (!permissions) {
return false;
}
return SessionUser.isSubuser.value
? SessionUser.hasPermission(permissions.subuser)
: SessionUser.hasPermission(permissions.customer);
},
canAccessDepartment: (id = 0) => {
return SessionUser.hasPermission("department_access_" + parseInt(id)) || SessionUser.canAccessSuperUser();
},
canAccessAssignedDepartment: (id = 0) => {
const departmentId = parseInt(id, 10);
if (!Number.isInteger(departmentId)) {
return false;
}
const permission = "department_access_" + departmentId;
if (SessionUser.isSubuser.value) {
const selectedGrant = SessionUser.subuser.getSelectedGrant();
if (selectedGrant && selectedGrant.permissions) {
return selectedGrant.permissions.includes(permission);
}
}
return SessionUser.permissions.value.includes(permission);
},
canAccessDeveloper: () => {
// If the url is localhost, we can assume that the user is a developer
return window.location.hostname === "localhost";
},
canAccessGuest: () => {
return true; // All users can access guest features
},
hasToken: () => {
return SessionUser.token.value !== null || hydrateSessionFromStorage();
},
/** Shortcuts for the user's group */
superUser: SuperUserObject,
adminUser: AdminUserObject,
/** Objects */
objects: {
categories: Categories,
products: Products,
product_options: ProductOptions,
departments: Departments,
department_categories: DepartmentCategories,
economic_departments: EconomicDepartments,
economic_products: EconomicProducts,
roles: Roles,
permissions: Permissions,
collectedOrderInvoices: CollectedOrderInvoices,
department_daily_reports: DepartmentDailyReports,
department_daily_report_complaints: DepartmentDailyReportComplaints,
goals: Goals,
notifications: Notifications,
orders: Orders,
forms: Forms,
bookings: Bookings,
Branding: Branding,
vehicles: Vehicles,
department_notification_sms: DepartmentNotificationSms,
department_time_bookings_opening_hours: DepartmentTimeBookingsOpeningHours,
department_time_bookings_types: DepartmentTimeBookingsTypes,
department_time_bookings_entries: DepartmentTimeBookingsEntries,
department_variables: DepartmentVariables,
xlvask_vehicle_types: XLVaskVehicleTypes,
openai: OpenAI,
department_lanes: DepartmentLanes,
department_gates: DepartmentGates,
department_relays: DepartmentRelays,
order_bookings: OrderBookings,
module_action_logs: ModuleActionLogs,
self_serve_questions: SelfServeQuestions,
self_serve_tasks: SelfServeTasks,
self_serve_conditions: SelfServeConditions,
self_serve_vehicle_conditions: SelfServeVehicleConditions,
self_serve_condition_rules: SelfServeConditionRules,
self_serve_machine_types: SelfServeMachineTypes,
subusers: Subusers,
subuser_grants: SubuserGrants,
global: ObjectsGlobal,
},
/** The user's token and authentication status */
token: ref(null),
authenticated: ref(false),
/** The user's e-conomic data */
economicData: {
customerNumber: ref(null),
name: ref(null),
address: ref(null),
zip: ref(null),
city: ref(null),
mobilePhone: ref(null),
email: ref(null),
cvr: ref(null),
currency: ref(null),
country: ref(null),
cached_at: ref(null),
},
/** Check if the user's data is outdated (Older than 24 hours) */
isDataOutdated: () => {
return new Date() - SessionUser.user.cached_at.value > 1000 * 60 * 60 * 24;
},
/** Check if the user's e-conomic data is outdated (Older than 24 hours) */
isEconomicDataOutdated: () => {
return new Date() - SessionUser.economicData.cached_at.value > 1000 * 60 * 60 * 24;
},
/** Check if the user has e-conomic data */
hasEconomicData: () => {
return SessionUser.economicData.customerNumber.value !== null;
},
getCustomerNumber: () => {
return SessionUser.user.customer_number.value;
},
/** Is the object fully initiated */
initiated: ref(false),
isInitiated: () => {
return SessionUser.initiated.value;
},
/**
* Check if the user has a superuser token stored (This is used when the superuser is impersonating a user)
* @returns {boolean}
*/
hasSuperUserToken: () => {
return localStorage.getItem("superuser_token") !== null;
},
/**
* Set the user's token
* @param {string} token
*/
setToken: (token) => {
clearPrivilegedSessionCaches();
SessionUser.token.value = token;
localStorage.setItem("token", token);
// Reload the window to make sure the user's session is initiated
window.location.reload();
},
/**
* Does the user have a specific attribute (Basically just a link to check if the user has permission`has_attribute_{{attribute}}`) // E.g. hasAttribute('admin') would check if the user has the 'has_attribute_key_admin' permission
* @param {string} attribute
* @returns {boolean}
*/
hasAttribute: (attribute) => {
return SessionUser.hasPermission("has_attribute_" + attribute);
},
/**
* Get the user's display name
*/
getName: () => {
// If the user is a subuser, return the subuser's name
if (SessionUser.isSubuser.value) {
return SessionUser.subuser.name.value || SessionUser.subuser.username.value || "Unnamed";
}
return SessionUser.user.display_name.value || "Unnamed";
},
/** Reset the user's session data (Cache) */
forceRefresh: () => {
resetSessionState();
},
auth: {
/** Authenticate the user */
authenticate: authenticateUser,
/** Authenticate a subuser with password */
authenticateSubuser: authenticateSubuser,
/** Logout the user */
logout: destroySession,
/** Force clear the user's local session state and stored token */
forceClearSession: forceClearSession,
/** reCAPTCHA */
reCAPTCHA: reCAPTCHA,
},
functions: {
/** Contact functions */
contact: {
onClickCallPhoneNumber: () => {
// Call the phone number
window.location.href = "tel:+4543717886";
},
},
/** Device type checks */
device: {
dimensions: {
// Using breakpoints in accordance with Bulma CSS framework
// https://bulma.io/documentation/start/responsiveness/#breakpoints
mobileMaxWidth: 768,
tabletMinWidth: 769,
tabletMaxWidth: 1023,
desktopMinWidth: 1024,
desktopMaxWidth: 1215,
widescreenMinWidth: 1216,
widescreenMaxWidth: 1407,
fullhdMinWidth: 1408,
// Custom additions
fullhdMaxWidth: 1919,
ultrawideMinWidth: 1920,
ultrawideMaxWidth: 3840,
},
isMobile: () => {
return windowInnerWidth.value < SessionUser.functions.device.dimensions.tabletMinWidth;
//return /Mobi|Android/i.test(navigator.userAgent);
},
isDesktop: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.desktopMinWidth;
//return !/Mobi|Android/i.test(navigator.userAgent);
},
isTablet: () => {
return (
windowInnerWidth.value >= SessionUser.functions.device.dimensions.tabletMinWidth &&
windowInnerWidth.value <= SessionUser.functions.device.dimensions.tabletMaxWidth
);
//return /Tablet|iPad/i.test(navigator.userAgent);
},
isWidescreen: () => {
return (
windowInnerWidth.value >= SessionUser.functions.device.dimensions.widescreenMinWidth &&
windowInnerWidth.value <= SessionUser.functions.device.dimensions.widescreenMaxWidth
);
},
isFullHD: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.fullhdMinWidth;
},
isUltraWideScreen: () => {
return windowInnerWidth.value >= SessionUser.functions.device.dimensions.ultrawideMinWidth;
},
},
/** Check if the user has a token */
hasToken: () => {
return SessionUser.token.value !== null || hydrateSessionFromStorage();
},
ucFirst: (str) => {
return str.charAt(0).toUpperCase() + str.slice(1);
},
/** Select department */
selectDepartment: (departmentId) => {
// Check if the department id is valid
if (isNaN(departmentId) || departmentId <= 0) {
console.error("Invalid department id: " + departmentId);
return;
}
if (departmentId === "default") {
// Redirect to the default department page
SessionUser.functions.redirectTo.department("");
return;
}
// Check if the user has access to the department
if (!SessionUser.canAccessDepartment(departmentId)) {
console.error("User does not have access to department: " + departmentId);
return;
}
// Check if the current path is within the /admin/:departmentId route
if (!window.location.pathname.startsWith("/admin/")) {
// Redirect to the department page
SessionUser.functions.redirectTo.department(departmentId);
return;
}
// Get the router instance
// Change the department in the url (while keeping the current path)
const currentPath = window.location.pathname;
// If
// If the current path is not an admin path, redirect to the department page
const newPath = currentPath
.replace(/\/admin\/\d+/, "")
.replace(/^\/admin\/?/, "")
.replace(/^\//, "");
const shouldPreserveCurrentQuery = newPath === "modules/pos" || newPath.startsWith("modules/pos/");
const queryString = shouldPreserveCurrentQuery ? window.location.search : "";
// Redirect to the department page
SessionUser.functions.redirectTo.department(departmentId, newPath + queryString);
},
/** Show confirm logout dialog */
showConfirmLogoutDialog: () => {
return Swal.fire({
title: "Er du sikker?",
text: "Du vil blive logget ud af systemet",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Ja, log mig ud",
cancelButtonText: "Annuller",
}).then(async (result) => {
if (result.isConfirmed) {
const wasSubuserSession = SessionUser.isSubuser.value || getStoredSessionSnapshot()?.isSubuser === true;
try {
await SessionUser.auth.logout();
} finally {
SessionUser.auth.forceClearSession();
if (wasSubuserSession) {
SessionUser.functions.redirectTo.pages.subuserLogin();
return;
}
SessionUser.functions.redirectTo.pages.login();
}
}
});
},
/** Get the department id from the url */
getDepartmentIdFromUrl: () => {
// Avoid router import cycles by parsing the current pathname directly.
const path = window.location.pathname || "";
const match = path.match(/^\/admin\/(\d+)(?:\/|$)/);
if (!match) {
return undefined;
}
const departmentId = Number.parseInt(match[1], 10);
return Number.isInteger(departmentId) && departmentId > 0 ? departmentId : undefined;
},
/** Get the department ids the user has access to */
getAccessibleDepartments: () => {
return SessionUser.permissions.value
.filter((permission) => permission.includes("department_access_"))
.map((permission) => parseInt(permission.split("_").pop()));
},
currency: {
toLocal: (value) => {
return new Intl.NumberFormat("da-DK", { style: "currency", currency: "DKK" }).format(value);
},
},
text: {
truncate: (text, length = 25) => {
return text.length > length ? text.substring(0, length) + "..." : text;
},
},
date: {
// Format to 2023-01-01 12:00:00
format: (date) => {
return new Date(date).toISOString().slice(0, 19).replace("T", " ");
},
toWords: (date) => {
const options = { weekday: "long", year: "numeric", month: "long", day: "numeric" };
return new Date(date).toLocaleDateString("da-DK", options);
},
toLocal: (date) => {
return new Date(date).toLocaleString("da-DK");
},
toWordsWithTime: (date) => {
const options = {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
};
return new Date(date).toLocaleDateString("da-DK", options);
},
isToday(date) {
const created_at_val = new Date(date);
const today = new Date();
return created_at_val.toDateString() === today.toDateString();
},
toLocalTimeHours: (date) => {
return new Date(date).toLocaleTimeString("da-DK", { hour: "2-digit", minute: "2-digit" });
},
getDateWeekdayNumber: (date) => {
const created_at_val = new Date(date);
const weekday = created_at_val.getDay();
return weekday === 0 ? 7 : weekday;
},
timeAgo: (date) => {
// Returns e.g. "5 minutter siden"
const now = new Date();
const past = new Date(date);
const secondsAgo = Math.floor((now - past) / 1000);
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(secondsAgo / secondsInUnit);
if (interval >= 1) {
// If the unit is singular or plural in danish
if (interval === 1) {
const unitSingularMap = {
year: "år",
month: "måned",
week: "uge",
day: "dag",
hour: "time",
minute: "minut",
second: "sekund",
};
return interval + " " + unitSingularMap[unit] + " siden";
} else {
const unitPluralMap = {
year: "år",
month: "måneder",
week: "uger",
day: "dage",
hour: "timer",
minute: "minutter",
second: "sekunder",
};
return interval + " " + unitPluralMap[unit] + " siden";
}
}
}
return "lige nu";
},
weekdayNumbers: {
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6,
sunday: 7,
},
getDateMonthStart() {
return new Date(new Date().getFullYear(), new Date().getMonth(), 1);
},
getDateMonthEnd() {
return new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0);
},
getDateMonthName(date) {
console.log(date);
return new Date(date).toLocaleString("da-DK", { month: "long" });
},
getDateYear(date) {
return new Date(date).getFullYear();
},
getDateAtTime(date, hours, minutes, seconds) {
const newDate = new Date(date);
newDate.setHours(hours);
newDate.setMinutes(minutes);
newDate.setSeconds(seconds);
return newDate;
},
getWeekdayNameToLocal(date) {
return new Date(date).toLocaleString("da-DK", { weekday: "long" });
},
},
/** Redirect to */
redirectTo: {
/**
* Pages to redirect to
*/
pages: {
login: () => {
window.location.href = "/login";
},
subuserLogin: () => {
window.location.href = "/login/driver";
},
redirect: () => {
window.location.href = "/redirect";
},
},
/**
* Redirect to a department
* @note This will redirect the user to '/admin/:department(/:path)'
* @param department The department id
* @param path The path to redirect to (Optional)
* @param new_tab Open the url in a new tab
*/
department: (department, path = null, new_tab = false) => {
if (!!new_tab) {
SessionUser.functions.redirectTo.external("/admin/" + department + (path !== null ? "/" + path : ""), true);
} else {
window.location.href = "/admin/" + department + (path !== null ? "/" + path : "");
}
},
/**
* Redirect to a superuser page
* @note This will redirect the user to '/superuser(/:path)'
* @param path The path to redirect to (Optional)
* @param new_tab Open the url in a new tab
*/
superUser: (path = null, new_tab = false) => {
if (new_tab) {
window.open("/superuser" + (path !== null ? path : ""), "_blank");
return;
}
window.location.href = "/superuser" + (path !== null ? path : "");
},
/**
* Redirect to a user page
* @note This will redirect the user to '/user(/:path)'
* @param path The path to redirect to (Optional)
* @param new_tab Open the url in a new tab
*/
user: (path = null, new_tab = false) => {
if (new_tab) {
window.open("/user" + (path !== null ? path : ""), "_blank");
return;
}
window.location.href = "/user" + (path !== null ? path : "");
},
/**
* External redirect
* @param url The url to redirect to
* @param newTab Open the url in a new tab
*/
external: (url, newTab = false) => {
if (newTab) {
window.open(url, "_blank");
} else {
window.location.href = url;
}
},
},
parseErrorMessage(error) {
console.log("Parsing error message: ", error);
// Check if the error is a string
if (typeof error === "string") {
return error;
}
// check if the response.data.data.message exists
if (error.response && error.response.data && error.response.data.data && error.response.data.data.message) {
return error.response.data.data.message;
}
// check if the response.data.message exists
if (error.response && error.response.data && error.response.data.message) {
return error.response.data.message;
}
// check if the response.data exists
if (error.response && error.response.data) {
return error.response.data;
}
// check if the response exists
if (error.response) {
return error.response;
}
},
getBusinessDaysBetweenDates(start, end, weekdays = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"]) {
let count = 0;
const curDate = new Date(start);
while (curDate <= end) {
const dayOfWeek = curDate.toLocaleString("en-US", { weekday: "long" }).toUpperCase();
if (weekdays.includes(dayOfWeek)) {
count++;
}
curDate.setDate(curDate.getDate() + 1);
}
return count;
},
},
request: authenticatedRequest,
editField: EditFieldForm,
getSessionData: getSessionData,
getSubuserSessionData: getSubuserSessionData,
refreshSessionData: refreshSessionData,
refreshReleaseRuntime: refreshReleaseRuntime,
initiateOnAppStart: initiateOnAppStart,
checkForUpdates() {
// Check if this is the latest version of the PWA
},
};
const windowInnerWidth = ref(window.innerWidth);
const updateWindowInnerWidth = () => {
windowInnerWidth.value = window.innerWidth;
};
window.addEventListener("resize", updateWindowInnerWidth);
export default SessionUser;
</script>