Skip to content

Commit 6ef270f

Browse files
Merge pull request #170 from blockful/feat/OAuth_and_Multi-Workspace_support
Feat/o auth and multi workspace support
2 parents bbaf9c1 + f37ed62 commit 6ef270f

45 files changed

Lines changed: 1127 additions & 640 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,21 @@
11
# Notification System Environment Variables
2-
# Copy this file to .env and fill in your values
2+
# Copy this file to .env and configure your settings
33

4-
# ========================================
5-
# GraphQL Endpoint (Required for all services)
6-
# ========================================
4+
# === REQUIRED CONFIGURATION ===
75
ANTICAPTURE_GRAPHQL_ENDPOINT=https://api-gateway-production-0879.up.railway.app/graphql
86

9-
# ========================================
10-
# Telegram Configuration
11-
# ========================================
12-
# Get your bot token from @BotFather on Telegram
13-
TELEGRAM_BOT_TOKEN=your-telegram-bot-token-here
7+
# === NOTIFICATION PLATFORMS ===
8+
TELEGRAM_BOT_TOKEN=
9+
SLACK_CLIENT_ID=
10+
SLACK_CLIENT_SECRET=
11+
SLACK_REDIRECT_URI=https://your-domain.com/slack/oauth/callback
12+
SLACK_APP_TOKEN=xapp-
13+
SLACK_SIGNING_SECRET=
14+
TOKEN_ENCRYPTION_KEY= # Generate: openssl rand -hex 32
1415

15-
# ========================================
16-
# Slack Configuration
17-
# ========================================
18-
# Get these from https://api.slack.com/apps after creating your app
19-
20-
# Bot User OAuth Token (starts with xoxb-)
21-
# From: OAuth & Permissions > Bot User OAuth Token
22-
SLACK_BOT_TOKEN=xoxb-your-bot-token-here
23-
24-
# App-Level Token for Socket Mode (starts with xapp-)
25-
# From: Basic Information > App-Level Tokens
26-
SLACK_APP_TOKEN=xapp-your-app-token-here
27-
28-
# Signing Secret for request verification
29-
# From: Basic Information > App Credentials > Signing Secret
30-
SLACK_SIGNING_SECRET=your-signing-secret-here
31-
32-
# Optional: Default channel for notifications (starts with C)
33-
# Leave empty to use DMs only
34-
SLACK_CHANNEL_ID=
35-
36-
# ========================================
37-
# Database Configuration (for local development)
38-
# ========================================
39-
# These are set in docker-compose.yml for containerized services
40-
# Only needed if running services locally outside Docker
16+
# === LOCAL DEV CONFIGURATION ===
4117
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres
4218
RABBITMQ_URL=amqp://admin:admin@localhost:5672
43-
44-
# ========================================
45-
# Optional: Service Configuration
46-
# ========================================
47-
# Subscription server port (default: 3003)
4819
PORT=3003
49-
50-
# Logic system trigger interval in milliseconds (default: 30000)
51-
TRIGGER_INTERVAL=30000
52-
53-
# Proposal status to monitor (default: ACTIVE)
20+
TRIGGER_INTERVAL=30000 # ms
5421
PROPOSAL_STATUS=ACTIVE

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Recent Changes
66

7+
### Slack OAuth Multi-Workspace Support (2025-09-18)
8+
- OAuth 2.0 flow implementation in Subscription Server (`/slack/install`, `/slack/oauth/callback`)
9+
- Workspace token storage with AES-256-CBC encryption in `slack_workspaces` table
10+
- Dynamic token distribution via RabbitMQ messages (bot_token field)
11+
- Backward compatible with workspace:user ID format (T_DEFAULT for legacy)
12+
- See `docs/slack-oauth-setup.md` for configuration guide
13+
714
### Slack Integration (2025-09-17)
815
- Added Socket Mode support for interactive Slack features in Consumer service
916
- New service classes: SlackDAOService, SlackWalletService for command handling

Caddyfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Caddy configuration for local HTTPS development
2+
# Automatically generates self-signed certificates for localhost
3+
4+
localhost {
5+
# Reverse proxy all requests to subscription-server
6+
reverse_proxy subscription-server:3003
7+
8+
# Caddy automatically handles:
9+
# - Self-signed certificate generation
10+
# - HTTPS setup
11+
# - HTTP/2 support
12+
}

apps/consumers/src/clients/slack.client.test.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ describe('SlackClient', () => {
2020
mockWebClient = new WebClient() as jest.Mocked<WebClient>;
2121
(WebClient as unknown as jest.Mock).mockImplementation(() => mockWebClient);
2222

23-
slackClient = new SlackClient('test-token', 'test-app-token', 'test-signing-secret');
23+
slackClient = new SlackClient('test-app-token', 'test-signing-secret');
2424
});
2525

2626
describe('constructor', () => {
2727
it('should create client with valid token', () => {
28-
expect(new SlackClient('valid-token', 'valid-app-token', 'valid-signing-secret')).toBeInstanceOf(SlackClient);
28+
expect(new SlackClient('valid-app-token', 'valid-signing-secret')).toBeInstanceOf(SlackClient);
2929
});
3030
});
3131

@@ -45,7 +45,7 @@ describe('SlackClient', () => {
4545

4646
(mockWebClient.chat.postMessage as jest.Mock).mockResolvedValue(mockResponse as never);
4747

48-
const result = await slackClient.sendMessage('C1234567890', 'Test message');
48+
const result = await slackClient.sendMessage('C1234567890', 'Test message', { token: 'xoxb-test-token' });
4949

5050
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
5151
channel: 'C1234567890',
@@ -73,7 +73,7 @@ describe('SlackClient', () => {
7373

7474
(mockWebClient.chat.postMessage as jest.Mock).mockResolvedValue(mockResponse as never);
7575

76-
await slackClient.sendMessage('C1234567890', 'Check [this link](https://example.com)');
76+
await slackClient.sendMessage('C1234567890', 'Check [this link](https://example.com)', { token: 'xoxb-test-token' });
7777

7878
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
7979
channel: 'C1234567890',
@@ -95,7 +95,7 @@ describe('SlackClient', () => {
9595

9696
(mockWebClient.chat.postMessage as jest.Mock).mockResolvedValue(mockResponse as never);
9797

98-
await slackClient.sendMessage('C1234567890', 'This is **bold** text');
98+
await slackClient.sendMessage('C1234567890', 'This is **bold** text', { token: 'xoxb-test-token' });
9999

100100
expect(mockWebClient.chat.postMessage).toHaveBeenCalledWith({
101101
channel: 'C1234567890',
@@ -118,6 +118,7 @@ describe('SlackClient', () => {
118118
(mockWebClient.chat.postMessage as jest.Mock).mockResolvedValue(mockResponse as never);
119119

120120
await slackClient.sendMessage('C1234567890', 'Test message', {
121+
token: 'xoxb-test-token',
121122
parse: 'full',
122123
link_names: false,
123124
unfurl_links: true,
@@ -155,20 +156,20 @@ describe('SlackClient', () => {
155156
});
156157

157158
it('should always initialize with Socket Mode', () => {
158-
const client = new SlackClient('xoxb-token', 'xapp-token', 'signing-secret');
159+
const client = new SlackClient('xapp-token', 'signing-secret');
159160

160161
expect(App).toHaveBeenCalledWith({
161-
token: 'xoxb-token',
162162
appToken: 'xapp-token',
163163
signingSecret: 'signing-secret',
164164
socketMode: true,
165-
processBeforeResponse: true
165+
processBeforeResponse: true,
166+
authorize: expect.any(Function)
166167
});
167168
});
168169

169170

170171
it('should setup command handlers', () => {
171-
const client = new SlackClient('xoxb-token', 'xapp-token', 'signing-secret');
172+
const client = new SlackClient('xapp-token', 'signing-secret');
172173

173174
client.setupHandlers?.((handlers) => {
174175
handlers.command('/test', async (ctx) => {
@@ -180,7 +181,7 @@ describe('SlackClient', () => {
180181
});
181182

182183
it('should setup action handlers', () => {
183-
const client = new SlackClient('xoxb-token', 'xapp-token', 'signing-secret');
184+
const client = new SlackClient('xapp-token', 'signing-secret');
184185

185186
client.setupHandlers?.((handlers) => {
186187
handlers.action('button_click', async (ctx) => {
@@ -192,15 +193,15 @@ describe('SlackClient', () => {
192193
});
193194

194195
it('should launch the Bolt app', async () => {
195-
const client = new SlackClient('xoxb-token', 'xapp-token', 'signing-secret');
196+
const client = new SlackClient('xapp-token', 'signing-secret');
196197

197198
await client.launch?.();
198199

199200
expect(mockBoltApp.start).toHaveBeenCalled();
200201
});
201202

202203
it('should stop the Bolt app', () => {
203-
const client = new SlackClient('xoxb-token', 'xapp-token', 'signing-secret');
204+
const client = new SlackClient('xapp-token', 'signing-secret');
204205

205206
client.stop?.('SIGTERM');
206207

apps/consumers/src/clients/slack.client.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,40 +21,49 @@ import {
2121
} from '../interfaces/slack-context.interface';
2222

2323
export class SlackClient implements SlackClientInterface {
24-
private client: WebClient;
2524
private boltApp: App;
2625
private sessionStorage: SlackSessionStorage;
2726

2827
constructor(
29-
token: string,
3028
appToken: string,
3129
signingSecret: string
3230
) {
33-
this.client = new WebClient(token);
34-
3531
// Initialize session storage
3632
this.sessionStorage = new InMemorySessionStorage();
3733

3834
// Initialize Bolt app with Socket Mode
35+
// For OAuth multi-workspace support, we use an authorize function
36+
// that returns empty credentials since we handle tokens per-message
3937
this.boltApp = new App({
40-
token,
4138
appToken,
4239
signingSecret,
4340
socketMode: true,
44-
processBeforeResponse: true
41+
processBeforeResponse: true,
42+
authorize: async () => {
43+
return {
44+
botToken: '',
45+
botId: 'oauth-bot',
46+
botUserId: 'oauth-bot-user'
47+
};
48+
}
4549
});
46-
console.log('✅ Slack client initialized with Socket Mode support');
50+
console.log('✅ Slack client initialized with Socket Mode support (OAuth mode)');
4751
}
4852

4953
async sendMessage(
5054
channel: string,
5155
text: string,
5256
options?: SlackSendMessageOptions
5357
): Promise<SlackMessage> {
58+
if (!options?.token) {
59+
throw new Error('Slack notification requires workspace OAuth token. No token provided in message options.');
60+
}
61+
5462
// Convert markdown to Slack mrkdwn format
5563
const slackText = this.convertMarkdownToSlackFormat(text);
64+
const clientToUse = new WebClient(options.token);
5665

57-
const result = await this.client.chat.postMessage({
66+
const result = await clientToUse.chat.postMessage({
5867
channel,
5968
text: slackText,
6069
parse: options?.parse || 'none',

apps/consumers/src/config/env.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import { z } from 'zod';
77
import * as dotenv from 'dotenv';
88

99
const envSchema = z.object({
10-
TELEGRAM_BOT_TOKEN: z.string().min(1, "Telegram bot token is required"),
11-
SLACK_BOT_TOKEN: z.string().min(1, "Slack bot token is required"),
12-
SLACK_APP_TOKEN: z.string().min(1, "Slack app token is required for Socket Mode"),
13-
SLACK_SIGNING_SECRET: z.string().min(1, "Slack signing secret is required for request verification"),
10+
TELEGRAM_BOT_TOKEN: z.string(),
11+
SLACK_APP_TOKEN: z.string(),
12+
SLACK_SIGNING_SECRET: z.string(),
1413
ANTICAPTURE_GRAPHQL_ENDPOINT: z.string().url("ANTICAPTURE_GRAPHQL_ENDPOINT must be a valid URL"),
15-
SUBSCRIPTION_SERVER_URL: z.string().min(1, "Subscription server URL is required"),
14+
SUBSCRIPTION_SERVER_URL: z.string(),
1615
RABBITMQ_URL: z.string().url(),
1716
});
1817

@@ -22,7 +21,6 @@ export function loadConfig() {
2221

2322
return {
2423
telegramBotToken: env.TELEGRAM_BOT_TOKEN,
25-
slackBotToken: env.SLACK_BOT_TOKEN,
2624
slackAppToken: env.SLACK_APP_TOKEN,
2725
slackSigningSecret: env.SLACK_SIGNING_SECRET,
2826
anticaptureGraphqlEndpoint: env.ANTICAPTURE_GRAPHQL_ENDPOINT,

apps/consumers/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ const telegramClient = new TelegramClient(config.telegramBotToken);
2626

2727
// Create Slack client
2828
const slackClient = new SlackClient(
29-
config.slackBotToken,
3029
config.slackAppToken,
3130
config.slackSigningSecret
3231
);

apps/consumers/src/interfaces/notification.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface NotificationPayload {
88
channel: string;
99
channelUserId: string | number;
1010
message: string;
11+
bot_token?: string; // Optional bot token for multi-workspace
1112
metadata?: {
1213
addresses?: Record<string, string>; // key: placeholder name, value: ethereum address
1314
transaction?: {

apps/consumers/src/interfaces/slack-client.interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface SlackSendMessageOptions {
1313
mrkdwn?: boolean;
1414
blocks?: any[];
1515
attachments?: any[];
16+
token?: string; // Optional token for multi-workspace support
1617
}
1718

1819
export interface SlackMessage {

apps/consumers/src/services/bot/slack-bot.service.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ describe('SlackBotService', () => {
4040
const mockPayload: NotificationPayload = {
4141
userId: 'user123',
4242
channel: 'slack',
43-
channelUserId: 'U1234567890',
44-
message: 'Test notification message'
43+
channelUserId: 'T_WORKSPACE:U1234567890',
44+
message: 'Test notification message',
45+
bot_token: 'xoxb-test-workspace-token'
4546
};
4647

4748
beforeEach(() => {
@@ -59,6 +60,7 @@ describe('SlackBotService', () => {
5960
'U1234567890',
6061
'Test notification message',
6162
{
63+
token: 'xoxb-test-workspace-token',
6264
mrkdwn: true,
6365
unfurl_links: false
6466
}
@@ -71,6 +73,7 @@ describe('SlackBotService', () => {
7173
const payloadWithTx: NotificationPayload = {
7274
...mockPayload,
7375
message: 'New proposal created!\n\n{{txLink}}',
76+
bot_token: 'xoxb-test-workspace-token',
7477
metadata: {
7578
transaction: {
7679
hash: '0x123abc',
@@ -87,6 +90,7 @@ describe('SlackBotService', () => {
8790
'U1234567890',
8891
'New proposal created!\n\n<https://etherscan.io/tx/0x123abc|Transaction details>',
8992
{
93+
token: 'xoxb-test-workspace-token',
9094
mrkdwn: true,
9195
unfurl_links: false
9296
}
@@ -96,7 +100,8 @@ describe('SlackBotService', () => {
96100
it('should remove transaction link placeholder when no transaction metadata', async () => {
97101
const payloadWithTx: NotificationPayload = {
98102
...mockPayload,
99-
message: 'New proposal created!\n\n{{txLink}}'
103+
message: 'New proposal created!\n\n{{txLink}}',
104+
bot_token: 'xoxb-test-workspace-token'
100105
};
101106

102107
await slackBotService.sendNotification(payloadWithTx);
@@ -105,6 +110,7 @@ describe('SlackBotService', () => {
105110
'U1234567890',
106111
'New proposal created!',
107112
{
113+
token: 'xoxb-test-workspace-token',
108114
mrkdwn: true,
109115
unfurl_links: false
110116
}
@@ -115,6 +121,7 @@ describe('SlackBotService', () => {
115121
const payloadWithAddresses: NotificationPayload = {
116122
...mockPayload,
117123
message: 'Proposal by {{proposer}} in {{dao}}',
124+
bot_token: 'xoxb-test-workspace-token',
118125
metadata: {
119126
addresses: {
120127
proposer: '0x742d35Cc6634C0532925a3b8D76be9D5B65F6a',
@@ -136,6 +143,7 @@ describe('SlackBotService', () => {
136143
'U1234567890',
137144
'Proposal by alice.eth in coolDAO.eth',
138145
{
146+
token: 'xoxb-test-workspace-token',
139147
mrkdwn: true,
140148
unfurl_links: false
141149
}
@@ -146,6 +154,7 @@ describe('SlackBotService', () => {
146154
const complexPayload: NotificationPayload = {
147155
...mockPayload,
148156
message: 'New proposal by {{proposer}}!\n\n{{txLink}}',
157+
bot_token: 'xoxb-test-workspace-token',
149158
metadata: {
150159
addresses: {
151160
proposer: '0x742d35Cc6634C0532925a3b8D76be9D5B65F6a'
@@ -166,6 +175,7 @@ describe('SlackBotService', () => {
166175
'U1234567890',
167176
'New proposal by alice.eth!\n\n<https://etherscan.io/tx/0x123abc|Transaction details>',
168177
{
178+
token: 'xoxb-test-workspace-token',
169179
mrkdwn: true,
170180
unfurl_links: false
171181
}

0 commit comments

Comments
 (0)