improvement(lib): refactored lib/ to be more aligned with queries and api directory (#2160)

* fix(lib): consolidate into core dir in lib/

* refactored lib/
This commit is contained in:
Waleed
2025-12-02 14:17:41 -08:00
committed by GitHub
parent 6fda9bd72e
commit 41c068c023
707 changed files with 1771 additions and 1748 deletions

View File

@@ -2,8 +2,8 @@ import { db } from '@sim/db'
import { permissions, workflow, workflowFolder, workspace as workspaceTable } from '@sim/db/schema'
import { eq } from 'drizzle-orm'
import { createLogger } from '@/lib/logs/console/logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { duplicateWorkflow } from '@/lib/workflows/duplicate'
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('WorkspaceDuplicate')

View File

@@ -2,7 +2,7 @@
* Utility functions for generating names for workspaces and folders
*/
import type { Workspace } from '@/lib/organization/types'
import type { Workspace } from '@/lib/workspaces/organization/types'
import type { WorkflowFolder } from '@/stores/folders/store'
export interface NameableEntity {

View File

@@ -0,0 +1,23 @@
// Export types
export type {
Invitation,
Member,
MemberUsageData,
Organization,
OrganizationBillingData,
OrganizationFormData,
Subscription,
User,
Workspace,
WorkspaceInvitation,
} from '@/lib/workspaces/organization/types'
// Export utility functions
export {
calculateSeatUsage,
generateSlug,
getUsedSeats,
getUserRole,
isAdminOrOwner,
validateEmail,
validateSlug,
} from '@/lib/workspaces/organization/utils'

View File

@@ -0,0 +1,167 @@
export interface User {
name?: string
email?: string
id?: string
image?: string | null
}
export interface Member {
id: string
role: string
user?: User
}
export interface Invitation {
id: string
email: string
status: string
}
export interface Organization {
id: string
name: string
slug: string
logo?: string | null
members?: Member[]
invitations?: Invitation[]
createdAt: string | Date
[key: string]: unknown
}
export interface Subscription {
id: string
plan: string
status: string
seats?: number
referenceId: string
cancelAtPeriodEnd?: boolean
periodEnd?: number | Date
trialEnd?: number | Date
metadata?: any
[key: string]: unknown
}
export interface WorkspaceInvitation {
workspaceId: string
permission: string
}
export interface Workspace {
id: string
name: string
ownerId: string
isOwner: boolean
canInvite: boolean
}
export interface OrganizationFormData {
name: string
slug: string
logo: string
}
export interface MemberUsageData {
userId: string
userName: string
userEmail: string
currentUsage: number
usageLimit: number
percentUsed: number
isOverLimit: boolean
role: string
joinedAt: string
lastActive: string | null
}
export interface OrganizationBillingData {
organizationId: string
organizationName: string
subscriptionPlan: string
subscriptionStatus: string
totalSeats: number
usedSeats: number
seatsCount: number
totalCurrentUsage: number
totalUsageLimit: number
minimumBillingAmount: number
averageUsagePerMember: number
billingPeriodStart: string | null
billingPeriodEnd: string | null
members?: MemberUsageData[]
userRole?: string
billingBlocked?: boolean
}
export interface OrganizationState {
// Core organization data
organizations: Organization[]
activeOrganization: Organization | null
// Team management
subscriptionData: Subscription | null
userWorkspaces: Workspace[]
// Organization billing and usage
organizationBillingData: OrganizationBillingData | null
// Organization settings
orgFormData: OrganizationFormData
// Loading states
isLoading: boolean
isLoadingSubscription: boolean
isLoadingOrgBilling: boolean
isCreatingOrg: boolean
isInviting: boolean
isSavingOrgSettings: boolean
// Error states
error: string | null
orgSettingsError: string | null
// Success states
inviteSuccess: boolean
orgSettingsSuccess: string | null
// Cache timestamps
lastFetched: number | null
lastSubscriptionFetched: number | null
lastOrgBillingFetched: number | null
// User permissions
hasTeamPlan: boolean
hasEnterprisePlan: boolean
}
export interface OrganizationStore extends OrganizationState {
loadData: () => Promise<void>
loadOrganizationSubscription: (orgId: string) => Promise<void>
loadOrganizationBillingData: (organizationId: string, force?: boolean) => Promise<void>
loadUserWorkspaces: (userId?: string) => Promise<void>
refreshOrganization: () => Promise<void>
// Organization management
createOrganization: (name: string, slug: string) => Promise<void>
setActiveOrganization: (orgId: string) => Promise<void>
updateOrganizationSettings: () => Promise<void>
// Team management
inviteMember: (email: string, workspaceInvitations?: WorkspaceInvitation[]) => Promise<void>
removeMember: (memberId: string, shouldReduceSeats?: boolean) => Promise<void>
cancelInvitation: (invitationId: string) => Promise<void>
// Seat management
addSeats: (newSeatCount: number) => Promise<void>
reduceSeats: (newSeatCount: number) => Promise<void>
transferSubscriptionToOrganization: (orgId: string) => Promise<void>
getUserRole: (userEmail?: string) => string
isAdminOrOwner: (userEmail?: string) => boolean
getUsedSeats: () => { used: number; members: number; pending: number }
setOrgFormData: (data: Partial<OrganizationFormData>) => void
clearError: () => void
clearSuccessMessages: () => void
}

View File

@@ -0,0 +1,89 @@
/**
* Utility functions for organization-related operations
* These are pure functions that compute values from organization data
*/
import { quickValidateEmail } from '@/lib/messaging/email/validation'
import type { Organization } from '@/lib/workspaces/organization/types'
/**
* Get the role of a user in an organization
*/
export function getUserRole(
organization: Organization | null | undefined,
userEmail?: string
): string {
if (!userEmail || !organization?.members) {
return 'member'
}
const currentMember = organization.members.find((m) => m.user?.email === userEmail)
return currentMember?.role ?? 'member'
}
/**
* Check if a user is an admin or owner in an organization
*/
export function isAdminOrOwner(
organization: Organization | null | undefined,
userEmail?: string
): boolean {
const role = getUserRole(organization, userEmail)
return role === 'owner' || role === 'admin'
}
/**
* Calculate seat usage for an organization
*/
export function calculateSeatUsage(organization: Organization | null | undefined): {
used: number
members: number
pending: number
} {
if (!organization) {
return { used: 0, members: 0, pending: 0 }
}
const membersCount = organization.members?.length || 0
const pendingInvitationsCount =
organization.invitations?.filter((inv) => inv.status === 'pending').length || 0
return {
used: membersCount + pendingInvitationsCount,
members: membersCount,
pending: pendingInvitationsCount,
}
}
/**
* Get used seats from an organization
* Alias for calculateSeatUsage
*/
export function getUsedSeats(organization: Organization | null | undefined) {
return calculateSeatUsage(organization)
}
/**
* Generate a URL-friendly slug from a name
*/
export function generateSlug(name: string): string {
return name
.toLowerCase()
.replace(/[^a-z0-9]/g, '-') // Replace non-alphanumeric with hyphens
.replace(/-+/g, '-') // Replace consecutive hyphens with single hyphen
.replace(/^-|-$/g, '') // Remove leading and trailing hyphens
}
/**
* Validate organization slug format
*/
export function validateSlug(slug: string): boolean {
const slugRegex = /^[a-z0-9-_]+$/
return slugRegex.test(slug)
}
/**
* Validate email format
*/
export function validateEmail(email: string): boolean {
return quickValidateEmail(email.trim().toLowerCase()).isValid
}

View File

@@ -0,0 +1,616 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@sim/db', () => ({
db: {
select: vi.fn(),
from: vi.fn(),
where: vi.fn(),
limit: vi.fn(),
innerJoin: vi.fn(),
leftJoin: vi.fn(),
orderBy: vi.fn(),
},
}))
vi.mock('@sim/db/schema', () => ({
permissions: {
permissionType: 'permission_type',
userId: 'user_id',
entityType: 'entity_type',
entityId: 'entity_id',
id: 'permission_id',
},
permissionTypeEnum: {
enumValues: ['admin', 'write', 'read'] as const,
},
user: {
id: 'user_id',
email: 'user_email',
name: 'user_name',
},
workspace: {
id: 'workspace_id',
name: 'workspace_name',
ownerId: 'workspace_owner_id',
},
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn().mockReturnValue('and-condition'),
eq: vi.fn().mockReturnValue('eq-condition'),
or: vi.fn().mockReturnValue('or-condition'),
}))
import { db } from '@sim/db'
import {
getManageableWorkspaces,
getUserEntityPermissions,
getUsersWithPermissions,
hasAdminPermission,
hasWorkspaceAdminAccess,
} from '@/lib/workspaces/permissions/utils'
const mockDb = db as any
type PermissionType = 'admin' | 'write' | 'read'
function createMockChain(finalResult: any) {
const chain: any = {}
chain.then = vi.fn().mockImplementation((resolve: any) => resolve(finalResult))
chain.select = vi.fn().mockReturnValue(chain)
chain.from = vi.fn().mockReturnValue(chain)
chain.where = vi.fn().mockReturnValue(chain)
chain.limit = vi.fn().mockReturnValue(chain)
chain.innerJoin = vi.fn().mockReturnValue(chain)
chain.orderBy = vi.fn().mockReturnValue(chain)
return chain
}
describe('Permission Utils', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getUserEntityPermissions', () => {
it('should return null when user has no permissions', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workspace', 'workspace456')
expect(result).toBeNull()
})
it('should return the highest permission when user has multiple permissions', async () => {
const mockResults = [
{ permissionType: 'read' as PermissionType },
{ permissionType: 'admin' as PermissionType },
{ permissionType: 'write' as PermissionType },
]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workspace', 'workspace456')
expect(result).toBe('admin')
})
it('should return single permission when user has only one', async () => {
const mockResults = [{ permissionType: 'read' as PermissionType }]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workflow', 'workflow789')
expect(result).toBe('read')
})
it('should prioritize admin over other permissions', async () => {
const mockResults = [
{ permissionType: 'write' as PermissionType },
{ permissionType: 'admin' as PermissionType },
{ permissionType: 'read' as PermissionType },
]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user999', 'workspace', 'workspace999')
expect(result).toBe('admin')
})
it('should return write permission when user only has write access', async () => {
const mockResults = [{ permissionType: 'write' as PermissionType }]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workspace', 'workspace456')
expect(result).toBe('write')
})
it('should prioritize write over read permissions', async () => {
const mockResults = [
{ permissionType: 'read' as PermissionType },
{ permissionType: 'write' as PermissionType },
]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workspace', 'workspace456')
expect(result).toBe('write')
})
it('should work with workflow entity type', async () => {
const mockResults = [{ permissionType: 'admin' as PermissionType }]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workflow', 'workflow789')
expect(result).toBe('admin')
})
it('should work with organization entity type', async () => {
const mockResults = [{ permissionType: 'read' as PermissionType }]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'organization', 'org456')
expect(result).toBe('read')
})
it('should handle generic entity types', async () => {
const mockResults = [{ permissionType: 'write' as PermissionType }]
const chain = createMockChain(mockResults)
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'custom_entity', 'entity123')
expect(result).toBe('write')
})
})
describe('hasAdminPermission', () => {
it('should return true when user has admin permission for workspace', async () => {
const chain = createMockChain([{ id: 'perm1' }])
mockDb.select.mockReturnValue(chain)
const result = await hasAdminPermission('admin-user', 'workspace123')
expect(result).toBe(true)
})
it('should return false when user has no admin permission for workspace', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasAdminPermission('regular-user', 'workspace123')
expect(result).toBe(false)
})
it('should return false when user has write permission but not admin', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasAdminPermission('write-user', 'workspace123')
expect(result).toBe(false)
})
it('should return false when user has read permission but not admin', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasAdminPermission('read-user', 'workspace123')
expect(result).toBe(false)
})
it('should handle non-existent workspace', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasAdminPermission('user123', 'non-existent-workspace')
expect(result).toBe(false)
})
it('should handle empty user ID', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasAdminPermission('', 'workspace123')
expect(result).toBe(false)
})
})
describe('getUsersWithPermissions', () => {
it('should return empty array when no users have permissions for workspace', async () => {
const usersChain = createMockChain([])
mockDb.select.mockReturnValue(usersChain)
const result = await getUsersWithPermissions('workspace123')
expect(result).toEqual([])
})
it('should return users with their permissions for workspace', async () => {
const mockUsersResults = [
{
userId: 'user1',
email: 'alice@example.com',
name: 'Alice Smith',
permissionType: 'admin' as PermissionType,
},
]
const usersChain = createMockChain(mockUsersResults)
mockDb.select.mockReturnValue(usersChain)
const result = await getUsersWithPermissions('workspace456')
expect(result).toEqual([
{
userId: 'user1',
email: 'alice@example.com',
name: 'Alice Smith',
permissionType: 'admin',
},
])
})
it('should return multiple users with different permission levels', async () => {
const mockUsersResults = [
{
userId: 'user1',
email: 'admin@example.com',
name: 'Admin User',
permissionType: 'admin' as PermissionType,
},
{
userId: 'user2',
email: 'writer@example.com',
name: 'Writer User',
permissionType: 'write' as PermissionType,
},
{
userId: 'user3',
email: 'reader@example.com',
name: 'Reader User',
permissionType: 'read' as PermissionType,
},
]
const usersChain = createMockChain(mockUsersResults)
mockDb.select.mockReturnValue(usersChain)
const result = await getUsersWithPermissions('workspace456')
expect(result).toHaveLength(3)
expect(result[0].permissionType).toBe('admin')
expect(result[1].permissionType).toBe('write')
expect(result[2].permissionType).toBe('read')
})
it('should handle users with empty names', async () => {
const mockUsersResults = [
{
userId: 'user1',
email: 'test@example.com',
name: '',
permissionType: 'read' as PermissionType,
},
]
const usersChain = createMockChain(mockUsersResults)
mockDb.select.mockReturnValue(usersChain)
const result = await getUsersWithPermissions('workspace123')
expect(result[0].name).toBe('')
})
})
describe('hasWorkspaceAdminAccess', () => {
it('should return true when user owns the workspace', async () => {
const chain = createMockChain([{ ownerId: 'user123' }])
mockDb.select.mockReturnValue(chain)
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(true)
})
it('should return true when user has direct admin permission', async () => {
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([{ ownerId: 'other-user' }])
}
return createMockChain([{ id: 'perm1' }])
})
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(true)
})
it('should return false when workspace does not exist', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(false)
})
it('should return false when user has no admin access', async () => {
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([{ ownerId: 'other-user' }])
}
return createMockChain([])
})
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(false)
})
it('should return false when user has write permission but not admin', async () => {
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([{ ownerId: 'other-user' }])
}
return createMockChain([])
})
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(false)
})
it('should return false when user has read permission but not admin', async () => {
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([{ ownerId: 'other-user' }])
}
return createMockChain([])
})
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(false)
})
it('should handle empty workspace ID', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasWorkspaceAdminAccess('user123', '')
expect(result).toBe(false)
})
it('should handle empty user ID', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await hasWorkspaceAdminAccess('', 'workspace456')
expect(result).toBe(false)
})
})
describe('Edge Cases and Security Tests', () => {
it('should handle SQL injection attempts in user IDs', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions(
"'; DROP TABLE users; --",
'workspace',
'workspace123'
)
expect(result).toBeNull()
})
it('should handle very long entity IDs', async () => {
const longEntityId = 'a'.repeat(1000)
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', 'workspace', longEntityId)
expect(result).toBeNull()
})
it('should handle unicode characters in entity names', async () => {
const chain = createMockChain([{ permissionType: 'read' as PermissionType }])
mockDb.select.mockReturnValue(chain)
const result = await getUserEntityPermissions('user123', '📝workspace', '🏢org-id')
expect(result).toBe('read')
})
it('should verify permission hierarchy ordering is consistent', () => {
const permissionOrder: Record<PermissionType, number> = { admin: 3, write: 2, read: 1 }
expect(permissionOrder.admin).toBeGreaterThan(permissionOrder.write)
expect(permissionOrder.write).toBeGreaterThan(permissionOrder.read)
})
it('should handle workspace ownership checks with null owner IDs', async () => {
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([{ ownerId: null }])
}
return createMockChain([])
})
const result = await hasWorkspaceAdminAccess('user123', 'workspace456')
expect(result).toBe(false)
})
it('should handle null user ID correctly when owner ID is different', async () => {
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([{ ownerId: 'other-user' }])
}
return createMockChain([])
})
const result = await hasWorkspaceAdminAccess(null as any, 'workspace456')
expect(result).toBe(false)
})
})
describe('getManageableWorkspaces', () => {
it('should return empty array when user has no manageable workspaces', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await getManageableWorkspaces('user123')
expect(result).toEqual([])
})
it('should return owned workspaces', async () => {
const mockWorkspaces = [
{ id: 'ws1', name: 'My Workspace 1', ownerId: 'user123' },
{ id: 'ws2', name: 'My Workspace 2', ownerId: 'user123' },
]
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain(mockWorkspaces) // Owned workspaces
}
return createMockChain([]) // No admin workspaces
})
const result = await getManageableWorkspaces('user123')
expect(result).toEqual([
{ id: 'ws1', name: 'My Workspace 1', ownerId: 'user123', accessType: 'owner' },
{ id: 'ws2', name: 'My Workspace 2', ownerId: 'user123', accessType: 'owner' },
])
})
it('should return workspaces with direct admin permissions', async () => {
const mockAdminWorkspaces = [{ id: 'ws1', name: 'Shared Workspace', ownerId: 'other-user' }]
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([]) // No owned workspaces
}
return createMockChain(mockAdminWorkspaces) // Admin workspaces
})
const result = await getManageableWorkspaces('user123')
expect(result).toEqual([
{ id: 'ws1', name: 'Shared Workspace', ownerId: 'other-user', accessType: 'direct' },
])
})
it('should combine owned and admin workspaces without duplicates', async () => {
const mockOwnedWorkspaces = [
{ id: 'ws1', name: 'My Workspace', ownerId: 'user123' },
{ id: 'ws2', name: 'Another Workspace', ownerId: 'user123' },
]
const mockAdminWorkspaces = [
{ id: 'ws1', name: 'My Workspace', ownerId: 'user123' }, // Duplicate (should be filtered)
{ id: 'ws3', name: 'Shared Workspace', ownerId: 'other-user' },
]
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain(mockOwnedWorkspaces) // Owned workspaces
}
return createMockChain(mockAdminWorkspaces) // Admin workspaces
})
const result = await getManageableWorkspaces('user123')
expect(result).toHaveLength(3)
expect(result).toEqual([
{ id: 'ws1', name: 'My Workspace', ownerId: 'user123', accessType: 'owner' },
{ id: 'ws2', name: 'Another Workspace', ownerId: 'user123', accessType: 'owner' },
{ id: 'ws3', name: 'Shared Workspace', ownerId: 'other-user', accessType: 'direct' },
])
})
it('should handle empty workspace names', async () => {
const mockWorkspaces = [{ id: 'ws1', name: '', ownerId: 'user123' }]
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain(mockWorkspaces)
}
return createMockChain([])
})
const result = await getManageableWorkspaces('user123')
expect(result[0].name).toBe('')
})
it('should handle multiple admin permissions for same workspace', async () => {
const mockAdminWorkspaces = [
{ id: 'ws1', name: 'Shared Workspace', ownerId: 'other-user' },
{ id: 'ws1', name: 'Shared Workspace', ownerId: 'other-user' }, // Duplicate
]
let callCount = 0
mockDb.select.mockImplementation(() => {
callCount++
if (callCount === 1) {
return createMockChain([]) // No owned workspaces
}
return createMockChain(mockAdminWorkspaces) // Admin workspaces with duplicates
})
const result = await getManageableWorkspaces('user123')
expect(result).toHaveLength(2) // Should include duplicates from admin permissions
})
it('should handle empty user ID gracefully', async () => {
const chain = createMockChain([])
mockDb.select.mockReturnValue(chain)
const result = await getManageableWorkspaces('')
expect(result).toEqual([])
})
})
})

View File

@@ -0,0 +1,184 @@
import { db } from '@sim/db'
import { permissions, type permissionTypeEnum, user, workspace } from '@sim/db/schema'
import { and, eq } from 'drizzle-orm'
export type PermissionType = (typeof permissionTypeEnum.enumValues)[number]
/**
* Get the highest permission level a user has for a specific entity
*
* @param userId - The ID of the user to check permissions for
* @param entityType - The type of entity (e.g., 'workspace', 'workflow', etc.)
* @param entityId - The ID of the specific entity
* @returns Promise<PermissionType | null> - The highest permission the user has for the entity, or null if none
*/
export async function getUserEntityPermissions(
userId: string,
entityType: string,
entityId: string
): Promise<PermissionType | null> {
const result = await db
.select({ permissionType: permissions.permissionType })
.from(permissions)
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, entityType),
eq(permissions.entityId, entityId)
)
)
if (result.length === 0) {
return null
}
const permissionOrder: Record<PermissionType, number> = { admin: 3, write: 2, read: 1 }
const highestPermission = result.reduce((highest, current) => {
return permissionOrder[current.permissionType] > permissionOrder[highest.permissionType]
? current
: highest
})
return highestPermission.permissionType
}
/**
* Check if a user has admin permission for a specific workspace
*
* @param userId - The ID of the user to check
* @param workspaceId - The ID of the workspace to check
* @returns Promise<boolean> - True if the user has admin permission for the workspace, false otherwise
*/
export async function hasAdminPermission(userId: string, workspaceId: string): Promise<boolean> {
const result = await db
.select({ id: permissions.id })
.from(permissions)
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId),
eq(permissions.permissionType, 'admin')
)
)
.limit(1)
return result.length > 0
}
/**
* Retrieves a list of users with their associated permissions for a given workspace.
*
* @param workspaceId - The ID of the workspace to retrieve user permissions for.
* @returns A promise that resolves to an array of user objects, each containing user details and their permission type.
*/
export async function getUsersWithPermissions(workspaceId: string): Promise<
Array<{
userId: string
email: string
name: string
permissionType: PermissionType
}>
> {
const usersWithPermissions = await db
.select({
userId: user.id,
email: user.email,
name: user.name,
permissionType: permissions.permissionType,
})
.from(permissions)
.innerJoin(user, eq(permissions.userId, user.id))
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)))
.orderBy(user.email)
return usersWithPermissions.map((row) => ({
userId: row.userId,
email: row.email,
name: row.name,
permissionType: row.permissionType,
}))
}
/**
* Check if a user has admin access to a specific workspace
*
* @param userId - The ID of the user to check
* @param workspaceId - The ID of the workspace to check
* @returns Promise<boolean> - True if the user has admin access to the workspace, false otherwise
*/
export async function hasWorkspaceAdminAccess(
userId: string,
workspaceId: string
): Promise<boolean> {
const workspaceResult = await db
.select({ ownerId: workspace.ownerId })
.from(workspace)
.where(eq(workspace.id, workspaceId))
.limit(1)
if (workspaceResult.length === 0) {
return false
}
if (workspaceResult[0].ownerId === userId) {
return true
}
return await hasAdminPermission(userId, workspaceId)
}
/**
* Get a list of workspaces that the user has access to
*
* @param userId - The ID of the user to check
* @returns Promise<Array<{
* id: string
* name: string
* ownerId: string
* accessType: 'direct' | 'owner'
* }>> - A list of workspaces that the user has access to
*/
export async function getManageableWorkspaces(userId: string): Promise<
Array<{
id: string
name: string
ownerId: string
accessType: 'direct' | 'owner'
}>
> {
const ownedWorkspaces = await db
.select({
id: workspace.id,
name: workspace.name,
ownerId: workspace.ownerId,
})
.from(workspace)
.where(eq(workspace.ownerId, userId))
const adminWorkspaces = await db
.select({
id: workspace.id,
name: workspace.name,
ownerId: workspace.ownerId,
})
.from(workspace)
.innerJoin(permissions, eq(permissions.entityId, workspace.id))
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, 'workspace'),
eq(permissions.permissionType, 'admin')
)
)
const ownedSet = new Set(ownedWorkspaces.map((w) => w.id))
const combined = [
...ownedWorkspaces.map((ws) => ({ ...ws, accessType: 'owner' as const })),
...adminWorkspaces
.filter((ws) => !ownedSet.has(ws.id))
.map((ws) => ({ ...ws, accessType: 'direct' as const })),
]
return combined
}

View File

@@ -0,0 +1,82 @@
const APP_COLORS = [
{ from: '#4F46E5', to: '#7C3AED' }, // indigo to purple
{ from: '#7C3AED', to: '#C026D3' }, // purple to fuchsia
{ from: '#EC4899', to: '#F97316' }, // pink to orange
{ from: '#14B8A6', to: '#10B981' }, // teal to emerald
{ from: '#6366F1', to: '#8B5CF6' }, // indigo to violet
{ from: '#F59E0B', to: '#F97316' }, // amber to orange
]
interface PresenceColorPalette {
gradient: string
accentColor: string
baseColor: string
}
const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/
function hashIdentifier(identifier: string | number): number {
if (typeof identifier === 'number' && Number.isFinite(identifier)) {
return Math.abs(Math.trunc(identifier))
}
if (typeof identifier === 'string') {
return Math.abs(Array.from(identifier).reduce((acc, char) => acc + char.charCodeAt(0), 0))
}
return 0
}
function withAlpha(hexColor: string, alpha: number): string {
if (!HEX_COLOR_REGEX.test(hexColor)) {
return hexColor
}
const normalized = hexColor.slice(1)
const expanded =
normalized.length === 3
? normalized
.split('')
.map((char) => `${char}${char}`)
.join('')
: normalized
const r = Number.parseInt(expanded.slice(0, 2), 16)
const g = Number.parseInt(expanded.slice(2, 4), 16)
const b = Number.parseInt(expanded.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${Math.min(Math.max(alpha, 0), 1)})`
}
function buildGradient(fromColor: string, toColor: string, rotationSeed: number): string {
const rotation = (rotationSeed * 25) % 360
return `linear-gradient(${rotation}deg, ${fromColor}, ${toColor})`
}
export function getPresenceColors(
identifier: string | number,
explicitColor?: string
): PresenceColorPalette {
const paletteIndex = hashIdentifier(identifier)
if (explicitColor) {
const normalizedColor = explicitColor.trim()
const lighterShade = HEX_COLOR_REGEX.test(normalizedColor)
? withAlpha(normalizedColor, 0.85)
: normalizedColor
return {
gradient: buildGradient(lighterShade, normalizedColor, paletteIndex),
accentColor: normalizedColor,
baseColor: lighterShade,
}
}
const colorPair = APP_COLORS[paletteIndex % APP_COLORS.length]
return {
gradient: buildGradient(colorPair.from, colorPair.to, paletteIndex),
accentColor: colorPair.to,
baseColor: colorPair.from,
}
}