Release 2025-05-19
This commit is contained in:
@@ -1,69 +0,0 @@
|
||||
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'
|
||||
|
||||
const dataSchema = z.object({
|
||||
actionName: z.string(),
|
||||
actionResult: z.union([
|
||||
z.object({
|
||||
type: z.literal('data'),
|
||||
contentType: z.literal('application/json+devalue'),
|
||||
status: z.literal(200),
|
||||
body: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('error'),
|
||||
contentType: z.literal('application/json'),
|
||||
status: z.number(),
|
||||
body: z.string(),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('empty'),
|
||||
status: z.literal(204),
|
||||
}),
|
||||
]),
|
||||
})
|
||||
|
||||
class RedisActionsSessions extends RedisGenericManager {
|
||||
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), {
|
||||
EX: this.expirationTime,
|
||||
})
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
async get(sessionId: string | null | undefined) {
|
||||
if (!sessionId) return null
|
||||
|
||||
const key = `actions-session:${sessionId}`
|
||||
|
||||
const rawData = await this.redisClient.get(key)
|
||||
if (!rawData) return null
|
||||
|
||||
const data = dataSchema.parse(JSON.parse(rawData))
|
||||
const deserializedActionResult = deserializeActionResult(data.actionResult)
|
||||
|
||||
return {
|
||||
deserializedActionResult,
|
||||
...data,
|
||||
}
|
||||
}
|
||||
|
||||
async delete(sessionId: string | null | undefined) {
|
||||
if (!sessionId) return
|
||||
|
||||
await this.redisClient.del(`actions-session:${sessionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const redisActionsSessions = await RedisActionsSessions.createAndConnect({
|
||||
expirationTime: REDIS_ACTIONS_SESSION_EXPIRY_SECONDS,
|
||||
})
|
||||
@@ -1,45 +0,0 @@
|
||||
import { REDIS_URL } from 'astro:env/server'
|
||||
import { createClient } from 'redis'
|
||||
|
||||
type RedisGenericManagerOptions = {
|
||||
expirationTime: number
|
||||
}
|
||||
|
||||
export abstract class RedisGenericManager {
|
||||
protected redisClient
|
||||
/** The expiration time of the Redis session. In seconds. */
|
||||
readonly expirationTime: number
|
||||
|
||||
/** @deprecated Use {@link createAndConnect} instead */
|
||||
constructor(options: RedisGenericManagerOptions) {
|
||||
this.redisClient = createClient({
|
||||
url: REDIS_URL,
|
||||
})
|
||||
|
||||
this.expirationTime = options.expirationTime
|
||||
|
||||
this.redisClient.on('error', (err) => {
|
||||
console.error(`[${this.constructor.name}] `, err)
|
||||
})
|
||||
}
|
||||
|
||||
/** Closes the Redis connection */
|
||||
async close(): Promise<void> {
|
||||
await this.redisClient.quit()
|
||||
}
|
||||
|
||||
/** Connects to the Redis connection */
|
||||
async connect(): Promise<void> {
|
||||
await this.redisClient.connect()
|
||||
}
|
||||
|
||||
static async createAndConnect<T extends RedisGenericManager>(
|
||||
this: new (options: RedisGenericManagerOptions) => T,
|
||||
options: RedisGenericManagerOptions
|
||||
): Promise<T> {
|
||||
const instance = new this(options)
|
||||
|
||||
await instance.connect()
|
||||
return instance
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { z } from 'astro:content'
|
||||
import { REDIS_IMPERSONATION_SESSION_EXPIRY_SECONDS } from 'astro:env/server'
|
||||
|
||||
import { RedisGenericManager } from './redisGenericManager'
|
||||
|
||||
const dataSchema = z.object({
|
||||
adminId: z.number(),
|
||||
targetId: z.number(),
|
||||
})
|
||||
|
||||
class RedisImpersonationSessions extends RedisGenericManager {
|
||||
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), {
|
||||
EX: this.expirationTime,
|
||||
})
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
async get(sessionId: string | null | undefined) {
|
||||
if (!sessionId) return null
|
||||
|
||||
const key = `impersonation-session:${sessionId}`
|
||||
|
||||
const rawData = await this.redisClient.get(key)
|
||||
if (!rawData) return null
|
||||
|
||||
return dataSchema.parse(JSON.parse(rawData))
|
||||
}
|
||||
|
||||
async delete(sessionId: string | null | undefined) {
|
||||
if (!sessionId) return
|
||||
|
||||
await this.redisClient.del(`impersonation-session:${sessionId}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const redisImpersonationSessions = await RedisImpersonationSessions.createAndConnect({
|
||||
expirationTime: REDIS_IMPERSONATION_SESSION_EXPIRY_SECONDS,
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import { REDIS_PREGENERATED_TOKEN_EXPIRY_SECONDS } from 'astro:env/server'
|
||||
|
||||
import { RedisGenericManager } from './redisGenericManager'
|
||||
|
||||
class RedisPreGeneratedSecretTokens extends RedisGenericManager {
|
||||
/**
|
||||
* 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', {
|
||||
EX: this.expirationTime,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates and consumes a pre-generated token
|
||||
* @param token The token to validate
|
||||
* @returns true if token was valid and consumed, false otherwise
|
||||
*/
|
||||
async validateAndConsumePreGeneratedToken(token: string): Promise<boolean> {
|
||||
const key = `pregenerated-user-secret-token:${token}`
|
||||
const exists = await this.redisClient.exists(key)
|
||||
if (exists) {
|
||||
await this.redisClient.del(key)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const redisPreGeneratedSecretTokens = await RedisPreGeneratedSecretTokens.createAndConnect({
|
||||
expirationTime: REDIS_PREGENERATED_TOKEN_EXPIRY_SECONDS,
|
||||
})
|
||||
@@ -1,78 +0,0 @@
|
||||
import { randomBytes } from 'crypto'
|
||||
|
||||
import { REDIS_USER_SESSION_EXPIRY_SECONDS } from 'astro:env/server'
|
||||
|
||||
import { RedisGenericManager } from './redisGenericManager'
|
||||
|
||||
class RedisSessions extends RedisGenericManager {
|
||||
/**
|
||||
* Generates a random session ID
|
||||
*/
|
||||
private generateSessionId(): string {
|
||||
return randomBytes(32).toString('hex')
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new session for a user
|
||||
* @param userSecretTokenHash The ID of the user
|
||||
* @returns The generated session ID
|
||||
*/
|
||||
async createSession(userSecretTokenHash: string): Promise<string> {
|
||||
const sessionId = this.generateSessionId()
|
||||
// Store the session with user ID
|
||||
await this.redisClient.set(`session:${sessionId}`, userSecretTokenHash, {
|
||||
EX: this.expirationTime,
|
||||
})
|
||||
|
||||
// Store session ID in user's sessions set
|
||||
await this.redisClient.sAdd(`user:${userSecretTokenHash}:sessions`, sessionId)
|
||||
|
||||
return sessionId
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the user ID associated with a session
|
||||
* @param sessionId The session ID to look up
|
||||
* @returns The user ID or null if session not found
|
||||
*/
|
||||
async getUserBySessionId(sessionId: string): Promise<string | null> {
|
||||
const userSecretTokenHash = await this.redisClient.get(`session:${sessionId}`)
|
||||
return userSecretTokenHash
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all sessions for a user
|
||||
* @param userSecretTokenHash The ID of the user whose sessions should be deleted
|
||||
*/
|
||||
async deleteUserSessions(userSecretTokenHash: string): Promise<void> {
|
||||
// Get all session IDs for the user
|
||||
const sessionIds = await this.redisClient.sMembers(`user:${userSecretTokenHash}:sessions`)
|
||||
|
||||
if (sessionIds.length > 0) {
|
||||
// Delete each session
|
||||
// Delete sessions one by one to avoid type issues with spread operator
|
||||
for (const sessionId of sessionIds) {
|
||||
await this.redisClient.del(`session:${sessionId}`)
|
||||
}
|
||||
|
||||
// Delete the set of user's sessions
|
||||
await this.redisClient.del(`user:${userSecretTokenHash}:sessions`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a specific session
|
||||
* @param sessionId The session ID to delete
|
||||
*/
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
const userSecretTokenHash = await this.getUserBySessionId(sessionId)
|
||||
if (userSecretTokenHash) {
|
||||
await this.redisClient.del(`session:${sessionId}`)
|
||||
await this.redisClient.sRem(`user:${userSecretTokenHash}:sessions`, sessionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const redisSessions = await RedisSessions.createAndConnect({
|
||||
expirationTime: REDIS_USER_SESSION_EXPIRY_SECONDS,
|
||||
})
|
||||
Reference in New Issue
Block a user