@@ -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
@@ -1507,20 +1508,51 @@ export default class IntegrationService {
15071508
15081509 // integration data should have the following fields
15091510 // email, token, array of groups
1510- // we shouldn't store password and 2FA token in the database
1511- // user should update them every time thety change something
1511+ // password is optional - if provided, will be encrypted and stored for automatic cookie refresh
15121512
15131513 try {
15141514 this . options . log . info ( 'Creating Groups.io integration!' )
1515+
1516+ // Try to get existing integration to preserve encrypted password
1517+ let existingSettings : any = { }
1518+ try {
1519+ const existingIntegration = await IntegrationRepository . findByPlatform ( PlatformType . GROUPSIO , {
1520+ ...this . options ,
1521+ transaction,
1522+ } )
1523+ existingSettings = existingIntegration ?. settings || { }
1524+ } catch ( err ) {
1525+ // Integration doesn't exist yet, that's fine
1526+ }
1527+
1528+ // Prepare settings
1529+ const settings : any = {
1530+ email : integrationData . email ,
1531+ token : integrationData . token ,
1532+ groups : integrationData . groupNames ,
1533+ updateMemberAttributes : true ,
1534+ lastTokenRefresh : Date . now ( ) ,
1535+ }
1536+
1537+ // Encrypt and store password if provided
1538+ if ( integrationData . password ) {
1539+ const encryptionKey = process . env . GROUPSIO_ENCRYPTION_KEY || process . env . ENCRYPTION_KEY || 'default-key-change-in-production'
1540+ try {
1541+ settings . encryptedPassword = encrypt ( integrationData . password , encryptionKey )
1542+ this . options . log . info ( 'Password encrypted and stored for Groups.io integration' )
1543+ } catch ( encryptErr ) {
1544+ this . options . log . error ( encryptErr , 'Failed to encrypt password for Groups.io integration' )
1545+ // Continue without storing password - user will need to re-authenticate manually
1546+ }
1547+ } else if ( existingSettings ?. encryptedPassword ) {
1548+ // Preserve existing encrypted password if not updating
1549+ settings . encryptedPassword = existingSettings . encryptedPassword
1550+ }
1551+
15151552 integration = await this . createOrUpdate (
15161553 {
15171554 platform : PlatformType . GROUPSIO ,
1518- settings : {
1519- email : integrationData . email ,
1520- token : integrationData . token ,
1521- groups : integrationData . groupNames ,
1522- updateMemberAttributes : true ,
1523- } ,
1555+ settings,
15241556 status : 'in-progress' ,
15251557 } ,
15261558 transaction ,
@@ -1567,16 +1599,47 @@ export default class IntegrationService {
15671599 response = await axios ( config )
15681600
15691601 // we need to get cookie from the response
1602+ if ( ! response . headers [ 'set-cookie' ] || ! response . headers [ 'set-cookie' ] [ 0 ] ) {
1603+ this . options . log . error ( { email : data . email } , 'No set-cookie header in Groups.io login response' )
1604+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
1605+ }
15701606
15711607 const cookie = response . headers [ 'set-cookie' ] [ 0 ] . split ( ';' ) [ 0 ]
15721608
1609+ if ( ! cookie ) {
1610+ this . options . log . error ( { email : data . email } , 'Invalid cookie format in Groups.io login response' )
1611+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
1612+ }
1613+
15731614 return {
15741615 groupsioCookie : cookie ,
15751616 }
15761617 } catch ( err ) {
1577- if ( 'two_factor_required' in response . data ) {
1578- throw new Error400 ( this . options . language , 'errors.groupsio.twoFactorRequired' )
1618+ // Check if it's an axios error with response data
1619+ if ( err . response && err . response . data ) {
1620+ if ( 'two_factor_required' in err . response . data ) {
1621+ throw new Error400 ( this . options . language , 'errors.groupsio.twoFactorRequired' )
1622+ }
1623+ // Check for other specific error messages
1624+ if ( err . response . status === 401 || err . response . status === 403 ) {
1625+ this . options . log . error (
1626+ { email : data . email , status : err . response . status } ,
1627+ 'Authentication failed for Groups.io login' ,
1628+ )
1629+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
1630+ }
15791631 }
1632+
1633+ // If it's already an Error400, re-throw it
1634+ if ( err instanceof Error400 ) {
1635+ throw err
1636+ }
1637+
1638+ // For network errors or other unexpected errors
1639+ this . options . log . error (
1640+ { email : data . email , error : err . message } ,
1641+ 'Unexpected error during Groups.io login' ,
1642+ )
15801643 throw new Error400 ( this . options . language , 'errors.groupsio.invalidCredentials' )
15811644 }
15821645 }
@@ -1605,4 +1668,114 @@ export default class IntegrationService {
16051668 throw new Error400 ( this . options . language , 'errors.groupsio.invalidGroup' )
16061669 }
16071670 }
1671+
1672+ /**
1673+ * Refreshes the Groups.io cookie for an integration using stored credentials
1674+ * @param integrationId - The integration ID to refresh the cookie for
1675+ * @returns The new cookie string
1676+ * @throws Error400 if credentials are missing, decryption fails, or authentication fails
1677+ */
1678+ async refreshGroupsioCookie ( integrationId : string ) : Promise < string > {
1679+ this . options . log . info ( { integrationId } , 'Refreshing Groups.io cookie' )
1680+
1681+ // Get the integration
1682+ const integration = await IntegrationRepository . findById ( integrationId , this . options )
1683+ if ( ! integration ) {
1684+ throw new Error404 ( this . options . language , 'errors.integration.notFound' )
1685+ }
1686+
1687+ const settings = integration . settings as any
1688+ if ( ! settings ) {
1689+ throw new Error400 ( this . options . language , 'errors.groupsio.invalidSettings' )
1690+ }
1691+
1692+ // Check if we have encrypted password
1693+ if ( ! settings . encryptedPassword ) {
1694+ this . options . log . error (
1695+ { integrationId } ,
1696+ 'Cannot refresh Groups.io cookie: no encrypted password stored' ,
1697+ )
1698+ throw new Error400 (
1699+ this . options . language ,
1700+ 'errors.groupsio.noStoredCredentials' ,
1701+ 'No stored credentials available. Please reconnect the integration.' ,
1702+ )
1703+ }
1704+
1705+ if ( ! settings . email ) {
1706+ throw new Error400 ( this . options . language , 'errors.groupsio.missingEmail' )
1707+ }
1708+
1709+ // Decrypt password
1710+ const encryptionKey = process . env . GROUPSIO_ENCRYPTION_KEY || process . env . ENCRYPTION_KEY || 'default-key-change-in-production'
1711+ let password : string
1712+ try {
1713+ password = decrypt ( settings . encryptedPassword , encryptionKey )
1714+ } catch ( decryptErr ) {
1715+ this . options . log . error ( decryptErr , { integrationId } , 'Failed to decrypt Groups.io password' )
1716+ throw new Error400 (
1717+ this . options . language ,
1718+ 'errors.groupsio.decryptionFailed' ,
1719+ 'Failed to decrypt stored credentials. Please reconnect the integration.' ,
1720+ )
1721+ }
1722+
1723+ // Get new token
1724+ let newCookie : string
1725+ try {
1726+ const tokenResult = await this . groupsioGetToken ( {
1727+ email : settings . email ,
1728+ password,
1729+ } )
1730+ newCookie = tokenResult . groupsioCookie
1731+ } catch ( err ) {
1732+ // Check if it's a 2FA error
1733+ if ( err instanceof Error400 && err . message ?. includes ( 'twoFactorRequired' ) ) {
1734+ this . options . log . warn (
1735+ { integrationId } ,
1736+ 'Groups.io cookie refresh failed: 2FA required. Integration needs manual update.' ,
1737+ )
1738+ throw new Error400 (
1739+ this . options . language ,
1740+ 'errors.groupsio.twoFactorRequired' ,
1741+ 'Two-factor authentication is required. Please reconnect the integration with your 2FA code.' ,
1742+ )
1743+ }
1744+
1745+ this . options . log . error ( err , { integrationId } , 'Failed to refresh Groups.io cookie' )
1746+ throw new Error400 (
1747+ this . options . language ,
1748+ 'errors.groupsio.refreshFailed' ,
1749+ 'Failed to refresh authentication. Please check your credentials and reconnect the integration.' ,
1750+ )
1751+ }
1752+
1753+ // Update integration settings with new cookie
1754+ const transaction = await SequelizeRepository . createTransaction ( this . options )
1755+ try {
1756+ await this . update (
1757+ integrationId ,
1758+ {
1759+ settings : {
1760+ ...settings ,
1761+ token : newCookie ,
1762+ lastTokenRefresh : Date . now ( ) ,
1763+ } ,
1764+ } ,
1765+ transaction ,
1766+ )
1767+ await SequelizeRepository . commitTransaction ( transaction )
1768+
1769+ this . options . log . info (
1770+ { integrationId, refreshTime : new Date ( ) . toISOString ( ) } ,
1771+ 'Groups.io cookie refreshed successfully' ,
1772+ )
1773+ } catch ( updateErr ) {
1774+ await SequelizeRepository . rollbackTransaction ( transaction )
1775+ this . options . log . error ( updateErr , { integrationId } , 'Failed to update integration with new cookie' )
1776+ throw updateErr
1777+ }
1778+
1779+ return newCookie
1780+ }
16081781}
0 commit comments