refactor: export TipiCache as a non-instantiated class

This commit is contained in:
Nicolas Meienberger 2023-08-28 21:44:53 +02:00 committed by Nicolas Meienberger
parent 4e6a6b713d
commit 1d41f5de9b
13 changed files with 78 additions and 51 deletions

View File

@ -1,6 +1,7 @@
const values = new Map();
const expirations = new Map();
export const createClient = jest.fn(() => {
const values = new Map();
const expirations = new Map();
return {
isOpen: true,
connect: jest.fn(),

View File

@ -1,11 +1,17 @@
import merge from 'lodash.merge';
import { deleteCookie, setCookie } from 'cookies-next';
import { fromPartial } from '@total-typescript/shoehorn';
import TipiCache from '@/server/core/TipiCache/TipiCache';
import { TipiCache } from '@/server/core/TipiCache';
import { getAuthedPageProps, getMessagesPageProps } from '../page-helpers';
import englishMessages from '../../messages/en.json';
import frenchMessages from '../../messages/fr-FR.json';
const cache = new TipiCache();
afterAll(async () => {
await cache.close();
});
describe('test: getAuthedPageProps()', () => {
it('should redirect to /login if there is no user id in session', async () => {
// arrange
@ -23,7 +29,7 @@ describe('test: getAuthedPageProps()', () => {
it('should return props if there is a user id in session', async () => {
// arrange
const ctx = { req: { headers: { 'x-session-id': '123' } } };
await TipiCache.set('session:123', '456');
await cache.set('session:123', '456');
// act
// @ts-expect-error - we're passing in a partial context

View File

@ -2,11 +2,13 @@ import { GetServerSideProps } from 'next';
import merge from 'lodash.merge';
import { getLocaleFromString } from '@/shared/internationalization/locales';
import { getCookie } from 'cookies-next';
import TipiCache from '@/server/core/TipiCache/TipiCache';
import { TipiCache } from '@/server/core/TipiCache';
export const getAuthedPageProps: GetServerSideProps = async (ctx) => {
const cache = new TipiCache();
const sessionId = ctx.req.headers['x-session-id'];
const userId = await TipiCache.get(`session:${sessionId}`);
const userId = await cache.get(`session:${sessionId}`);
await cache.close();
if (!userId) {
return {

View File

@ -1,5 +1,5 @@
import { getConfig } from '@/server/core/TipiConfig/TipiConfig';
import TipiCache from '@/server/core/TipiCache/TipiCache';
import { TipiCache } from '@/server/core/TipiCache/TipiCache';
import { AuthQueries } from '@/server/queries/auth/auth.queries';
import { db } from '@/server/db';
@ -13,12 +13,16 @@ import fs from 'fs-extra';
* @param {NextApiResponse} res - The response
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const cache = new TipiCache();
const authService = new AuthQueries(db);
const sessionId = req.headers['x-session-id'];
const userId = await TipiCache.get(`session:${sessionId}`);
const userId = await cache.get(`session:${sessionId}`);
const user = await authService.getUserById(Number(userId));
await cache.close();
if (user?.operator) {
const filePath = `${getConfig().rootFolder}/traefik/tls/cert.pem`;

View File

@ -1,7 +1,7 @@
import { setCookie } from 'cookies-next';
import { NextApiRequest, NextApiResponse } from 'next';
import { v4 } from 'uuid';
import TipiCache from '../core/TipiCache/TipiCache';
import { TipiCache } from '../core/TipiCache/TipiCache';
const COOKIE_MAX_AGE = 60 * 60 * 24; // 1 day
const COOKIE_NAME = 'tipi.sid';
@ -11,10 +11,14 @@ export const generateSessionId = (prefix: string) => {
};
export const setSession = async (sessionId: string, userId: string, req: NextApiRequest, res: NextApiResponse) => {
const cache = new TipiCache();
setCookie(COOKIE_NAME, sessionId, { req, res, maxAge: COOKIE_MAX_AGE, httpOnly: true, secure: true, sameSite: false });
const sessionKey = `session:${sessionId}`;
await TipiCache.set(sessionKey, userId);
await TipiCache.set(`session:${userId}:${sessionId}`, sessionKey);
await cache.set(sessionKey, userId);
await cache.set(`session:${userId}:${sessionId}`, sessionKey);
await cache.close();
};

View File

@ -1,6 +1,6 @@
import { inferAsyncReturnType } from '@trpc/server';
import { CreateNextContextOptions } from '@trpc/server/adapters/next';
import TipiCache from './core/TipiCache/TipiCache';
import { TipiCache } from './core/TipiCache/TipiCache';
type CreateContextOptions = {
req: CreateNextContextOptions['req'];
@ -27,11 +27,14 @@ const createContextInner = async (opts: CreateContextOptions) => ({
* @param {CreateNextContextOptions} opts - options
*/
export const createContext = async (opts: CreateNextContextOptions) => {
const cache = new TipiCache();
const { req, res } = opts;
const sessionId = req.headers['x-session-id'] as string;
const userId = await TipiCache.get(`session:${sessionId}`);
const userId = await cache.get(`session:${sessionId}`);
await cache.close();
return createContextInner({
req,

View File

@ -4,7 +4,7 @@ import { getConfig } from '../TipiConfig';
const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
class TipiCache {
export class TipiCache {
private static instance: TipiCache;
private client: RedisClientType;
@ -78,5 +78,3 @@ class TipiCache {
return client.ttl(key);
}
}
export default TipiCache.getInstance();

View File

@ -1 +1 @@
export { default } from './TipiCache';
export { TipiCache } from './TipiCache';

View File

@ -7,7 +7,7 @@ const AuthService = new AuthServiceClass(db);
export const authRouter = router({
login: publicProcedure.input(z.object({ username: z.string(), password: z.string() })).mutation(async ({ input, ctx }) => AuthService.login({ ...input }, ctx.req, ctx.res)),
logout: protectedProcedure.mutation(async ({ ctx }) => AuthServiceClass.logout(ctx.sessionId)),
logout: protectedProcedure.mutation(async ({ ctx }) => AuthService.logout(ctx.sessionId)),
register: publicProcedure
.input(z.object({ username: z.string(), password: z.string(), locale: z.string() }))
.mutation(async ({ input, ctx }) => AuthService.register({ ...input }, ctx.req, ctx.res)),

View File

@ -11,12 +11,14 @@ import { encrypt } from '../../utils/encryption';
import { setConfig } from '../../core/TipiConfig';
import { createUser, getUserByEmail, getUserById } from '../../tests/user.factory';
import { AuthServiceClass } from './auth.service';
import TipiCache from '../../core/TipiCache';
import { TipiCache } from '../../core/TipiCache';
let AuthService: AuthServiceClass;
let database: TestDatabase;
const TEST_SUITE = 'authservice';
const cache = new TipiCache();
beforeAll(async () => {
setConfig('jwtSecret', 'test');
database = await createDatabase(TEST_SUITE);
@ -30,6 +32,7 @@ beforeEach(async () => {
afterAll(async () => {
await closeDatabase(database);
await cache.close();
});
describe('Login', () => {
@ -51,7 +54,7 @@ describe('Login', () => {
const sessionId = session.split(';')[0]?.split('=')[1];
const sessionKey = `session:${sessionId}`;
const userId = await TipiCache.get(sessionKey);
const userId = await cache.get(sessionKey);
// assert
expect(userId).toBeDefined();
@ -105,12 +108,12 @@ describe('Test: verifyTotp', () => {
const totpSessionId = generateSessionId('otp');
const otp = TotpAuthenticator.generate(totpSecret);
await TipiCache.set(totpSessionId, user.id.toString());
await cache.set(totpSessionId, user.id.toString());
// act
const result = await AuthService.verifyTotp({ totpSessionId, totpCode: otp }, fromPartial({}), fromPartial(res));
const sessionId = session.split(';')[0]?.split('=')[1];
const userId = await TipiCache.get(`session:${sessionId}`);
const userId = await cache.get(`session:${sessionId}`);
// assert
expect(result).toBeTruthy();
@ -128,7 +131,7 @@ describe('Test: verifyTotp', () => {
const encryptedTotpSecret = encrypt(totpSecret, salt);
const user = await createUser({ email, totpEnabled: true, totpSecret: encryptedTotpSecret, salt }, database);
const totpSessionId = generateSessionId('otp');
await TipiCache.set(totpSessionId, user.id.toString());
await cache.set(totpSessionId, user.id.toString());
// act & assert
await expect(AuthService.verifyTotp({ totpSessionId, totpCode: 'wrong' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-invalid-code');
@ -144,7 +147,7 @@ describe('Test: verifyTotp', () => {
const totpSessionId = generateSessionId('otp');
const otp = TotpAuthenticator.generate(totpSecret);
await TipiCache.set(totpSessionId, user.id.toString());
await cache.set(totpSessionId, user.id.toString());
// act & assert
await expect(AuthService.verifyTotp({ totpSessionId: 'wrong', totpCode: otp }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-session-not-found');
@ -153,7 +156,7 @@ describe('Test: verifyTotp', () => {
it('should throw if the user does not exist', async () => {
// arrange
const totpSessionId = generateSessionId('otp');
await TipiCache.set(totpSessionId, '1234');
await cache.set(totpSessionId, '1234');
// act & assert
await expect(AuthService.verifyTotp({ totpSessionId, totpCode: '1234' }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.user-not-found');
@ -169,7 +172,7 @@ describe('Test: verifyTotp', () => {
const totpSessionId = generateSessionId('otp');
const otp = TotpAuthenticator.generate(totpSecret);
await TipiCache.set(totpSessionId, user.id.toString());
await cache.set(totpSessionId, user.id.toString());
// act & assert
await expect(AuthService.verifyTotp({ totpSessionId, totpCode: otp }, fromPartial({}), fromPartial({}))).rejects.toThrowError('server-messages.errors.totp-not-enabled');
@ -477,7 +480,7 @@ describe('Register', () => {
describe('Test: logout', () => {
it('Should return true if there is no session to delete', async () => {
// act
const result = await AuthServiceClass.logout('session');
const result = await AuthService.logout('session');
// assert
expect(result).toBe(true);
@ -487,11 +490,11 @@ describe('Test: logout', () => {
// arrange
const sessionId = v4();
await TipiCache.set(`session:${sessionId}`, '1');
await cache.set(`session:${sessionId}`, '1');
// act
const result = await AuthServiceClass.logout(sessionId);
const session = await TipiCache.get(`session:${sessionId}`);
const result = await AuthService.logout(sessionId);
const session = await cache.get(`session:${sessionId}`);
// assert
expect(result).toBe(true);
@ -715,14 +718,14 @@ describe('Test: changePassword', () => {
const email = faker.internet.email();
const user = await createUser({ email }, database);
const newPassword = faker.internet.password();
await TipiCache.set(`session:${user.id}:${faker.lorem.word()}`, 'test');
await cache.set(`session:${user.id}:${faker.lorem.word()}`, 'test');
// act
await AuthService.changePassword({ userId: user.id, newPassword, currentPassword: 'password' });
// assert
// eslint-disable-next-line testing-library/no-await-sync-query
const sessions = await TipiCache.getByPrefix(`session:${user.id}:`);
const sessions = await cache.getByPrefix(`session:${user.id}:`);
expect(sessions).toHaveLength(0);
});
});

View File

@ -9,7 +9,7 @@ import { generateSessionId, setSession } from '@/server/common/session.helpers';
import { Database } from '@/server/db';
import { NextApiRequest, NextApiResponse } from 'next';
import { getConfig } from '../../core/TipiConfig';
import TipiCache from '../../core/TipiCache';
import { TipiCache } from '../../core/TipiCache';
import { fileExists, unlinkFile } from '../../common/fs.helpers';
import { decrypt, encrypt } from '../../utils/encryption';
@ -22,8 +22,11 @@ type UsernamePasswordInput = {
export class AuthServiceClass {
private queries;
private cache;
constructor(p: Database) {
this.queries = new AuthQueries(p);
this.cache = new TipiCache();
}
/**
@ -49,7 +52,7 @@ export class AuthServiceClass {
if (user.totpEnabled) {
const totpSessionId = generateSessionId('otp');
await TipiCache.set(totpSessionId, user.id.toString());
await this.cache.set(totpSessionId, user.id.toString());
return { totpSessionId };
}
@ -70,7 +73,7 @@ export class AuthServiceClass {
*/
public verifyTotp = async (params: { totpSessionId: string; totpCode: string }, req: NextApiRequest, res: NextApiResponse) => {
const { totpSessionId, totpCode } = params;
const userId = await TipiCache.get(totpSessionId);
const userId = await this.cache.get(totpSessionId);
if (!userId) {
throw new TranslatedError('server-messages.errors.totp-session-not-found');
@ -261,8 +264,8 @@ export class AuthServiceClass {
* @param {string} sessionId - The session token to remove
* @returns {Promise<boolean>} - Returns true if the session token is removed successfully
*/
public static logout = async (sessionId: string): Promise<boolean> => {
await TipiCache.del(`session:${sessionId}`);
public logout = async (sessionId: string): Promise<boolean> => {
await this.cache.del(`session:${sessionId}`);
return true;
};
@ -341,12 +344,12 @@ export class AuthServiceClass {
* @param {number} userId - The user ID
*/
private destroyAllSessionsByUserId = async (userId: number) => {
const sessions = await TipiCache.getByPrefix(`session:${userId}:`);
const sessions = await this.cache.getByPrefix(`session:${userId}:`);
await Promise.all(
sessions.map(async (session) => {
await TipiCache.del(session.key);
if (session.val) await TipiCache.del(session.val);
await this.cache.del(session.key);
if (session.val) await this.cache.del(session.val);
}),
);
};

View File

@ -5,7 +5,7 @@ import semver from 'semver';
import { faker } from '@faker-js/faker';
import { EventDispatcher } from '../../core/EventDispatcher';
import { setConfig } from '../../core/TipiConfig';
import TipiCache from '../../core/TipiCache';
import { TipiCache } from '../../core/TipiCache';
import { SystemServiceClass } from '.';
jest.mock('redis');
@ -14,6 +14,8 @@ const SystemService = new SystemServiceClass();
const server = setupServer();
const cache = new TipiCache();
beforeEach(async () => {
await setConfig('demoMode', false);
@ -71,14 +73,15 @@ describe('Test: getVersion', () => {
server.listen();
});
beforeEach(() => {
beforeEach(async () => {
server.resetHandlers();
TipiCache.del('latestVersion');
await cache.del('latestVersion');
});
afterAll(() => {
afterAll(async () => {
server.close();
jest.restoreAllMocks();
await cache.close();
});
it('It should return version with body', async () => {
@ -163,7 +166,7 @@ describe('Test: update', () => {
// Arrange
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: true });
setConfig('version', '0.0.1');
TipiCache.set('latestVersion', '0.0.2');
await cache.set('latestVersion', '0.0.2');
// Act
const update = await SystemService.update();
@ -174,7 +177,7 @@ describe('Test: update', () => {
it('Should throw an error if latest version is not set', async () => {
// Arrange
TipiCache.del('latestVersion');
await cache.del('latestVersion');
server.use(
rest.get('https://api.github.com/repos/meienberger/runtipi/releases/latest', (_, res, ctx) => {
return res(ctx.json({ name: null }));
@ -189,7 +192,7 @@ describe('Test: update', () => {
it('Should throw if current version is higher than latest', async () => {
// Arrange
setConfig('version', '0.0.2');
TipiCache.set('latestVersion', '0.0.1');
await cache.set('latestVersion', '0.0.1');
// Act & Assert
await expect(SystemService.update()).rejects.toThrow('server-messages.errors.current-version-is-latest');
@ -198,7 +201,7 @@ describe('Test: update', () => {
it('Should throw if current version is equal to latest', async () => {
// Arrange
setConfig('version', '0.0.1');
TipiCache.set('latestVersion', '0.0.1');
await cache.set('latestVersion', '0.0.1');
// Act & Assert
await expect(SystemService.update()).rejects.toThrow('server-messages.errors.current-version-is-latest');
@ -207,7 +210,7 @@ describe('Test: update', () => {
it('Should throw an error if there is a major version difference', async () => {
// Arrange
setConfig('version', '0.0.1');
TipiCache.set('latestVersion', '1.0.0');
await cache.set('latestVersion', '1.0.0');
// Act & Assert
await expect(SystemService.update()).rejects.toThrow('server-messages.errors.major-version-update');

View File

@ -5,7 +5,7 @@ import { TranslatedError } from '@/server/utils/errors';
import { readJsonFile } from '../../common/fs.helpers';
import { EventDispatcher } from '../../core/EventDispatcher';
import { Logger } from '../../core/Logger';
import TipiCache from '../../core/TipiCache';
import { TipiCache } from '../../core/TipiCache';
import * as TipiConfig from '../../core/TipiConfig';
const SYSTEM_STATUS = ['UPDATING', 'RESTARTING', 'RUNNING'] as const;
@ -33,7 +33,7 @@ export class SystemServiceClass {
private dispatcher;
constructor() {
this.cache = TipiCache;
this.cache = new TipiCache();
this.dispatcher = EventDispatcher;
}