Merge branch 'dev' into feat/login-page-customization

This commit is contained in:
Fred KISSIE
2025-11-15 06:32:03 +01:00
64 changed files with 2824 additions and 406 deletions

View File

@@ -51,6 +51,21 @@ export const internal = axios.create({
}
});
const remoteAPIURL =
process.env.NODE_ENV === "development"
? (process.env.NEXT_PUBLIC_FOSSORIAL_REMOTE_API_URL ??
"https://api.fossorial.io")
: "https://api.fossorial.io";
export const remote = axios.create({
baseURL: `${remoteAPIURL}/api/v1`,
timeout: 10000,
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": "x-csrf-protection"
}
});
export const priv = axios.create({
baseURL: `http://localhost:${process.env.SERVER_INTERNAL_PORT}/api/v1`,
timeout: 10000,

13
src/lib/durationToMs.ts Normal file
View File

@@ -0,0 +1,13 @@
export function durationToMs(
value: number,
unit: "seconds" | "minutes" | "hours" | "days" | "weeks"
): number {
const multipliers = {
seconds: 1000,
minutes: 60 * 1000,
hours: 60 * 60 * 1000,
days: 24 * 60 * 60 * 1000,
weeks: 7 * 24 * 60 * 60 * 1000
};
return value * multipliers[unit];
}

View File

@@ -21,7 +21,17 @@ export function pullEnv(): Env {
environment: process.env.ENVIRONMENT as string,
sandbox_mode: process.env.SANDBOX_MODE === "true" ? true : false,
version: process.env.APP_VERSION as string,
dashboardUrl: process.env.DASHBOARD_URL as string
dashboardUrl: process.env.DASHBOARD_URL as string,
notifications: {
product_updates:
process.env.PRODUCT_UPDATES_NOTIFICATION_ENABLED === "true"
? true
: false,
new_releases:
process.env.NEW_RELEASES_NOTIFICATION_ENABLED === "true"
? true
: false
}
},
email: {
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
@@ -50,9 +60,7 @@ export function pullEnv(): Env {
hideSupporterKey:
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
usePangolinDns:
process.env.USE_PANGOLIN_DNS === "true"
? true
: false
process.env.USE_PANGOLIN_DNS === "true" ? true : false
},
branding: {

67
src/lib/queries.ts Normal file
View File

@@ -0,0 +1,67 @@
import { keepPreviousData, queryOptions } from "@tanstack/react-query";
import { durationToMs } from "./durationToMs";
import { build } from "@server/build";
import { remote } from "./api";
import type ResponseT from "@server/types/Response";
export type ProductUpdate = {
link: string | null;
build: "enterprise" | "oss" | "saas" | null;
id: number;
type: "Update" | "Important" | "New" | "Warning";
title: string;
contents: string;
publishedAt: Date;
showUntil: Date;
};
export type LatestVersionResponse = {
pangolin: {
latestVersion: string;
releaseNotes: string;
};
};
export const productUpdatesQueries = {
list: (enabled: boolean) =>
queryOptions({
queryKey: ["PRODUCT_UPDATES"] as const,
queryFn: async ({ signal }) => {
const sp = new URLSearchParams({
build
});
const data = await remote.get<ResponseT<ProductUpdate[]>>(
`/product-updates?${sp.toString()}`,
{ signal }
);
return data.data;
},
refetchInterval: (query) => {
if (query.state.data) {
return durationToMs(5, "minutes");
}
return false;
},
enabled
}),
latestVersion: (enabled: boolean) =>
queryOptions({
queryKey: ["LATEST_VERSION"] as const,
queryFn: async ({ signal }) => {
const data = await remote.get<ResponseT<LatestVersionResponse>>(
"/versions",
{ signal }
);
return data.data;
},
placeholderData: keepPreviousData,
refetchInterval: (query) => {
if (query.state.data) {
return durationToMs(30, "minutes");
}
return false;
},
enabled: enabled && (build === "oss" || build === "enterprise") // disabled in cloud version
// because we don't need to listen for new versions there
})
};

View File

@@ -0,0 +1,47 @@
export function timeAgoFormatter(
dateInput: string | Date,
short: boolean = false
): string {
const date = new Date(dateInput);
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
const secondsInMinute = 60;
const secondsInHour = 60 * secondsInMinute;
const secondsInDay = 24 * secondsInHour;
const secondsInWeek = 7 * secondsInDay;
const secondsInMonth = 30 * secondsInDay;
const secondsInYear = 365 * secondsInDay;
let value: number;
let unit: Intl.RelativeTimeFormatUnit;
if (diffInSeconds < secondsInMinute) {
value = diffInSeconds;
unit = "second";
} else if (diffInSeconds < secondsInHour) {
value = Math.floor(diffInSeconds / secondsInMinute);
unit = "minute";
} else if (diffInSeconds < secondsInDay) {
value = Math.floor(diffInSeconds / secondsInHour);
unit = "hour";
} else if (diffInSeconds < secondsInWeek) {
value = Math.floor(diffInSeconds / secondsInDay);
unit = "day";
} else if (diffInSeconds < secondsInMonth) {
value = Math.floor(diffInSeconds / secondsInWeek);
unit = "week";
} else if (diffInSeconds < secondsInYear) {
value = Math.floor(diffInSeconds / secondsInMonth);
unit = "month";
} else {
value = Math.floor(diffInSeconds / secondsInYear);
unit = "year";
}
const rtf = new Intl.RelativeTimeFormat(navigator.languages[0] ?? "en", {
numeric: "auto",
style: short ? "narrow" : "long"
});
return rtf.format(-value, unit);
}

View File

@@ -4,6 +4,10 @@ export type Env = {
sandbox_mode: boolean;
version: string;
dashboardUrl: string;
notifications: {
product_updates: boolean;
new_releases: boolean;
};
};
server: {
externalPort: string;
@@ -29,11 +33,11 @@ export type Env = {
enableClients: boolean;
hideSupporterKey: boolean;
usePangolinDns: boolean;
},
};
branding: {
appName?: string;
background_image_path?: string;
logo?: {
logo: {
lightPath?: string;
darkPath?: string;
authPage?: {
@@ -43,22 +47,22 @@ export type Env = {
navbar?: {
width?: number;
height?: number;
}
},
loginPage?: {
};
};
loginPage: {
titleText?: string;
subtitleText?: string;
},
signupPage?: {
};
signupPage: {
titleText?: string;
subtitleText?: string;
},
resourceAuthPage?: {
};
resourceAuthPage: {
showLogo?: boolean;
hidePoweredBy?: boolean;
titleText?: string;
subtitleText?: string;
},
};
footer?: string;
};
};