refactor(web): simplify auth to local-only mode

Quentin Gliech and Claude Opus 4.6 (1M context) created

Remove multi-mode auth (external/readonly) from the frontend. The auth
context now always fetches the user identity via GraphQL (local mode).
Drop the ServerConfig query, OAuth select-identity route, and sign-in/
sign-out UI from the header.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Change summary

webui2/src/__generated__/graphql.ts        | 148 ++++++---------------
webui2/src/components/layout/header.tsx    |  54 -------
webui2/src/graphql/ServerConfig.graphql    |   7 -
webui2/src/lib/auth.tsx                    | 161 +++--------------------
webui2/src/routeTree.gen.ts                |  21 ---
webui2/src/routes/auth/select-identity.tsx | 139 --------------------
6 files changed, 76 insertions(+), 454 deletions(-)

Detailed changes

webui2/src/__generated__/graphql.ts ๐Ÿ”—

@@ -733,8 +733,6 @@ export type GitRef = {
   __typename?: 'GitRef';
   /** Commit hash the reference points to. */
   hash: Scalars['String']['output'];
-  /** True for the branch HEAD currently points to. */
-  isDefault: Scalars['Boolean']['output'];
   /** Full reference name, e.g. refs/heads/main or refs/tags/v1.0. */
   name: Scalars['String']['output'];
   /** Short name, e.g. main or v1.0. */
@@ -763,6 +761,11 @@ export type GitTreeEntry = {
   __typename?: 'GitTreeEntry';
   /** Git object hash. */
   hash: Scalars['String']['output'];
+  /**
+   * The last git commit that touched this tree entry. Null when the entry
+   * cannot be resolved within the history depth limit.
+   */
+  lastCommit?: Maybe<GitCommit>;
   /** File or directory name within the parent tree. */
   name: Scalars['String']['output'];
   /** Whether this entry is a file, directory, symlink, or submodule. */
@@ -967,8 +970,6 @@ export type Query = {
    * Returns null if the referenced repository does not exist.
    */
   repository?: Maybe<Repository>;
-  /** Server configuration and authentication mode. */
-  serverConfig: ServerConfig;
 };
 
 
@@ -1004,6 +1005,12 @@ export type Repository = {
    * touching path.
    */
   commits: GitCommitConnection;
+  /**
+   * The commit pointed to by HEAD in the git repository.
+   * Null if HEAD cannot be resolved to a commit, for example in an empty or unborn
+   * repository, or if HEAD is missing or invalid.
+   */
+  head?: Maybe<GitCommit>;
   /** Look up an identity by id prefix. Returns null if no identity matches the prefix. */
   identity?: Maybe<Identity>;
   /**
@@ -1116,21 +1123,6 @@ export type RepositoryEdge = {
   node: Repository;
 };
 
-/** Server-wide configuration, independent of any repository. */
-export type ServerConfig = {
-  __typename?: 'ServerConfig';
-  /**
-   * Authentication mode: 'local' (single user from git config),
-   * 'external' (multi-user via OAuth/OIDC providers), or 'readonly'.
-   */
-  authMode: Scalars['String']['output'];
-  /**
-   * Names of the login providers enabled on this server, e.g. ['github'].
-   * Empty when authMode is not 'external'.
-   */
-  loginProviders: Array<Scalars['String']['output']>;
-};
-
 export enum Status {
   Closed = 'CLOSED',
   Open = 'OPEN'
@@ -1162,12 +1154,6 @@ export type SubscriptionIdentityEventsArgs = {
   repoRef?: InputMaybe<Scalars['String']['input']>;
 };
 
-export type IdentitySummaryFragment = { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null };
-
-export type BugSummaryFragment = { __typename?: 'Bug', id: string, humanId: string, status: Status, title: string, createdAt: string, labels: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }>, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }, comments: { __typename?: 'BugCommentConnection', totalCount: number } };
-
-export type LabelFieldsFragment = { __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } };
-
 export type BugCreateCommentFieldsFragment = { __typename?: 'BugCreateTimelineItem', message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null } };
 
 export type BugAddCommentFieldsFragment = { __typename?: 'BugAddCommentTimelineItem', message: string, createdAt: string, lastEdit: string, edited: boolean, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null } };
@@ -1178,6 +1164,12 @@ export type StatusChangeFieldsFragment = { __typename?: 'BugSetStatusTimelineIte
 
 export type TitleChangeFieldsFragment = { __typename?: 'BugSetTitleTimelineItem', date: string, title: string, was: string, author: { __typename?: 'Identity', humanId: string, displayName: string } };
 
+export type IdentitySummaryFragment = { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null };
+
+export type BugSummaryFragment = { __typename?: 'Bug', id: string, humanId: string, status: Status, title: string, createdAt: string, labels: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }>, author: { __typename?: 'Identity', id: string, humanId: string, displayName: string, avatarUrl?: string | null }, comments: { __typename?: 'BugCommentConnection', totalCount: number } };
+
+export type LabelFieldsFragment = { __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } };
+
 export type AllIdentitiesQueryVariables = Exact<{
   ref?: InputMaybe<Scalars['String']['input']>;
 }>;
@@ -1279,11 +1271,6 @@ export type RepositoriesQueryVariables = Exact<{ [key: string]: never; }>;
 
 export type RepositoriesQuery = { __typename?: 'Query', repositories: { __typename?: 'RepositoryConnection', totalCount: number, nodes: Array<{ __typename?: 'Repository', name?: string | null }> } };
 
-export type ServerConfigQueryVariables = Exact<{ [key: string]: never; }>;
-
-
-export type ServerConfigQuery = { __typename?: 'Query', serverConfig: { __typename?: 'ServerConfig', authMode: string, loginProviders: Array<string> } };
-
 export type UserProfileQueryVariables = Exact<{
   ref?: InputMaybe<Scalars['String']['input']>;
   prefix: Scalars['String']['input'];
@@ -1303,16 +1290,6 @@ export type ValidLabelsQueryVariables = Exact<{
 
 export type ValidLabelsQuery = { __typename?: 'Query', repository?: { __typename?: 'Repository', validLabels: { __typename?: 'LabelConnection', nodes: Array<{ __typename?: 'Label', name: string, color: { __typename?: 'Color', R: number, G: number, B: number } }> } } | null };
 
-export const LabelFieldsFragmentDoc = gql`
-    fragment LabelFields on Label {
-  name
-  color {
-    R
-    G
-    B
-  }
-}
-    `;
 export const IdentitySummaryFragmentDoc = gql`
     fragment IdentitySummary on Identity {
   id
@@ -1321,25 +1298,6 @@ export const IdentitySummaryFragmentDoc = gql`
   avatarUrl
 }
     `;
-export const BugSummaryFragmentDoc = gql`
-    fragment BugSummary on Bug {
-  id
-  humanId
-  status
-  title
-  labels {
-    ...LabelFields
-  }
-  author {
-    ...IdentitySummary
-  }
-  createdAt
-  comments {
-    totalCount
-  }
-}
-    ${LabelFieldsFragmentDoc}
-${IdentitySummaryFragmentDoc}`;
 export const BugCreateCommentFieldsFragmentDoc = gql`
     fragment BugCreateCommentFields on BugCreateTimelineItem {
   author {
@@ -1362,6 +1320,16 @@ export const BugAddCommentFieldsFragmentDoc = gql`
   edited
 }
     ${IdentitySummaryFragmentDoc}`;
+export const LabelFieldsFragmentDoc = gql`
+    fragment LabelFields on Label {
+  name
+  color {
+    R
+    G
+    B
+  }
+}
+    `;
 export const LabelChangeFieldsFragmentDoc = gql`
     fragment LabelChangeFields on BugLabelChangeTimelineItem {
   author {
@@ -1398,6 +1366,25 @@ export const TitleChangeFieldsFragmentDoc = gql`
   was
 }
     `;
+export const BugSummaryFragmentDoc = gql`
+    fragment BugSummary on Bug {
+  id
+  humanId
+  status
+  title
+  labels {
+    ...LabelFields
+  }
+  author {
+    ...IdentitySummary
+  }
+  createdAt
+  comments {
+    totalCount
+  }
+}
+    ${LabelFieldsFragmentDoc}
+${IdentitySummaryFragmentDoc}`;
 export const AllIdentitiesDocument = gql`
     query AllIdentities($ref: String) {
   repository(ref: $ref) {
@@ -1965,49 +1952,6 @@ export type RepositoriesQueryHookResult = ReturnType<typeof useRepositoriesQuery
 export type RepositoriesLazyQueryHookResult = ReturnType<typeof useRepositoriesLazyQuery>;
 export type RepositoriesSuspenseQueryHookResult = ReturnType<typeof useRepositoriesSuspenseQuery>;
 export type RepositoriesQueryResult = Apollo.QueryResult<RepositoriesQuery, RepositoriesQueryVariables>;
-export const ServerConfigDocument = gql`
-    query ServerConfig {
-  serverConfig {
-    authMode
-    loginProviders
-  }
-}
-    `;
-
-/**
- * __useServerConfigQuery__
- *
- * To run a query within a React component, call `useServerConfigQuery` and pass it any options that fit your needs.
- * When your component renders, `useServerConfigQuery` returns an object from Apollo Client that contains loading, error, and data properties
- * you can use to render your UI.
- *
- * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
- *
- * @example
- * const { data, loading, error } = useServerConfigQuery({
- *   variables: {
- *   },
- * });
- */
-export function useServerConfigQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<ServerConfigQuery, ServerConfigQueryVariables>) {
-        const options = {...defaultOptions, ...baseOptions}
-        return ApolloReactHooks.useQuery<ServerConfigQuery, ServerConfigQueryVariables>(ServerConfigDocument, options);
-      }
-export function useServerConfigLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<ServerConfigQuery, ServerConfigQueryVariables>) {
-          const options = {...defaultOptions, ...baseOptions}
-          return ApolloReactHooks.useLazyQuery<ServerConfigQuery, ServerConfigQueryVariables>(ServerConfigDocument, options);
-        }
-// @ts-ignore
-export function useServerConfigSuspenseQuery(baseOptions?: ApolloReactHooks.SuspenseQueryHookOptions<ServerConfigQuery, ServerConfigQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<ServerConfigQuery, ServerConfigQueryVariables>;
-export function useServerConfigSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<ServerConfigQuery, ServerConfigQueryVariables>): ApolloReactHooks.UseSuspenseQueryResult<ServerConfigQuery | undefined, ServerConfigQueryVariables>;
-export function useServerConfigSuspenseQuery(baseOptions?: ApolloReactHooks.SkipToken | ApolloReactHooks.SuspenseQueryHookOptions<ServerConfigQuery, ServerConfigQueryVariables>) {
-          const options = baseOptions === ApolloReactHooks.skipToken ? baseOptions : {...defaultOptions, ...baseOptions}
-          return ApolloReactHooks.useSuspenseQuery<ServerConfigQuery, ServerConfigQueryVariables>(ServerConfigDocument, options);
-        }
-export type ServerConfigQueryHookResult = ReturnType<typeof useServerConfigQuery>;
-export type ServerConfigLazyQueryHookResult = ReturnType<typeof useServerConfigLazyQuery>;
-export type ServerConfigSuspenseQueryHookResult = ReturnType<typeof useServerConfigSuspenseQuery>;
-export type ServerConfigQueryResult = Apollo.QueryResult<ServerConfigQuery, ServerConfigQueryVariables>;
 export const UserProfileDocument = gql`
     query UserProfile($ref: String, $prefix: String!, $openQuery: String!, $closedQuery: String!, $listQuery: String!, $after: String) {
   repository(ref: $ref) {

webui2/src/components/layout/header.tsx ๐Ÿ”—

@@ -2,12 +2,9 @@
 // page (root) or inside a specific repo:
 //   - Root: shows logo only, no Code/Issues links
 //   - Repo: shows Code + Issues nav links scoped to the current repo slug
-//
-// In external mode, shows a "Sign in" button when logged out and a sign-out
-// action when logged in.
 
 import { Link, useParams, useRouterState } from "@tanstack/react-router";
-import { Plus, Sun, Moon, LogIn, LogOut } from "lucide-react";
+import { Plus, Sun, Moon } from "lucide-react";
 
 import Logo from "@/assets/logo.svg?react";
 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
@@ -17,33 +14,14 @@ import { useAuth } from "@/lib/auth";
 import { useTheme } from "@/lib/theme";
 import { cn } from "@/lib/utils";
 
-// SignOutButton sends a POST to /auth/logout and reloads the page.
-// A full reload is the simplest way to reset all Apollo cache + React state.
-function handleSignOut() {
-  void fetch("/auth/logout", { method: "POST", credentials: "include" }).finally(() =>
-    window.location.assign("/"),
-  );
-}
-
-function SignOutButton() {
-  return (
-    <Button variant="ghost" size="sm" onClick={handleSignOut} title="Sign out">
-      <LogOut className="size-4" />
-    </Button>
-  );
-}
-
 export function Header() {
-  const { user, mode, loginProviders } = useAuth();
+  const { user } = useAuth();
   const { theme, toggle } = useTheme();
 
   // Detect if we're inside a /$repo route and grab the slug.
   const params = useParams({ strict: false });
   const repo = params.repo ?? null;
 
-  // Don't show repo nav on the /auth/* pages.
-  const effectiveRepo = repo === "auth" ? null : repo;
-
   return (
     <header className="border-border bg-background/95 sticky top-0 z-50 border-b backdrop-blur">
       <div className="mx-auto flex h-14 max-w-screen-xl items-center gap-6 px-4">
@@ -54,34 +32,22 @@ export function Header() {
         </Link>
 
         {/* Repo-scoped nav links โ€” only shown when inside a repo */}
-        {effectiveRepo && <RepoNav repo={effectiveRepo} />}
+        {repo && <RepoNav repo={repo} />}
 
         <div className="ml-auto flex items-center gap-2">
-          {mode === "readonly" && <span className="text-muted-foreground text-xs">Read only</span>}
-
           <Button variant="ghost" size="icon-sm" onClick={toggle} title="Toggle theme">
             {theme === "light" ? <Moon className="size-4" /> : <Sun className="size-4" />}
           </Button>
 
-          {/* External mode: show sign-in buttons when logged out */}
-          {mode === "external" &&
-            !user &&
-            loginProviders.map((p) => (
-              <Button key={p} render={<a href={`/auth/login?provider=${p}`} />} size="sm">
-                  <LogIn className="size-4" />
-                  Sign in with {providerLabel(p)}
-              </Button>
-            ))}
-
-          {user && effectiveRepo && (
+          {user && repo && (
             <>
-              <ButtonLink to="/$repo/issues/new" params={{ repo: effectiveRepo }} size="sm">
+              <ButtonLink to="/$repo/issues/new" params={{ repo }} size="sm">
                 <Plus className="size-4" />
                 New issue
               </ButtonLink>
               <Link
                 to="/$repo/user/$id"
-                params={{ repo: effectiveRepo, id: user.humanId }}
+                params={{ repo, id: user.humanId }}
                 search={{ status: "open" as const, after: "" }}
               >
                 <Avatar className="size-7">
@@ -93,9 +59,6 @@ export function Header() {
               </Link>
             </>
           )}
-
-          {/* Sign out only shown in external mode when logged in */}
-          {mode === "external" && user && <SignOutButton />}
         </div>
       </div>
     </header>
@@ -135,8 +98,3 @@ function RepoNav({ repo }: { repo: string }) {
     </nav>
   );
 }
-
-function providerLabel(name: string): string {
-  const labels: Record<string, string> = { github: "GitHub", gitlab: "GitLab", gitea: "Gitea" };
-  return labels[name] ?? name;
-}

webui2/src/lib/auth.tsx ๐Ÿ”—

@@ -1,60 +1,11 @@
 // auth.tsx โ€” authentication context for the webui.
 //
-// Auth has three modes determined by the server's serverConfig.authMode:
-//
-//   local    Single-user mode. The identity is taken from git config at
-//            server startup. No login UI is needed.
-//
-//   external Multi-user mode. Users log in via an OAuth or OIDC provider.
-//            The current user is fetched from GET /auth/user and can be null
-//            (not logged in) even while the server is running.
-//
-//   readonly No writes allowed. No identity is ever returned.
-//
-// All three modes expose the same AuthContextValue shape, so the rest of the
-// component tree doesn't need to know which mode is active.
+// Currently only supports local (single-user) mode: the identity is taken from
+// git config at server startup and fetched via GraphQL.
 
 import { gql } from "@apollo/client";
 import { useQuery } from "@apollo/client/react";
-import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
-import * as v from "valibot";
-
-import { useServerConfigQuery } from "@/__generated__/graphql";
-
-const authUserSchema = v.object({
-  id: v.string(),
-  humanId: v.string(),
-  name: v.nullable(v.string()),
-  displayName: v.string(),
-  avatarUrl: v.nullable(v.string()),
-  email: v.nullable(v.string()),
-  login: v.nullable(v.string()),
-});
-
-// AuthUser matches the Identity type fields we care about for auth purposes.
-export type AuthUser = v.InferOutput<typeof authUserSchema>;
-
-// 'local'    โ€” single-user mode, identity from git config
-// 'external' โ€” multi-user mode, identity from OAuth/OIDC session
-// 'readonly' โ€” no identity, write operations disabled
-export type AuthMode = "local" | "external" | "readonly";
-
-export interface AuthContextValue {
-  user: AuthUser | null;
-  mode: AuthMode;
-  // List of enabled login provider names, e.g. ['github']. Only set in external mode.
-  loginProviders: string[];
-  loading: boolean;
-}
-
-const AuthContext = createContext<AuthContextValue>({
-  user: null,
-  mode: "readonly",
-  loginProviders: [],
-  loading: true,
-});
-
-// โ”€โ”€ Local mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
+import { createContext, useContext, type ReactNode } from "react";
 
 const USER_IDENTITY_QUERY = gql`
   query UserIdentity {
@@ -72,103 +23,39 @@ const USER_IDENTITY_QUERY = gql`
   }
 `;
 
-function LocalAuthProvider({
-  children,
-  loginProviders,
-}: {
-  children: ReactNode;
-  loginProviders: string[];
-}) {
-  const { data, loading } = useQuery<{ repository: { userIdentity: AuthUser | null } }>(
-    USER_IDENTITY_QUERY,
-  );
-  const user: AuthUser | null = data?.repository?.userIdentity ?? null;
-  const mode: AuthMode = loading ? "local" : user ? "local" : "readonly";
-  return (
-    <AuthContext.Provider value={{ user, mode, loginProviders, loading }}>
-      {children}
-    </AuthContext.Provider>
-  );
+export interface AuthUser {
+  id: string;
+  humanId: string;
+  name: string | null;
+  displayName: string;
+  avatarUrl: string | null;
+  email: string | null;
+  login: string | null;
 }
 
-// โ”€โ”€ External (OAuth / OIDC) mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
-
-// ExternalAuthProvider fetches the current user from the REST endpoint that the
-// Go auth handler exposes. A 401 response means "not logged in" (user is null),
-// not an error.
-function ExternalAuthProvider({
-  children,
-  loginProviders,
-}: {
-  children: ReactNode;
-  loginProviders: string[];
-}) {
-  const [user, setUser] = useState<AuthUser | null>(null);
-  const [loading, setLoading] = useState(true);
+export interface AuthContextValue {
+  user: AuthUser | null;
+  loading: boolean;
+}
 
-  useEffect(() => {
-    void fetch("/auth/user", { credentials: "include" })
-      .then(async (res) => {
-        if (res.status === 401) return null;
-        if (!res.ok) throw new Error(`/auth/user returned ${res.status}`);
-        return v.parse(authUserSchema, await res.json());
-      })
-      .then((u) => setUser(u))
-      .catch(() => setUser(null))
-      .finally(() => setLoading(false));
-  }, []);
+const AuthContext = createContext<AuthContextValue>({
+  user: null,
+  loading: true,
+});
 
-  return (
-    <AuthContext.Provider value={{ user, mode: "external", loginProviders, loading }}>
-      {children}
-    </AuthContext.Provider>
+export function AuthProvider({ children }: { children: ReactNode }) {
+  const { data, loading } = useQuery<{ repository: { userIdentity: AuthUser | null } }>(
+    USER_IDENTITY_QUERY,
   );
-}
-
-// โ”€โ”€ Read-only mode โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
+  const user: AuthUser | null = data?.repository?.userIdentity ?? null;
 
-function ReadonlyAuthProvider({ children }: { children: ReactNode }) {
   return (
-    <AuthContext.Provider
-      value={{ user: null, mode: "readonly", loginProviders: [], loading: false }}
-    >
+    <AuthContext.Provider value={{ user, loading }}>
       {children}
     </AuthContext.Provider>
   );
 }
 
-// โ”€โ”€ Root provider โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
-
-// AuthProvider first fetches serverConfig to learn the auth mode, then renders
-// the appropriate sub-provider. The split avoids conditional hook calls.
-export function AuthProvider({ children }: { children: ReactNode }) {
-  const { data, loading } = useServerConfigQuery();
-
-  if (loading || !data) {
-    // Keep the default context (readonly + loading:true) while the config loads.
-    return (
-      <AuthContext.Provider
-        value={{ user: null, mode: "readonly", loginProviders: [], loading: true }}
-      >
-        {children}
-      </AuthContext.Provider>
-    );
-  }
-
-  const { authMode, loginProviders } = data.serverConfig;
-
-  if (authMode === "readonly") {
-    return <ReadonlyAuthProvider>{children}</ReadonlyAuthProvider>;
-  }
-
-  if (authMode === "external") {
-    return <ExternalAuthProvider loginProviders={loginProviders}>{children}</ExternalAuthProvider>;
-  }
-
-  // Default: 'local'
-  return <LocalAuthProvider loginProviders={loginProviders}>{children}</LocalAuthProvider>;
-}
-
 export function useAuth(): AuthContextValue {
   return useContext(AuthContext);
 }

webui2/src/routeTree.gen.ts ๐Ÿ”—

@@ -12,7 +12,6 @@ import { Route as rootRouteImport } from './routes/__root'
 import { Route as RepoRouteImport } from './routes/$repo'
 import { Route as IndexRouteImport } from './routes/index'
 import { Route as RepoIndexRouteImport } from './routes/$repo/index'
-import { Route as AuthSelectIdentityRouteImport } from './routes/auth/select-identity'
 import { Route as RepoIssuesRouteImport } from './routes/$repo/_issues'
 import { Route as RepoCodeRouteImport } from './routes/$repo/_code'
 import { Route as RepoCommitHashRouteImport } from './routes/$repo/commit/$hash'
@@ -39,11 +38,6 @@ const RepoIndexRoute = RepoIndexRouteImport.update({
   path: '/',
   getParentRoute: () => RepoRoute,
 } as any)
-const AuthSelectIdentityRoute = AuthSelectIdentityRouteImport.update({
-  id: '/auth/select-identity',
-  path: '/auth/select-identity',
-  getParentRoute: () => rootRouteImport,
-} as any)
 const RepoIssuesRoute = RepoIssuesRouteImport.update({
   id: '/_issues',
   getParentRoute: () => RepoRoute,
@@ -96,7 +90,6 @@ const RepoCodeBlobRefSplatRoute = RepoCodeBlobRefSplatRouteImport.update({
 export interface FileRoutesByFullPath {
   '/': typeof IndexRoute
   '/$repo': typeof RepoIssuesRouteWithChildren
-  '/auth/select-identity': typeof AuthSelectIdentityRoute
   '/$repo/': typeof RepoIndexRoute
   '/$repo/commit/$hash': typeof RepoCommitHashRoute
   '/$repo/commits/$ref': typeof RepoCodeCommitsRefRoute
@@ -110,7 +103,6 @@ export interface FileRoutesByFullPath {
 export interface FileRoutesByTo {
   '/': typeof IndexRoute
   '/$repo': typeof RepoIndexRoute
-  '/auth/select-identity': typeof AuthSelectIdentityRoute
   '/$repo/commit/$hash': typeof RepoCommitHashRoute
   '/$repo/commits/$ref': typeof RepoCodeCommitsRefRoute
   '/$repo/issues/$id': typeof RepoIssuesIssuesIdRoute
@@ -126,7 +118,6 @@ export interface FileRoutesById {
   '/$repo': typeof RepoRouteWithChildren
   '/$repo/_code': typeof RepoCodeRouteWithChildren
   '/$repo/_issues': typeof RepoIssuesRouteWithChildren
-  '/auth/select-identity': typeof AuthSelectIdentityRoute
   '/$repo/': typeof RepoIndexRoute
   '/$repo/commit/$hash': typeof RepoCommitHashRoute
   '/$repo/_code/commits/$ref': typeof RepoCodeCommitsRefRoute
@@ -142,7 +133,6 @@ export interface FileRouteTypes {
   fullPaths:
     | '/'
     | '/$repo'
-    | '/auth/select-identity'
     | '/$repo/'
     | '/$repo/commit/$hash'
     | '/$repo/commits/$ref'
@@ -156,7 +146,6 @@ export interface FileRouteTypes {
   to:
     | '/'
     | '/$repo'
-    | '/auth/select-identity'
     | '/$repo/commit/$hash'
     | '/$repo/commits/$ref'
     | '/$repo/issues/$id'
@@ -171,7 +160,6 @@ export interface FileRouteTypes {
     | '/$repo'
     | '/$repo/_code'
     | '/$repo/_issues'
-    | '/auth/select-identity'
     | '/$repo/'
     | '/$repo/commit/$hash'
     | '/$repo/_code/commits/$ref'
@@ -186,7 +174,6 @@ export interface FileRouteTypes {
 export interface RootRouteChildren {
   IndexRoute: typeof IndexRoute
   RepoRoute: typeof RepoRouteWithChildren
-  AuthSelectIdentityRoute: typeof AuthSelectIdentityRoute
 }
 
 declare module '@tanstack/react-router' {
@@ -212,13 +199,6 @@ declare module '@tanstack/react-router' {
       preLoaderRoute: typeof RepoIndexRouteImport
       parentRoute: typeof RepoRoute
     }
-    '/auth/select-identity': {
-      id: '/auth/select-identity'
-      path: '/auth/select-identity'
-      fullPath: '/auth/select-identity'
-      preLoaderRoute: typeof AuthSelectIdentityRouteImport
-      parentRoute: typeof rootRouteImport
-    }
     '/$repo/_issues': {
       id: '/$repo/_issues'
       path: ''
@@ -345,7 +325,6 @@ const RepoRouteWithChildren = RepoRoute._addFileChildren(RepoRouteChildren)
 const rootRouteChildren: RootRouteChildren = {
   IndexRoute: IndexRoute,
   RepoRoute: RepoRouteWithChildren,
-  AuthSelectIdentityRoute: AuthSelectIdentityRoute,
 }
 export const routeTree = rootRouteImport
   ._addFileChildren(rootRouteChildren)

webui2/src/routes/auth/select-identity.tsx ๐Ÿ”—

@@ -1,139 +0,0 @@
-// Identity selection page (/auth/select-identity).
-//
-// Reached after a successful OAuth login when no existing git-bug identity
-// could be matched automatically (via provider metadata set by the bridge).
-// The user can either adopt an existing identity โ€” which links it to their
-// OAuth account for future logins โ€” or create a fresh one from their OAuth
-// profile.
-
-import { createFileRoute } from "@tanstack/react-router";
-import { UserCircle, Plus, AlertCircle } from "lucide-react";
-import { useEffect, useState } from "react";
-import * as v from "valibot";
-
-import { Button } from "@/components/ui/button";
-import { Skeleton } from "@/components/ui/skeleton";
-
-export const Route = createFileRoute("/auth/select-identity")({
-  component: RouteComponent,
-});
-
-const identityItemSchema = v.object({
-  repoSlug: v.string(),
-  id: v.string(),
-  humanId: v.string(),
-  displayName: v.string(),
-  login: v.optional(v.string()),
-  avatarUrl: v.optional(v.string()),
-});
-
-type IdentityItem = v.InferOutput<typeof identityItemSchema>;
-
-function RouteComponent() {
-  const [identities, setIdentities] = useState<IdentityItem[] | null>(null);
-  const [error, setError] = useState<string | null>(null);
-  const [working, setWorking] = useState(false);
-
-  useEffect(() => {
-    async function loadIdentities() {
-      try {
-        const res = await fetch("/auth/identities", { credentials: "include" });
-        if (!res.ok) throw new Error(`unexpected status ${res.status}`);
-        const data = v.parse(v.array(identityItemSchema), await res.json());
-        setIdentities(data);
-      } catch (e) {
-        setError(String(e));
-      }
-    }
-    void loadIdentities();
-  }, []);
-
-  async function adopt(identityId: string | null) {
-    setWorking(true);
-    try {
-      const res = await fetch("/auth/adopt", {
-        method: "POST",
-        credentials: "include",
-        headers: { "Content-Type": "application/json" },
-        body: JSON.stringify(identityId ? { identityId } : {}),
-      });
-      if (!res.ok) throw new Error(`adopt failed: ${res.status}`);
-      // Full page reload to reset Apollo cache and auth state cleanly.
-      window.location.assign("/");
-    } catch (e) {
-      setError(String(e));
-      setWorking(false);
-    }
-  }
-
-  return (
-    <div className="mx-auto max-w-lg py-12">
-      <div className="mb-2 flex items-center gap-3">
-        <UserCircle className="text-muted-foreground size-6" />
-        <h1 className="text-xl font-semibold">Choose your identity</h1>
-      </div>
-      <p className="text-muted-foreground mb-8 text-sm">
-        No git-bug identity was found linked to your account. Select an existing identity to link
-        it, or create a new one from your profile.
-      </p>
-
-      {error && (
-        <div className="border-destructive/30 bg-destructive/10 text-destructive mb-4 flex items-center gap-2 rounded-md border px-4 py-3 text-sm">
-          <AlertCircle className="size-4 shrink-0" />
-          {error}
-        </div>
-      )}
-
-      {!identities && !error && (
-        <div className="space-y-2">
-          {Array.from({ length: 3 }).map((_, i) => (
-            <Skeleton key={i} className="h-14 w-full rounded-md" />
-          ))}
-        </div>
-      )}
-
-      <div className="divide-border border-border divide-y rounded-md border">
-        {identities?.map((id) => (
-          <div key={id.id} className="flex items-center gap-3 px-4 py-3">
-            <div className="min-w-0 flex-1">
-              <p className="font-medium">{id.displayName}</p>
-              <p className="text-muted-foreground text-xs">
-                {id.login ? `@${id.login} ยท ` : ""}
-                {id.repoSlug} ยท {id.humanId}
-              </p>
-            </div>
-            <Button
-              size="sm"
-              disabled={working}
-              onClick={() => {
-                void adopt(id.id);
-              }}
-            >
-              Adopt
-            </Button>
-          </div>
-        ))}
-
-        {/* Always offer to create a new identity */}
-        <div className="flex items-center gap-3 px-4 py-3">
-          <div className="min-w-0 flex-1">
-            <p className="font-medium">Create new identity</p>
-            <p className="text-muted-foreground text-xs">
-              A fresh git-bug identity will be created from your OAuth profile.
-            </p>
-          </div>
-          <Button
-            size="sm"
-            disabled={working}
-            onClick={() => {
-              void adopt(null);
-            }}
-          >
-            <Plus className="size-4" />
-            Create
-          </Button>
-        </div>
-      </div>
-    </div>
-  );
-}