feat: patreon login

This commit is contained in:
TrèsAbhi committed 2024-06-13 14:52:43 -05:00
1 parent 03aa690247
commit a7e89c2147
14 files changed
+260 -191

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { code: string } },
) {
const response = await fetch(
`https://www.patreon.com/api/oauth2/token?code=${params.code}&grant_type=authorization_code&client_id=${process.env.NEXT_PUBLIC_PATREON_CLIENT_ID}&client_secret=${process.env.PATREON_CLIENT_SECRET}&redirect_uri=${process.env.NEXT_PUBLIC_PATREON_REDIRECT_URI}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
},
);
const json = await response.json();
return NextResponse.json(json);
}
@@ -0,0 +1,14 @@
import { NextResponse } from 'next/server';
export async function GET(
request: Request,
{ params }: { params: { token: string } },
) {
const response = await fetch(
`https://www.patreon.com/api/oauth2/token?grant_type=refresh_token&refresh_token=${params.token}&client_id=${process.env.NEXT_PUBLIC_PATREON_CLIENT_ID}&client_secret=${process.env.PATREON_CLIENT_SECRET}`,
{ method: 'POST' },
);
const json = await response.json();
return NextResponse.json(json);
}
+91
View File
@@ -0,0 +1,91 @@
'use client';
import { Flex, Heading, Text } from '@radix-ui/themes';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { mutateApp } from '../../../stores/app';
export interface PatreonAuthResponse {
access_token: string;
expires_in: number;
token_type: 'Bearer';
scope: string;
refresh_token: string;
version: string;
}
export default function Page({
params,
searchParams,
}: {
params: { provider: string };
searchParams: { [key: string]: string };
}) {
const router = useRouter();
useEffect(() => {
(async () => {
switch (params.provider) {
case 'wargaming': {
if (
!searchParams.expires_at ||
!searchParams.access_token ||
!searchParams.account_id
) {
break;
}
mutateApp((draft) => {
draft.logins.wargaming = {
id: Number(searchParams.account_id),
token: searchParams.access_token,
expires: Number(searchParams.expires_at) * 1000,
};
});
break;
}
case 'patreon': {
if (!searchParams.code) break;
const data = (await fetch(
`/api/auth/patreon/${searchParams.code}`,
).then((response) => response.json())) as PatreonAuthResponse;
mutateApp((draft) => {
draft.logins.patreon = {
token: data.access_token,
refreshToken: data.refresh_token,
expires: Date.now() + data.expires_in * 1000,
};
});
break;
}
default: {
console.error(`Unknown provider: ${params.provider}`);
}
}
router.push(searchParams.return ?? '/');
})();
});
return (
<PageWrapper>
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="2"
>
<Heading>Verifying you...</Heading>
<Text color="gray">You'll be on your way shortly.</Text>
</Flex>
</PageWrapper>
);
}
-62
View File
@@ -1,62 +0,0 @@
'use client';
import { Flex, Heading, Text } from '@radix-ui/themes';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import PageWrapper from '../../../components/PageWrapper';
import { mutateApp } from '../../../stores/app';
export default function Page({
params,
searchParams,
}: {
params: { provider: string };
searchParams: { [key: string]: string };
}) {
const router = useRouter();
useEffect(() => {
switch (params.provider) {
case 'wargaming': {
if (
!searchParams.expires_at ||
!searchParams.access_token ||
!searchParams.account_id
) {
break;
}
mutateApp((draft) => {
draft.login = {
expiresAt: Number(searchParams.expires_at),
id: Number(searchParams.account_id),
token: searchParams.access_token,
};
});
break;
}
default: {
console.error(`Unknown provider: ${params.provider}`);
}
}
router.push(searchParams.return ? searchParams.return : '/');
});
return (
<PageWrapper>
<Flex
style={{ flex: 1 }}
align="center"
justify="center"
direction="column"
gap="2"
>
<Heading>Verifying you...</Heading>
<Text color="gray">You'll be on your way shortly.</Text>
</Flex>
</PageWrapper>
);
}
+28 -10
View File
@@ -10,17 +10,24 @@ import {
} from '@radix-ui/themes';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { extendAuth } from '../../core/blitz/extendAuth';
import { logout } from '../../core/blitz/logout';
import { extendAuthPatreon } from '../../core/blitz/extendAuthPatreon';
import { extendAuthWargaming } from '../../core/blitz/extendAuthWargaming';
import { logoutPatreon } from '../../core/blitz/logoutPatreon';
import { logoutWargaming } from '../../core/blitz/logoutWargaming';
import isDev from '../../core/blitzkit/isDev';
import { isLocalhost } from '../../core/blitzkit/isLocalhost';
import { CURRENT_POLICIES_AGREEMENT_INDEX, useApp } from '../../stores/app';
const DEV_BUILD_AGREEMENT_COOLDOWN = 8 * 24 * 60 * 60 * 1000;
/**
* Wargaming: 14 (refresh: 7)
* Patreon: 31 (refresh: 15)
*/
export function Checks() {
const [showDevBuildAlert, setShowDevBuildAlert] = useState(false);
const login = useApp((state) => state.login);
const logins = useApp((state) => state.logins);
const policiesAgreementIndex = useApp(
(state) => state.policiesAgreementIndex,
);
@@ -43,15 +50,26 @@ export function Checks() {
}, [policiesAgreementIndex, isLegal]);
useEffect(() => {
if (!login) return;
if (logins.wargaming) {
const expiresIn = logins.wargaming.expires - Date.now();
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
const expiresIn = login.expiresAt - Date.now() / 1000;
const expiresInDays = expiresIn / 60 / 60 / 24;
if (expiresInDays < 0) {
logoutWargaming();
} else if (expiresInDays < 7) {
extendAuthWargaming();
}
}
if (expiresInDays < 0) {
logout();
} else if (expiresInDays < 7) {
extendAuth();
if (logins.patreon) {
const expiresIn = logins.patreon.expires - Date.now();
const expiresInDays = expiresIn / 1000 / 60 / 60 / 24;
if (expiresInDays < 0) {
logoutPatreon();
} else if (expiresInDays < 15) {
extendAuthPatreon();
}
}
});
@@ -1,26 +0,0 @@
import { ExitIcon } from '@radix-ui/react-icons';
import { Flex, IconButton, Text } from '@radix-ui/themes';
import { use } from 'react';
import { getAccountInfo } from '../../../core/blitz/getAccountInfo';
import { idToRegion } from '../../../core/blitz/idToRegion';
import { logout } from '../../../core/blitz/logout';
import { useApp } from '../../../stores/app';
export function LoggedIn() {
const login = useApp((state) => state.login!);
const accountInfo = use(
getAccountInfo(idToRegion(login.id), login.id, [], {
access_token: login.token,
}),
);
return (
<Flex gap="3" align="center">
<Text>Hello, {accountInfo.nickname}</Text>
<IconButton variant="ghost" color="red" onClick={logout}>
<ExitIcon />
</IconButton>
</Flex>
);
}
+32 -70
View File
@@ -5,28 +5,16 @@ import {
HamburgerMenuIcon,
PersonIcon,
} from '@radix-ui/react-icons';
import {
Box,
Button,
Dialog,
Flex,
Heading,
IconButton,
Popover,
Spinner,
Text,
} from '@radix-ui/themes';
import { Box, Button, Flex, IconButton, Popover, Text } from '@radix-ui/themes';
import { usePathname } from 'next/navigation';
import { Suspense, useState } from 'react';
import { REGIONS, UNLOCALIZED_REGION_NAMES } from '../../constants/regions';
import { useState } from 'react';
import { TOOLS } from '../../constants/tools';
import { authURL } from '../../core/blitz/authURL';
import { BlitzkitWide } from '../../icons/BlitzkitWide';
import { PatreonIcon } from '../../icons/Patreon';
import { WargamingIcon } from '../../icons/Wargaming';
import { theme } from '../../stitches.config';
import { useApp } from '../../stores/app';
import { Link } from '../Link';
import { LoggedIn } from './components/LoggedIn';
import * as styles from './index.css';
export const NAVBAR_HEIGHT = 64;
@@ -34,7 +22,7 @@ export const NAVBAR_HEIGHT = 64;
export default function Navbar() {
const [showHamburgerMenu, setShowHamburgerMenu] = useState(false);
const pathname = usePathname();
const login = useApp((state) => state.login!);
const logins = useApp((state) => state.logins);
return (
<Box
@@ -120,66 +108,40 @@ export default function Navbar() {
<PatreonIcon width={14} height={14} />
</Link>
<Link href="/settings" onClick={() => setShowHamburgerMenu(false)}>
<Link
href="/settings"
onClick={() => setShowHamburgerMenu(false)}
color="gray"
highContrast
>
<Flex style={{ width: '100%', height: '100%' }} justify="center">
<IconButton size="4" variant="ghost" color="gray">
<GearIcon />
</IconButton>
<GearIcon />
</Flex>
</Link>
{login && (
<Popover.Root>
<Popover.Trigger>
<IconButton size="4" variant="ghost" color="gray">
<PersonIcon />
</IconButton>
</Popover.Trigger>
<Popover.Root>
<Popover.Trigger>
<PersonIcon style={{ cursor: 'pointer' }} />
</Popover.Trigger>
<Popover.Content align="center">
<Flex direction="column" gap="2">
<Suspense fallback={<Spinner />}>
<LoggedIn />
</Suspense>
</Flex>
</Popover.Content>
</Popover.Root>
)}
<Popover.Content align="end" width="320px">
<Flex direction="column" gap="2">
<Text align="center">Log in with...</Text>
{!login && false && (
<Dialog.Root>
<Dialog.Trigger>
<Button variant="ghost">Log in</Button>
</Dialog.Trigger>
<Dialog.Content
style={{
maxWidth: 360,
}}
>
<Flex direction="column" gap="2" align="center">
<Heading size="3">Choose your region</Heading>
<Flex gap="4" align="center" justify="center" wrap="wrap">
{REGIONS.map((region) => (
<Link
onClick={() => setShowHamburgerMenu(false)}
key={region}
href={authURL(
region,
typeof window === 'undefined'
? undefined
: location.href,
)}
>
{UNLOCALIZED_REGION_NAMES[region]}
</Link>
))}
</Flex>
</Flex>
</Dialog.Content>
</Dialog.Root>
)}
<Link
style={{ width: '100%' }}
href={`https://www.patreon.com/oauth2/authorize?response_type=code&client_id=${process.env.NEXT_PUBLIC_PATREON_CLIENT_ID}&redirect_uri=${process.env.NEXT_PUBLIC_PATREON_REDIRECT_URI}`}
>
<Button style={{ width: '100%' }} color="tomato">
<PatreonIcon width={15} height={15} /> Patreon
</Button>
</Link>
<Button color="red">
<WargamingIcon width={15} height={15} /> Wargaming
</Button>
</Flex>
</Popover.Content>
</Popover.Root>
</Flex>
</Flex>
-10
View File
@@ -1,10 +0,0 @@
import { Region } from '../../constants/regions';
import { WARGAMING_APPLICATION_ID } from '../../constants/wargamingApplicationID';
export function authURL(region: Region, callback?: string) {
if (typeof window === 'undefined') throw new Error('window is undefined');
return `https://api.worldoftanks.${region}/wot/auth/login/?application_id=${WARGAMING_APPLICATION_ID}&redirect_uri=${encodeURIComponent(
`${window.location.origin}/callback/wargaming/${callback ? `?return=${location.href}` : ''}`,
)}`;
}
+20
View File
@@ -0,0 +1,20 @@
import { PatreonAuthResponse } from '../../app/auth/[provider]/page';
import { mutateApp, useApp } from '../../stores/app';
export async function extendAuthPatreon() {
const { patreon } = useApp.getState().logins;
if (!patreon) return;
const data = (await fetch(
`/api/auth/patreon/refresh/${patreon.refreshToken}`,
).then((response) => response.json())) as PatreonAuthResponse;
mutateApp((draft) => {
draft.logins.patreon = {
token: data.access_token,
refreshToken: data.refresh_token,
expires: Date.now() + data.expires_in * 1000,
};
});
}
@@ -10,10 +10,10 @@ interface Extension {
};
}
export async function extendAuth() {
const { login } = useApp.getState();
export async function extendAuthWargaming() {
const { wargaming } = useApp.getState().logins;
if (!login) return;
if (!wargaming) return;
// error handling? what's that?
/*
@@ -42,18 +42,18 @@ export async function extendAuth() {
const response = await fetch(
`https://api.worldoftanks.${idToRegion(
login.id,
wargaming.id,
)}/wot/auth/prolongate/?application_id=${WARGAMING_APPLICATION_ID}&access_token=${
login.token
wargaming.token
}`,
);
const json = (await response.json()) as Extension;
mutateApp((draft) => {
draft.login = {
id: login.id,
draft.logins.wargaming = {
id: wargaming.id,
token: json.data.access_token,
expiresAt: json.data.expires_at,
expires: json.data.expires_at * 1000,
};
});
}
@@ -1,7 +1,7 @@
import { mutateApp } from '../../stores/app';
export function logout() {
export function logoutPatreon() {
mutateApp((draft) => {
draft.login = undefined;
draft.logins.patreon = undefined;
});
}
+7
View File
@@ -0,0 +1,7 @@
import { mutateApp } from '../../stores/app';
export function logoutWargaming() {
mutateApp((draft) => {
draft.logins.wargaming = undefined;
});
}
+28
View File
@@ -0,0 +1,28 @@
import { ComponentProps } from 'react';
export function WargamingIcon(props: ComponentProps<'svg'>) {
return (
<svg
width="15"
height="15"
viewBox="0 0 15 15"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clip-path="url(#clip0_574_231)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.46409 0.156338C8.91652 0.149247 10.3384 0.57301 11.55 1.37404C12.7616 2.17507 13.7085 3.31739 14.2709 4.65654C14.8332 5.9957 14.9859 7.47154 14.7095 8.89745C14.4331 10.3234 13.7401 11.6353 12.7181 12.6673C11.6961 13.6994 10.391 14.4052 8.96789 14.6955C7.54475 14.9859 6.06748 14.8477 4.72288 14.2984C3.37829 13.7492 2.22676 12.8136 1.41391 11.6099C0.601062 10.4062 0.163403 8.98846 0.156278 7.53603V7.50009C0.150882 5.5586 0.916644 3.69442 2.28523 2.31733C3.65382 0.940243 5.51323 0.162958 7.45472 0.156338H7.46409ZM7.46409 1.56259C8.76962 1.56799 10.0372 2.00218 11.0719 2.79837C12.1065 3.59456 12.8509 4.70863 13.1906 5.96922C13.5302 7.2298 13.4463 8.56706 12.9517 9.77529C12.4571 10.9835 11.5792 11.9958 10.4532 12.6563C10.8305 11.3103 10.9857 9.91172 10.9125 8.51571C11.2938 8.62353 11.5094 8.68446 11.8922 8.8079C11.47 7.70829 10.9518 6.64795 10.3438 5.63915C9.79373 6.67527 9.10279 7.63018 8.29065 8.47665L9.51565 8.40009C8.46097 10.2938 6.97347 11.2126 5.08909 11.4798C4.6594 9.33759 4.98128 5.85478 7.38597 4.21728L7.62972 5.38603C8.09847 4.63134 9.02503 3.6329 9.71409 2.93759C8.57965 2.49502 7.34402 2.37866 6.1469 2.60165C6.34534 2.77196 6.59065 2.98759 6.78909 3.15634C4.07815 4.3079 2.58909 6.20009 1.6094 8.40009C1.48639 7.55586 1.54531 6.69509 1.78221 5.87549C2.0191 5.0559 2.4285 4.29643 2.98295 3.64802C3.5374 2.99961 4.22409 2.47724 4.99695 2.11595C5.76982 1.75465 6.611 1.56279 7.46409 1.55321V1.56259Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_574_231">
<rect width="15" height="15" fill="currentColor" />
</clipPath>
</defs>
</svg>
);
}
+13 -3
View File
@@ -3,16 +3,25 @@ import { merge } from 'lodash';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Login {
interface WargamingLogin {
id: number;
token: string;
expiresAt: number;
expires: number;
}
interface PatreonLogin {
token: string;
refreshToken: string;
expires: number;
}
export interface App {
devBuildAgreementTime: number;
developerMode: boolean;
login?: Login;
logins: {
wargaming?: WargamingLogin;
patreon?: PatreonLogin;
};
policiesAgreementIndex: number;
}
@@ -24,6 +33,7 @@ export const useApp = create<App>()(
devBuildAgreementTime: 0,
developerMode: false,
policiesAgreementIndex: -1,
logins: {},
}),
{ name: 'app', merge: (a, b) => merge(b, a) },
),