From a7e89c2147849824c6fef6a9870f1748e6ad3b2b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tr=C3=A8sAbhi?= <43380238+tresabhi@users.noreply.github.com>
Date: Thu, 13 Jun 2024 14:52:43 -0500
Subject: [PATCH] feat: patreon login
---
src/app/api/auth/patreon/[code]/route.ts | 17 +++
.../api/auth/patreon/refresh/[token]/route.ts | 14 +++
src/app/auth/[provider]/page.tsx | 91 ++++++++++++++++
src/app/callback/[provider]/page.tsx | 62 -----------
src/app/components/Checks.tsx | 38 +++++--
src/components/Navbar/components/LoggedIn.tsx | 26 -----
src/components/Navbar/index.tsx | 102 ++++++------------
src/core/blitz/authURL.ts | 10 --
src/core/blitz/extendAuthPatreon.ts | 20 ++++
.../{extendAuth.ts => extendAuthWargaming.ts} | 16 +--
.../blitz/{logout.ts => logoutPatreon.ts} | 4 +-
src/core/blitz/logoutWargaming.ts | 7 ++
src/icons/Wargaming.tsx | 28 +++++
src/stores/app.ts | 16 ++-
14 files changed, 260 insertions(+), 191 deletions(-)
create mode 100644 src/app/api/auth/patreon/[code]/route.ts
create mode 100644 src/app/api/auth/patreon/refresh/[token]/route.ts
create mode 100644 src/app/auth/[provider]/page.tsx
delete mode 100644 src/app/callback/[provider]/page.tsx
delete mode 100644 src/components/Navbar/components/LoggedIn.tsx
delete mode 100644 src/core/blitz/authURL.ts
create mode 100644 src/core/blitz/extendAuthPatreon.ts
rename src/core/blitz/{extendAuth.ts => extendAuthWargaming.ts} (93%)
rename src/core/blitz/{logout.ts => logoutPatreon.ts} (52%)
create mode 100644 src/core/blitz/logoutWargaming.ts
create mode 100644 src/icons/Wargaming.tsx
diff --git a/src/app/api/auth/patreon/[code]/route.ts b/src/app/api/auth/patreon/[code]/route.ts
new file mode 100644
index 000000000..9230c4b55
--- /dev/null
+++ b/src/app/api/auth/patreon/[code]/route.ts
@@ -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);
+}
diff --git a/src/app/api/auth/patreon/refresh/[token]/route.ts b/src/app/api/auth/patreon/refresh/[token]/route.ts
new file mode 100644
index 000000000..a34e8e376
--- /dev/null
+++ b/src/app/api/auth/patreon/refresh/[token]/route.ts
@@ -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);
+}
diff --git a/src/app/auth/[provider]/page.tsx b/src/app/auth/[provider]/page.tsx
new file mode 100644
index 000000000..43323a52b
--- /dev/null
+++ b/src/app/auth/[provider]/page.tsx
@@ -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 (
+
+
+ Verifying you...
+ You'll be on your way shortly.
+
+
+ );
+}
diff --git a/src/app/callback/[provider]/page.tsx b/src/app/callback/[provider]/page.tsx
deleted file mode 100644
index 1c21aac6c..000000000
--- a/src/app/callback/[provider]/page.tsx
+++ /dev/null
@@ -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 (
-
-
- Verifying you...
- You'll be on your way shortly.
-
-
- );
-}
diff --git a/src/app/components/Checks.tsx b/src/app/components/Checks.tsx
index a935d348d..719907fcb 100644
--- a/src/app/components/Checks.tsx
+++ b/src/app/components/Checks.tsx
@@ -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();
+ }
}
});
diff --git a/src/components/Navbar/components/LoggedIn.tsx b/src/components/Navbar/components/LoggedIn.tsx
deleted file mode 100644
index bafdbe7ed..000000000
--- a/src/components/Navbar/components/LoggedIn.tsx
+++ /dev/null
@@ -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 (
-
- Hello, {accountInfo.nickname}
-
-
-
-
-
- );
-}
diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx
index 34c259ab4..26ac4c298 100644
--- a/src/components/Navbar/index.tsx
+++ b/src/components/Navbar/index.tsx
@@ -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 (
- setShowHamburgerMenu(false)}>
+ setShowHamburgerMenu(false)}
+ color="gray"
+ highContrast
+ >
-
-
-
+
- {login && (
-
-
-
-
-
-
+
+
+
+
-
-
- }>
-
-
-
-
-
- )}
+
+
+ Log in with...
- {!login && false && (
-
-
-
-
-
-
-
- Choose your region
-
-
- {REGIONS.map((region) => (
- setShowHamburgerMenu(false)}
- key={region}
- href={authURL(
- region,
- typeof window === 'undefined'
- ? undefined
- : location.href,
- )}
- >
- {UNLOCALIZED_REGION_NAMES[region]}
-
- ))}
-
-
-
-
- )}
+
+
+
+
+
+
+
diff --git a/src/core/blitz/authURL.ts b/src/core/blitz/authURL.ts
deleted file mode 100644
index b6853e9dd..000000000
--- a/src/core/blitz/authURL.ts
+++ /dev/null
@@ -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}` : ''}`,
- )}`;
-}
diff --git a/src/core/blitz/extendAuthPatreon.ts b/src/core/blitz/extendAuthPatreon.ts
new file mode 100644
index 000000000..64bdea9c9
--- /dev/null
+++ b/src/core/blitz/extendAuthPatreon.ts
@@ -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,
+ };
+ });
+}
diff --git a/src/core/blitz/extendAuth.ts b/src/core/blitz/extendAuthWargaming.ts
similarity index 93%
rename from src/core/blitz/extendAuth.ts
rename to src/core/blitz/extendAuthWargaming.ts
index 578e7da38..fe507db07 100644
--- a/src/core/blitz/extendAuth.ts
+++ b/src/core/blitz/extendAuthWargaming.ts
@@ -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,
};
});
}
diff --git a/src/core/blitz/logout.ts b/src/core/blitz/logoutPatreon.ts
similarity index 52%
rename from src/core/blitz/logout.ts
rename to src/core/blitz/logoutPatreon.ts
index 290aa4774..fe7152882 100644
--- a/src/core/blitz/logout.ts
+++ b/src/core/blitz/logoutPatreon.ts
@@ -1,7 +1,7 @@
import { mutateApp } from '../../stores/app';
-export function logout() {
+export function logoutPatreon() {
mutateApp((draft) => {
- draft.login = undefined;
+ draft.logins.patreon = undefined;
});
}
diff --git a/src/core/blitz/logoutWargaming.ts b/src/core/blitz/logoutWargaming.ts
new file mode 100644
index 000000000..f96421385
--- /dev/null
+++ b/src/core/blitz/logoutWargaming.ts
@@ -0,0 +1,7 @@
+import { mutateApp } from '../../stores/app';
+
+export function logoutWargaming() {
+ mutateApp((draft) => {
+ draft.logins.wargaming = undefined;
+ });
+}
diff --git a/src/icons/Wargaming.tsx b/src/icons/Wargaming.tsx
new file mode 100644
index 000000000..458b7951a
--- /dev/null
+++ b/src/icons/Wargaming.tsx
@@ -0,0 +1,28 @@
+import { ComponentProps } from 'react';
+
+export function WargamingIcon(props: ComponentProps<'svg'>) {
+ return (
+
+ );
+}
diff --git a/src/stores/app.ts b/src/stores/app.ts
index 0931749f3..f0355572b 100644
--- a/src/stores/app.ts
+++ b/src/stores/app.ts
@@ -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()(
devBuildAgreementTime: 0,
developerMode: false,
policiesAgreementIndex: -1,
+ logins: {},
}),
{ name: 'app', merge: (a, b) => merge(b, a) },
),