chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,8 @@
// Export types
export type { Member } from '@/lib/workspaces/organization/types'
// Export utility functions
export {
generateSlug,
getUserRole,
isAdminOrOwner,
} from '@/lib/workspaces/organization/utils'
@@ -0,0 +1,163 @@
interface User {
name?: string
email?: string
id?: string
image?: string | null
}
export interface Member {
id: string
role: string
user?: User
}
interface Invitation {
id: string
email: string
status: string
membershipIntent?: 'internal' | 'external'
}
export interface Organization {
id: string
name: string
slug: string
logo?: string | null
members?: Member[]
invitations?: Invitation[]
createdAt: string | Date
[key: string]: unknown
}
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
}
interface WorkspaceInvitation {
workspaceId: string
permission: string
}
interface Workspace {
id: string
name: string
ownerId: string
isOwner: boolean
canInvite: boolean
}
interface OrganizationFormData {
name: string
slug: string
logo: string
}
interface MemberUsageData {
userId: string
userName: string
userEmail: string
currentUsage: number
usageLimit: number
percentUsed: number
isOverLimit: boolean
role: string
joinedAt: string
}
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
}
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
}
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) => Promise<void>
cancelInvitation: (invitationId: string) => 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
}
@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest'
import { calculateSeatUsage } from '@/lib/workspaces/organization/utils'
describe('calculateSeatUsage', () => {
it('does not count external pending workspace invitations as occupied seats', () => {
const seats = calculateSeatUsage({
id: 'org-1',
name: 'Acme',
slug: 'acme',
createdAt: new Date(),
members: [
{ id: 'member-1', role: 'owner' },
{ id: 'member-2', role: 'member' },
],
invitations: [
{
id: 'inv-1',
email: 'internal@example.com',
status: 'pending',
membershipIntent: 'internal',
},
{
id: 'inv-2',
email: 'external@example.com',
status: 'pending',
membershipIntent: 'external',
},
],
})
expect(seats).toEqual({ used: 3, members: 2, pending: 1 })
})
})
@@ -0,0 +1,93 @@
/**
* Utility functions for organization-related operations
* These are pure functions that compute values from organization data
*/
import { isOrgAdminRole } from '@sim/platform-authz/predicates'
import { normalizeEmail } from '@sim/utils/string'
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 isOrgAdminRole(role)
}
/**
* 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' && inv.membershipIntent !== 'external'
).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(normalizeEmail(email)).isValid
}