mirror of https://github.com/buster-so/buster.git
Add default channel support for Slack integration
Co-authored-by: dallin <dallin@buster.so>
This commit is contained in:
parent
0a51562ca6
commit
ba01e881e6
|
@ -20,12 +20,17 @@ const OAuthCallbackSchema = z.object({
|
|||
state: z.string(),
|
||||
});
|
||||
|
||||
const UpdateDefaultChannelSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
id: z.string().min(1),
|
||||
});
|
||||
|
||||
// Custom error class
|
||||
export class SlackError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public statusCode: 500 | 400 | 401 | 403 | 404 | 409 | 503 = 500,
|
||||
public code?: string
|
||||
public code?: string,
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SlackError';
|
||||
|
@ -72,7 +77,7 @@ export class SlackHandler {
|
|||
error: 'Slack integration is not configured',
|
||||
code: 'INTEGRATION_NOT_CONFIGURED',
|
||||
},
|
||||
503
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -83,7 +88,7 @@ export class SlackHandler {
|
|||
error: 'Slack integration is not enabled',
|
||||
code: 'INTEGRATION_DISABLED',
|
||||
},
|
||||
503
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -131,14 +136,14 @@ export class SlackHandler {
|
|||
throw new SlackError(
|
||||
'Organization already has an active Slack integration',
|
||||
409,
|
||||
'INTEGRATION_EXISTS'
|
||||
'INTEGRATION_EXISTS',
|
||||
);
|
||||
}
|
||||
|
||||
throw new SlackError(
|
||||
error instanceof Error ? error.message : 'Failed to initiate OAuth',
|
||||
500,
|
||||
'OAUTH_INIT_ERROR'
|
||||
'OAUTH_INIT_ERROR',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -193,7 +198,7 @@ export class SlackHandler {
|
|||
console.error('OAuth callback error:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : 'callback_failed';
|
||||
return c.redirect(
|
||||
`/settings/integrations?status=error&error=${encodeURIComponent(errorMessage)}`
|
||||
`/settings/integrations?status=error&error=${encodeURIComponent(errorMessage)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -214,7 +219,7 @@ export class SlackHandler {
|
|||
error: 'Slack integration is not configured',
|
||||
code: 'INTEGRATION_NOT_CONFIGURED',
|
||||
},
|
||||
503
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -243,7 +248,7 @@ export class SlackHandler {
|
|||
throw new SlackError(
|
||||
error instanceof Error ? error.message : 'Failed to get integration status',
|
||||
500,
|
||||
'GET_INTEGRATION_ERROR'
|
||||
'GET_INTEGRATION_ERROR',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -264,7 +269,7 @@ export class SlackHandler {
|
|||
error: 'Slack integration is not configured',
|
||||
code: 'INTEGRATION_NOT_CONFIGURED',
|
||||
},
|
||||
503
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -282,14 +287,14 @@ export class SlackHandler {
|
|||
|
||||
const result = await slackOAuthService.removeIntegration(
|
||||
organizationGrant.organizationId,
|
||||
user.id
|
||||
user.id,
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new SlackError(
|
||||
result.error || 'Failed to remove integration',
|
||||
404,
|
||||
'INTEGRATION_NOT_FOUND'
|
||||
'INTEGRATION_NOT_FOUND',
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -306,7 +311,83 @@ export class SlackHandler {
|
|||
throw new SlackError(
|
||||
error instanceof Error ? error.message : 'Failed to remove integration',
|
||||
500,
|
||||
'REMOVE_INTEGRATION_ERROR'
|
||||
'REMOVE_INTEGRATION_ERROR',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v2/slack/integration/default-channel
|
||||
* Update default channel for Slack integration
|
||||
*/
|
||||
async updateDefaultChannel(c: Context) {
|
||||
try {
|
||||
// Get service instance (lazy initialization)
|
||||
const slackOAuthService = this.getSlackOAuthService();
|
||||
|
||||
// Check if service is available
|
||||
if (!slackOAuthService) {
|
||||
return c.json(
|
||||
{
|
||||
error: 'Slack integration is not configured',
|
||||
code: 'INTEGRATION_NOT_CONFIGURED',
|
||||
},
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
const user = c.get('busterUser');
|
||||
|
||||
if (!user) {
|
||||
throw new HTTPException(401, { message: 'Authentication required' });
|
||||
}
|
||||
|
||||
const organizationGrant = await getUserOrganizationId(user.id);
|
||||
|
||||
if (!organizationGrant) {
|
||||
throw new HTTPException(400, { message: 'Organization not found' });
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
const body = await c.req.json();
|
||||
const parsed = UpdateDefaultChannelSchema.safeParse(body);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new SlackError(
|
||||
`Invalid request body: ${parsed.error.errors.map((e) => e.message).join(', ')}`,
|
||||
400,
|
||||
'INVALID_REQUEST_BODY',
|
||||
);
|
||||
}
|
||||
|
||||
// Get active integration
|
||||
const { getActiveIntegration, updateDefaultChannel } = await import(
|
||||
'./services/slack-helpers'
|
||||
);
|
||||
const integration = await getActiveIntegration(organizationGrant.organizationId);
|
||||
|
||||
if (!integration) {
|
||||
throw new SlackError('No active Slack integration found', 404, 'INTEGRATION_NOT_FOUND');
|
||||
}
|
||||
|
||||
// Update default channel
|
||||
await updateDefaultChannel(integration.id, parsed.data);
|
||||
|
||||
return c.json({
|
||||
message: 'Default channel updated successfully',
|
||||
defaultChannel: parsed.data,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update default channel:', error);
|
||||
|
||||
if (error instanceof HTTPException || error instanceof SlackError) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new SlackError(
|
||||
error instanceof Error ? error.message : 'Failed to update default channel',
|
||||
500,
|
||||
'UPDATE_DEFAULT_CHANNEL_ERROR',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ const app = new Hono()
|
|||
// Protected endpoints
|
||||
.get('/integration', requireAuth, (c) => slackHandler.getIntegration(c))
|
||||
.delete('/integration', requireAuth, (c) => slackHandler.removeIntegration(c))
|
||||
.put('/integration/default-channel', requireAuth, (c) => slackHandler.updateDefaultChannel(c))
|
||||
// Error handling
|
||||
.onError((e, c) => {
|
||||
if (e instanceof SlackError) {
|
||||
|
|
|
@ -9,7 +9,7 @@ export type SlackIntegration = InferSelectModel<typeof slackIntegrations>;
|
|||
* Get active Slack integration for an organization
|
||||
*/
|
||||
export async function getActiveIntegration(
|
||||
organizationId: string
|
||||
organizationId: string,
|
||||
): Promise<SlackIntegration | null> {
|
||||
try {
|
||||
const [integration] = await db
|
||||
|
@ -19,8 +19,8 @@ export async function getActiveIntegration(
|
|||
and(
|
||||
eq(slackIntegrations.organizationId, organizationId),
|
||||
eq(slackIntegrations.status, 'active'),
|
||||
isNull(slackIntegrations.deletedAt)
|
||||
)
|
||||
isNull(slackIntegrations.deletedAt),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
|
@ -28,7 +28,9 @@ export async function getActiveIntegration(
|
|||
} catch (error) {
|
||||
console.error('Failed to get active Slack integration:', error);
|
||||
throw new Error(
|
||||
`Failed to get active Slack integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to get active Slack integration: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +39,7 @@ export async function getActiveIntegration(
|
|||
* Get pending integration by OAuth state
|
||||
*/
|
||||
export async function getPendingIntegrationByState(
|
||||
state: string
|
||||
state: string,
|
||||
): Promise<SlackIntegration | null> {
|
||||
try {
|
||||
const [integration] = await db
|
||||
|
@ -47,8 +49,8 @@ export async function getPendingIntegrationByState(
|
|||
and(
|
||||
eq(slackIntegrations.oauthState, state),
|
||||
eq(slackIntegrations.status, 'pending'),
|
||||
gt(slackIntegrations.oauthExpiresAt, new Date().toISOString())
|
||||
)
|
||||
gt(slackIntegrations.oauthExpiresAt, new Date().toISOString()),
|
||||
),
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
|
@ -56,7 +58,9 @@ export async function getPendingIntegrationByState(
|
|||
} catch (error) {
|
||||
console.error('Failed to get pending integration by state:', error);
|
||||
throw new Error(
|
||||
`Failed to get pending integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to get pending integration: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -118,7 +122,9 @@ export async function createPendingIntegration(params: {
|
|||
} catch (error) {
|
||||
console.error('Failed to create pending Slack integration:', error);
|
||||
throw new Error(
|
||||
`Failed to create pending integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to create pending integration: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -137,7 +143,7 @@ export async function updateIntegrationAfterOAuth(
|
|||
scope: string;
|
||||
tokenVaultKey: string;
|
||||
installedBySlackUserId?: string;
|
||||
}
|
||||
},
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db
|
||||
|
@ -155,7 +161,7 @@ export async function updateIntegrationAfterOAuth(
|
|||
} catch (error) {
|
||||
console.error('Failed to update integration after OAuth:', error);
|
||||
throw new Error(
|
||||
`Failed to activate integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to activate integration: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -165,7 +171,7 @@ export async function updateIntegrationAfterOAuth(
|
|||
*/
|
||||
export async function markIntegrationAsFailed(
|
||||
integrationId: string,
|
||||
_error?: string
|
||||
_error?: string,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Due to database constraint, we cannot mark a pending integration as failed
|
||||
|
@ -206,7 +212,9 @@ export async function markIntegrationAsFailed(
|
|||
} catch (error) {
|
||||
console.error('Failed to mark integration as failed:', error);
|
||||
throw new Error(
|
||||
`Failed to mark integration as failed: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to mark integration as failed: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -227,7 +235,9 @@ export async function softDeleteIntegration(integrationId: string): Promise<void
|
|||
} catch (error) {
|
||||
console.error('Failed to soft delete integration:', error);
|
||||
throw new Error(
|
||||
`Failed to soft delete integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to soft delete integration: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +257,9 @@ export async function updateLastUsedAt(integrationId: string): Promise<void> {
|
|||
} catch (error) {
|
||||
console.error('Failed to update last used timestamp:', error);
|
||||
throw new Error(
|
||||
`Failed to update last used timestamp: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to update last used timestamp: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -267,7 +279,9 @@ export async function getIntegrationById(integrationId: string): Promise<SlackIn
|
|||
} catch (error) {
|
||||
console.error('Failed to get integration by ID:', error);
|
||||
throw new Error(
|
||||
`Failed to get integration by ID: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to get integration by ID: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -282,7 +296,9 @@ export async function hasActiveIntegration(organizationId: string): Promise<bool
|
|||
} catch (error) {
|
||||
console.error('Failed to check active integration:', error);
|
||||
throw new Error(
|
||||
`Failed to check active integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to check active integration: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -291,7 +307,7 @@ export async function hasActiveIntegration(organizationId: string): Promise<bool
|
|||
* Get any existing integration for an organization (active, revoked, or failed)
|
||||
*/
|
||||
export async function getExistingIntegration(
|
||||
organizationId: string
|
||||
organizationId: string,
|
||||
): Promise<SlackIntegration | null> {
|
||||
try {
|
||||
// Get the most recent non-deleted integration for this organization
|
||||
|
@ -306,7 +322,34 @@ export async function getExistingIntegration(
|
|||
} catch (error) {
|
||||
console.error('Failed to get existing Slack integration:', error);
|
||||
throw new Error(
|
||||
`Failed to get existing Slack integration: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to get existing Slack integration: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update default channel for Slack integration
|
||||
*/
|
||||
export async function updateDefaultChannel(
|
||||
integrationId: string,
|
||||
defaultChannel: { name: string; id: string },
|
||||
): Promise<void> {
|
||||
try {
|
||||
await db
|
||||
.update(slackIntegrations)
|
||||
.set({
|
||||
defaultChannel,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(slackIntegrations.id, integrationId));
|
||||
} catch (error) {
|
||||
console.error('Failed to update default channel:', error);
|
||||
throw new Error(
|
||||
`Failed to update default channel: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -323,8 +366,8 @@ export async function cleanupExpiredPendingIntegrations(): Promise<number> {
|
|||
.where(
|
||||
and(
|
||||
eq(slackIntegrations.status, 'pending'),
|
||||
lt(slackIntegrations.oauthExpiresAt, new Date().toISOString())
|
||||
)
|
||||
lt(slackIntegrations.oauthExpiresAt, new Date().toISOString()),
|
||||
),
|
||||
);
|
||||
|
||||
// Clean up vault tokens for each expired integration
|
||||
|
@ -345,8 +388,8 @@ export async function cleanupExpiredPendingIntegrations(): Promise<number> {
|
|||
.where(
|
||||
and(
|
||||
eq(slackIntegrations.status, 'pending'),
|
||||
lt(slackIntegrations.oauthExpiresAt, new Date().toISOString())
|
||||
)
|
||||
lt(slackIntegrations.oauthExpiresAt, new Date().toISOString()),
|
||||
),
|
||||
)
|
||||
.returning({ id: slackIntegrations.id });
|
||||
|
||||
|
@ -354,7 +397,9 @@ export async function cleanupExpiredPendingIntegrations(): Promise<number> {
|
|||
} catch (error) {
|
||||
console.error('Failed to cleanup expired pending integrations:', error);
|
||||
throw new Error(
|
||||
`Failed to cleanup expired integrations: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
`Failed to cleanup expired integrations: ${
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
-- Add default_channel column to slack_integrations table
|
||||
ALTER TABLE "slack_integrations"
|
||||
ADD COLUMN "default_channel" jsonb DEFAULT '{}'::jsonb;
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue