Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
de650e0
feat: auto-link GitHub profile URL from OAuth display_name
evanjacobson Mar 21, 2026
ff36a1b
fix: exclude GitHub field from edit form when OAuth-linked
evanjacobson Mar 21, 2026
8d738a2
refactor: extract getOAuthDisplayNames helper with SQL filtering
evanjacobson Mar 21, 2026
583a41a
Sync display name on every sign in
evanjacobson Mar 21, 2026
a8252f2
Merge branch 'main' of github.com:Kilo-Org/cloud into feature/user-pr…
evanjacobson Mar 23, 2026
7a48407
Remove duplicate field from merge
evanjacobson Mar 23, 2026
49561c9
Format/lint
evanjacobson Mar 23, 2026
93ccb8c
Merge branch 'main' into feature/user-profile-autoconnect-github
evanjacobson Mar 23, 2026
63f4124
Merge branch 'main' of github.com:Kilo-Org/cloud into feature/user-pr…
evanjacobson Mar 23, 2026
791f235
re-add display_name to several places
evanjacobson Mar 24, 2026
dce0785
Merge branch 'feature/user-profile-autoconnect-github' of github.com:…
evanjacobson Mar 24, 2026
890f417
Add migration for user_auth_provider display_name column
evanjacobson Mar 24, 2026
8352b3e
Remove changes to packages/db
evanjacobson Mar 24, 2026
49832ce
Undo changes to src/tests/multi-auth.test.ts
evanjacobson Mar 24, 2026
060f5cd
Merge branch 'main' of github.com:Kilo-Org/cloud into feature/user-pr…
evanjacobson Mar 24, 2026
edd1809
fix: remove duplicate display_name property in linkAuthProviderToUser…
evanjacobson Mar 24, 2026
5e38a13
chore(trpc): rebuild bundled declarations with discord types
evanjacobson Mar 24, 2026
99f2730
Remove duplicate migration
evanjacobson Mar 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions packages/trpc/dist/index.d.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

trpc changes from #1451 did not get regenerated

Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ declare const OrganizationPlanSchema: z.ZodEnum<{
enterprise: "enterprise";
}>;
type OrganizationPlan = z.infer<typeof OrganizationPlanSchema>;
type AuthProviderId = 'email' | 'google' | 'github' | 'gitlab' | 'linkedin' | 'fake-login' | 'workos';
type AuthProviderId = 'email' | 'google' | 'github' | 'gitlab' | 'linkedin' | 'discord' | 'fake-login' | 'workos';
type IntegrationPermissions = Record<string, string>;
type PlatformRepository = {
id: number;
Expand Down Expand Up @@ -616,6 +616,23 @@ declare const kilocode_users: drizzle_orm_pg_core.PgTableWithColumns<{
identity: undefined;
generated: undefined;
}, {}, {}>;
discord_server_membership_verified_at: drizzle_orm_pg_core.PgColumn<{
name: "discord_server_membership_verified_at";
tableName: "kilocode_users";
dataType: "string";
columnType: "PgTimestampString";
data: string;
driverParam: string;
notNull: false;
hasDefault: false;
isPrimaryKey: false;
isAutoincrement: false;
hasRuntimeDefault: false;
enumValues: undefined;
baseColumn: never;
identity: undefined;
generated: undefined;
}, {}, {}>;
openrouter_upstream_safety_identifier: drizzle_orm_pg_core.PgColumn<{
name: "openrouter_upstream_safety_identifier";
tableName: "kilocode_users";
Expand Down Expand Up @@ -7552,7 +7569,7 @@ declare const rootRouter: _trpc_server.TRPCBuiltRouter<{
}>;
linkAuthProvider: _trpc_server.TRPCMutationProcedure<{
input: {
provider: "email" | "google" | "github" | "gitlab" | "linkedin" | "fake-login" | "workos";
provider: "email" | "google" | "github" | "gitlab" | "linkedin" | "discord" | "fake-login" | "workos";
};
output: {
success: true;
Expand All @@ -7561,7 +7578,7 @@ declare const rootRouter: _trpc_server.TRPCBuiltRouter<{
}>;
unlinkAuthProvider: _trpc_server.TRPCMutationProcedure<{
input: {
provider: "email" | "google" | "github" | "gitlab" | "linkedin" | "fake-login" | "workos";
provider: "email" | "google" | "github" | "gitlab" | "linkedin" | "discord" | "fake-login" | "workos";
};
output: {
success: true;
Expand Down Expand Up @@ -7693,6 +7710,23 @@ declare const rootRouter: _trpc_server.TRPCBuiltRouter<{
};
meta: object;
}>;
getDiscordGuildStatus: _trpc_server.TRPCQueryProcedure<{
input: void;
output: SuccessResult<{
linked: boolean;
discord_avatar_url: string | null;
discord_display_name: string | null;
discord_server_membership_verified_at: string | null;
}>;
meta: object;
}>;
verifyDiscordGuildMembership: _trpc_server.TRPCMutationProcedure<{
input: void;
output: SuccessResult<{
is_member: boolean;
}>;
meta: object;
}>;
}>>;
admin: _trpc_server.TRPCBuiltRouter<{
ctx: TRPCContext;
Expand Down Expand Up @@ -7893,6 +7927,7 @@ declare const rootRouter: _trpc_server.TRPCBuiltRouter<{
completed_welcome_form: boolean;
linkedin_url: string | null;
github_url: string | null;
discord_server_membership_verified_at: string | null;
openrouter_upstream_safety_identifier: string | null;
customer_source: string | null;
};
Expand Down
5 changes: 5 additions & 0 deletions src/app/(app)/profile/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getCustomerInfo } from '@/lib/customerInfo';
import { DevNukeAccountButton } from '@/components/dev/DevNukeAccountButton';
import { DevConsumeCreditsButton } from '@/components/dev/DevConsumeCreditsButton';
import { getUserFromAuthOrRedirect } from '@/lib/user.server';
import { getOAuthDisplayNames } from '@/lib/user';
import { getExtensionUrl } from '@/components/auth/getExtensionUrl';
import { cookies } from 'next/headers';
import CreditPurchaseOptions from '@/components/payment/CreditPurchaseOptions';
Expand All @@ -30,6 +31,9 @@ export default async function ProfilePage({ searchParams }: AppPageProps) {
const params = await searchParams;
const customerInfo = await getCustomerInfo(user, params);

const oauthDisplayNames = await getOAuthDisplayNames(user.id);
const githubOAuthDisplayName = oauthDisplayNames.get('github') ?? null;

const isDevelopment = process.env.NODE_ENV === 'development';
const isKiloPassUiEnabled =
isDevelopment || (await isFeatureFlagEnabled('kilo-pass-ui', user.id));
Expand All @@ -54,6 +58,7 @@ export default async function ProfilePage({ searchParams }: AppPageProps) {
imageUrl={user.google_user_image_url}
linkedinUrl={user.linkedin_url ?? null}
githubUrl={user.github_url ?? null}
githubOAuthDisplayName={githubOAuthDisplayName}
/>
</CardContent>
</Card>
Expand Down
78 changes: 51 additions & 27 deletions src/components/profile/EditProfileDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import {
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import Link from 'next/link';

type EditProfileDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
linkedinUrl: string | null;
githubUrl: string | null;
githubLinkedViaOAuth: boolean;
};

function isValidHttpUrlOrEmpty(value: string): boolean {
Expand All @@ -37,24 +39,27 @@ export function EditProfileDialog({
onOpenChange,
linkedinUrl,
githubUrl,
githubLinkedViaOAuth,
}: EditProfileDialogProps) {
const router = useRouter();
const trpc = useTRPC();

const [linkedinValue, setLinkedinValue] = useState(linkedinUrl ?? '');
const [githubValue, setGithubValue] = useState(githubUrl ?? '');
const [linkedinError, setLinkedinError] = useState<string | null>(null);
const [githubValue, setGithubValue] = useState(githubUrl ?? '');
const [githubError, setGithubError] = useState<string | null>(null);

// Reset form state when dialog opens
useEffect(() => {
if (open) {
setLinkedinValue(linkedinUrl ?? '');
setGithubValue(githubUrl ?? '');
setLinkedinError(null);
setGithubError(null);
if (!githubLinkedViaOAuth) {
setGithubValue(githubUrl ?? '');
setGithubError(null);
}
}
}, [open, linkedinUrl, githubUrl]);
}, [open, linkedinUrl, githubUrl, githubLinkedViaOAuth]);

const updateProfileMutation = useMutation(
trpc.user.updateProfile.mutationOptions({
Expand All @@ -67,7 +72,6 @@ export function EditProfileDialog({

function handleSave() {
const trimmedLinkedin = linkedinValue.trim();
const trimmedGithub = githubValue.trim();

let hasError = false;

Expand All @@ -78,19 +82,28 @@ export function EditProfileDialog({
setLinkedinError(null);
}

if (trimmedGithub !== '' && !isValidHttpUrlOrEmpty(trimmedGithub)) {
setGithubError('URL must start with http:// or https://');
hasError = true;
} else {
setGithubError(null);
}
if (!githubLinkedViaOAuth) {
const trimmedGithub = githubValue.trim();
if (trimmedGithub !== '' && !isValidHttpUrlOrEmpty(trimmedGithub)) {
setGithubError('URL must start with http:// or https://');
hasError = true;
} else {
setGithubError(null);
}

if (hasError) return;

if (hasError) return;
updateProfileMutation.mutate({
linkedin_url: trimmedLinkedin === '' ? null : trimmedLinkedin,
github_url: trimmedGithub === '' ? null : trimmedGithub,
});
} else {
if (hasError) return;

updateProfileMutation.mutate({
linkedin_url: trimmedLinkedin === '' ? null : trimmedLinkedin,
github_url: trimmedGithub === '' ? null : trimmedGithub,
});
updateProfileMutation.mutate({
linkedin_url: trimmedLinkedin === '' ? null : trimmedLinkedin,
});
}
}

return (
Expand All @@ -116,17 +129,28 @@ export function EditProfileDialog({
</div>
<div className="space-y-2">
<Label htmlFor="github-url">GitHub Profile URL</Label>
<Input
id="github-url"
placeholder="https://github.com/yourusername"
value={githubValue}
onChange={e => {
setGithubValue(e.target.value);
setGithubError(null);
}}
aria-invalid={githubError !== null}
/>
{githubError && <p className="text-destructive text-sm">{githubError}</p>}
{githubLinkedViaOAuth ? (
<p className="text-muted-foreground text-sm">
Linked via GitHub.{' '}
<Link href="/connected-accounts" className="text-primary hover:underline">
Change in Connected Accounts
</Link>
</p>
) : (
<>
<Input
id="github-url"
placeholder="https://github.com/yourusername"
value={githubValue}
onChange={e => {
setGithubValue(e.target.value);
setGithubError(null);
}}
aria-invalid={githubError !== null}
/>
{githubError && <p className="text-destructive text-sm">{githubError}</p>}
</>
)}
</div>
{updateProfileMutation.error && (
<p className="text-destructive text-sm">Failed to save profile. Please try again.</p>
Expand Down
9 changes: 8 additions & 1 deletion src/components/profile/UserProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type UserProfileCardProps = {
imageUrl: string | null;
linkedinUrl: string | null;
githubUrl: string | null;
githubOAuthDisplayName: string | null;
};

export function UserProfileCard({
Expand All @@ -20,9 +21,14 @@ export function UserProfileCard({
imageUrl,
linkedinUrl,
githubUrl,
githubOAuthDisplayName,
}: UserProfileCardProps) {
const [editDialogOpen, setEditDialogOpen] = useState(false);

const effectiveGithubUrl = githubOAuthDisplayName
? `https://github.com/${githubOAuthDisplayName}`
: githubUrl;

return (
<>
<div className="flex items-start justify-between">
Expand All @@ -47,7 +53,7 @@ export function UserProfileCard({
/>
<ProfileLink
icon={<Github className="mr-1.5 h-3.5 w-3.5" />}
url={githubUrl}
url={effectiveGithubUrl}
label="GitHub"
/>
</div>
Expand All @@ -67,6 +73,7 @@ export function UserProfileCard({
onOpenChange={setEditDialogOpen}
linkedinUrl={linkedinUrl}
githubUrl={githubUrl}
githubLinkedViaOAuth={!!githubOAuthDisplayName}
/>
</>
);
Expand Down
17 changes: 15 additions & 2 deletions src/lib/user.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,32 @@ function createGoogleAccountInfo(
hosted_domain: googleProfile.hd ?? hosted_domain_specials.non_workspace_google_account,
provider: account.provider,
provider_account_id: account.providerAccountId,
display_name: null, // Google OAuth does not provide a public profile URL
};
}

function createGitHubAccountInfo(
account: Account,
user: NextUser | AdapterUser
user: NextUser | AdapterUser,
profile: Profile | undefined
): CreateOrUpdateUserArgs | null {
if (account.provider !== 'github') return null;
assert(user.email, 'User email is required for GitHub auth');
assert(user.name, 'User name is required for GitHub auth');

const githubProfile = profile as { login?: string } | undefined;
const login = githubProfile?.login;
const validLogin =
login && /^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(login) ? login : null;

return {
google_user_email: user.email,
google_user_name: user.name || '',
hosted_domain: hosted_domain_specials.github,
google_user_image_url: user.image || '',
provider: account.provider,
provider_account_id: account.providerAccountId,
display_name: validLogin,
Comment thread
evanjacobson marked this conversation as resolved.
};
}

Expand All @@ -144,6 +152,7 @@ function createGitlabAccountInfo(
google_user_image_url: user.image || '',
provider: account.provider,
provider_account_id: account.providerAccountId,
display_name: null, // TODO: populate with profile.username when GitLab auto-link is implemented
};
}

Expand All @@ -162,6 +171,7 @@ function createLinkedInAccountInfo(
google_user_image_url: user.image || '',
provider: account.provider,
provider_account_id: account.providerAccountId,
display_name: null, // LinkedIn OAuth response does not include the vanity URL slug needed to construct a profile link
};
}

Expand Down Expand Up @@ -199,6 +209,7 @@ function createFakeAccountInfo(
hosted_domain: hosted_domain_specials.fake_devonly,
provider: account.provider,
provider_account_id: account.providerAccountId,
display_name: null,
};
}

Expand All @@ -218,6 +229,7 @@ function createSSOAccountInfo(
google_user_image_url: user.image || '',
provider: account.provider,
provider_account_id: account.providerAccountId,
display_name: null, // WorkOS SSO does not provide an upstream IdP profile URL
};
}

Expand Down Expand Up @@ -261,6 +273,7 @@ function createEmailAccountInfo(
hosted_domain,
provider: account.provider,
provider_account_id: user.email,
display_name: null,
};
}

Expand All @@ -271,7 +284,7 @@ function createAccountInfo(
): CreateOrUpdateUserArgs {
const accountInfo =
createGoogleAccountInfo(account, user, profile) ??
createGitHubAccountInfo(account, user) ??
createGitHubAccountInfo(account, user, profile) ??
createGitlabAccountInfo(account, user) ??
createLinkedInAccountInfo(account, user) ??
createDiscordAccountInfo(account, user) ??
Expand Down
Loading
Loading