Merge remote-tracking branch 'origin' into chuck/web-2435-move-comments-and-webhooks-without-attachments

This commit is contained in:
Chuck Driesler
2025-02-18 16:04:20 +00:00
609 changed files with 5668 additions and 51059 deletions
@@ -53,7 +53,7 @@ import {
} from '@/modules/serverinvites/services/operations'
import { authorizeResolver } from '@/modules/shared'
import { getFrontendOrigin } from '@/modules/shared/helpers/envHelper'
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants'
import {
GetWorkspace,
GetWorkspaceBySlug,
@@ -57,7 +57,7 @@ import {
FindVerifiedEmailsByUserId
} from '@/modules/core/domain/userEmails/operations'
import { DeleteAllResourceInvites } from '@/modules/serverinvites/domain/operations'
import { WorkspaceInviteResourceType } from '@/modules/workspaces/domain/constants'
import { WorkspaceInviteResourceType } from '@/modules/workspacesCore/domain/constants'
import { ProjectInviteResourceType } from '@/modules/serverinvites/domain/constants'
import { chunk, isEmpty, omit } from 'lodash'
import { userEmailsCompliantWithWorkspaceDomains } from '@/modules/workspaces/domain/logic'
@@ -291,13 +291,15 @@ export const deleteWorkspaceFactory =
deleteProject,
queryAllWorkspaceProjects,
deleteAllResourceInvites,
deleteSsoProvider
deleteSsoProvider,
emitWorkspaceEvent
}: {
deleteWorkspace: DeleteWorkspace
deleteProject: DeleteStreamRecord
queryAllWorkspaceProjects: QueryAllWorkspaceProjects
deleteAllResourceInvites: DeleteAllResourceInvites
deleteSsoProvider: DeleteSsoProvider
emitWorkspaceEvent: EventBus['emit']
}) =>
async ({ workspaceId }: WorkspaceDeleteArgs): Promise<void> => {
// Delete workspace SSO provider, if present
@@ -328,6 +330,10 @@ export const deleteWorkspaceFactory =
for (const projectIdsChunk of chunk(projectIds, 25)) {
await Promise.all(projectIdsChunk.map((projectId) => deleteProject(projectId)))
}
await emitWorkspaceEvent({
eventName: WorkspaceEvents.Deleted,
payload: { workspaceId }
})
}
type WorkspaceRoleDeleteArgs = {
@@ -10,7 +10,8 @@ import {
import {
OidcProvider,
OidcProviderRecord,
OidcProviderAttributes
OidcProviderAttributes,
OidcProfile
} from '@/modules/workspaces/domain/sso/types'
import cryptoRandomString from 'crypto-random-string'
import { UserinfoResponse } from 'openid-client'
@@ -33,7 +34,11 @@ import {
} from '@/modules/workspaces/errors/sso'
import { WorkspaceInvalidRoleError } from '@/modules/workspaces/errors/workspace'
import { LimitedWorkspace } from '@/modules/workspacesCore/domain/types'
import { isValidSsoSession } from '@/modules/workspaces/domain/sso/logic'
import {
getEmailFromOidcProfile,
isValidSsoSession
} from '@/modules/workspaces/domain/sso/logic'
import { Logger } from '@/logging/logging'
// this probably should go a lean validation endpoint too
const validateOidcProviderAttributes = ({
@@ -129,7 +134,7 @@ export const createWorkspaceUserFromSsoProfileFactory =
deleteInvite: DeleteInvite
}) =>
async (args: {
ssoProfile: UserinfoResponse<{ email: string }>
ssoProfile: UserinfoResponse<OidcProfile>
workspaceId: string
}): Promise<Pick<UserWithOptionalRole, 'id' | 'email'>> => {
// Check if user has email-based invite to given workspace
@@ -146,7 +151,8 @@ export const createWorkspaceUserFromSsoProfileFactory =
}
// Create Speckle user
const { name, email } = args.ssoProfile
const { name } = args.ssoProfile
const email = getEmailFromOidcProfile(args.ssoProfile)
if (!name) {
throw new SsoProviderProfileInvalidError('SSO provider user requires a name')
@@ -185,42 +191,56 @@ export const linkUserWithSsoProviderFactory =
({
findEmailsByUserId,
createUserEmail,
updateUserEmail
updateUserEmail,
logger
}: {
findEmailsByUserId: FindEmailsByUserId
createUserEmail: CreateUserEmail
updateUserEmail: UpdateUserEmail
logger?: Logger
}) =>
async (args: {
userId: string
ssoProfile: UserinfoResponse<{ email: string }>
ssoProfile: UserinfoResponse<OidcProfile>
}): Promise<void> => {
// TODO: Chuck's soapbox -
//
// Assert link between req.user.id & { providerId: decryptedOidcProvider.id, email: oidcProviderUserData.email }
// Create link implicitly if req.context.userId exists (user performed SSO flow while signed in)
// If req.context.userId does not exist, and link does not exist, throw and require user to sign in before SSO
//
// In addition, investigate using oidcProviderUserData.sub as source of truth here. Some providers appear to allow
// `email` fields to change, or do not guarantee they will exist (Entra ID)
// Add oidcProviderUserData.email to req.user.id verified emails, if not already present
// Add SSO provider email to req.user.id verified emails, if not already present
const userEmails = await findEmailsByUserId({ userId: args.userId })
const maybeSsoEmail = userEmails.find(
(entry) => entry.email === args.ssoProfile.email
const providerEmail = getEmailFromOidcProfile(args.ssoProfile)
const maybeExistingEmail = userEmails.find(
(entry) => entry.email === providerEmail.toLowerCase()
)
if (!maybeSsoEmail) {
logger?.info(
{
userEmails: userEmails.map((entry) => entry.email),
providerEmail
},
'Comparing existing user emails against SSO provider email:'
)
if (!maybeExistingEmail) {
await createUserEmail({
userEmail: {
userId: args.userId,
email: args.ssoProfile.email,
email: getEmailFromOidcProfile(args.ssoProfile),
verified: true
}
})
}
if (!!maybeSsoEmail && !maybeSsoEmail.verified) {
if (!!maybeExistingEmail && !maybeExistingEmail.verified) {
await updateUserEmail({
query: {
id: maybeSsoEmail.id,
id: maybeExistingEmail.id,
userId: args.userId
},
update: {