diff --git a/next-auth.d.ts b/next-auth.d.ts new file mode 100644 index 0000000..c218784 --- /dev/null +++ b/next-auth.d.ts @@ -0,0 +1,23 @@ +import { Session as DefaultSession } from 'next-auth'; +import { JWT as DefaultJWT } from 'next-auth/jwt'; + +declare module 'next-auth' { + /** + * Returned by `useSession`, `getSession` and received as a prop on the `SessionProvider` React Context + */ + interface Session { + user: {} & DefaultSession['user']; + } +} + +declare module 'next-auth/jwt' { + /** Returned by the `jwt` callback and `getToken`, when using JWT sessions */ + type JWT = DefaultJWT & + Record< + string, + { + accessToken?: string; + refreshToken?: string; + } + >; +} diff --git a/package.json b/package.json index 59496f6..cc6bae2 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "react": "^18", "react-dom": "^18", "react-use": "^17.4.0", + "twitter-api-sdk": "^1.2.1", "urlcat": "^3.1.0", "uuid": "^9.0.0", "viem": "^1.16.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 57e3796..5d5591e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,9 @@ dependencies: react-use: specifier: ^17.4.0 version: 17.4.0(react-dom@0.0.0-experimental-8039e6d0b-20231026)(react@0.0.0-experimental-8039e6d0b-20231026) + twitter-api-sdk: + specifier: ^1.2.1 + version: 1.2.1 urlcat: specifier: ^3.1.0 version: 3.1.0(patch_hash=7ub65l4luhyyxhheskfq5pcuuq) @@ -5450,6 +5453,13 @@ packages: zod: 3.22.4 dev: false + /abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + dependencies: + event-target-shim: 5.0.1 + dev: false + /acorn-import-assertions@1.9.0(acorn@8.11.3): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: @@ -7630,6 +7640,11 @@ packages: fast-safe-stringify: 2.1.1 dev: false + /event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + dev: false + /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false @@ -11418,6 +11433,16 @@ packages: yargs: 17.7.2 dev: false + /twitter-api-sdk@1.2.1: + resolution: {integrity: sha512-tNQ6DGYucFk94JlnUMsHCkHg5o1wnCdHh71Y2ukygNVssOdD1gNVjOpaojJrdwbEAhoZvcWdGHerCa55F8HKxQ==} + engines: {node: '>=14'} + dependencies: + abort-controller: 3.0.0 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + dev: false + /type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -12419,6 +12444,7 @@ time: /svgo-loader@4.0.0: '2023-01-21T06:39:04.523Z' /swc-loader@0.2.3: '2022-06-09T15:26:53.432Z' /tailwindcss@3.3.0: '2023-03-28T14:26:47.492Z' + /twitter-api-sdk@1.2.1: '2022-10-21T06:50:18.052Z' /typescript@5.0.2: '2023-03-16T16:41:18.461Z' /urlcat@3.1.0: '2023-01-17T18:49:25.457Z' /uuid@9.0.0: '2022-09-05T20:03:54.869Z' diff --git a/src/app/[...not_found]/page.tsx b/src/app/[...not_found]/page.tsx new file mode 100644 index 0000000..7c791c1 --- /dev/null +++ b/src/app/[...not_found]/page.tsx @@ -0,0 +1,24 @@ +export default function NotFound() { + return ( +
+
+

404

+

Page not found

+

+ Sorry, we couldn’t find the page you’re looking for. +

+
+ + Go back home + + + Contact support + +
+
+
+ ); +} diff --git a/src/app/api/auth/[...nextauth]/options.ts b/src/app/api/auth/[...nextauth]/options.ts new file mode 100644 index 0000000..829cc05 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/options.ts @@ -0,0 +1,73 @@ +/* cspell:disable */ + +import { AuthOptions } from 'next-auth'; + +import { CredentialsProvider } from '@/esm/CredentialsProvider.js'; +import { Twitter } from '@/esm/Twitter.js'; + +export const authOptions = { + debug: process.env.NODE_ENV === 'development', + providers: [ + Twitter({ + id: 'twitter_legacy', + clientId: process.env.TWITTER_CLIENT_ID, + clientSecret: process.env.TWITTER_CLIENT_SECRET, + }), + Twitter({ + clientId: process.env.TWITTER_CLIENT_ID, + clientSecret: process.env.TWITTER_CLIENT_SECRET, + version: '2.0', + }), + CredentialsProvider({ + name: 'Credentials', + credentials: { + username: { label: 'Username', type: 'text', placeholder: 'jsmith' }, + password: { label: 'Password', type: 'password' }, + }, + async authorize(credentials: Record<'username' | 'password', string> | undefined) { + const user = { id: '1', name: 'jsmith', email: 'smith@jsmith.com' }; + + if (credentials?.username === user.name && credentials?.password === 'password') { + return user; + } + return null; + }, + }), + ], + callbacks: { + jwt: async ({ token, user, account, profile, trigger, session }) => { + console.log('DEBUG: jwt'); + console.log({ + token, + user, + account, + profile, + session, + trigger, + }); + + // export tokens to session + if (account && session) { + session[account.provider] = { + ...session[account.provider], + accessToken: account.accessToken, + refreshToken: account.refreshToken, + }; + } + + if (account?.provider && !token[account.provider]) { + token[account.provider] = {}; + } + + if (account?.access_token) { + token[account.provider].accessToken = account.access_token; + } + + if (account?.refresh_token) { + token[account.provider].refreshToken = account.refresh_token!; + } + + return token; + }, + }, +} satisfies AuthOptions; diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..cfe1112 --- /dev/null +++ b/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,6 @@ +import { authOptions } from '@/app/api/auth/[...nextauth]/options.js'; +import { Auth } from '@/esm/Auth.js'; + +const handler = Auth(authOptions); + +export { handler as GET, handler as POST }; diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..66203a8 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,3 @@ +export async function GET(request: Request) { + return new Response('OK', { status: 200 }); +} diff --git a/src/app/api/twitter/me/route.ts b/src/app/api/twitter/me/route.ts new file mode 100644 index 0000000..756b5b2 --- /dev/null +++ b/src/app/api/twitter/me/route.ts @@ -0,0 +1,36 @@ +import { StatusCodes } from 'http-status-codes'; +import { NextRequest } from 'next/server.js'; +import { getServerSession } from 'next-auth'; +import { getToken, JWT } from 'next-auth/jwt'; +import { Client } from 'twitter-api-sdk'; + +import { authOptions } from '@/app/api/auth/[...nextauth]/options.js'; +import { createErrorResponseJSON } from '@/helpers/createErrorResponseJSON.js'; +import { createSuccessResponseJSON } from '@/helpers/createSuccessResponseJSON.js'; + +export function createTwitterClientV2(token: JWT) { + if (!token.twitter.accessToken) throw new Error('No Twitter token found'); + return new Client(token.twitter.accessToken); +} + +export async function GET(req: NextRequest) { + try { + const token = await getToken({ + req, + }); + const session = await getServerSession(authOptions); + + if (!token || !session) return createErrorResponseJSON('Unauthorized', { status: StatusCodes.UNAUTHORIZED }); + + const client = createTwitterClientV2(token as JWT); + const results = await client.users.findMyUser(); + + return createSuccessResponseJSON(results, { status: StatusCodes.OK }); + } catch (error) { + console.log(error); + + return createErrorResponseJSON(error instanceof Error ? error.message : 'Internal Server Error', { + status: StatusCodes.INTERNAL_SERVER_ERROR, + }); + } +} diff --git a/src/esm/Auth.ts b/src/esm/Auth.ts new file mode 100644 index 0000000..df46d2d --- /dev/null +++ b/src/esm/Auth.ts @@ -0,0 +1,3 @@ +import NextAuth from 'next-auth'; + +export const Auth = NextAuth as unknown as typeof NextAuth.default; diff --git a/src/esm/CredentialsProvider.ts b/src/esm/CredentialsProvider.ts new file mode 100644 index 0000000..41f50f9 --- /dev/null +++ b/src/esm/CredentialsProvider.ts @@ -0,0 +1,4 @@ +import NextAuthCredentialsProvider from 'next-auth/providers/credentials'; + +export const CredentialsProvider = + NextAuthCredentialsProvider as unknown as typeof NextAuthCredentialsProvider.default as unknown as typeof NextAuthCredentialsProvider.default; diff --git a/src/esm/Twitter.ts b/src/esm/Twitter.ts new file mode 100644 index 0000000..a367083 --- /dev/null +++ b/src/esm/Twitter.ts @@ -0,0 +1,3 @@ +import NextAuthTwitter from 'next-auth/providers/twitter'; + +export const Twitter = NextAuthTwitter as unknown as typeof NextAuthTwitter.default; diff --git a/tsconfig.json b/tsconfig.json index 9ab1694..bc99a8b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -37,6 +37,6 @@ "@/*": ["./src/*"] } }, - "include": ["globals.d.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": ["globals.d.ts", "next-env.d.ts", "next-auth.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }