Skip to content

Commit 2b6bbdf

Browse files
committed
Add dashboard Slack authentication
1 parent 06add96 commit 2b6bbdf

17 files changed

Lines changed: 1523 additions & 112 deletions

.env.example

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,27 @@
1212
APP_NAME=Gooseherd
1313

1414
# ── Slack (optional — omit for dashboard-only mode) ──
15+
# Required bot scopes for Slack command handling:
16+
# - app_mentions:read
17+
# - channels:history
18+
# - chat:write
19+
# Required bot scope for Slack user-group → Gooseherd team sync:
20+
# - usergroups:read
21+
# Required Sign in with Slack OpenID scopes for browser login:
22+
# - openid
23+
# - email
24+
# - profile
1525
SLACK_BOT_TOKEN=xoxb-...
1626
SLACK_APP_TOKEN=xapp-...
1727
SLACK_SIGNING_SECRET=...
28+
#
29+
# Dashboard browser login via Slack OpenID Connect:
30+
# - client ID / secret power Sign in / Sign up with Slack
31+
# - bot token powers Slack user-group → Gooseherd team sync on login
32+
# - redirect URI defaults to <DASHBOARD_PUBLIC_URL>/auth/slack/callback if not set
33+
# SLACK_CLIENT_ID=
34+
# SLACK_CLIENT_SECRET=
35+
# SLACK_AUTH_REDIRECT_URI=
1836
SLACK_COMMAND_NAME=gooseherd
1937
# SLACK_CONFIG_OVERRIDE_FROM_ENV=false
2038
# SLACK_ALLOWED_CHANNELS=C123,C456

README.md

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,33 @@ Thread follow-ups reuse the repo from the latest run in that thread:
6868

6969
1. Create app at https://api.slack.com/apps
7070
2. Enable **Socket Mode** (no public webhook needed).
71-
3. Add bot token scopes: `app_mentions:read`, `channels:history`, `chat:write`
72-
4. Install to your workspace.
73-
5. Copy tokens into `.env`:
74-
- `SLACK_BOT_TOKEN` (`xoxb-...`)
75-
- `SLACK_APP_TOKEN` (`xapp-...`)
71+
3. Add bot token scopes: `app_mentions:read`, `channels:history`, `chat:write`, `usergroups:read`
72+
4. For dashboard browser auth, enable **Sign in with Slack** and configure OpenID scopes: `openid`, `email`, `profile`
73+
5. Add a redirect URL for `https://<your-dashboard>/auth/slack/callback`
74+
6. Copy tokens into `.env`:
75+
- `SLACK_BOT_TOKEN` (`xoxb-...`) for Slack command handling and user-group sync
76+
- `SLACK_APP_TOKEN` (`xapp-...`) for Socket Mode
7677
- `SLACK_SIGNING_SECRET`
78+
- `SLACK_CLIENT_ID`
79+
- `SLACK_CLIENT_SECRET`
80+
7. Optional: set `SLACK_AUTH_REDIRECT_URI` if the callback URL should differ from `DASHBOARD_PUBLIC_URL`
81+
82+
Required Slack scopes by feature:
83+
84+
- Bot scopes for Slack commands: `app_mentions:read`, `channels:history`, `chat:write`
85+
- Bot scope for Slack user-group team sync: `usergroups:read`
86+
- OpenID scopes for dashboard Sign in / Sign up: `openid`, `email`, `profile`
87+
88+
Relevant `.env` variables:
89+
90+
- `SLACK_BOT_TOKEN`: required for Slack command handling and Slack user-group membership sync
91+
- `SLACK_APP_TOKEN`: required for Socket Mode
92+
- `SLACK_SIGNING_SECRET`: required for validating Slack requests
93+
- `SLACK_CLIENT_ID`: required for browser login via Sign in with Slack
94+
- `SLACK_CLIENT_SECRET`: required for browser login via Sign in with Slack
95+
- `SLACK_AUTH_REDIRECT_URI`: optional override for the browser auth callback; defaults to `<DASHBOARD_PUBLIC_URL>/auth/slack/callback`
96+
97+
If you add new scopes after the app is already installed, reinstall the Slack app in the workspace so the new permissions take effect.
7798

7899
## GitHub Setup
79100

docs/smoke-test.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
# Work Items V1 Smoke Test
2+
3+
This guide describes a practical local smoke test for the `WorkItem v1` flow.
4+
5+
It covers:
6+
- `product_discovery` board rendering
7+
- `ReviewRequest` history rendering
8+
- PM confirmation flow
9+
- `feature_delivery` board rendering
10+
11+
It does not replace automated tests. It is meant to validate that the live dashboard and runtime wiring behave correctly end-to-end.
12+
13+
## Prerequisites
14+
15+
Start PostgreSQL:
16+
17+
```bash
18+
docker compose up -d postgres
19+
```
20+
21+
Apply migrations:
22+
23+
```bash
24+
docker compose run --rm \
25+
-v "$PWD:/app" \
26+
--entrypoint sh gooseherd \
27+
-lc 'cd /app && npm run db:migrate'
28+
```
29+
30+
Start the app with the local dashboard:
31+
32+
```bash
33+
docker compose run --rm --service-ports \
34+
-v "$PWD:/app" \
35+
--entrypoint sh gooseherd \
36+
-lc 'cd /app && APP_NAME=Huble DASHBOARD_ENABLED=true DASHBOARD_HOST=0.0.0.0 DASHBOARD_PORT=8787 npm run dev'
37+
```
38+
39+
The dashboard should be available at `http://127.0.0.1:8787/`.
40+
41+
## Local Login
42+
43+
If the local dashboard is protected by a setup password, either use the existing password or set a known one directly in the local dev database.
44+
45+
Example password used for smoke checks:
46+
47+
```text
48+
smoketest123
49+
```
50+
51+
Example hash generation:
52+
53+
```bash
54+
node -e "const {scryptSync, randomBytes}=require('node:crypto'); const password='smoketest123'; const salt=randomBytes(16); const hash=scryptSync(password,salt,64); process.stdout.write('scrypt:'+salt.toString('hex')+':'+hash.toString('hex'));"
55+
```
56+
57+
Then write the generated hash into the `setup.password_hash` field and restart the local app process so it reloads the new hash.
58+
59+
## Smoke Dataset
60+
61+
Create a PM, engineer, QA, and one team:
62+
63+
```bash
64+
docker compose exec -T postgres psql -U gooseherd -d gooseherd -c "
65+
insert into users (id, slack_user_id, jira_account_id, display_name) values
66+
('11111111-1111-1111-1111-111111111111','U_PM_SMOKE','JIRA_PM_SMOKE','Smoke PM'),
67+
('22222222-2222-2222-2222-222222222222','U_ENG_SMOKE',null,'Smoke Engineer'),
68+
('33333333-3333-3333-3333-333333333333','U_QA_SMOKE',null,'Smoke QA')
69+
on conflict (id) do nothing;
70+
71+
insert into teams (id, name, slack_channel_id) values
72+
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa','growth','C_GROWTH_SMOKE')
73+
on conflict (id) do nothing;
74+
75+
insert into team_members (team_id, user_id, functional_roles) values
76+
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa','11111111-1111-1111-1111-111111111111',array['pm']),
77+
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa','22222222-2222-2222-2222-222222222222',array['engineer']),
78+
('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa','33333333-3333-3333-3333-333333333333',array['qa'])
79+
on conflict (team_id, user_id) do nothing;
80+
"
81+
```
82+
83+
Create a discovery work item through the real dashboard API:
84+
85+
```bash
86+
curl -H 'Cookie: gooseherd-session=<session-cookie>' \
87+
-X POST http://127.0.0.1:8787/api/work-items/discovery \
88+
-H 'content-type: application/json' \
89+
-d '{
90+
"title":"Smoke discovery item",
91+
"summary":"Browser smoke for discovery workflow",
92+
"ownerTeamId":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
93+
"homeChannelId":"C_GROWTH_SMOKE",
94+
"homeThreadTs":"1740001111.100",
95+
"createdByUserId":"11111111-1111-1111-1111-111111111111"
96+
}'
97+
```
98+
99+
Save the returned discovery id. It will be referred to below as `<discovery-id>`.
100+
101+
Drive the discovery item to `waiting_for_pm_confirmation` and add review history:
102+
103+
```bash
104+
docker compose run --rm \
105+
-v "$PWD:/app" \
106+
--entrypoint sh gooseherd \
107+
-lc 'cd /app && node --import tsx -e "
108+
import { initDatabase, closeDatabase } from \"./src/db/index.ts\";
109+
import { WorkItemService } from \"./src/work-items/service.ts\";
110+
111+
const db = await initDatabase(\"postgres://gooseherd:gooseherd@postgres:5432/gooseherd\");
112+
const svc = new WorkItemService(db);
113+
114+
await svc.startDiscovery(\"<discovery-id>\");
115+
116+
const requests = await svc.requestReview({
117+
workItemId: \"<discovery-id>\",
118+
requestedByUserId: \"11111111-1111-1111-1111-111111111111\",
119+
requests: [{
120+
type: \"review\",
121+
targetType: \"user\",
122+
targetRef: { userId: \"22222222-2222-2222-2222-222222222222\" },
123+
title: \"Engineering review for smoke discovery\",
124+
requestMessage: \"Please review the spec draft\",
125+
focusPoints: [\"scope\", \"naming\"]
126+
}]
127+
});
128+
129+
await svc.recordReviewOutcome({
130+
reviewRequestId: requests[0].id,
131+
outcome: \"approved\",
132+
authorUserId: \"22222222-2222-2222-2222-222222222222\",
133+
comment: \"Looks ready to me.\",
134+
source: \"dashboard\"
135+
});
136+
137+
await closeDatabase();
138+
"'
139+
```
140+
141+
Create a delivery work item:
142+
143+
```bash
144+
docker compose run --rm \
145+
-v "$PWD:/app" \
146+
--entrypoint sh gooseherd \
147+
-lc 'cd /app && node --import tsx -e "
148+
import { initDatabase, closeDatabase } from \"./src/db/index.ts\";
149+
import { WorkItemService } from \"./src/work-items/service.ts\";
150+
151+
const db = await initDatabase(\"postgres://gooseherd:gooseherd@postgres:5432/gooseherd\");
152+
const svc = new WorkItemService(db);
153+
154+
await svc.createDeliveryFromJira({
155+
title: \"Smoke delivery item\",
156+
summary: \"Browser smoke for delivery workflow\",
157+
ownerTeamId: \"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa\",
158+
homeChannelId: \"C_GROWTH_SMOKE\",
159+
homeThreadTs: \"1740002222.200\",
160+
jiraIssueKey: \"HBL-SMOKE-1\",
161+
createdByUserId: \"11111111-1111-1111-1111-111111111111\"
162+
});
163+
164+
await closeDatabase();
165+
"'
166+
```
167+
168+
## Manual Dashboard Smoke
169+
170+
Open the dashboard:
171+
172+
```text
173+
http://127.0.0.1:8787/
174+
```
175+
176+
Log in if required.
177+
178+
Switch to `Board`.
179+
180+
### Product Discovery
181+
182+
Set workflow to `Product Discovery`.
183+
184+
Expected results:
185+
- one item appears in `Waiting For PM Confirmation`
186+
- the card title is `Smoke discovery item`
187+
- the detail pane shows:
188+
- `Waiting For PM Confirmation`
189+
- `Awaiting PM Decision`
190+
- `All Required Reviews Received`
191+
192+
In `Review Requests`, verify:
193+
- the completed review request is visible
194+
- the review comment history is visible
195+
- the comment body `Looks ready to me.` is rendered
196+
197+
In `Events`, verify at least:
198+
- `review_request.created`
199+
- `review_request.comment_added`
200+
- `review_request.completed`
201+
- `work_item.state_changed`
202+
203+
### PM Approve
204+
205+
Click `PM Approve`.
206+
207+
When prompted, use:
208+
209+
```text
210+
PM user id: 11111111-1111-1111-1111-111111111111
211+
Jira issue key: HBL-SMOKE-2
212+
```
213+
214+
Expected results:
215+
- the discovery item moves to `Done`
216+
- the detail pane shows:
217+
- `pm_approved`
218+
- `jira_created`
219+
- `delivery_work_item_created`
220+
221+
### Feature Delivery
222+
223+
Switch workflow to `Feature Delivery`.
224+
225+
Expected results:
226+
- the board shows at least one delivery item with `Jira` key `HBL-SMOKE-1`
227+
- after PM approval, an additional delivery item linked to `HBL-SMOKE-2` should exist
228+
- both items should render normally in the board and detail pane
229+
230+
## Optional API Checks
231+
232+
Check work item list:
233+
234+
```bash
235+
curl -H 'Cookie: gooseherd-session=<session-cookie>' \
236+
http://127.0.0.1:8787/api/work-items?workflow=product_discovery
237+
```
238+
239+
Check review request comments:
240+
241+
```bash
242+
curl -H 'Cookie: gooseherd-session=<session-cookie>' \
243+
http://127.0.0.1:8787/api/work-items/<discovery-id>/review-requests/<review-request-id>/comments
244+
```
245+
246+
## Known Caveats
247+
248+
- If a real Slack config is loaded, creating review requests through the live API can try to post notifications into Slack immediately.
249+
- Using fake `homeChannelId` values can cause `channel_not_found`.
250+
- For purely local smoke checks, it is often simpler to create the review request through `WorkItemService` directly, as shown above.
251+
- Browser automation using the system Firefox wrapper may fail on some machines because the wrapper delegates through Snap and DBus.
252+
- If that happens, use the real Firefox binary directly instead of the wrapper.
253+
254+
## Suggested Post-Smoke Cleanup
255+
256+
If you want to remove the smoke dataset:
257+
258+
```bash
259+
docker compose exec -T postgres psql -U gooseherd -d gooseherd -c "
260+
delete from review_request_comments where review_request_id in (select id from review_requests where work_item_id in (select id from work_items where title like 'Smoke%'));
261+
delete from review_requests where work_item_id in (select id from work_items where title like 'Smoke%');
262+
delete from work_item_events where work_item_id in (select id from work_items where title like 'Smoke%');
263+
delete from work_items where title like 'Smoke%';
264+
delete from team_members where team_id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
265+
delete from teams where id = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
266+
delete from users where id in (
267+
'11111111-1111-1111-1111-111111111111',
268+
'22222222-2222-2222-2222-222222222222',
269+
'33333333-3333-3333-3333-333333333333'
270+
);
271+
"
272+
```
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
ALTER TABLE "teams" ADD COLUMN "slack_user_group_id" text;
2+
--> statement-breakpoint
3+
ALTER TABLE "teams" ADD COLUMN "slack_user_group_handle" text;
4+
--> statement-breakpoint
5+
ALTER TABLE "team_members" ADD COLUMN "membership_source" text DEFAULT 'manual' NOT NULL;
6+
--> statement-breakpoint
7+
CREATE UNIQUE INDEX "teams_slack_user_group_id_idx"
8+
ON "teams" USING btree ("slack_user_group_id")
9+
WHERE "slack_user_group_id" IS NOT NULL;
10+
--> statement-breakpoint
11+
CREATE TABLE "dashboard_auth_sessions" (
12+
"id" uuid PRIMARY KEY NOT NULL,
13+
"token_hash" text NOT NULL,
14+
"principal_type" text NOT NULL,
15+
"user_id" uuid,
16+
"auth_method" text NOT NULL,
17+
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
18+
"expires_at" timestamp with time zone NOT NULL,
19+
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
20+
"revoked_at" timestamp with time zone
21+
);
22+
--> statement-breakpoint
23+
ALTER TABLE "dashboard_auth_sessions"
24+
ADD CONSTRAINT "dashboard_auth_sessions_user_id_users_id_fk"
25+
FOREIGN KEY ("user_id") REFERENCES "users"("id")
26+
ON DELETE cascade
27+
ON UPDATE no action;
28+
--> statement-breakpoint
29+
CREATE UNIQUE INDEX "dashboard_auth_sessions_token_hash_idx"
30+
ON "dashboard_auth_sessions" USING btree ("token_hash");
31+
--> statement-breakpoint
32+
CREATE INDEX "dashboard_auth_sessions_user_id_idx"
33+
ON "dashboard_auth_sessions" USING btree ("user_id")
34+
WHERE "user_id" IS NOT NULL;
35+
--> statement-breakpoint
36+
CREATE INDEX "dashboard_auth_sessions_expires_at_idx"
37+
ON "dashboard_auth_sessions" USING btree ("expires_at");
38+
--> statement-breakpoint
39+
CREATE INDEX "dashboard_auth_sessions_revoked_at_idx"
40+
ON "dashboard_auth_sessions" USING btree ("revoked_at");

drizzle/meta/_journal.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@
6464
"when": 1778886000000,
6565
"tag": "0008_feature_delivery_jira_unique",
6666
"breakpoints": true
67+
},
68+
{
69+
"idx": 9,
70+
"version": "7",
71+
"when": 1779100000000,
72+
"tag": "0009_dashboard_auth_slack",
73+
"breakpoints": true
6774
}
6875
]
6976
}

0 commit comments

Comments
 (0)