@@ -48,6 +48,7 @@ import {
4848 GroupsioGetToken ,
4949 GroupsioVerifyGroup ,
5050} from '@/serverless/integrations/usecases/groupsio/types'
51+ import { encrypt , decrypt } from '../utils/crypto'
5152import SearchSyncService from './searchSyncService'
5253
5354const discordToken = DISCORD_CONFIG . token || DISCORD_CONFIG . token2
@@ -1555,20 +1556,51 @@ export default class IntegrationService {
15551556
15561557 // integration data should have the following fields
15571558 // email, token, array of groups
1558- // we shouldn't store password and 2FA token in the database
1559- // user should update them every time thety change something
1559+ // password is optional - if provided, will be encrypted and stored for automatic cookie refresh
15601560
15611561 try {
15621562 this . options . log . info ( 'Creating Groups.io integration!' )
1563+
1564+ // Try to get existing integration to preserve encrypted password
1565+ let existingSettings : any = { }
1566+ try {
1567+ const existingIntegration = await IntegrationRepository . findByPlatform ( PlatformType . GROUPSIO , {
1568+ ...this . options ,
1569+ transaction,
1570+ } )
1571+ existingSettings = existingIntegration ?. settings || { }
1572+ } catch ( err ) {
1573+ // Integration doesn't exist yet, that's fine
1574+ }
1575+
1576+ // Prepare settings
1577+ const settings : any = {
1578+ email : integrationData . email ,
1579+ token : integrationData . token ,
1580+ groups : integrationData . groupNames ,
1581+ updateMemberAttributes : true ,
1582+ lastTokenRefresh : Date . now ( ) ,
1583+ }
1584+
1585+ // Encrypt and store password if provided
1586+ if ( integrationData . password ) {
1587+ const encryptionKey = process . env . GROUPSIO_ENCRYPTION_KEY || process . env . ENCRYPTION_KEY || 'default-key-change-in-production'
1588+ try {
1589+ settings . encryptedPassword = encrypt ( integrationData . password , encryptionKey )
1590+ this . options . log . info ( 'Password encrypted and stored for Groups.io integration' )
1591+ } catch ( encryptErr ) {
1592+ this . options . log . error ( encryptErr , 'Failed to encrypt password for Groups.io integration' )
1593+ // Continue without storing password - user will need to re-authenticate manually
1594+ }
1595+ } else if ( existingSettings ?. encryptedPassword ) {
1596+ // Preserve existing encrypted password if not updating
1597+ settings . encryptedPassword = existingSettings . encryptedPassword
1598+ }
1599+
15631600 integration = await this . createOrUpdate (
15641601 {
15651602 platform : PlatformType . GROUPSIO ,
1566- settings : {
1567- email : integrationData . email ,
1568- token : integrationData . token ,
1569- groups : integrationData . groupNames ,
1570- updateMemberAttributes : true ,
1571- } ,
1603+ settings,
15721604 status : 'in-progress' ,
15731605 } ,
15741606 transaction ,
@@ -1619,16 +1651,47 @@ export default class IntegrationService {
16191651 response = await axios ( config )
16201652
16211653 // we need to get cookie from the response
1654+ if ( ! response . headers [ 'set-cookie' ] || ! response . headers [ 'set-cookie' ] [ 0 ] ) {
1655+ this . options . log . error ( { email : data . email } , 'No set-cookie header in Groups.io login response' )
1656+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
1657+ }
16221658
16231659 const cookie = response . headers [ 'set-cookie' ] [ 0 ] . split ( ';' ) [ 0 ]
16241660
1661+ if ( ! cookie ) {
1662+ this . options . log . error ( { email : data . email } , 'Invalid cookie format in Groups.io login response' )
1663+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
1664+ }
1665+
16251666 return {
16261667 groupsioCookie : cookie ,
16271668 }
16281669 } catch ( err ) {
1629- if ( 'two_factor_required' in response . data ) {
1630- throw new Error400 ( this . options . language , 'errors.groupsio.twoFactorRequired' )
1670+ // Check if it's an axios error with response data
1671+ if ( err . response && err . response . data ) {
1672+ if ( 'two_factor_required' in err . response . data ) {
1673+ throw new Error400 ( this . options . language , 'errors.groupsio.twoFactorRequired' )
1674+ }
1675+ // Check for other specific error messages
1676+ if ( err . response . status === 401 || err . response . status === 403 ) {
1677+ this . options . log . error (
1678+ { email : data . email , status : err . response . status } ,
1679+ 'Authentication failed for Groups.io login' ,
1680+ )
1681+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
1682+ }
16311683 }
1684+
1685+ // If it's already an Error400, re-throw it
1686+ if ( err instanceof Error400 ) {
1687+ throw err
1688+ }
1689+
1690+ // For network errors or other unexpected errors
1691+ this . options . log . error (
1692+ { email : data . email , error : err . message } ,
1693+ 'Unexpected error during Groups.io login' ,
1694+ )
16321695 throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
16331696 }
16341697 }
@@ -1657,4 +1720,114 @@ export default class IntegrationService {
16571720 throw new Error400 ( this . options . language , 'errors.groupsio.invalidGroup' )
16581721 }
16591722 }
1723+
1724+ /**
1725+ * Refreshes the Groups.io cookie for an integration using stored credentials
1726+ * @param integrationId - The integration ID to refresh the cookie for
1727+ * @returns The new cookie string
1728+ * @throws Error400 if credentials are missing, decryption fails, or authentication fails
1729+ */
1730+ async refreshGroupsioCookie ( integrationId : string ) : Promise < string > {
1731+ this . options . log . info ( { integrationId } , 'Refreshing Groups.io cookie' )
1732+
1733+ // Get the integration
1734+ const integration = await IntegrationRepository . findById ( integrationId , this . options )
1735+ if ( ! integration ) {
1736+ throw new Error404 ( this . options . language , 'errors.integration.notFound' )
1737+ }
1738+
1739+ const settings = integration . settings as any
1740+ if ( ! settings ) {
1741+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidSettings' )
1742+ }
1743+
1744+ // Check if we have encrypted password
1745+ if ( ! settings . encryptedPassword ) {
1746+ this . options . log . error (
1747+ { integrationId } ,
1748+ 'Cannot refresh Groups.io cookie: no encrypted password stored' ,
1749+ )
1750+ throw new Error400 (
1751+ this . options . language ,
1752+ 'errors.groupsio.noStoredCredentials' ,
1753+ 'No stored credentials available. Please reconnect the integration.' ,
1754+ )
1755+ }
1756+
1757+ if ( ! settings . email ) {
1758+ throw new Error400 ( this . options . language , 'errors.groupsio.missingEmail' )
1759+ }
1760+
1761+ // Decrypt password
1762+ const encryptionKey = process . env . GROUPSIO_ENCRYPTION_KEY || process . env . ENCRYPTION_KEY || 'default-key-change-in-production'
1763+ let password : string
1764+ try {
1765+ password = decrypt ( settings . encryptedPassword , encryptionKey )
1766+ } catch ( decryptErr ) {
1767+ this . options . log . error ( decryptErr , { integrationId } , 'Failed to decrypt Groups.io password' )
1768+ throw new Error400 (
1769+ this . options . language ,
1770+ 'errors.groupsio.decryptionFailed' ,
1771+ 'Failed to decrypt stored credentials. Please reconnect the integration.' ,
1772+ )
1773+ }
1774+
1775+ // Get new token
1776+ let newCookie : string
1777+ try {
1778+ const tokenResult = await this . groupsioGetToken ( {
1779+ email : settings . email ,
1780+ password,
1781+ } )
1782+ newCookie = tokenResult . groupsioCookie
1783+ } catch ( err ) {
1784+ // Check if it's a 2FA error
1785+ if ( err instanceof Error400 && err . message ?. includes ( 'twoFactorRequired' ) ) {
1786+ this . options . log . warn (
1787+ { integrationId } ,
1788+ 'Groups.io cookie refresh failed: 2FA required. Integration needs manual update.' ,
1789+ )
1790+ throw new Error400 (
1791+ this . options . language ,
1792+ 'errors.groupsio.twoFactorRequired' ,
1793+ 'Two-factor authentication is required. Please reconnect the integration with your 2FA code.' ,
1794+ )
1795+ }
1796+
1797+ this . options . log . error ( err , { integrationId } , 'Failed to refresh Groups.io cookie' )
1798+ throw new Error400 (
1799+ this . options . language ,
1800+ 'errors.groupsio.refreshFailed' ,
1801+ 'Failed to refresh authentication. Please check your credentials and reconnect the integration.' ,
1802+ )
1803+ }
1804+
1805+ // Update integration settings with new cookie
1806+ const transaction = await SequelizeRepository . createTransaction ( this . options )
1807+ try {
1808+ await this . update (
1809+ integrationId ,
1810+ {
1811+ settings : {
1812+ ...settings ,
1813+ token : newCookie ,
1814+ lastTokenRefresh : Date . now ( ) ,
1815+ } ,
1816+ } ,
1817+ transaction ,
1818+ )
1819+ await SequelizeRepository . commitTransaction ( transaction )
1820+
1821+ this . options . log . info (
1822+ { integrationId, refreshTime : new Date ( ) . toISOString ( ) } ,
1823+ 'Groups.io cookie refreshed successfully' ,
1824+ )
1825+ } catch ( updateErr ) {
1826+ await SequelizeRepository . rollbackTransaction ( transaction )
1827+ this . options . log . error ( updateErr , { integrationId } , 'Failed to update integration with new cookie' )
1828+ throw updateErr
1829+ }
1830+
1831+ return newCookie
1832+ }
16601833}
0 commit comments