From 5df1234d1ac990caeba57a6cd2b09a71f6a7acdb Mon Sep 17 00:00:00 2001 From: Caspian Zhao Date: Thu, 15 Oct 2026 18:42:45 -0700 Subject: [PATCH] feat: sign in again to a saved way in, and a Delete button under every Slack message - Sign in again: every saved way in except account + password can be signed in to again; the finished takeover saves onto the same row (takeoverRow), keeping its Projects, its run binding and, for the same GitHub account, its TOTP authenticator. - Blocked walk cover: the empty-frame placeholder keeps its box under the cover, so a Journey blocked before its first frame shows its reason instead of collapsing. - Slack Delete row: every message ShipBash posts carries a confirmed Delete at its foot; the click deletes the message (and any shared recording) as the bot, and the store closes alert threads and drops to-do replies hung from it, also on Slack's message_deleted event. --- .../settings/sign-ins/[signIn]/again/page.tsx | 18 +++++ .../[project]/settings/sign-ins/new/page.tsx | 2 +- .../app/_components/sign-in-add-page.tsx | 66 +++++++++++++--- .../app/_components/sign-in-setup-head.ts | 4 +- .../console/app/_components/team-sign-ins.tsx | 42 +++++++--- .../app/_components/use-sign-in-binding.ts | 20 +++-- apps/console/app/globals.css | 5 +- .../lib/production-sign-in-takeover.ts | 14 ++-- apps/console/lib/web-client-sign-ins.ts | 9 ++- .../contracts/src/api/sign-in-verification.ts | 9 ++- packages/control-plane/src/api/gateways.ts | 2 + .../src/api/routes-sign-in-verification.ts | 4 +- .../src/service/sign-in-verification.ts | 73 ++++++++++++----- .../control-plane/src/slack-alert-delivery.ts | 10 ++- .../control-plane/src/slack-alert-messages.ts | 3 +- packages/control-plane/src/slack-channels.ts | 5 +- .../control-plane/src/slack-chat/adapter.ts | 67 +++++++++++++++- packages/control-plane/src/slack-chat/bot.ts | 17 +++- .../src/slack-chat/message-deletion.ts | 79 +++++++++++++++++++ .../control-plane/src/slack-delete-row.ts | 67 ++++++++++++++++ .../src/store/internal-lanes-slack.ts | 15 ++++ .../src/store/slack-alert-threads.ts | 24 ++++++ .../src/store/slack-thread-lists.ts | 24 ++++++ 23 files changed, 501 insertions(+), 78 deletions(-) create mode 100644 apps/console/app/[team]/[project]/settings/sign-ins/[signIn]/again/page.tsx create mode 100644 packages/control-plane/src/slack-chat/message-deletion.ts create mode 100644 packages/control-plane/src/slack-delete-row.ts diff --git a/apps/console/app/[team]/[project]/settings/sign-ins/[signIn]/again/page.tsx b/apps/console/app/[team]/[project]/settings/sign-ins/[signIn]/again/page.tsx new file mode 100644 index 0000000..7e0c992 --- /dev/null +++ b/apps/console/app/[team]/[project]/settings/sign-ins/[signIn]/again/page.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from "next"; + +import { SignInAddSkeleton, SignInAgainPage } from "../../../../../../_components/sign-in-add-page"; +import { serverReadQuery } from "../../../../../../../lib/server-client"; +import { ProductPage } from "../../../../../_lib/workspace-page"; + +export const metadata: Metadata = { title: "Sign in again" }; +export default function Page({ params }: { params: Promise<{ team: string; project: string; signIn: string }> }) { + return ( + } + prefetch={(client, { projectId }) => client.prefetchQuery(serverReadQuery("projectSignIns", projectId))} + > + {({ signIn }) => } + + ); +} diff --git a/apps/console/app/[team]/[project]/settings/sign-ins/new/page.tsx b/apps/console/app/[team]/[project]/settings/sign-ins/new/page.tsx index f4a40d6..fac4fed 100644 --- a/apps/console/app/[team]/[project]/settings/sign-ins/new/page.tsx +++ b/apps/console/app/[team]/[project]/settings/sign-ins/new/page.tsx @@ -9,7 +9,7 @@ export default function Page({ params }: { params: Promise<{ team: string; proje return ( } + fallback={} prefetch={(client, { projectId }) => client.prefetchQuery(serverReadQuery("projectSignIns", projectId))} > diff --git a/apps/console/app/_components/sign-in-add-page.tsx b/apps/console/app/_components/sign-in-add-page.tsx index 40861c9..9a14765 100644 --- a/apps/console/app/_components/sign-in-add-page.tsx +++ b/apps/console/app/_components/sign-in-add-page.tsx @@ -4,18 +4,20 @@ import { useQuery } from "@tanstack/react-query"; import { ChevronLeft, TriangleAlert } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import type { BoundSignIn } from "../../lib/web-client"; import { readQuery } from "../../lib/web-client"; import { PageColumn, PageTitle, ProductGate, QueryError, useProduct } from "./shell"; import { PageSkeleton } from "./page-skeleton"; +import { PASSWORD_METHOD } from "./sign-in-methods"; import { GitHubAuthenticatorAction, GitHubAuthenticatorEditor } from "./github-authenticator"; import { SignInStageBody, useSignInStage } from "./sign-in-stage"; import type { Binding } from "./use-sign-in-binding"; /** - * Adding a way in from a product's settings, as an address of its own. + * Adding a way in from a product's settings, or signing in again to one the + * product already uses, as an address of its own. * * A route of its own rather than a modal: settings is a narrow page and the * browser wants the width, the job already has a go in, finish, @@ -56,20 +58,39 @@ function GitHubAuthenticatorOffer({ projectId, binding }: { projectId: string; b ); } -function AddStage({ bound }: { bound: readonly BoundSignIn[] }) { +/** + * Signing in again opens the way in's browser on arrival: the press that led + * here was the ask, and the page has nothing else to offer before it. + */ +function useOpenOnArrival(pick: (method: BoundSignIn["method"], signInId: string) => void, again: BoundSignIn | null) { + const asked = useRef(false); + useEffect(() => { + if (!again || asked.current) return; + asked.current = true; + pick(again.method, again.id); + }, [again, pick]); +} + +function AddStage({ bound, again, title }: { + bound: readonly BoundSignIn[]; + /** The saved way in being signed in to again; null adds a new one. */ + again: BoundSignIn | null; + title: string; +}) { const router = useRouter(); const { project, teamHref } = useProduct(); const settings = teamHref("/settings/sign-ins"); const stage = useSignInStage(project, bound); const { binding, busy } = stage; + useOpenOnArrival(stage.bind.actions.pick, again); return (