@@ -142,6 +142,25 @@ export default class UserModel extends AbstractModel<UserDBScheme> implements Us
142142 */
143143 public utm ?: UserDBScheme [ 'utm' ] ;
144144
145+ /**
146+ * External identities for SSO (keyed by workspaceId)
147+ */
148+ public identities ?: {
149+ [ workspaceId : string ] : {
150+ saml : {
151+ /**
152+ * NameID value from IdP (stable identifier)
153+ */
154+ id : string ;
155+
156+ /**
157+ * Email at the time of linking (for audit)
158+ */
159+ email : string ;
160+ } ;
161+ } ;
162+ } ;
163+
145164 /**
146165 * Model's collection
147166 */
@@ -418,4 +437,68 @@ export default class UserModel extends AbstractModel<UserDBScheme> implements Us
418437 } ,
419438 } ) ;
420439 }
440+
441+ /**
442+ * Link SAML identity to user for specific workspace
443+ *
444+ * @param workspaceId - workspace ID
445+ * @param samlId - NameID value from IdP (stable identifier)
446+ * @param email - user email at the time of linking
447+ */
448+ public async linkSamlIdentity ( workspaceId : string , samlId : string , email : string ) : Promise < void > {
449+ /**
450+ * Use Record<string, any> for MongoDB dot notation keys
451+ */
452+ const updateData : Record < string , any > = {
453+ [ `identities.${ workspaceId } .saml.id` ] : samlId ,
454+ [ `identities.${ workspaceId } .saml.email` ] : email ,
455+ } ;
456+
457+ await this . update (
458+ { _id : new ObjectId ( this . _id ) } ,
459+ { $set : updateData }
460+ ) ;
461+
462+ /**
463+ * Update local state
464+ */
465+ if ( ! this . identities ) {
466+ this . identities = { } ;
467+ }
468+ if ( ! this . identities [ workspaceId ] ) {
469+ this . identities [ workspaceId ] = { saml : { id : samlId , email } } ;
470+ } else {
471+ this . identities [ workspaceId ] . saml = { id : samlId , email } ;
472+ }
473+ }
474+
475+ /**
476+ * Find user by SAML identity
477+ *
478+ * @param collection - users collection
479+ * @param workspaceId - workspace ID
480+ * @param samlId - NameID value from IdP
481+ * @returns UserModel or null if not found
482+ */
483+ public static async findBySamlIdentity (
484+ collection : Collection < UserDBScheme > ,
485+ workspaceId : string ,
486+ samlId : string
487+ ) : Promise < UserModel | null > {
488+ const userData = await collection . findOne ( {
489+ [ `identities.${ workspaceId } .saml.id` ] : samlId ,
490+ } ) ;
491+
492+ return userData ? new UserModel ( userData ) : null ;
493+ }
494+
495+ /**
496+ * Get SAML identity for workspace
497+ *
498+ * @param workspaceId - workspace ID
499+ * @returns SAML identity or null if not found
500+ */
501+ public getSamlIdentity ( workspaceId : string ) : { id : string ; email : string } | null {
502+ return this . identities ?. [ workspaceId ] ?. saml || null ;
503+ }
421504}
0 commit comments