Release 202506091000

This commit is contained in:
pluja
2025-06-09 10:00:55 +00:00
parent 8b90b3eef6
commit 87f0f36aa1
61 changed files with 5216 additions and 730 deletions

View File

@@ -2,7 +2,6 @@ import { randomUUID } from 'node:crypto'
import { deserializeActionResult } from 'astro:actions'
import { z } from 'astro:content'
import { REDIS_ACTIONS_SESSION_EXPIRY_SECONDS } from 'astro:env/server'
import { RedisGenericManager } from './redisGenericManager'
@@ -29,11 +28,13 @@ const dataSchema = z.object({
})
class RedisActionsSessions extends RedisGenericManager {
private readonly prefix = 'actions_session:'
async store(data: z.input<typeof dataSchema>) {
const sessionId = randomUUID()
const parsedData = dataSchema.parse(data)
await this.redisClient.set(`actions-session:${sessionId}`, JSON.stringify(parsedData), {
await this.redisClient.set(`${this.prefix}${sessionId}`, JSON.stringify(parsedData), {
EX: this.expirationTime,
})
@@ -43,7 +44,7 @@ class RedisActionsSessions extends RedisGenericManager {
async get(sessionId: string | null | undefined) {
if (!sessionId) return null
const key = `actions-session:${sessionId}`
const key = `${this.prefix}${sessionId}`
const rawData = await this.redisClient.get(key)
if (!rawData) return null
@@ -60,10 +61,10 @@ class RedisActionsSessions extends RedisGenericManager {
async delete(sessionId: string | null | undefined) {
if (!sessionId) return
await this.redisClient.del(`actions-session:${sessionId}`)
await this.redisClient.del(`${this.prefix}${sessionId}`)
}
}
export const redisActionsSessions = await RedisActionsSessions.createAndConnect({
expirationTime: REDIS_ACTIONS_SESSION_EXPIRY_SECONDS,
expirationTime: 60 * 5, // 5 minutes in seconds
})

View File

@@ -1,6 +1,9 @@
import { REDIS_URL } from 'astro:env/server'
import { createClient } from 'redis'
import { getServerEnvVariable } from '../serverEnvVariables'
const REDIS_URL = getServerEnvVariable('REDIS_URL')
type RedisGenericManagerOptions = {
expirationTime: number
}

View File

@@ -1,7 +1,6 @@
import { randomUUID } from 'node:crypto'
import { z } from 'astro:content'
import { REDIS_IMPERSONATION_SESSION_EXPIRY_SECONDS } from 'astro:env/server'
import { RedisGenericManager } from './redisGenericManager'
@@ -11,11 +10,13 @@ const dataSchema = z.object({
})
class RedisImpersonationSessions extends RedisGenericManager {
private readonly prefix = 'impersonation_session:'
async store(data: z.input<typeof dataSchema>) {
const sessionId = randomUUID()
const parsedData = dataSchema.parse(data)
await this.redisClient.set(`impersonation-session:${sessionId}`, JSON.stringify(parsedData), {
await this.redisClient.set(`${this.prefix}${sessionId}`, JSON.stringify(parsedData), {
EX: this.expirationTime,
})
@@ -25,7 +26,7 @@ class RedisImpersonationSessions extends RedisGenericManager {
async get(sessionId: string | null | undefined) {
if (!sessionId) return null
const key = `impersonation-session:${sessionId}`
const key = `${this.prefix}${sessionId}`
const rawData = await this.redisClient.get(key)
if (!rawData) return null
@@ -36,10 +37,10 @@ class RedisImpersonationSessions extends RedisGenericManager {
async delete(sessionId: string | null | undefined) {
if (!sessionId) return
await this.redisClient.del(`impersonation-session:${sessionId}`)
await this.redisClient.del(`${this.prefix}${sessionId}`)
}
}
export const redisImpersonationSessions = await RedisImpersonationSessions.createAndConnect({
expirationTime: REDIS_IMPERSONATION_SESSION_EXPIRY_SECONDS,
expirationTime: 60 * 60 * 24, // 24 hours in seconds
})

View File

@@ -1,14 +1,14 @@
import { REDIS_PREGENERATED_TOKEN_EXPIRY_SECONDS } from 'astro:env/server'
import { RedisGenericManager } from './redisGenericManager'
class RedisPreGeneratedSecretTokens extends RedisGenericManager {
private readonly prefix = 'pregenerated_user_secret_token:'
/**
* Stores a pre-generated token with expiration
* @param token The pre-generated token
*/
async storePreGeneratedToken(token: string): Promise<void> {
await this.redisClient.set(`pregenerated-user-secret-token:${token}`, '1', {
await this.redisClient.set(`${this.prefix}${token}`, '1', {
EX: this.expirationTime,
})
}
@@ -19,7 +19,7 @@ class RedisPreGeneratedSecretTokens extends RedisGenericManager {
* @returns true if token was valid and consumed, false otherwise
*/
async validateAndConsumePreGeneratedToken(token: string): Promise<boolean> {
const key = `pregenerated-user-secret-token:${token}`
const key = `${this.prefix}${token}`
const exists = await this.redisClient.exists(key)
if (exists) {
await this.redisClient.del(key)
@@ -30,5 +30,5 @@ class RedisPreGeneratedSecretTokens extends RedisGenericManager {
}
export const redisPreGeneratedSecretTokens = await RedisPreGeneratedSecretTokens.createAndConnect({
expirationTime: REDIS_PREGENERATED_TOKEN_EXPIRY_SECONDS,
expirationTime: 60 * 5, // 5 minutes in seconds
})

View File

@@ -0,0 +1,72 @@
import { allServerEventsData, type ServerEventsData, type ServerEventsEvent } from '../serverEventsTypes'
import { RedisGenericManager } from './redisGenericManager'
export class RedisServerEvents extends RedisGenericManager {
private readonly prefix = 'server_events:'
/**
* Broadcast an event to a user's server events listener.
*
* @param eventName - The event name to broadcast.
* @param userId - The user ID to broadcast to.
* @param data - The event to broadcast.
*/
async send<T extends keyof ServerEventsData>(
userId: number,
eventName: T,
data: ServerEventsData[T]
): Promise<void> {
const channel = `${this.prefix}${String(userId)}:${eventName}` as const
await this.redisClient.publish(channel, JSON.stringify(data))
}
/**
* Subscribe to server events for a user.
*
* @param eventName - The event name to subscribe to.
* @param userId - The user ID to subscribe to.
* @param callback - The callback to call when the event is received.
* @returns A cleanup function to unsubscribe.
*/
async addListener<T extends keyof ServerEventsData | 'all'>(
eventName: T,
userId: number,
callback: (event: T extends 'all' ? ServerEventsEvent : Extract<ServerEventsEvent, { type: T }>) => void
): Promise<() => Promise<void>> {
const subscriber = this.redisClient.duplicate()
await subscriber.connect()
const channel =
eventName === 'all'
? allServerEventsData.map((eventName) => `${this.prefix}${String(userId)}:${eventName}` as const)
: (`${this.prefix}${String(userId)}:${eventName}` as const)
await subscriber.subscribe(channel, (message, channelKey) => {
try {
const data = JSON.parse(message) as ServerEventsData[T extends 'all' ? keyof ServerEventsData : T]
const type = channelKey.split(':')[2] as T extends 'all' ? keyof ServerEventsData : T
const event = { type, data } as T extends 'all'
? ServerEventsEvent
: Extract<ServerEventsEvent, { type: T }>
callback(event)
} catch (error) {
console.error('Failed to parse notification stream event:', error)
}
})
return async () => {
await subscriber.unsubscribe(channel)
subscriber.destroy()
}
}
}
let redisServerEvents: RedisServerEvents | null = null
export async function getRedisServerEvents() {
redisServerEvents ??= await RedisServerEvents.createAndConnect({
expirationTime: 60 * 60 * 24, // 24 hours in seconds
})
return redisServerEvents
}

View File

@@ -1,7 +1,5 @@
import { randomBytes } from 'crypto'
import { REDIS_USER_SESSION_EXPIRY_SECONDS } from 'astro:env/server'
import { RedisGenericManager } from './redisGenericManager'
class RedisSessions extends RedisGenericManager {
@@ -74,5 +72,5 @@ class RedisSessions extends RedisGenericManager {
}
export const redisSessions = await RedisSessions.createAndConnect({
expirationTime: REDIS_USER_SESSION_EXPIRY_SECONDS,
expirationTime: 60 * 60 * 24, // 24 hours in seconds
})