diff --git a/packages/api/package.json b/packages/api/package.json index 0631570ed..14cc66423 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -77,6 +77,7 @@ "pg": "^8.3.3", "postgrator": "^4.2.0", "private-ip": "^2.3.3", + "redis": "^4.3.1", "sanitize-html": "^2.3.2", "search-query-parser": "^1.6.0", "snake-case": "^3.0.3", diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 62dbc8d40..de07a3315 100755 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -46,6 +46,7 @@ import rateLimit from 'express-rate-limit' import { webhooksServiceRouter } from './routers/svc/webhooks' import { integrationsServiceRouter } from './routers/svc/integrations' import { textToSpeechRouter } from './routers/text_to_speech' +import { connectRedisClient, redisClient } from './utils/redis' const PORT = process.env.PORT || 4000 @@ -133,6 +134,18 @@ export const createApp = (): { // The error handler must be before any other error middleware and after all routes app.use(Sentry.Handlers.errorHandler()) + // set user device from request header to Redis + app.use('/api/', async (req, res, next) => { + const device = req.header('X-Device') + const token = + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + req.header('Authorization') || (req.cookies['auth'] as string | undefined) + if (device && token) { + await redisClient.set(`device:${token}`, device) + } + next() + }) + const apollo = makeApolloServer() const httpServer = createServer(app) @@ -148,6 +161,8 @@ const main = async (): Promise => { await initElasticsearch() + await connectRedisClient() + const { app, apollo, httpServer } = createApp() await apollo.start() diff --git a/packages/api/src/util.ts b/packages/api/src/util.ts index 0ca9ad8b8..5d3f52fb0 100755 --- a/packages/api/src/util.ts +++ b/packages/api/src/util.ts @@ -97,6 +97,10 @@ interface BackendEnv { gcp: { location: string } + redis: { + url?: string + cert?: string + } } /*** @@ -152,6 +156,8 @@ const nullableEnvVars = [ 'AZURE_SPEECH_KEY', 'AZURE_SPEECH_REGION', 'GCP_LOCATION', + 'REDIS_URL', + 'REDIS_CERT', ] // Allow some vars to be null/empty /* If not in GAE and Prod/QA/Demo env (f.e. on localhost/dev env), allow following env vars to be null */ @@ -281,6 +287,11 @@ export function getEnv(): BackendEnv { location: parse('GCP_LOCATION'), } + const redis = { + url: parse('REDIS_URL'), + cert: parse('REDIS_CERT'), + } + return { pg, client, @@ -301,6 +312,7 @@ export function getEnv(): BackendEnv { readwise, azure, gcp, + redis, } } diff --git a/packages/api/src/utils/redis.ts b/packages/api/src/utils/redis.ts new file mode 100644 index 000000000..6550f435a --- /dev/null +++ b/packages/api/src/utils/redis.ts @@ -0,0 +1,25 @@ +import { createClient } from 'redis' +import { env } from '../env' + +export const redisClient = createClient({ + url: env.redis.url, + socket: { + tls: env.redis.url?.startsWith('rediss://'), // rediss:// is the protocol for TLS + cert: env.redis.cert?.replace(/\\n/g, '\n'), // replace \n with new line + rejectUnauthorized: false, // for self-signed certs + connectTimeout: 10000, // 10 seconds + reconnectStrategy(retries: number): number | Error { + if (retries > 10) { + return new Error('Retries exhausted') + } + return 1000 + }, + }, +}) + +export const connectRedisClient = async () => { + redisClient.on('error', (err) => console.error('Redis Client Error', err)) + + await redisClient.connect() + console.log('Redis Client Connected') +}