mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 22:53:08 +08:00
641ce142df
* SCIM - init (EE) * accept db transaction * sync * Content parser support for scim+json * patch scimmy * sync * return early if userIds is empty * sync * SCIM db table * fixes * scim tokens * backfill * feat(audit): add scim token events * rename scim migration * fix * fix translation * cleanup
193 lines
5.5 KiB
TypeScript
193 lines
5.5 KiB
TypeScript
import * as path from 'path';
|
|
import * as bcrypt from 'bcrypt';
|
|
import sanitize = require('sanitize-filename');
|
|
import { FastifyRequest } from 'fastify';
|
|
import { Readable, Transform } from 'stream';
|
|
|
|
export const envPath = path.resolve(process.cwd(), '..', '..', '.env');
|
|
|
|
export async function hashPassword(password: string) {
|
|
const saltRounds = 12;
|
|
return bcrypt.hash(password, saltRounds);
|
|
}
|
|
|
|
export async function comparePasswordHash(
|
|
plainPassword: string,
|
|
passwordHash: string,
|
|
): Promise<boolean> {
|
|
return bcrypt.compare(plainPassword, passwordHash);
|
|
}
|
|
|
|
export function generateRandomSuffixNumbers(length: number) {
|
|
return Math.random()
|
|
.toFixed(length)
|
|
.substring(2, 2 + length);
|
|
}
|
|
|
|
export type RedisConfig = {
|
|
host: string;
|
|
port: number;
|
|
db: number;
|
|
password?: string;
|
|
family?: number;
|
|
};
|
|
|
|
export function parseRedisUrl(redisUrl: string): RedisConfig {
|
|
// format - redis[s]://[[username][:password]@][host][:port][/db-number][?family=4|6]
|
|
const url = new URL(redisUrl);
|
|
const { hostname, port, password, pathname, searchParams } = url;
|
|
const portInt = parseInt(port, 10);
|
|
|
|
let db: number = 0;
|
|
// extract db value if present
|
|
if (pathname.length > 1) {
|
|
const value = pathname.slice(1);
|
|
if (!isNaN(parseInt(value))) {
|
|
db = parseInt(value, 10);
|
|
}
|
|
}
|
|
|
|
// extract family from query parameters
|
|
let family: number | undefined;
|
|
const familyParam = searchParams.get('family');
|
|
if (familyParam && !isNaN(parseInt(familyParam))) {
|
|
family = parseInt(familyParam, 10);
|
|
}
|
|
|
|
return { host: hostname, port: portInt, password, db, family };
|
|
}
|
|
|
|
export function createRetryStrategy() {
|
|
return function (times: number): number {
|
|
return Math.max(Math.min(Math.exp(times), 20000), 3000);
|
|
};
|
|
}
|
|
|
|
export function extractDateFromUuid7(uuid7: string) {
|
|
//https://park.is/blog_posts/20240803_extracting_timestamp_from_uuid_v7/
|
|
const parts = uuid7.split('-');
|
|
const highBitsHex = parts[0] + parts[1].slice(0, 4);
|
|
const timestamp = parseInt(highBitsHex, 16);
|
|
|
|
return new Date(timestamp);
|
|
}
|
|
|
|
export type SanitizeFileNameOptions = {
|
|
/** Keep spaces and `#` instead of replacing them with `_`. Useful for
|
|
* download filenames where readability matters. Defaults to false. */
|
|
preserveSpaces?: boolean;
|
|
};
|
|
|
|
export function sanitizeFileName(
|
|
fileName: string,
|
|
options: SanitizeFileNameOptions = {},
|
|
): string {
|
|
// Decode percent-encoded sequences so that bypasses like "..%2F" reach
|
|
// sanitize() as literal "../" and get stripped. sanitize-filename only
|
|
// strips literal characters and won't catch encoded path separators
|
|
// on its own.
|
|
const decoded = fileName.replace(/%[0-9a-fA-F]{2}/g, (m) => {
|
|
try {
|
|
return decodeURIComponent(m);
|
|
} catch {
|
|
return m;
|
|
}
|
|
});
|
|
|
|
const sanitized = sanitize(decoded);
|
|
if (options.preserveSpaces) {
|
|
return sanitized;
|
|
}
|
|
return sanitized.replace(/ /g, '_').replace(/#/g, '_');
|
|
}
|
|
|
|
export function removeAccent(str: string): string {
|
|
if (!str) return str;
|
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
}
|
|
|
|
export function extractBearerTokenFromHeader(
|
|
request: FastifyRequest,
|
|
): string | undefined {
|
|
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
|
return type?.toLowerCase() === 'bearer' ? token : undefined;
|
|
}
|
|
|
|
/**
|
|
* Normalizes a database URL for postgres.js compatibility.
|
|
* - Removes `sslmode=no-verify` (not supported by postgres.js), keeps other sslmode values
|
|
* - Removes `schema` parameter (has no effect via connection string)
|
|
* Note: If we don't strip them, the connection will fail
|
|
*/
|
|
export function normalizePostgresUrl(url: string): string {
|
|
const parsed = new URL(url);
|
|
const newParams = new URLSearchParams();
|
|
|
|
for (const [key, value] of parsed.searchParams) {
|
|
if (key === 'sslmode' && value === 'no-verify') continue;
|
|
if (key === 'schema') continue;
|
|
newParams.append(key, value);
|
|
}
|
|
|
|
parsed.search = newParams.toString();
|
|
return parsed.toString();
|
|
}
|
|
|
|
export function diffAuditTrackedFields(
|
|
fields: readonly string[],
|
|
dto: Record<string, any>,
|
|
before: Record<string, any> | undefined | null,
|
|
after: Record<string, any> | undefined | null,
|
|
): { before: Record<string, any>; after: Record<string, any> } | null {
|
|
const beforeDiff: Record<string, any> = {};
|
|
const afterDiff: Record<string, any> = {};
|
|
let hasChanges = false;
|
|
|
|
for (const field of fields) {
|
|
if (typeof dto[field] === 'undefined') continue;
|
|
const oldVal = JSON.stringify(before?.[field] ?? null);
|
|
const newVal = JSON.stringify(after?.[field] ?? null);
|
|
if (oldVal !== newVal) {
|
|
beforeDiff[field] = before?.[field];
|
|
afterDiff[field] = after?.[field];
|
|
hasChanges = true;
|
|
}
|
|
}
|
|
|
|
return hasChanges ? { before: beforeDiff, after: afterDiff } : null;
|
|
}
|
|
|
|
export function isUserDisabled(user: {
|
|
deactivatedAt?: Date | null;
|
|
deletedAt?: Date | null;
|
|
}): boolean {
|
|
return !!(user.deactivatedAt || user.deletedAt);
|
|
}
|
|
|
|
const SENSITIVE_URL_PREFIXES = ['/api/sso/'];
|
|
|
|
export function redactSensitiveUrl(url: string): string {
|
|
if (url && SENSITIVE_URL_PREFIXES.some((prefix) => url.includes(prefix))) {
|
|
const qsIndex = url.indexOf('?');
|
|
if (qsIndex !== -1) {
|
|
return url.substring(0, qsIndex);
|
|
}
|
|
}
|
|
return url;
|
|
}
|
|
|
|
export function createByteCountingStream(source: Readable) {
|
|
let bytesRead = 0;
|
|
const stream = new Transform({
|
|
transform(chunk, encoding, callback) {
|
|
bytesRead += chunk.length;
|
|
callback(null, chunk);
|
|
},
|
|
});
|
|
|
|
source.pipe(stream);
|
|
source.on('error', (err) => stream.emit('error', err));
|
|
|
|
return { stream, getBytesRead: () => bytesRead };
|
|
}
|