Skip to content
Merged
Show file tree
Hide file tree
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
6 changes: 3 additions & 3 deletions frontend/src/components/app-layout/NavUserProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from "@mui/material";
import AccountCircleIcon from "@mui/icons-material/AccountCircle";
import LogoutIcon from "@mui/icons-material/Logout";
import { DiscordIcon } from "../common/DiscordDefaultIcon";
import LoginIcon from "@mui/icons-material/Login";
import AlertBar from "../alert/AlertBar";
import { useAuthStore } from "../../lib/store/authStore";
// Optional: reduce re-renders
Expand Down Expand Up @@ -66,13 +66,13 @@ export default function NavUserProfile() {
};

const defaultUser = () => (
<Tooltip title="Login with Discord">
<Tooltip title="Login">
<Button
variant="outlined"
href={"/v2/login"}
size="small"
sx={styles.loginButoon}
startIcon={<DiscordIcon />}
startIcon={<LoginIcon />}
data-testid="login-btn"
>
Login
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/components/common/GoogleIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const GoogleIconSvg =
"https://cdn.jsdelivr.net/gh/simple-icons/simple-icons/icons/google.svg";

export function GoogleIcon({ size = 18 }: { size?: number }) {
return (
<img
src={GoogleIconSvg}
alt="Google"
width={size}
height={size}
style={{ display: "block" }}
/>
);
}
14 changes: 14 additions & 0 deletions frontend/src/pages/login/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConstrainedContainer } from "../../components/app-layout/ConstrainedCon
import { useSearchParams } from "react-router-dom";
import { useCallback, useEffect, useState } from "react";
import { DiscordIcon } from "../../components/common/DiscordDefaultIcon";
import { GoogleIcon } from "../../components/common/GoogleIcon";
import AlertBar from "../../components/alert/AlertBar";

export default function Login() {
Expand All @@ -20,6 +21,10 @@ export default function Login() {
const loginDiscrodHref = `/api/auth/discord?next=/v2/`;
return loginDiscrodHref;
};
const googleLoginUrl = () => {
const loginGoogleHref = `/api/auth/google?next=/v2/`;
return loginGoogleHref;
};
const error = params.get("error");

const [err, setErr] = useState<{
Expand Down Expand Up @@ -102,6 +107,15 @@ export default function Login() {
>
Continue with Discord
</Button>
<Button
variant="outlined"
href={googleLoginUrl()}
size="small"
sx={{ borderRadius: 999 }}
startIcon={<GoogleIcon />}
>
Continue with Google
</Button>
<Divider>or</Divider>
<Stack spacing={1}>
<Typography
Expand Down
46 changes: 43 additions & 3 deletions kernelboard/api/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,18 @@ def providers():
"identity": lambda json: json["id"],
},
"scopes": ["identify"],
}
},
"google": {
"client_id": os.getenv("GOOGLE_CLIENT_ID"),
"client_secret": os.getenv("GOOGLE_CLIENT_SECRET"),
"authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
"token_url": "https://oauth2.googleapis.com/token",
"userinfo": {
"url": "https://www.googleapis.com/oauth2/v2/userinfo",
"identity": lambda json: json["id"],
},
"scopes": ["openid", "email", "profile"],
},
Comment on lines +70 to +80
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Missing test coverage for the new Google authentication provider. The existing tests in test_auth_api.py only cover Discord authentication. Tests should be added to verify that Google OAuth flow works correctly, including successful authentication, username extraction from Google user data, and avatar URL handling.

Copilot uses AI. Check for mistakes.
}


Expand All @@ -86,6 +97,35 @@ def _discord_avatar_url(uid: str, avatar_hash: str | None) -> str | None:
return f"https://cdn.discordapp.com/avatars/{uid}/{avatar_hash}.{ext}"


def _google_avatar_url(picture_url: str | None) -> str | None:
"""
Return Google's picture URL directly (already a full URL).
"""
return picture_url if picture_url else None


def _get_username_from_provider(provider: str, data: dict) -> str:
"""
Extract username from provider-specific user data.
"""
if provider == "discord":
return data.get("global_name") or data.get("username") or "unknown"
elif provider == "google":
return data.get("name") or data.get("email", "").split("@")[0] or "unknown"
return "unknown"


def _get_avatar_url_from_provider(provider: str, identity: str, data: dict) -> str | None:
"""
Extract avatar URL from provider-specific user data.
"""
if provider == "discord":
return _discord_avatar_url(identity, data.get("avatar"))
elif provider == "google":
return _google_avatar_url(data.get("picture"))
return None

Comment on lines +107 to +127
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Missing test coverage for the new helper functions. The _get_username_from_provider and _get_avatar_url_from_provider functions should have tests to verify they correctly handle Google-specific data structures, including edge cases like missing "name" or "picture" fields.

Copilot uses AI. Check for mistakes.

# ----- Routes -----


Expand Down Expand Up @@ -235,11 +275,11 @@ def callback(provider: str):

data = me_res.json() or {}
identity = provider_data["userinfo"]["identity"](data)
username = data.get("global_name") or data.get("username") or "unknown"
username = _get_username_from_provider(provider, data)

# 4) Stash display-only info (safe for SPA header)
session["display_name"] = username
session["avatar_url"] = _discord_avatar_url(identity, data.get("avatar"))
session["avatar_url"] = _get_avatar_url_from_provider(provider, identity, data)

# ensure user exists and has web_auth_id
# if not, update the user with the new token
Expand Down
Loading