Skip to content

Commit

Permalink
Google auth website (#2391)
Browse files Browse the repository at this point in the history
Add google login to website

lucide Icons has no Google icon, and as it turns out, they don't allow
any new branch icons lucide-icons/lucide#670
and will be removing old ones.

So only for logos of brands, we can use this new library, otherwise we
stick to lucide
  • Loading branch information
AbdBarho authored Apr 14, 2023
1 parent e114f5f commit 9835bf5
Show file tree
Hide file tree
Showing 20 changed files with 126 additions and 113 deletions.
4 changes: 4 additions & 0 deletions website/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ const nextConfig = {
protocol: "https",
hostname: "api.dicebear.com",
},
{
protocol: "https",
hostname: "*.googleusercontent.com",
},
],
},
experimental: {
Expand Down
9 changes: 9 additions & 0 deletions website/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions website/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@dnd-kit/utilities": "^3.2.1",
"@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.6",
"@icons-pack/react-simple-icons": "^7.2.0",
"@marsidev/react-turnstile": "^0.0.7",
"@next-auth/prisma-adapter": "^1.0.5",
"@next/bundle-analyzer": "^13.2.4",
Expand Down
8 changes: 4 additions & 4 deletions website/src/components/CallToAction.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Box, Link, Text, useColorMode } from "@chakra-ui/react";
import { Github, Users } from "lucide-react";
import { SiDiscord, SiGithub } from "@icons-pack/react-simple-icons";
import { Users } from "lucide-react";
import { useTranslation } from "next-i18next";
import { useId } from "react";

import { Container } from "./Container";
import { Discord } from "./Icons/Discord";

const CIRCLE_HEIGHT = 558;
const CIRCLE_WIDTH = 558;
Expand Down Expand Up @@ -73,7 +73,7 @@ export function CallToAction() {
type="button"
className="mb-2 flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<Discord size={20} />
<SiDiscord size={20} />
<Text as="span" className="ml-3" fontSize={["sm", "md", "lg"]}>
{t("discord")}
</Text>
Expand All @@ -84,7 +84,7 @@ export function CallToAction() {
type="button"
className="mb-2 flex items-center rounded-md border border-transparent bg-blue-600 px-4 py-3 text-base font-medium text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
>
<Github size={20} />
<SiGithub size={20} />
<Text as="span" className="ml-3" fontSize={["sm", "md", "lg"]}>
{t("github")}
</Text>
Expand Down
18 changes: 0 additions & 18 deletions website/src/components/Icons/Discord.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions website/src/components/TeamMember.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Avatar, Badge, Box, Flex, Text, useColorModeValue } from "@chakra-ui/react";
import { Github } from "lucide-react";
import { SiGithub } from "@icons-pack/react-simple-icons";
import Link from "next/link";

export interface TeamMemberProps {
Expand All @@ -26,7 +26,7 @@ export function TeamMember({ name, url, imageURL, githubURL, title }: TeamMember
{githubURL && (
<Link href={githubURL} target="_default" rel="noreferrer" title="github">
<Badge mb="1">
<Github size={12} />
<SiGithub size={12} />
</Badge>
</Link>
)}
Expand Down
9 changes: 6 additions & 3 deletions website/src/components/UserAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Image from "next/image";
import { useEffect, useState } from "react";
export function UserAvatar({ displayName, avatarUrl }: { displayName: string; avatarUrl?: string }) {
const diceBearURL = `https://api.dicebear.com/5.x/initials/png?seed=${displayName}&radius=50&backgroundType=gradientLinear`;
const diceBearURL = `https://api.dicebear.com/5.x/initials/png?seed=${encodeURIComponent(
displayName
)}&radius=50&backgroundType=gradientLinear`;

const [src, setSrc] = useState(avatarUrl ?? diceBearURL);
const [src, setSrc] = useState(avatarUrl ? avatarUrl : diceBearURL);
useEffect(() => {
setSrc(avatarUrl ?? diceBearURL);
setSrc(avatarUrl ? avatarUrl : diceBearURL);
}, [avatarUrl, diceBearURL]);

return (
Expand All @@ -15,6 +17,7 @@ export function UserAvatar({ displayName, avatarUrl }: { displayName: string; av
alt={`${displayName}'s avatar`}
width={30}
height={30}
referrerPolicy="no-referrer"
className="rounded-full"
/>
);
Expand Down
16 changes: 10 additions & 6 deletions website/src/components/UserDisplayNameCell.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import { Flex, Link, Tooltip } from "@chakra-ui/react";
import { SiDiscord, SiGoogle } from "@icons-pack/react-simple-icons";
import { Mail } from "lucide-react";
import NextLink from "next/link";
import { useHasAnyRole } from "src/hooks/auth/useHasAnyRole";
import { ROUTES } from "src/lib/routes";
import type { AuthMethod } from "src/types/Providers";

import { Discord } from "./Icons/Discord";
import { UserAvatar } from "./UserAvatar";

const AUTH_METHOD_TO_ICON: Record<AuthMethod, JSX.Element> = {
local: <Mail size="20" />,
discord: <SiDiscord size="20" />,
google: <SiGoogle size="20" />,
};

export const UserDisplayNameCell = ({
displayName,
avatarUrl,
Expand All @@ -19,19 +26,16 @@ export const UserDisplayNameCell = ({
authMethod: string;
}) => {
const isAdminOrMod = useHasAnyRole(["admin", "moderator"]);
const isEmail = authMethod === "local";

return (
<Flex gap="2" alignItems="center">
{avatarUrl !== undefined && <UserAvatar displayName={displayName} avatarUrl={avatarUrl} />}
<UserAvatar displayName={displayName} avatarUrl={avatarUrl} />
{isAdminOrMod ? (
<>
<Link as={NextLink} href={ROUTES.ADMIN_USER_DETAIL(userId)}>
{displayName}
</Link>
<Tooltip label={`This user signin with ${isEmail ? "email" : "discord"}`}>
{isEmail ? <Mail size="20"></Mail> : <Discord size="20"></Discord>}
</Tooltip>
<Tooltip label={`Signed in with ${authMethod}`}>{AUTH_METHOD_TO_ICON[authMethod]}</Tooltip>
</>
) : (
displayName
Expand Down
5 changes: 4 additions & 1 deletion website/src/lib/leaderboard_utilities.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getValidDisplayName } from "src/lib/display_name_validation";
import prisma from "src/lib/prismadb";
import { getBatchFrontendUserIdFromBackendUser } from "src/lib/users";
import { AuthMethod } from "src/types/Providers";

export const updateUsersDisplayNames = <T extends { display_name: string; username: string }>(entries: T[]) => {
return entries.map((entry) => ({
Expand All @@ -9,7 +10,9 @@ export const updateUsersDisplayNames = <T extends { display_name: string; userna
}));
};

export const updateUsersProfilePictures = async <T extends { auth_method: string; username: string }>(entires: T[]) => {
export const updateUsersProfilePictures = async <T extends { auth_method: AuthMethod; username: string }>(
entires: T[]
) => {
const frontendUserIds = await getBatchFrontendUserIdFromBackendUser(entires);

const items = await prisma.user.findMany({
Expand Down
59 changes: 23 additions & 36 deletions website/src/lib/users.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Account } from "@prisma/client";
import prisma from "src/lib/prismadb";
import { AuthMethod } from "src/types/Providers";
import type { BackendUserCore } from "src/types/Users";

/**
Expand Down Expand Up @@ -39,7 +40,7 @@ export const convertToBackendUserCore = <T extends { accounts: Account[]; id: st
return {
id: user.accounts[0].providerAccountId,
display_name: user.name,
auth_method: user.accounts[0].provider,
auth_method: user.accounts[0].provider as AuthMethod,
};
};

Expand All @@ -54,60 +55,46 @@ export const convertToBackendUserCore = <T extends { accounts: Account[]; id: st
* @param {string} id the id of the user, this field is called 'username' in the python backend's user table
* not to be confused with the user's UUID
*/
export const getFrontendUserIdForDiscordUser = async (id: string) => {
const { userId } = await prisma.account.findFirst({ where: { provider: "discord", providerAccountId: id } });
export const getFrontendUserIdForUser = async (id: string, provider: Exclude<AuthMethod, "local">) => {
const { userId } = await prisma.account.findFirst({ where: { provider: provider, providerAccountId: id } });
return userId;
};

/**
*
* @param {string} username the id of the user, this field is called 'username' in the python backend's user table
* not to be confused with the user's UUID
* @param auth_method either "local" or "discord"
*/
export const getFrontendUserIdFromBackendUser = async (username: string, auth_method: string) => {
if (auth_method === "local") {
return username;
} else if (auth_method === "discord") {
return getFrontendUserIdForDiscordUser(username);
}
throw new Error(`Unexpected auth_method: ${auth_method}`);
};

/**
* this function is similar to `getFrontendUserIdFromBackendUser`, but optimized for reducing the
* number of database calls if fetching the data for many users (i.e. leaderboard)
* Map backend users to their frontend ids, we might have to do a db call to
*/
export const getBatchFrontendUserIdFromBackendUser = async (users: { username: string; auth_method: string }[]) => {
export const getBatchFrontendUserIdFromBackendUser = async (users: { username: string; auth_method: AuthMethod }[]) => {
// for users signed up with email, the 'username' field from the backend is the id of the user in the frontend db
// we initialize the output for all users with the username for now:
const outputIds = users.map((user) => user.username);

// handle discord users a bit differently
const indicesOfDiscordUsers = users
.map((user, idx) => ({ idx, isDiscord: user.auth_method === "discord" }))
.filter((x) => x.isDiscord)
// handle non-local users differently
const indicesOfNonLocalUsers = users
.map((user, idx) => ({ idx, isNonLocal: user.auth_method !== "local" }))
.filter((x) => x.isNonLocal)
.map((x) => x.idx);

if (indicesOfDiscordUsers.length === 0) {
// no discord users, save a database call
if (indicesOfNonLocalUsers.length === 0) {
// no external users, save a database call
return outputIds;
}

// get the frontendUserIds for the discord users
// the `username` field for discord users is the id of the discord account
const discordAccountIds = indicesOfDiscordUsers.map((idx) => users[idx].username);
const discordAccounts = await prisma.account.findMany({
// get the frontendUserIds for the external users
// the `username` field for external users is the id of the their account at the provider
const externalAccountIds = indicesOfNonLocalUsers.map((idx) => users[idx].username);
const externalAccounts = await prisma.account.findMany({
where: {
provider: "discord",
providerAccountId: { in: discordAccountIds },
provider: { in: ["discord", "google"] },
providerAccountId: { in: externalAccountIds },
},
select: { userId: true, providerAccountId: true },
select: { userId: true, providerAccountId: true, provider: true },
});

indicesOfDiscordUsers.forEach((userIdx) => {
indicesOfNonLocalUsers.forEach((userIdx) => {
// NOTE: findMany will return the values unsorted, which is why we have to 'find' here
const account = discordAccounts.find((a) => a.providerAccountId === users[userIdx].username);
const account = externalAccounts.find(
(a) => a.provider === users[userIdx].auth_method && a.providerAccountId === users[userIdx].username
);
outputIds[userIdx] = account.userId;
});

Expand Down
12 changes: 7 additions & 5 deletions website/src/pages/admin/manage_user/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,17 @@ import { get } from "src/lib/api";
import { getValidDisplayName } from "src/lib/display_name_validation";
import { userlessApiClient } from "src/lib/oasst_client_factory";
import prisma from "src/lib/prismadb";
import { getFrontendUserIdForDiscordUser } from "src/lib/users";
import { getFrontendUserIdForUser } from "src/lib/users";
import { LeaderboardEntity, LeaderboardTimeFrame } from "src/types/Leaderboard";
import { AuthMethod } from "src/types/Providers";
import { User } from "src/types/Users";
import uswSWRImmutable from "swr/immutable";
import useSWRMutation from "swr/mutation";

interface UserForm {
user_id: string;
id: string;
auth_method: string;
auth_method: AuthMethod;
display_name: string;
role: Role;
notes: string;
Expand All @@ -51,7 +52,8 @@ const ManageUser = ({ user }: InferGetServerSidePropsType<typeof getServerSidePr
const toast = useToast();

const { data: stats } = uswSWRImmutable<Partial<{ [time in LeaderboardTimeFrame]: LeaderboardEntity }>>(
"/api/user_stats?uid=" + user.id,
"/api/user_stats?" +
new URLSearchParams({ id: user.id, auth_method: user.auth_method, display_name: user.display_name }),
get,
{
fallbackData: {},
Expand Down Expand Up @@ -165,8 +167,8 @@ export const getServerSideProps: GetServerSideProps<{ user: User<Role> }, { id:
}

let frontendUserId = backend_user.id;
if (backend_user.auth_method === "discord") {
frontendUserId = await getFrontendUserIdForDiscordUser(backend_user.id);
if (backend_user.auth_method === "discord" || backend_user.auth_method === "google") {
frontendUserId = await getFrontendUserIdForUser(backend_user.id, backend_user.auth_method);
}

const local_user = await prisma.user.findUnique({
Expand Down
3 changes: 2 additions & 1 deletion website/src/pages/api/admin/status.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { withAnyRole } from "src/lib/auth";
import { createApiClientFromUser } from "src/lib/oasst_client_factory";
import { BackendUserCore } from "src/types/Users";

/**
* Returns tasks availability, stats, and tree manager stats.
*/
const handler = withAnyRole(["admin", "moderator"], async (req, res) => {
// NOTE: why are we using a dummy user here?
const dummyUser = {
const dummyUser: BackendUserCore = {
id: "__dummy_user__",
display_name: "Dummy User",
auth_method: "local",
Expand Down
6 changes: 3 additions & 3 deletions website/src/pages/api/admin/update_user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ROLES } from "src/components/RoleSelect";
import { withAnyRole } from "src/lib/auth";
import { createApiClient } from "src/lib/oasst_client_factory";
import prisma from "src/lib/prismadb";
import { getFrontendUserIdForDiscordUser } from "src/lib/users";
import { getFrontendUserIdForUser } from "src/lib/users";

/**
* Update's the user's data in the database. Accessible only to admins.
Expand All @@ -17,8 +17,8 @@ const handler = withAnyRole(["admin", "moderator"], async (req, res, token) => {
}

let frontendUserId = id;
if (auth_method === "discord") {
frontendUserId = await getFrontendUserIdForDiscordUser(id);
if (auth_method === "discord" || auth_method === "google") {
frontendUserId = await getFrontendUserIdForUser(id, auth_method);
}

const oasstApiClient = await createApiClient(token);
Expand Down
Loading

0 comments on commit 9835bf5

Please sign in to comment.