Release 2025-05-19

This commit is contained in:
pluja
2025-05-19 10:19:49 +00:00
parent 046c4559e5
commit 2657f936bc
267 changed files with 0 additions and 49432 deletions

View File

@@ -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,
})

View File

@@ -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
}
}

View File

@@ -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,
})

View File

@@ -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,
})

View File

@@ -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,
})