Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions app/utils/auth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import bcrypt from 'bcryptjs'
import { redirect } from 'react-router'
import { Authenticator } from 'remix-auth'
import { safeRedirect } from 'remix-utils/safe-redirect'
import { cachified, lruCache } from './cache.server.ts'
import { providers } from './connections.server.ts'
import { prisma } from './db.server.ts'
import { combineHeaders, downloadFile } from './misc.tsx'
Expand All @@ -19,6 +20,39 @@ export const sessionKey = 'sessionId'

export const authenticator = new Authenticator<ProviderUser>()

const sessionCacheKey = (sessionId: string) => `session-user-id:${sessionId}`

type SessionUserIdCacheEntry = {
userId: string
expirationDate: string
}

async function getCachedSessionEntry(sessionId: string) {
return cachified<SessionUserIdCacheEntry | null>({
key: sessionCacheKey(sessionId),
cache: lruCache,
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
ttl: SESSION_EXPIRATION_TIME,
async getFreshValue(context) {
const session = await prisma.session.findUnique({
select: { userId: true, expirationDate: true },
where: { id: sessionId },
})
if (!session) return null
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
const now = Date.now()
const expiresAt = session.expirationDate.getTime()
if (expiresAt <= now) {
context.metadata.ttl = 0
return null
}
context.metadata.ttl = expiresAt - now
return {
userId: session.userId,
expirationDate: session.expirationDate.toISOString(),
}
},
})
}
Comment thread
cursor[bot] marked this conversation as resolved.

for (const [providerName, provider] of Object.entries(providers)) {
const strategy = provider.getAuthStrategy()
if (strategy) {
Expand All @@ -32,18 +66,24 @@ export async function getUserId(request: Request) {
)
const sessionId = authSession.get(sessionKey)
if (!sessionId) return null
const session = await prisma.session.findUnique({
select: { userId: true },
where: { id: sessionId, expirationDate: { gt: new Date() } },
})
if (!session?.userId) {
const cachedSession = await getCachedSessionEntry(sessionId)
if (!cachedSession) {
throw redirect('/', {
headers: {
'set-cookie': await authSessionStorage.destroySession(authSession),
},
})
}
const expirationDate = new Date(cachedSession.expirationDate)
if (expirationDate <= new Date()) {
lruCache.delete(sessionCacheKey(sessionId))
throw redirect('/', {
headers: {
'set-cookie': await authSessionStorage.destroySession(authSession),
},
})
}
return session.userId
return cachedSession.userId
}

export async function requireUserId(
Expand Down Expand Up @@ -217,6 +257,7 @@ export async function logout(
// if this fails, we still need to delete the session from the user's browser
// and it doesn't do any harm staying in the db anyway.
if (sessionId) {
lruCache.delete(sessionCacheKey(sessionId))
// the .catch is important because that's what triggers the query.
// learn more about PrismaPromise: https://www.prisma.io/docs/orm/reference/prisma-client-reference#prismapromise-behavior
void prisma.session.deleteMany({ where: { id: sessionId } }).catch(() => {})
Expand Down
60 changes: 41 additions & 19 deletions app/utils/permissions.server.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,47 @@
import { data } from 'react-router'
import { cachified, lruCache } from './cache.server.ts'
import { requireUserId } from './auth.server.ts'

Check warning on line 3 in app/utils/permissions.server.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`./auth.server.ts` import should occur before import of `./cache.server.ts`
import { prisma } from './db.server.ts'
import { type PermissionString, parsePermissionString } from './user.ts'

const permissionCacheKey = (userId: string, permission: PermissionString) =>
`permission-check:${userId}:${permission}`
const roleCacheKey = (userId: string, name: string) =>
`role-check:${userId}:${name}`

export async function requireUserWithPermission(
request: Request,
permission: PermissionString,
) {
const userId = await requireUserId(request)
const permissionData = parsePermissionString(permission)
const user = await prisma.user.findFirst({
select: { id: true },
where: {
id: userId,
roles: {
some: {
permissions: {
const allowed = await cachified({
key: permissionCacheKey(userId, permission),
cache: lruCache,
ttl: 1000 * 60 * 2,
async getFreshValue() {
const user = await prisma.user.findFirst({
select: { id: true },
where: {
id: userId,
roles: {
some: {
...permissionData,
access: permissionData.access
? { in: permissionData.access }
: undefined,
permissions: {
some: {
...permissionData,
access: permissionData.access
? { in: permissionData.access }
: undefined,
},
},
},
},
},
},
})
return Boolean(user)
Comment thread
cursor[bot] marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
},
})
if (!user) {
if (!allowed) {
throw data(
{
error: 'Unauthorized',
Expand All @@ -37,16 +51,24 @@
{ status: 403 },
)
}
return user.id
return userId
}

export async function requireUserWithRole(request: Request, name: string) {
const userId = await requireUserId(request)
const user = await prisma.user.findFirst({
select: { id: true },
where: { id: userId, roles: { some: { name } } },
const allowed = await cachified({
key: roleCacheKey(userId, name),
cache: lruCache,
ttl: 1000 * 60 * 2,
async getFreshValue() {
const user = await prisma.user.findFirst({
select: { id: true },
where: { id: userId, roles: { some: { name } } },
})
return Boolean(user)
},
})
if (!user) {
if (!allowed) {
throw data(
{
error: 'Unauthorized',
Expand All @@ -56,5 +78,5 @@
{ status: 403 },
)
}
return user.id
return userId
}
Loading