Created
July 18, 2022 10:53
-
-
Save beerose/80f37b4b36cbd7ba2745701959e3cb8b to your computer and use it in GitHub Desktop.
Blitz.js auth + Redis example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import IoRedis from 'ioredis'; | |
import { setupBlitz } from '@blitzjs/next'; | |
import { AuthServerPlugin, simpleRolesIsAuthorized, SessionModel, Session } from '@blitzjs/auth'; | |
const dbs: Record<string, IoRedis.Redis | undefined> = { | |
default: undefined, | |
auth: undefined | |
}; | |
export function getRedis(): IoRedis.Redis { | |
if (dbs.default) { | |
return dbs.default; | |
} | |
return (dbs.default = createRedis(0)); | |
} | |
export function getAuthRedis(): IoRedis.Redis { | |
if (dbs.auth) { | |
return dbs.auth; | |
} | |
return (dbs.auth = createRedis(1)); | |
} | |
export function createRedis(db: number) { | |
return new IoRedis({ | |
port: 6379, | |
host: 'localhost', | |
keepAlive: 60, | |
keyPrefix: 'auth:', | |
db | |
}); | |
} | |
const { gSSP, gSP, api } = setupBlitz({ | |
plugins: [ | |
AuthServerPlugin({ | |
cookiePrefix: 'blitz-app-prefix', | |
isAuthorized: simpleRolesIsAuthorized, | |
storage: { | |
createSession: (session: SessionModel): Promise<SessionModel> => { | |
return new Promise<SessionModel>((resolve, reject) => { | |
getAuthRedis().set(`token:${session.handle}`, JSON.stringify(session), (err) => { | |
if (err) { | |
reject(err); | |
} else { | |
getAuthRedis().lpush(`device:${String(session.userId)}`, session.handle); | |
resolve(session); | |
} | |
}); | |
}); | |
}, | |
deleteSession(handle: string): Promise<SessionModel> { | |
return new Promise<SessionModel>((resolve, reject) => { | |
getAuthRedis().get(`token:${handle}`).then((result) => { | |
if (result) { | |
const session = JSON.parse(result) as SessionModel; | |
const userId = (session.userId as unknown) as string; | |
getAuthRedis().lrem(userId, 0, handle).catch(reject); | |
} | |
getAuthRedis().del(handle, (err) => { | |
if (err) { | |
reject(err); | |
} else { | |
resolve({ handle }); | |
} | |
}); | |
}); | |
}); | |
}, | |
getSession(handle: string): Promise<SessionModel | null> { | |
return new Promise<SessionModel | null>((resolve, reject) => { | |
getAuthRedis() | |
.get(`token:${handle}`) | |
.then((data: string | null) => { | |
if (data) { | |
resolve(JSON.parse(data)); | |
} else { | |
resolve(null); | |
} | |
}) | |
.catch(reject); | |
}); | |
}, | |
getSessions(userId: Session.PublicData['userId']): Promise<SessionModel[]> { | |
return new Promise<SessionModel[]>((resolve, reject) => { | |
getAuthRedis() | |
.lrange(`device:${String(userId)}`, 0, -1) | |
.then((result) => { | |
if (result) { | |
resolve( | |
result.map((handle) => { | |
return this.getSession(handle); | |
}) | |
); | |
} else { | |
resolve([]); | |
} | |
}) | |
.catch(reject); | |
}); | |
}, | |
updateSession(handle: string, session: Partial<SessionModel>): Promise<SessionModel> { | |
return new Promise<SessionModel>((resolve, reject) => { | |
getAuthRedis().get(`token:${handle}`).then((result) => { | |
if (result) { | |
const oldSession = JSON.parse(result) as SessionModel; | |
const merge = Object.assign(oldSession, session); | |
getAuthRedis().set(`token:${handle}`, JSON.stringify(merge)).catch(reject); | |
} | |
reject(new Error('cant update session')); | |
}); | |
}); | |
} | |
} | |
}) | |
] | |
}); | |
export { gSSP, gSP, api }; |
Hey, I've just created a new version with Redis TTL Feature that I think will help manage applications on bigger scales.
import { SessionConfigMethods, SessionModel } from "@blitzjs/auth"
import { loadEnvConfig } from "@next/env"
import { differenceInSeconds } from "date-fns"
import IoRedis from "ioredis"
// Type validation filter for null / undefined values
const notEmpty = <TValue>(value: TValue | null | undefined): value is TValue => {
if (value === null || value === undefined) return false
// eslint-disable-next-line no-unused-vars
const testDummy: TValue = value
return true
}
const { REDIS_HOST, REDIS_PORT, REDIS_USER, REDIS_PASSWORD } = loadEnvConfig(
process.cwd()
).combinedEnv
/**
* Global is used here to ensure the connection
* is cached across hot-reloads in development
*
* see https://github.com/vercel/next.js/discussions/12229#discussioncomment-83372
*/
let redisClient: Record<string, IoRedis> = global.redisClient
if (!redisClient) redisClient = global.redisClient = {}
const getRedisClient = () => {
if (!redisClient.authClient) {
redisClient.authClient = new IoRedis({
port: Number(REDIS_PORT!),
host: REDIS_HOST!,
username: REDIS_USER!,
password: REDIS_PASSWORD!,
})
}
return redisClient.authClient
}
const getSession: SessionConfigMethods["getSession"] = async (handle: string) => {
const client = getRedisClient()
const session = await client.get(`session:${handle}`)
if (!session) {
return null
}
const parsedSession = JSON.parse(session) as SessionModel
if (parsedSession.expiresAt) {
parsedSession.expiresAt = new Date(parsedSession.expiresAt)
const expiryInSeconds = differenceInSeconds(parsedSession.expiresAt!, new Date())
await client.expire(`session:${parsedSession.handle}`, expiryInSeconds)
await client.expire(`user:${parsedSession.userId!}`, expiryInSeconds)
}
return parsedSession
}
const getSessions: SessionConfigMethods["getSessions"] = async (userId: string) => {
const client = getRedisClient()
const sessionKeys = await client.lrange(`user:${userId}`, 0, -1)
const sessions = (
await Promise.all(
sessionKeys.map(async (handle) => {
const session = await getSession(handle)
return session
})
)
).filter(notEmpty)
return sessions
}
const createSession: SessionConfigMethods["createSession"] = async (session: SessionModel) => {
const client = getRedisClient()
const expiryInSeconds = differenceInSeconds(session.expiresAt!, new Date())
await client.set(`session:${session.handle}`, JSON.stringify(session), "EX", expiryInSeconds)
await client.lpush(`user:${session.userId}`, session.handle)
await client.expire(`user:${session.userId}`, expiryInSeconds)
return session
}
const deleteSession: SessionConfigMethods["deleteSession"] = async (handle: string) => {
const client = getRedisClient()
const session = await getSession(handle)
if (session) {
await client.lrem(`user:${session.userId}`, 0, handle)
await client.del(`session:${session.handle}`)
return session
}
return undefined
}
const updateSession: SessionConfigMethods["updateSession"] = async (
handle: string,
session: SessionModel
) => {
const client = getRedisClient()
const oldSession = await getSession(handle)
if (oldSession) {
const newSession = Object.assign(oldSession, session)
const expiryInSeconds = differenceInSeconds(newSession.expiresAt!, new Date())
await client.set(`session:${handle}`, JSON.stringify(newSession), "EX", expiryInSeconds)
await client.expire(`user:${session.userId}`, expiryInSeconds)
return newSession
}
return undefined
}
export default {
getSession,
getSessions,
createSession,
deleteSession,
updateSession,
} as SessionConfigMethods
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
very helpful gist and gave me a good starting point when enabling redis on my blitz app, only issue I ran into is in getSession. Redis stores expiresAt as a standard string so it needs to be converted to a JS Date Object or blitz throws an error when it attempts to determine if the session should have expired with isPast(date). Solved it similar to this code snippet