mirror of
https://github.com/blitzkit/blitzkit.git
synced 2026-10-21 18:57:03 +02:00
feat: patreon login
This commit is contained in:
1 parent
03aa690247
commit
a7e89c2147
14 files changed
+260
-191
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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}` : ''}`,
|
||||
)}`;
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { mutateApp } from '../../stores/app';
|
||||
|
||||
export function logoutWargaming() {
|
||||
mutateApp((draft) => {
|
||||
draft.logins.wargaming = undefined;
|
||||
});
|
||||
}
|
||||
@@ -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
@@ -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) },
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user