mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-25 14:26:39 +00:00
Compare commits
4 Commits
dependabot
...
cache
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f50d1549b0 | ||
|
|
55e24df671 | ||
|
|
b37e1d0cc0 | ||
|
|
afa26c0dd4 |
@@ -28,9 +28,9 @@ LICENSE
|
||||
CONTRIBUTING.md
|
||||
dist
|
||||
.git
|
||||
migrations/
|
||||
server/migrations/
|
||||
config/
|
||||
build.ts
|
||||
tsconfig.json
|
||||
Dockerfile*
|
||||
migrations/
|
||||
drizzle.config.ts
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
FROM node:24-alpine AS base
|
||||
FROM node:24-slim AS base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache python3 make g++
|
||||
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
@@ -27,11 +27,11 @@ FROM base AS builder
|
||||
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
FROM node:24-alpine AS runner
|
||||
FROM node:24-slim AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN apk add --no-cache curl tzdata
|
||||
RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
|
||||
@@ -230,7 +230,7 @@ export class UsageService {
|
||||
const orgIdToUse = await this.getBillingOrg(orgId);
|
||||
|
||||
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
|
||||
const cached = cache.get<string>(cacheKey);
|
||||
const cached = await cache.get<string>(cacheKey);
|
||||
|
||||
if (cached) {
|
||||
return cached;
|
||||
@@ -253,7 +253,7 @@ export class UsageService {
|
||||
const customerId = customer.customerId;
|
||||
|
||||
// Cache the result
|
||||
cache.set(cacheKey, customerId, 300); // 5 minute TTL
|
||||
await cache.set(cacheKey, customerId, 300); // 5 minute TTL
|
||||
|
||||
return customerId;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import NodeCache from "node-cache";
|
||||
import logger from "@server/logger";
|
||||
import { redisManager } from "@server/private/lib/redis";
|
||||
|
||||
// Create cache with maxKeys limit to prevent memory leaks
|
||||
// Create local cache with maxKeys limit to prevent memory leaks
|
||||
// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient
|
||||
export const cache = new NodeCache({
|
||||
const localCache = new NodeCache({
|
||||
stdTTL: 3600,
|
||||
checkperiod: 120,
|
||||
maxKeys: 10000
|
||||
@@ -11,10 +12,255 @@ export const cache = new NodeCache({
|
||||
|
||||
// Log cache statistics periodically for monitoring
|
||||
setInterval(() => {
|
||||
const stats = cache.getStats();
|
||||
const stats = localCache.getStats();
|
||||
logger.debug(
|
||||
`Cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
|
||||
`Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%`
|
||||
);
|
||||
}, 300000); // Every 5 minutes
|
||||
|
||||
export default cache;
|
||||
/**
|
||||
* Adaptive cache that uses Redis when available in multi-node environments,
|
||||
* otherwise falls back to local memory cache for single-node deployments.
|
||||
*/
|
||||
class AdaptiveCache {
|
||||
private useRedis(): boolean {
|
||||
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)
|
||||
* @returns boolean indicating success
|
||||
*/
|
||||
async set(key: string, value: any, ttl?: number): Promise<boolean> {
|
||||
const effectiveTtl = ttl === 0 ? undefined : ttl;
|
||||
|
||||
if (this.useRedis()) {
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
const success = await redisManager.set(key, serialized, effectiveTtl);
|
||||
|
||||
if (success) {
|
||||
logger.debug(`Set key in Redis: ${key}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Redis failed, fall through 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
|
||||
}
|
||||
}
|
||||
|
||||
// Use local cache as fallback or primary
|
||||
const success = localCache.set(key, value, effectiveTtl || 0);
|
||||
if (success) {
|
||||
logger.debug(`Set key in local cache: ${key}`);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the cache
|
||||
* @param key - Cache key
|
||||
* @returns The cached value or undefined if not found
|
||||
*/
|
||||
async get<T = any>(key: string): Promise<T | undefined> {
|
||||
if (this.useRedis()) {
|
||||
try {
|
||||
const value = await redisManager.get(key);
|
||||
|
||||
if (value !== null) {
|
||||
logger.debug(`Cache hit in Redis: ${key}`);
|
||||
return JSON.parse(value) as T;
|
||||
}
|
||||
|
||||
logger.debug(`Cache miss in Redis: ${key}`);
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
logger.error(`Redis get error for key ${key}:`, error);
|
||||
// Fall through to local cache
|
||||
}
|
||||
}
|
||||
|
||||
// Use local cache as fallback or primary
|
||||
const value = localCache.get<T>(key);
|
||||
if (value !== undefined) {
|
||||
logger.debug(`Cache hit in local cache: ${key}`);
|
||||
} else {
|
||||
logger.debug(`Cache miss in local cache: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a value from the cache
|
||||
* @param key - Cache key or array of keys
|
||||
* @returns Number of deleted entries
|
||||
*/
|
||||
async del(key: string | string[]): Promise<number> {
|
||||
const keys = Array.isArray(key) ? key : [key];
|
||||
let deletedCount = 0;
|
||||
|
||||
if (this.useRedis()) {
|
||||
try {
|
||||
for (const k of keys) {
|
||||
const success = await redisManager.del(k);
|
||||
if (success) {
|
||||
deletedCount++;
|
||||
logger.debug(`Deleted key from Redis: ${k}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount === keys.length) {
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
// Some Redis deletes failed, fall through 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);
|
||||
// Fall through to local cache
|
||||
deletedCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Use local cache as fallback or primary
|
||||
for (const k of keys) {
|
||||
const success = localCache.del(k);
|
||||
if (success > 0) {
|
||||
deletedCount++;
|
||||
logger.debug(`Deleted key from local cache: ${k}`);
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key exists in the cache
|
||||
* @param key - Cache key
|
||||
* @returns boolean indicating if key exists
|
||||
*/
|
||||
async has(key: string): Promise<boolean> {
|
||||
if (this.useRedis()) {
|
||||
try {
|
||||
const value = await redisManager.get(key);
|
||||
return value !== null;
|
||||
} catch (error) {
|
||||
logger.error(`Redis has error for key ${key}:`, error);
|
||||
// Fall through to local cache
|
||||
}
|
||||
}
|
||||
|
||||
// Use local cache as fallback or primary
|
||||
return localCache.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple values from the cache
|
||||
* @param keys - Array of cache keys
|
||||
* @returns Array of values (undefined for missing keys)
|
||||
*/
|
||||
async mget<T = any>(keys: string[]): Promise<(T | undefined)[]> {
|
||||
if (this.useRedis()) {
|
||||
try {
|
||||
const results: (T | undefined)[] = [];
|
||||
|
||||
for (const key of keys) {
|
||||
const value = await redisManager.get(key);
|
||||
if (value !== null) {
|
||||
results.push(JSON.parse(value) as T);
|
||||
} else {
|
||||
results.push(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
logger.error(`Redis mget error:`, error);
|
||||
// Fall through to local cache
|
||||
}
|
||||
}
|
||||
|
||||
// Use local cache as fallback or primary
|
||||
return keys.map((key) => localCache.get<T>(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush all keys from the cache
|
||||
*/
|
||||
async flushAll(): Promise<void> {
|
||||
if (this.useRedis()) {
|
||||
logger.warn("Adaptive cache flushAll called - Redis flush not implemented, only local cache will be flushed");
|
||||
}
|
||||
|
||||
localCache.flushAll();
|
||||
logger.debug("Flushed local cache");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* Note: Only returns local cache stats, Redis stats are not included
|
||||
*/
|
||||
getStats() {
|
||||
return localCache.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current cache backend being used
|
||||
* @returns "redis" if Redis is available and healthy, "local" otherwise
|
||||
*/
|
||||
getCurrentBackend(): "redis" | "local" {
|
||||
return this.useRedis() ? "redis" : "local";
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a key from the cache and delete it
|
||||
* @param key - Cache key
|
||||
* @returns The value or undefined if not found
|
||||
*/
|
||||
async take<T = any>(key: string): Promise<T | undefined> {
|
||||
const value = await this.get<T>(key);
|
||||
if (value !== undefined) {
|
||||
await this.del(key);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TTL (time to live) for a key
|
||||
* @param key - Cache key
|
||||
* @returns TTL in seconds, 0 if no expiration, -1 if key doesn't exist
|
||||
*/
|
||||
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`);
|
||||
}
|
||||
|
||||
const ttl = localCache.getTtl(key);
|
||||
if (ttl === undefined) {
|
||||
return -1;
|
||||
}
|
||||
return Math.max(0, Math.floor((ttl - Date.now()) / 1000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys from the cache
|
||||
* Note: Only returns local cache keys, Redis keys are not included
|
||||
*/
|
||||
keys(): string[] {
|
||||
if (this.useRedis()) {
|
||||
logger.warn("keys() called but Redis keys are not included, only local cache keys returned");
|
||||
}
|
||||
return localCache.keys();
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const cache = new AdaptiveCache();
|
||||
export default cache;
|
||||
@@ -55,7 +55,7 @@ export async function getValidCertificatesForDomains(
|
||||
if (useCache) {
|
||||
for (const domain of domains) {
|
||||
const cacheKey = `cert:${domain}`;
|
||||
const cachedCert = cache.get<CertificateResult>(cacheKey);
|
||||
const cachedCert = await cache.get<CertificateResult>(cacheKey);
|
||||
if (cachedCert) {
|
||||
finalResults.push(cachedCert); // Valid cache hit
|
||||
} else {
|
||||
@@ -169,7 +169,7 @@ export async function getValidCertificatesForDomains(
|
||||
// Add to cache for future requests, using the *requested domain* as the key
|
||||
if (useCache) {
|
||||
const cacheKey = `cert:${domain}`;
|
||||
cache.set(cacheKey, resultCert, 180);
|
||||
await cache.set(cacheKey, resultCert, 180);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ import { stripPortFromHost } from "@server/lib/ip";
|
||||
|
||||
async function getAccessDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
const cached = cache.get<number>(`org_${orgId}_accessDays`);
|
||||
const cached = await cache.get<number>(`org_${orgId}_accessDays`);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ async function getAccessDays(orgId: string): Promise<number> {
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(
|
||||
await cache.set(
|
||||
`org_${orgId}_accessDays`,
|
||||
org.settingsLogRetentionDaysAction,
|
||||
300
|
||||
@@ -146,14 +146,14 @@ export async function logAccessAudit(data: {
|
||||
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||
const geoIpCacheKey = `geoip_access:${ip}`;
|
||||
|
||||
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
||||
let cachedCountryCode: string | undefined = await cache.get(geoIpCacheKey);
|
||||
|
||||
if (!cachedCountryCode) {
|
||||
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
||||
// Only cache successful lookups to avoid filling cache with undefined values
|
||||
if (cachedCountryCode) {
|
||||
// Cache for longer since IP geolocation doesn't change frequently
|
||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
await cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
|
||||
|
||||
async function getActionDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
const cached = cache.get<number>(`org_${orgId}_actionDays`);
|
||||
const cached = await cache.get<number>(`org_${orgId}_actionDays`);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ async function getActionDays(orgId: string): Promise<number> {
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(
|
||||
await cache.set(
|
||||
`org_${orgId}_actionDays`,
|
||||
org.settingsLogRetentionDaysAction,
|
||||
300
|
||||
|
||||
@@ -130,7 +130,7 @@ export async function shutdownAuditLogger() {
|
||||
|
||||
async function getRetentionDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
|
||||
const cached = await cache.get<number>(`org_${orgId}_retentionDays`);
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
@@ -149,7 +149,7 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(
|
||||
await cache.set(
|
||||
`org_${orgId}_retentionDays`,
|
||||
org.settingsLogRetentionDaysRequest,
|
||||
300
|
||||
|
||||
@@ -137,7 +137,7 @@ export async function verifyResourceSession(
|
||||
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||
org: Org;
|
||||
}
|
||||
| undefined = cache.get(resourceCacheKey);
|
||||
| undefined = await cache.get(resourceCacheKey);
|
||||
|
||||
if (!resourceData) {
|
||||
const result = await getResourceByDomain(cleanHost);
|
||||
@@ -161,7 +161,7 @@ export async function verifyResourceSession(
|
||||
}
|
||||
|
||||
resourceData = result;
|
||||
cache.set(resourceCacheKey, resourceData, 5);
|
||||
await cache.set(resourceCacheKey, resourceData, 5);
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -405,7 +405,7 @@ export async function verifyResourceSession(
|
||||
// check for HTTP Basic Auth header
|
||||
const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
|
||||
if (headerAuth && clientHeaderAuth) {
|
||||
if (cache.get(clientHeaderAuthKey)) {
|
||||
if (await cache.get(clientHeaderAuthKey)) {
|
||||
logger.debug(
|
||||
"Resource allowed because header auth is valid (cached)"
|
||||
);
|
||||
@@ -428,7 +428,7 @@ export async function verifyResourceSession(
|
||||
headerAuth.headerAuthHash
|
||||
)
|
||||
) {
|
||||
cache.set(clientHeaderAuthKey, clientHeaderAuth, 5);
|
||||
await cache.set(clientHeaderAuthKey, clientHeaderAuth, 5);
|
||||
logger.debug("Resource allowed because header auth is valid");
|
||||
|
||||
logRequestAudit(
|
||||
@@ -520,7 +520,7 @@ export async function verifyResourceSession(
|
||||
|
||||
if (resourceSessionToken) {
|
||||
const sessionCacheKey = `session:${resourceSessionToken}`;
|
||||
let resourceSession: any = cache.get(sessionCacheKey);
|
||||
let resourceSession: any = await cache.get(sessionCacheKey);
|
||||
|
||||
if (!resourceSession) {
|
||||
const result = await validateResourceSessionToken(
|
||||
@@ -529,7 +529,7 @@ export async function verifyResourceSession(
|
||||
);
|
||||
|
||||
resourceSession = result?.resourceSession;
|
||||
cache.set(sessionCacheKey, resourceSession, 5);
|
||||
await cache.set(sessionCacheKey, resourceSession, 5);
|
||||
}
|
||||
|
||||
if (resourceSession?.isRequestToken) {
|
||||
@@ -662,7 +662,7 @@ export async function verifyResourceSession(
|
||||
}:${resource.resourceId}`;
|
||||
|
||||
let allowedUserData: BasicUserData | null | undefined =
|
||||
cache.get(userAccessCacheKey);
|
||||
await cache.get(userAccessCacheKey);
|
||||
|
||||
if (allowedUserData === undefined) {
|
||||
allowedUserData = await isUserAllowedToAccessResource(
|
||||
@@ -671,7 +671,7 @@ export async function verifyResourceSession(
|
||||
resourceData.org
|
||||
);
|
||||
|
||||
cache.set(userAccessCacheKey, allowedUserData, 5);
|
||||
await cache.set(userAccessCacheKey, allowedUserData, 5);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -974,11 +974,11 @@ async function checkRules(
|
||||
): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> {
|
||||
const ruleCacheKey = `rules:${resourceId}`;
|
||||
|
||||
let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey);
|
||||
let rules: ResourceRule[] | undefined = await cache.get(ruleCacheKey);
|
||||
|
||||
if (!rules) {
|
||||
rules = await getResourceRules(resourceId);
|
||||
cache.set(ruleCacheKey, rules, 5);
|
||||
await cache.set(ruleCacheKey, rules, 5);
|
||||
}
|
||||
|
||||
if (rules.length === 0) {
|
||||
@@ -1208,13 +1208,13 @@ async function isIpInAsn(
|
||||
async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
||||
const asnCacheKey = `asn:${ip}`;
|
||||
|
||||
let cachedAsn: number | undefined = cache.get(asnCacheKey);
|
||||
let cachedAsn: number | undefined = await cache.get(asnCacheKey);
|
||||
|
||||
if (!cachedAsn) {
|
||||
cachedAsn = await getAsnForIp(ip); // do it locally
|
||||
// Cache for longer since IP ASN doesn't change frequently
|
||||
if (cachedAsn) {
|
||||
cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes
|
||||
await cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1224,14 +1224,14 @@ async function getAsnFromIp(ip: string): Promise<number | undefined> {
|
||||
async function getCountryCodeFromIp(ip: string): Promise<string | undefined> {
|
||||
const geoIpCacheKey = `geoip:${ip}`;
|
||||
|
||||
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
||||
let cachedCountryCode: string | undefined = await cache.get(geoIpCacheKey);
|
||||
|
||||
if (!cachedCountryCode) {
|
||||
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
||||
// Only cache successful lookups to avoid filling cache with undefined values
|
||||
if (cachedCountryCode) {
|
||||
// Cache for longer since IP geolocation doesn't change frequently
|
||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
await cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ export const handleDockerStatusMessage: MessageHandler = async (context) => {
|
||||
|
||||
if (available) {
|
||||
logger.info(`Newt ${newt.newtId} has Docker socket access`);
|
||||
cache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
||||
cache.set(`${newt.newtId}:isAvailable`, available, 0);
|
||||
await cache.set(`${newt.newtId}:socketPath`, socketPath, 0);
|
||||
await cache.set(`${newt.newtId}:isAvailable`, available, 0);
|
||||
} else {
|
||||
logger.warn(`Newt ${newt.newtId} does not have Docker socket access`);
|
||||
}
|
||||
@@ -54,7 +54,7 @@ export const handleDockerContainersMessage: MessageHandler = async (
|
||||
);
|
||||
|
||||
if (containers && containers.length > 0) {
|
||||
cache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
||||
await cache.set(`${newt.newtId}:dockerContainers`, containers, 0);
|
||||
} else {
|
||||
logger.warn(`Newt ${newt.newtId} does not have Docker containers`);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||
import {
|
||||
generateSessionToken,
|
||||
validateSessionToken
|
||||
} from "@server/auth/sessions/app";
|
||||
import {
|
||||
clients,
|
||||
db,
|
||||
@@ -26,8 +29,9 @@ import { APP_VERSION } from "@server/lib/consts";
|
||||
|
||||
export const olmGetTokenBodySchema = z.object({
|
||||
olmId: z.string(),
|
||||
secret: z.string(),
|
||||
token: z.string().optional(),
|
||||
secret: z.string().optional(),
|
||||
userToken: z.string().optional(),
|
||||
token: z.string().optional(), // this is the olm token
|
||||
orgId: z.string().optional()
|
||||
});
|
||||
|
||||
@@ -49,7 +53,7 @@ export async function getOlmToken(
|
||||
);
|
||||
}
|
||||
|
||||
const { olmId, secret, token, orgId } = parsedBody.data;
|
||||
const { olmId, secret, token, orgId, userToken } = parsedBody.data;
|
||||
|
||||
try {
|
||||
if (token) {
|
||||
@@ -84,19 +88,45 @@ export async function getOlmToken(
|
||||
);
|
||||
}
|
||||
|
||||
const validSecret = await verifyPassword(
|
||||
secret,
|
||||
existingOlm.secretHash
|
||||
);
|
||||
|
||||
if (!validSecret) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.`
|
||||
if (userToken) {
|
||||
const { session: userSession, user } =
|
||||
await validateSessionToken(userToken);
|
||||
if (!userSession || !user) {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid user token")
|
||||
);
|
||||
}
|
||||
if (user.userId !== existingOlm.userId) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"User token does not match olm"
|
||||
)
|
||||
);
|
||||
}
|
||||
} else if (secret) {
|
||||
// this is for backward compatibility, we want to move towards userToken but some old clients may still be using secret so we will support both for now
|
||||
const validSecret = await verifyPassword(
|
||||
secret,
|
||||
existingOlm.secretHash
|
||||
);
|
||||
|
||||
if (!validSecret) {
|
||||
if (config.getRawConfig().app.log_failed_attempts) {
|
||||
logger.info(
|
||||
`Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.`
|
||||
);
|
||||
}
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect")
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"Either secret or userToken is required"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -194,9 +194,9 @@ export async function updateOrg(
|
||||
}
|
||||
|
||||
// invalidate the cache for all of the orgs retention days
|
||||
cache.del(`org_${orgId}_retentionDays`);
|
||||
cache.del(`org_${orgId}_actionDays`);
|
||||
cache.del(`org_${orgId}_accessDays`);
|
||||
await cache.del(`org_${orgId}_retentionDays`);
|
||||
await cache.del(`org_${orgId}_actionDays`);
|
||||
await cache.del(`org_${orgId}_accessDays`);
|
||||
|
||||
return response(res, {
|
||||
data: updatedOrg[0],
|
||||
|
||||
@@ -23,7 +23,7 @@ import { fromError } from "zod-validation-error";
|
||||
|
||||
async function getLatestNewtVersion(): Promise<string | null> {
|
||||
try {
|
||||
const cachedVersion = cache.get<string>("latestNewtVersion");
|
||||
const cachedVersion = await cache.get<string>("latestNewtVersion");
|
||||
if (cachedVersion) {
|
||||
return cachedVersion;
|
||||
}
|
||||
@@ -55,7 +55,7 @@ async function getLatestNewtVersion(): Promise<string | null> {
|
||||
tags = tags.filter((version) => !version.name.includes("rc"));
|
||||
const latestVersion = tags[0].name;
|
||||
|
||||
cache.set("latestNewtVersion", latestVersion);
|
||||
await cache.set("latestNewtVersion", latestVersion);
|
||||
|
||||
return latestVersion;
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -150,7 +150,7 @@ async function triggerFetch(siteId: number) {
|
||||
|
||||
// clear the cache for this Newt ID so that the site has to keep asking for the containers
|
||||
// this is to ensure that the site always gets the latest data
|
||||
cache.del(`${newt.newtId}:dockerContainers`);
|
||||
await cache.del(`${newt.newtId}:dockerContainers`);
|
||||
|
||||
return { siteId, newtId: newt.newtId };
|
||||
}
|
||||
@@ -158,7 +158,7 @@ async function triggerFetch(siteId: number) {
|
||||
async function queryContainers(siteId: number) {
|
||||
const { newt } = await getSiteAndNewt(siteId);
|
||||
|
||||
const result = cache.get(`${newt.newtId}:dockerContainers`) as Container[];
|
||||
const result = await cache.get<Container[]>(`${newt.newtId}:dockerContainers`);
|
||||
if (!result) {
|
||||
throw createHttpError(
|
||||
HttpCode.TOO_EARLY,
|
||||
@@ -173,7 +173,7 @@ async function isDockerAvailable(siteId: number): Promise<boolean> {
|
||||
const { newt } = await getSiteAndNewt(siteId);
|
||||
|
||||
const key = `${newt.newtId}:isAvailable`;
|
||||
const isAvailable = cache.get(key);
|
||||
const isAvailable = await cache.get(key);
|
||||
|
||||
return !!isAvailable;
|
||||
}
|
||||
@@ -186,9 +186,11 @@ async function getDockerStatus(
|
||||
const keys = ["isAvailable", "socketPath"];
|
||||
const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`);
|
||||
|
||||
const values = await cache.mget<boolean | string>(mappedKeys);
|
||||
|
||||
const result = {
|
||||
isAvailable: cache.get(mappedKeys[0]) as boolean,
|
||||
socketPath: cache.get(mappedKeys[1]) as string | undefined
|
||||
isAvailable: values[0] as boolean,
|
||||
socketPath: values[1] as string | undefined
|
||||
};
|
||||
|
||||
return result;
|
||||
|
||||
@@ -191,7 +191,7 @@ export async function inviteUser(
|
||||
}
|
||||
|
||||
if (existingInvite.length) {
|
||||
const attempts = cache.get<number>(email) || 0;
|
||||
const attempts = (await cache.get<number>(email)) || 0;
|
||||
if (attempts >= 3) {
|
||||
return next(
|
||||
createHttpError(
|
||||
@@ -201,7 +201,7 @@ export async function inviteUser(
|
||||
);
|
||||
}
|
||||
|
||||
cache.set(email, attempts + 1);
|
||||
await cache.set(email, attempts + 1);
|
||||
|
||||
const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId
|
||||
const token = generateRandomString(
|
||||
|
||||
Reference in New Issue
Block a user