Compare commits

...

20 Commits

Author SHA1 Message Date
Owen Schwartz
b6c9046405 New translations en-us.json (Spanish) 2026-03-21 10:56:55 -07:00
Owen Schwartz
ea2ec84f08 New translations en-us.json (Norwegian Bokmal) 2026-03-21 10:56:54 -07:00
Owen Schwartz
40a14ac5f6 New translations en-us.json (Chinese Simplified) 2026-03-21 10:56:53 -07:00
Owen Schwartz
78169f087f New translations en-us.json (Turkish) 2026-03-21 10:56:51 -07:00
Owen Schwartz
4021107d08 New translations en-us.json (Russian) 2026-03-21 10:56:50 -07:00
Owen Schwartz
e78aaeb344 New translations en-us.json (Portuguese) 2026-03-21 10:56:48 -07:00
Owen Schwartz
0087ca997d New translations en-us.json (Polish) 2026-03-21 10:56:47 -07:00
Owen Schwartz
61d4d314d6 New translations en-us.json (Dutch) 2026-03-21 10:56:46 -07:00
Owen Schwartz
2e8b4ddbdb New translations en-us.json (Korean) 2026-03-21 10:56:44 -07:00
Owen Schwartz
3996b86f14 New translations en-us.json (Italian) 2026-03-21 10:56:43 -07:00
Owen Schwartz
7388ef8588 New translations en-us.json (German) 2026-03-21 10:56:42 -07:00
Owen Schwartz
bb2edb23e5 New translations en-us.json (Czech) 2026-03-21 10:56:40 -07:00
Owen Schwartz
4cad384476 New translations en-us.json (Bulgarian) 2026-03-21 10:56:39 -07:00
Owen Schwartz
ba07eb1303 New translations en-us.json (French) 2026-03-21 10:56:37 -07:00
Owen
7c2b4f422a Merge branch 'main' into dev 2026-03-21 10:45:13 -07:00
Owen
ad2a0ae127 Use the log database in hybrid as well 2026-03-21 10:42:31 -07:00
miloschwartz
6c2c620c99 set cache ttl and default ttl 2026-03-20 17:52:07 -07:00
miloschwartz
f643abf19a dont show create org for oidc users 2026-03-20 16:04:00 -07:00
Owen Schwartz
a1729033cf Merge pull request #2682 from fosrl/dev
Fix offline issue
2026-03-20 15:31:38 -07:00
Owen
7311766512 Fix offline issue 2026-03-20 15:30:41 -07:00
23 changed files with 70 additions and 43 deletions

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Въведете конфигурационния токен от сървърната конзола.",
"setupTokenRequired": "Необходим е конфигурационен токен",
"actionUpdateSite": "Актуализиране на сайт",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Изброяване на позволените роли за сайта",
"actionCreateResource": "Създаване на ресурс",
"actionDeleteResource": "Изтриване на ресурс",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Zadejte nastavovací token z konzole serveru.",
"setupTokenRequired": "Je vyžadován token nastavení",
"actionUpdateSite": "Aktualizovat stránku",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Seznam povolených rolí webu",
"actionCreateResource": "Vytvořit zdroj",
"actionDeleteResource": "Odstranit dokument",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Geben Sie das Setup-Token von der Serverkonsole ein.",
"setupTokenRequired": "Setup-Token ist erforderlich",
"actionUpdateSite": "Standorte aktualisieren",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Erlaubte Standort-Rollen auflisten",
"actionCreateResource": "Ressource erstellen",
"actionDeleteResource": "Ressource löschen",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Ingrese el token de configuración desde la consola del servidor.",
"setupTokenRequired": "Se requiere el token de configuración",
"actionUpdateSite": "Actualizar sitio",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Lista de roles permitidos del sitio",
"actionCreateResource": "Crear Recurso",
"actionDeleteResource": "Eliminar Recurso",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Entrez le jeton de configuration depuis la console du serveur.",
"setupTokenRequired": "Le jeton de configuration est requis.",
"actionUpdateSite": "Mettre à jour un site",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Lister les rôles autorisés du site",
"actionCreateResource": "Créer une ressource",
"actionDeleteResource": "Supprimer une ressource",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Inserisci il token di configurazione dalla console del server.",
"setupTokenRequired": "Il token di configurazione è richiesto",
"actionUpdateSite": "Aggiorna Sito",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Elenca Ruoli Sito Consentiti",
"actionCreateResource": "Crea Risorsa",
"actionDeleteResource": "Elimina Risorsa",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "서버 콘솔에서 설정 토큰 입력.",
"setupTokenRequired": "설정 토큰이 필요합니다",
"actionUpdateSite": "사이트 업데이트",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "허용된 사이트 역할 목록",
"actionCreateResource": "리소스 생성",
"actionDeleteResource": "리소스 삭제",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Skriv inn oppsetttoken fra serverkonsollen.",
"setupTokenRequired": "Oppsetttoken er nødvendig",
"actionUpdateSite": "Oppdater område",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "List opp tillatte områderoller",
"actionCreateResource": "Opprett ressurs",
"actionDeleteResource": "Slett ressurs",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Voer het setup-token in vanaf de serverconsole.",
"setupTokenRequired": "Setup-token is vereist",
"actionUpdateSite": "Site bijwerken",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Toon toegestane sitenollen",
"actionCreateResource": "Bron maken",
"actionDeleteResource": "Document verwijderen",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Wprowadź token konfiguracji z konsoli serwera.",
"setupTokenRequired": "Wymagany jest token konfiguracji",
"actionUpdateSite": "Aktualizuj witrynę",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Lista dozwolonych ról witryny",
"actionCreateResource": "Utwórz zasób",
"actionDeleteResource": "Usuń zasób",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Digite o token de configuração do console do servidor.",
"setupTokenRequired": "Token de configuração é necessário",
"actionUpdateSite": "Atualizar Site",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Listar Funções Permitidas do Site",
"actionCreateResource": "Criar Recurso",
"actionDeleteResource": "Eliminar Recurso",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Введите токен настройки из консоли сервера.",
"setupTokenRequired": "Токен настройки обязателен",
"actionUpdateSite": "Обновить сайт",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "Список разрешенных ролей сайта",
"actionCreateResource": "Создать ресурс",
"actionDeleteResource": "Удалить ресурс",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "Sunucu konsolundan kurulum simgesini girin.",
"setupTokenRequired": "Kurulum simgesi gerekli",
"actionUpdateSite": "Siteyi Güncelle",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "İzin Verilen Site Rolleri Listele",
"actionCreateResource": "Kaynak Oluştur",
"actionDeleteResource": "Kaynağı Sil",

View File

@@ -1119,6 +1119,7 @@
"setupTokenDescription": "从服务器控制台输入设置令牌。",
"setupTokenRequired": "需要设置令牌",
"actionUpdateSite": "更新站点",
"actionResetSiteBandwidth": "Reset Organization Bandwidth",
"actionListSiteRoles": "允许站点角色列表",
"actionCreateResource": "创建资源",
"actionDeleteResource": "删除资源",

View File

@@ -24,23 +24,31 @@ setInterval(() => {
*/
class AdaptiveCache {
private useRedis(): boolean {
return redisManager.isRedisEnabled() && redisManager.getHealthStatus().isHealthy;
return (
redisManager.isRedisEnabled() &&
redisManager.getHealthStatus().isHealthy
);
}
/**
* Set a value in the cache
* @param key - Cache key
* @param value - Value to cache (will be JSON stringified for Redis)
* @param ttl - Time to live in seconds (0 = no expiration)
* @param ttl - Time to live in seconds (0 = no expiration; omit = 3600s for Redis)
* @returns boolean indicating success
*/
async set(key: string, value: any, ttl?: number): Promise<boolean> {
const effectiveTtl = ttl === 0 ? undefined : ttl;
const redisTtl = ttl === 0 ? undefined : (ttl ?? 3600);
if (this.useRedis()) {
try {
const serialized = JSON.stringify(value);
const success = await redisManager.set(key, serialized, effectiveTtl);
const success = await redisManager.set(
key,
serialized,
redisTtl
);
if (success) {
logger.debug(`Set key in Redis: ${key}`);
@@ -48,7 +56,9 @@ class AdaptiveCache {
}
// Redis failed, fall through to local cache
logger.debug(`Redis set failed for key ${key}, falling back to local cache`);
logger.debug(
`Redis set failed for key ${key}, falling back to local cache`
);
} catch (error) {
logger.error(`Redis set error for key ${key}:`, error);
// Fall through to local cache
@@ -120,9 +130,14 @@ class AdaptiveCache {
}
// Some Redis deletes failed, fall through to local cache
logger.debug(`Some Redis deletes failed, falling back to local cache`);
logger.debug(
`Some Redis deletes failed, falling back to local cache`
);
} catch (error) {
logger.error(`Redis del error for keys ${keys.join(", ")}:`, error);
logger.error(
`Redis del error for keys ${keys.join(", ")}:`,
error
);
// Fall through to local cache
deletedCount = 0;
}
@@ -195,7 +210,9 @@ class AdaptiveCache {
*/
async flushAll(): Promise<void> {
if (this.useRedis()) {
logger.warn("Adaptive cache flushAll called - Redis flush not implemented, only local cache will be flushed");
logger.warn(
"Adaptive cache flushAll called - Redis flush not implemented, only local cache will be flushed"
);
}
localCache.flushAll();
@@ -239,7 +256,9 @@ class AdaptiveCache {
getTtl(key: string): number {
// Note: This only works for local cache, Redis TTL is not supported
if (this.useRedis()) {
logger.warn(`getTtl called for key ${key} but Redis TTL lookup is not implemented`);
logger.warn(
`getTtl called for key ${key} but Redis TTL lookup is not implemented`
);
}
const ttl = localCache.getTtl(key);
@@ -255,7 +274,9 @@ class AdaptiveCache {
*/
keys(): string[] {
if (this.useRedis()) {
logger.warn("keys() called but Redis keys are not included, only local cache keys returned");
logger.warn(
"keys() called but Redis keys are not included, only local cache keys returned"
);
}
return localCache.keys();
}

View File

@@ -15,6 +15,7 @@ import { verifySessionRemoteExitNodeMiddleware } from "#private/middlewares/veri
import { Router } from "express";
import {
db,
logsDb,
exitNodes,
Resource,
ResourcePassword,
@@ -1885,7 +1886,7 @@ hybridRouter.post(
const batchSize = 100;
for (let i = 0; i < logEntries.length; i += batchSize) {
const batch = logEntries.slice(i, i + batchSize);
await db.insert(requestAuditLog).values(batch);
await logsDb.insert(requestAuditLog).values(batch);
}
return response(res, {

View File

@@ -38,7 +38,7 @@ export const startRemoteExitNodeOfflineChecker = (): void => {
);
// Find clients that haven't pinged in the last 2 minutes and mark them as offline
const newlyOfflineNodes = await db
const offlineNodes = await db
.update(exitNodes)
.set({ online: false })
.where(
@@ -53,32 +53,15 @@ export const startRemoteExitNodeOfflineChecker = (): void => {
)
.returning();
// Update the sites to offline if they have not pinged either
const exitNodeIds = newlyOfflineNodes.map(
(node) => node.exitNodeId
);
const sitesOnNode = await db
.select()
.from(sites)
.where(
and(
eq(sites.online, true),
inArray(sites.exitNodeId, exitNodeIds)
)
if (offlineNodes.length > 0) {
logger.info(
`checkRemoteExitNodeOffline: Marked ${offlineNodes.length} remoteExitNode client(s) offline due to inactivity`
);
// loop through the sites and process their lastBandwidthUpdate as an iso string and if its more than 1 minute old then mark the site offline
for (const site of sitesOnNode) {
if (!site.lastBandwidthUpdate) {
continue;
}
const lastBandwidthUpdate = new Date(site.lastBandwidthUpdate);
if (Date.now() - lastBandwidthUpdate.getTime() > 60 * 1000) {
await db
.update(sites)
.set({ online: false })
.where(eq(sites.siteId, site.siteId));
for (const offlineClient of offlineNodes) {
logger.debug(
`checkRemoteExitNodeOffline: Client ${offlineClient.exitNodeId} marked offline (lastPing: ${offlineClient.lastPing})`
);
}
}
} catch (error) {

View File

@@ -70,7 +70,7 @@ async function getLatestOlmVersion(): Promise<string | null> {
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion);
olmVersionCache.set("latestOlmVersion", latestVersion, 3600);
return latestVersion;
} catch (error: any) {

View File

@@ -71,7 +71,7 @@ async function getLatestOlmVersion(): Promise<string | null> {
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion);
olmVersionCache.set("latestOlmVersion", latestVersion, 3600);
return latestVersion;
} catch (error: any) {

View File

@@ -6,7 +6,9 @@ import logger from "@server/logger";
/**
* Handles disconnecting messages from sites to show disconnected in the ui
*/
export const handleNewtDisconnectingMessage: MessageHandler = async (context) => {
export const handleNewtDisconnectingMessage: MessageHandler = async (
context
) => {
const { message, client: c, sendToClient } = context;
const newt = c as Newt;
@@ -27,7 +29,7 @@ export const handleNewtDisconnectingMessage: MessageHandler = async (context) =>
.set({
online: false
})
.where(eq(sites.siteId, sites.siteId));
.where(eq(sites.siteId, newt.siteId));
} catch (error) {
logger.error("Error handling disconnecting message", { error });
}

View File

@@ -55,7 +55,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name;
await cache.set("latestNewtVersion", latestVersion);
await cache.set("latestNewtVersion", latestVersion, 3600);
return latestVersion;
} catch (error: any) {
@@ -180,7 +180,7 @@ registry.registerPath({
method: "get",
path: "/org/{orgId}/sites",
description: "List all sites in an organization",
tags: [OpenAPITags.Site],
tags: [OpenAPITags.Org, OpenAPITags.Site],
request: {
params: listSitesParamsSchema,
query: listSitesSchema

View File

@@ -201,7 +201,7 @@ export async function inviteUser(
);
}
await cache.set(email, attempts + 1);
await cache.set("regenerateInvite:" + email, attempts + 1, 3600);
const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId
const token = generateRandomString(

View File

@@ -29,6 +29,7 @@ import { usePathname, useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { useUserContext } from "@app/hooks/useUserContext";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
interface OrgSelectorProps {
orgId?: string;
@@ -50,6 +51,11 @@ export function OrgSelector({
const selectedOrg = orgs?.find((org) => org.orgId === orgId);
let canCreateOrg = !env.flags.disableUserCreateOrg || user.serverAdmin;
if (build === "saas" && user.type !== "internal") {
canCreateOrg = false;
}
const sortedOrgs = useMemo(() => {
if (!orgs?.length) return orgs ?? [];
return [...orgs].sort((a, b) => {
@@ -161,7 +167,7 @@ export function OrgSelector({
</CommandGroup>
</CommandList>
</Command>
{(!env.flags.disableUserCreateOrg || user.serverAdmin) && (
{canCreateOrg && (
<div className="p-2 border-t border-border">
<Button
variant="ghost"