Skip to content
Merged
Changes from all commits
Commits
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
56 changes: 45 additions & 11 deletions src/levelcode/pages/ProviderSignIn.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,55 @@
import type { ReactNode } from "react";
import { API_BASE } from "../api";

// "Continue with Google" for the LevelCode Cloud login.
// Social sign-in for the LevelCode Cloud login: Google + GitHub.
//
// A real <a> (top-level GET), NOT an api() fetch and NOT the GIS id_token button: Rails' oauth_start
// 302s to Google and relies on the session cookie it sets to carry the CSRF `state`, neither of which
// survives an XHR. As a link it keeps native semantics too — keyboard activation, Cmd/Ctrl-click to
// open in a new tab, and it works without JS.
// Each is a real <a> (top-level GET), NOT an api() fetch and NOT a GIS-style token button: Rails'
// oauth_start 302s to the provider and relies on the session cookie it sets to carry the CSRF `state`,
// neither of which survives an XHR. As links they keep native semantics too — keyboard, Cmd/Ctrl-click
// to open in a new tab, works without JS.
//
// The editor's redirect_uri + code_challenge are a PKCE PAIR: forwarded only when BOTH are present, so a
// partial (unbound) handoff never reaches the backend. Absent — a plain web login — oauth_start just
// opens a web session. The backend already accepts `google` here; GitHub would come "free" the same way.
// The editor's redirect_uri + code_challenge are a PKCE PAIR, forwarded only when BOTH are present (see
// oauthStartUrl) so a partial handoff never reaches the backend. Both providers share the exact same
// oauth_start/oauth_callback path — the callback dispatches on the stashed provider.
export default function ProviderSignIn({
redirectUri,
codeChallenge,
}: {
redirectUri?: string;
codeChallenge?: string;
}) {
return (
<div className="flex flex-col gap-3">
<ProviderLink provider="google" label="Continue with Google" redirectUri={redirectUri} codeChallenge={codeChallenge}>
<GoogleG />
</ProviderLink>
<ProviderLink provider="github" label="Continue with GitHub" redirectUri={redirectUri} codeChallenge={codeChallenge}>
<GitHubMark />
</ProviderLink>
</div>
);
}

function ProviderLink({
provider,
label,
redirectUri,
codeChallenge,
children,
}: {
provider: string;
label: string;
redirectUri?: string;
codeChallenge?: string;
children: ReactNode;
Comment on lines +39 to +44
}) {
return (
<a
href={oauthStartUrl("google", redirectUri, codeChallenge)}
href={oauthStartUrl(provider, redirectUri, codeChallenge)}
className="flex w-full items-center justify-center gap-2.5 rounded-full border border-rule bg-card px-6 py-3 text-[15px] font-medium text-ink transition-colors hover:border-ink"
>
<GoogleG />
Continue with Google
{children}
{label}
</a>
);
}
Expand Down Expand Up @@ -62,3 +88,11 @@ function GoogleG() {
</svg>
);
}

function GitHubMark() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" className="shrink-0">
<path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12" />
</svg>
);
}