Integrate Oudelaa backend features, security, tests, and deployment updates

This commit is contained in:
boutmoun123
2026-07-26 16:58:53 +03:00
parent 1b24ca4294
commit 2fd5322ef7
178 changed files with 19068 additions and 2592 deletions
+7 -3
View File
@@ -9,10 +9,14 @@ export const decodeOffsetCursor = (cursor?: string): number | null => {
try {
const raw = Buffer.from(cursor, 'base64url').toString('utf8');
const parsed = Number(raw);
if (!Number.isInteger(parsed) || parsed < 0) {
return null;
if (Number.isInteger(parsed) && parsed >= 0) {
return parsed;
}
return parsed;
const payload = JSON.parse(raw) as { offset?: unknown };
return Number.isInteger(payload.offset) && Number(payload.offset) >= 0
? Number(payload.offset)
: null;
} catch {
return null;
}
+22
View File
@@ -0,0 +1,22 @@
import * as bcrypt from 'bcrypt';
import { compareHash, compareStoredHighEntropyValue, hashHighEntropyValue, hashValue } from './hash.util';
describe('hash utilities', () => {
it('hashes and verifies passwords with bcrypt', async () => {
const hash = await hashValue('secret', 4);
await expect(compareHash('secret', hash)).resolves.toBe(true);
await expect(compareHash('wrong', hash)).resolves.toBe(false);
});
it('uses deterministic secret-keyed hashes for high entropy tokens', async () => {
const stored = hashHighEntropyValue('token', 'pepper');
expect(stored).toMatch(/^sha256:[0-9a-f]{64}$/);
await expect(compareStoredHighEntropyValue('token', stored, 'pepper')).resolves.toBe(true);
await expect(compareStoredHighEntropyValue('other', stored, 'pepper')).resolves.toBe(false);
});
it('supports legacy bcrypt token hashes', async () => {
const stored = await bcrypt.hash('legacy-token', 4);
await expect(compareStoredHighEntropyValue('legacy-token', stored, 'unused')).resolves.toBe(true);
});
});
+1
View File
@@ -0,0 +1 @@
export const escapeRegex = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+33
View File
@@ -0,0 +1,33 @@
import { ConfigService } from '@nestjs/config';
import { buildAiMusicShareUrl, buildPostShareUrl, buildProfileShareUrl, normalizeShareBaseUrl, resolveShareBaseUrl } from './share-url.util';
describe('share URL utilities', () => {
const originalShare = process.env.SHARE_BASE_URL;
const originalPublicWeb = process.env.PUBLIC_WEB_URL;
afterEach(() => {
if (originalShare === undefined) delete process.env.SHARE_BASE_URL;
else process.env.SHARE_BASE_URL = originalShare;
if (originalPublicWeb === undefined) delete process.env.PUBLIC_WEB_URL;
else process.env.PUBLIC_WEB_URL = originalPublicWeb;
});
it('normalizes defaults and trailing slashes', () => {
expect(normalizeShareBaseUrl(undefined)).toBe('https://oudelaa.com');
expect(normalizeShareBaseUrl(' https://web.test/// ')).toBe('https://web.test');
});
it('resolves environment before configuration', () => {
process.env.SHARE_BASE_URL = 'https://env.test/';
const config = { get: jest.fn(() => 'https://config.test') } as unknown as ConfigService;
expect(resolveShareBaseUrl(config)).toBe('https://env.test');
expect(config.get).not.toHaveBeenCalled();
});
it('builds encoded profile, post, and AI music links', () => {
expect(buildProfileShareUrl('https://web', { username: 'a/b' })).toBe('https://web/u/a%2Fb');
expect(buildProfileShareUrl('https://web', { _id: { toString: () => 'id one' } })).toBe('https://web/profile/id%20one');
expect(buildPostShareUrl('https://web', 'post/1')).toBe('https://web/posts/post%2F1');
expect(buildAiMusicShareUrl('https://web', 'track 1')).toBe('https://web/ai/music/track%201');
});
});
+9
View File
@@ -0,0 +1,9 @@
import { verifyTotp } from './totp.util';
describe('verifyTotp', () => {
it('accepts a known code and rejects invalid values', () => {
expect(verifyTotp('JBSWY3DPEHPK3PXP', '282760', 0)).toBe(true);
expect(verifyTotp('JBSWY3DPEHPK3PXP', '000000', 0)).toBe(false);
expect(verifyTotp('not-base32!', '282760', 0)).toBe(false);
});
});
+39
View File
@@ -0,0 +1,39 @@
import { createHmac, timingSafeEqual } from 'crypto';
const decodeBase32 = (value: string): Buffer => {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
const normalized = value.toUpperCase().replace(/=|\s|-/g, '');
let bits = '';
for (const character of normalized) {
const index = alphabet.indexOf(character);
if (index < 0) return Buffer.alloc(0);
bits += index.toString(2).padStart(5, '0');
}
const bytes: number[] = [];
for (let offset = 0; offset + 8 <= bits.length; offset += 8) {
bytes.push(Number.parseInt(bits.slice(offset, offset + 8), 2));
}
return Buffer.from(bytes);
};
const generateTotp = (secret: Buffer, counter: number): string => {
const message = Buffer.alloc(8);
message.writeBigUInt64BE(BigInt(counter));
const digest = createHmac('sha1', secret).update(message).digest();
const offset = digest[digest.length - 1] & 0x0f;
return ((digest.readUInt32BE(offset) & 0x7fffffff) % 1_000_000)
.toString()
.padStart(6, '0');
};
export const verifyTotp = (base32Secret: string, suppliedCode: string, nowMs = Date.now()): boolean => {
if (!/^\d{6}$/.test(suppliedCode)) return false;
const secret = decodeBase32(base32Secret);
if (!secret.length) return false;
const counter = Math.floor(nowMs / 30_000);
return [-1, 0, 1].filter((drift) => counter + drift >= 0).some((drift) => {
const expected = Buffer.from(generateTotp(secret, counter + drift));
const supplied = Buffer.from(suppliedCode);
return expected.length === supplied.length && timingSafeEqual(expected, supplied);
});
};