mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-17 00:02:59 +00:00
* feat: single-use login links and oauth-claimable pending accounts * docs: capture the auth surface facts behind login links * fix: accept stringified email_verified from oauth userinfo * docs: describe the oauth claim rule in the auth surface notes * fix: harden login-link redirects and sweep expired links * chore: bump ee-repo-ref * fix: keep expired login links a day so an open still reads as expired * fix: refuse login links for superadmin and devops accounts * fix: re-check the account's roles when a login link is opened * feat: pre-approved cloud accounts finish their setup and start their trial from Windmill * feat: dev-only localStorage opt-in to the cloud UI on localhost * feat: finish-setup entry in the desktop settings menu * style: pulse the settings row while account setup is pending; shorter, blue finish-setup entry * fix: list the configured providers in the finish-setup modal * fix: open the finish-setup modal after the menu has closed * feat: finish-setup provider sign-in keeps the session when the provider asserts another address * chore: pin the EE companion commit * fix: plain toast for the finish-setup refusal * style: format the dev cloud override Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * feat: onboarding skips the source question an invite already answered Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: type the finish-setup icons and login_type as the frontend uses them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * feat: invited accounts get a workspace name, hub picks and starter prompts from their invite Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: the workspace form reads the invite's name itself, so the picker prefills it too Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * feat: an empty workspace offers the projects its invite picked, one click from importing Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * style: picked projects get identical import buttons Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * feat: a pinned sidebar banner until an invited account has credentials of its own Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * style: the account-setup row speaks the rail's language, tinted not filled Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * chore: pin ee-repo-ref to the import fix Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * refactor: picked projects live in the template picker only; account-setup row moves to the rail footer Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: review round — no portal login for job tokens, finish-setup failures keep the session, prompt labels deduped Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: CI round — trial start is a POST, profile cache follows the session, setup row on MenuButton Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: CI round — no password road where password login is off, cache note on the login form, trial refusal surfaced Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: CI round — set_password guarded on its read, refusal stays on the page, docs and formatting Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: CI round — popup OAuth clears the profile cache, portal helper crate-private, refusal toast stays Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: a refused trial is recorded inline in the rail, not in a day-long toast Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: the refusal notice uses the rail's button and has a collapsed form Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: CI round — SSO can finish account setup, with the same mismatch refusal as OAuth Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: SSO finish-setup rides in RelayState and the refusal notice is a status region Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: keep the finish-setup cookie beside RelayState, hoist the status region, pin session-keyed profile cache Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: empty live region for the trial refusal, drop the setup cookie once adopted, telemetry inventory Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: the trial refusal survives the responsive sidebar swap Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: the trial refusal is shown to the account it answers, modal open prop is required Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * feat: an invited account skips the whole onboarding survey Its source is the invite and its use case was researched before the invite went out, so neither question is asked: the known source is recorded and onboarding opens on naming the workspace. Accounts without an invite profile see the survey exactly as before. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: an invited account with a workspace leaves onboarding before anything paints Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: account-setup state resets on sign-out, onboarding shows a loading state while it settles Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * style: keep the refresh doc comment on refresh Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * fix: profile lists are distinct, and the offer table notes what a users-import does to it Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011HMniEf5hapoKEB6TEBcGy * chore: update ee-repo-ref to 1ba6fe83451f0a1f8fafe04b7187087d51e0f769 This commit updates the EE repository reference after PR #750 was merged in windmill-ee-private. Previous ee-repo-ref: be42722d09832ffff709a1f710f3e97e34d513b2 New ee-repo-ref: 1ba6fe83451f0a1f8fafe04b7187087d51e0f769 Automated by sync-ee-ref workflow. --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
117 lines
4.5 KiB
TypeScript
117 lines
4.5 KiB
TypeScript
import { UserService } from './gen'
|
|
import { WORKSPACE_NAME_MAX_LENGTH } from './utils/workspaceId'
|
|
|
|
/**
|
|
* What an invited cloud account arrives knowing about itself: the context the invite
|
|
* carried, recorded by the provisioning script before the person ever signs in. Every key
|
|
* is optional and the whole thing is absent for a self-signup, so each consumer must read
|
|
* it as a hint and fall back to its default.
|
|
*
|
|
* The blob is written by an outbound pipeline that may grow new keys at any time; this is
|
|
* the only place that decides which of them mean anything to the frontend, and how far a
|
|
* value is trusted before it is shown.
|
|
*/
|
|
export interface OnboardingProfile {
|
|
/** Answers onboarding's source question; `outbound:<campaign>`. */
|
|
touch_point?: string
|
|
company?: string
|
|
/** What to call the first workspace; `company` is the fallback. */
|
|
workspace_name?: string
|
|
/** Hub project slugs to surface first on an empty workspace. */
|
|
hub_projects?: string[]
|
|
/** Replace the home page's example prompts with ones written for this person. */
|
|
starter_prompts?: StarterPrompt[]
|
|
/** Integrations they are known to use, for picking hub projects when none are named. */
|
|
tools?: string[]
|
|
}
|
|
|
|
export interface StarterPrompt {
|
|
label: string
|
|
prompt: string
|
|
}
|
|
|
|
const MAX_LIST = 12
|
|
const MAX_LABEL = 40
|
|
const MAX_PROMPT = 500
|
|
|
|
function str(v: unknown, max: number): string | undefined {
|
|
if (typeof v !== 'string') return undefined
|
|
const t = v.trim()
|
|
return t && t.length <= max ? t : undefined
|
|
}
|
|
|
|
/** Distinct entries in their first order: a repeated slug would pin one project twice. */
|
|
function strList(v: unknown, max: number): string[] | undefined {
|
|
if (!Array.isArray(v)) return undefined
|
|
const out = [...new Set(v.map((x) => str(x, max)).filter((x): x is string => !!x))]
|
|
return out.length ? out.slice(0, MAX_LIST) : undefined
|
|
}
|
|
|
|
/**
|
|
* The keys the frontend acts on, shape-checked one by one. A malformed key is dropped, not
|
|
* the whole profile: the touch point that skips a survey question must survive a pipeline
|
|
* that produced a bad prompt list, and vice versa.
|
|
*/
|
|
export function parseOnboardingProfile(raw: unknown): OnboardingProfile | null {
|
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null
|
|
const r = raw as Record<string, unknown>
|
|
// Labels are what the home page keys its tags by, so two prompts sharing one would
|
|
// break its list: the first wins.
|
|
const seen = new Set<string>()
|
|
const prompts = Array.isArray(r.starter_prompts)
|
|
? r.starter_prompts
|
|
.map((p) => {
|
|
if (!p || typeof p !== 'object') return undefined
|
|
const label = str((p as Record<string, unknown>).label, MAX_LABEL)
|
|
const prompt = str((p as Record<string, unknown>).prompt, MAX_PROMPT)
|
|
return label && prompt ? { label, prompt } : undefined
|
|
})
|
|
.filter((p): p is StarterPrompt => !!p && !seen.has(p.label) && !!seen.add(p.label))
|
|
.slice(0, MAX_LIST)
|
|
: []
|
|
const profile: OnboardingProfile = {
|
|
touch_point: str(r.touch_point, 200),
|
|
company: str(r.company, WORKSPACE_NAME_MAX_LENGTH),
|
|
workspace_name: str(r.workspace_name, WORKSPACE_NAME_MAX_LENGTH),
|
|
hub_projects: strList(r.hub_projects, 100),
|
|
starter_prompts: prompts.length ? prompts : undefined,
|
|
tools: strList(r.tools, 50)?.map((t) => t.toLowerCase())
|
|
}
|
|
return Object.values(profile).some((v) => v !== undefined) ? profile : null
|
|
}
|
|
|
|
let cached: Promise<OnboardingProfile | null> | undefined
|
|
let cachedFor: string | undefined
|
|
|
|
/**
|
|
* The profile belongs to a session, not to the page: sign-in and sign-out are client-side
|
|
* navigations, so a cache keyed on nothing would hand account A's invite context to
|
|
* account B signing in on the same tab. The root layout reports the session's address
|
|
* whenever it learns it, and logout clears; a change drops what was cached.
|
|
*/
|
|
export function noteSessionEmail(email: string | undefined) {
|
|
if (email !== cachedFor) {
|
|
cached = undefined
|
|
cachedFor = email
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The signed-in account's profile, fetched once per session. Off cloud the server answers
|
|
* `null` without a lookup, so the cost is one request per session. A failed fetch reads as
|
|
* "no profile" and is retried on the next call rather than cached.
|
|
*/
|
|
export function onboardingProfile(): Promise<OnboardingProfile | null> {
|
|
if (!cached) {
|
|
const p = UserService.getOnboardingProfile()
|
|
.then((r) => parseOnboardingProfile(r.profile))
|
|
.catch((e) => {
|
|
console.error('Could not read the onboarding profile:', e)
|
|
if (cached === p) cached = undefined
|
|
return null
|
|
})
|
|
cached = p
|
|
}
|
|
return cached
|
|
}
|