Skip to content

Commit

Permalink
split permissions server and ui code
Browse files Browse the repository at this point in the history
  • Loading branch information
nichtsam committed Jan 29, 2024
1 parent 8b7d5d9 commit 0499f71
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 95 deletions.
2 changes: 1 addition & 1 deletion app/routes/admin+/cache.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
getInstanceInfo,
} from '#app/utils/litefs.server.ts'
import { useDebounce, useDoubleCheck } from '#app/utils/misc.tsx'
import { requireUserWithRole } from '#app/utils/permissions.ts'
import { requireUserWithRole } from '#app/utils/permissions.server.ts'

export const handle: SEOHandle = {
getSitemapEntries: () => null,
Expand Down
2 changes: 1 addition & 1 deletion app/routes/admin+/cache_.lru.$cacheKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { getAllInstances, getInstanceInfo } from 'litefs-js'
import { ensureInstance } from 'litefs-js/remix.js'
import { lruCache } from '#app/utils/cache.server.ts'
import { requireUserWithRole } from '#app/utils/permissions.ts'
import { requireUserWithRole } from '#app/utils/permissions.server.ts'

export async function loader({ request, params }: LoaderFunctionArgs) {
await requireUserWithRole(request, 'admin')
Expand Down
2 changes: 1 addition & 1 deletion app/routes/admin+/cache_.sqlite.$cacheKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { json, type LoaderFunctionArgs } from '@remix-run/node'
import { getAllInstances, getInstanceInfo } from 'litefs-js'
import { ensureInstance } from 'litefs-js/remix.js'
import { cache } from '#app/utils/cache.server.ts'
import { requireUserWithRole } from '#app/utils/permissions.ts'
import { requireUserWithRole } from '#app/utils/permissions.server.ts'

export async function loader({ request, params }: LoaderFunctionArgs) {
await requireUserWithRole(request, 'admin')
Expand Down
7 changes: 2 additions & 5 deletions app/routes/users+/$username_+/notes.$noteId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,9 @@ import { requireUserId } from '#app/utils/auth.server.ts'
import { validateCSRF } from '#app/utils/csrf.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx'
import {
requireUserWithPermission,
userHasPermission,
} from '#app/utils/permissions.ts'
import { requireUserWithPermission } from '#app/utils/permissions.server.ts'
import { redirectWithToast } from '#app/utils/toast.server.ts'
import { useOptionalUser } from '#app/utils/user.ts'
import { userHasPermission, useOptionalUser } from '#app/utils/user.ts'
import { type loader as notesLoader } from './notes.tsx'

export async function loader({ params }: LoaderFunctionArgs) {
Expand Down
60 changes: 60 additions & 0 deletions app/utils/permissions.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { json } from '@remix-run/node'
import { requireUserId } from './auth.server.ts'
import { prisma } from './db.server.ts'
import { type PermissionString, parsePermissionString } from './permissions.ts'

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: {
some: {
...permissionData,
access: permissionData.access
? { in: permissionData.access }
: undefined,
},
},
},
},
},
})
if (!user) {
throw json(
{
error: 'Unauthorized',
requiredPermission: permissionData,
message: `Unauthorized: required permissions: ${permission}`,
},
{ status: 403 },
)
}
return user.id
}

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 } } },
})
if (!user) {
throw json(
{
error: 'Unauthorized',
requiredRole: name,
message: `Unauthorized: required role: ${name}`,
},
{ status: 403 },
)
}
return user.id
}
92 changes: 5 additions & 87 deletions app/utils/permissions.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,11 @@
import { json } from '@remix-run/node'
import { requireUserId } from './auth.server.ts'
import { prisma } from './db.server.ts'
import { type useUser } from './user.ts'

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: {
some: {
...permissionData,
access: permissionData.access
? { in: permissionData.access }
: undefined,
},
},
},
},
},
})
if (!user) {
throw json(
{
error: 'Unauthorized',
requiredPermission: permissionData,
message: `Unauthorized: required permissions: ${permission}`,
},
{ status: 403 },
)
}
return user.id
}

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 } } },
})
if (!user) {
throw json(
{
error: 'Unauthorized',
requiredRole: name,
message: `Unauthorized: required role: ${name}`,
},
{ status: 403 },
)
}
return user.id
}

type Action = 'create' | 'read' | 'update' | 'delete'
type Entity = 'user' | 'note'
type Access = 'own' | 'any' | 'own,any' | 'any,own'
type PermissionString = `${Action}:${Entity}` | `${Action}:${Entity}:${Access}`
function parsePermissionString(permissionString: PermissionString) {
export type PermissionString =
| `${Action}:${Entity}`
| `${Action}:${Entity}:${Access}`

export function parsePermissionString(permissionString: PermissionString) {
const [action, entity, access] = permissionString.split(':') as [
Action,
Entity,
Expand All @@ -75,27 +17,3 @@ function parsePermissionString(permissionString: PermissionString) {
access: access ? (access.split(',') as Array<Access>) : undefined,
}
}

export function userHasPermission(
user: Pick<ReturnType<typeof useUser>, 'roles'> | null | undefined,
permission: PermissionString,
) {
if (!user) return false
const { action, entity, access } = parsePermissionString(permission)
return user.roles.some(role =>
role.permissions.some(
permission =>
permission.entity === entity &&
permission.action === action &&
(!access || access.includes(permission.access)),
),
)
}

export function userHasRole(
user: Pick<ReturnType<typeof useUser>, 'roles'> | null,
role: string,
) {
if (!user) return false
return user.roles.some(r => r.name === role)
}
25 changes: 25 additions & 0 deletions app/utils/user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type SerializeFrom } from '@remix-run/node'
import { useRouteLoaderData } from '@remix-run/react'
import { type loader as rootLoader } from '#app/root.tsx'
import { type PermissionString, parsePermissionString } from './permissions'

function isUser(user: any): user is SerializeFrom<typeof rootLoader>['user'] {
return user && typeof user === 'object' && typeof user.id === 'string'
Expand All @@ -23,3 +24,27 @@ export function useUser() {
}
return maybeUser
}

export function userHasPermission(
user: Pick<ReturnType<typeof useUser>, 'roles'> | null | undefined,
permission: PermissionString,
) {
if (!user) return false
const { action, entity, access } = parsePermissionString(permission)
return user.roles.some(role =>
role.permissions.some(
permission =>
permission.entity === entity &&
permission.action === action &&
(!access || access.includes(permission.access)),
),
)
}

export function userHasRole(
user: Pick<ReturnType<typeof useUser>, 'roles'> | null,
role: string,
) {
if (!user) return false
return user.roles.some(r => r.name === role)
}

0 comments on commit 0499f71

Please sign in to comment.