@@ -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) {
@@ -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;
-}
@@ -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);
}
@@ -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)
@@ -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>
- );
-}