diff --git a/app/api/exchange_token/route.ts b/app/api/exchange_token/route.ts new file mode 100644 index 0000000..f092eca --- /dev/null +++ b/app/api/exchange_token/route.ts @@ -0,0 +1,37 @@ +import { redirect } from 'next/navigation'; +import { login } from '@/lib'; + +export const GET = async (req: Request) => { + // Get the auth code and scope from the query string + const url = new URL(req.url); + const params = new URLSearchParams(url.search); + const error = params.get("error"); + const authCode = params.get("code"); + const scope = params.get("scope"); + if (error || !authCode) { + console.error("error", error); + redirect("/login/error"); + } + if (scope !== "read,activity:read_all") { + console.error("scope", scope); + redirect("/login/error"); + } + + // Exchange the auth code for an access token + const token_response = await fetch("https://www.strava.com/oauth/token", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + client_id: process.env.CLIENT_ID, + client_secret: process.env.CLIENT_SECRET, + code: authCode, + grant_type: "authorization_code", + }), + }); + const token_data = await token_response.json(); + console.log("token_data", token_data); + await login(token_data); + redirect(`/user/${token_data.athlete.id}?first_name=${token_data.athlete.firstname}`); +} diff --git a/app/globals.css b/app/globals.css index 875c01e..c6f17e2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -17,13 +17,6 @@ } body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); } @layer utilities { diff --git a/app/layout.tsx b/app/layout.tsx index 3314e47..b557131 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -15,7 +15,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + {children} ); diff --git a/app/login/error/page.tsx b/app/login/error/page.tsx new file mode 100644 index 0000000..38b7a6b --- /dev/null +++ b/app/login/error/page.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const page = () => { + return ( +

Login was unsuccessful.

+ ) +} + +export default page diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..4c6eed4 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import Link from "next/link"; + +const redirectUri = "http://localhost:3000/api/exchange_token"; +const stravaUrl = `http://www.strava.com/oauth/authorize?client_id=${process.env.CLIENT_ID}&response_type=code&redirect_uri=${redirectUri}&approval_prompt=force&scope=activity:read_all`; + +const page = () => { + return ( + <> +

Welcome to VO2 Max Calculator

+ + Login to Strava + + + ); +}; + +export default page; diff --git a/app/login/success/page.tsx b/app/login/success/page.tsx new file mode 100644 index 0000000..b947670 --- /dev/null +++ b/app/login/success/page.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +const page = () => { + return ( +

Successfully logged in

+ ) +} + +export default page diff --git a/app/page.tsx b/app/page.tsx index 5705d4e..c9db8da 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,112 +2,8 @@ import Image from "next/image"; export default function Home() { return ( -
-
-

- Get started by editing  - app/page.tsx -

-
- - By{" "} - Vercel Logo - -
-
- -
- Next.js Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
-
+ <> +

You are now on the home page

+ ); } diff --git a/app/user/[id]/page.tsx b/app/user/[id]/page.tsx new file mode 100644 index 0000000..1489523 --- /dev/null +++ b/app/user/[id]/page.tsx @@ -0,0 +1,18 @@ +import React from "react"; +import { getSession } from "@/lib"; + +const Greeting = () => { + return

Welcome

; +}; + +const page = async () => { + const session = await getSession(); + return( + <> + {Greeting()} +
{JSON.stringify(session, null, 2)}
+ + ) +}; + +export default page; diff --git a/lib.ts b/lib.ts new file mode 100644 index 0000000..7a6df3b --- /dev/null +++ b/lib.ts @@ -0,0 +1,53 @@ +import { SignJWT, jwtVerify } from "jose"; +import { cookies } from "next/headers"; +import { NextRequest, NextResponse } from "next/server"; + +const secretKey = "secret"; +const key = new TextEncoder().encode(secretKey); + +export async function encrypt(payload: any) { + return await new SignJWT(payload) + .setProtectedHeader({ alg: "HS256" }) + .setIssuedAt() + .setExpirationTime("15 minutes from now") + .sign(key); +} + +export async function decrypt(input: string): Promise { + const { payload } = await jwtVerify(input, key, { + algorithms: ["HS256"], + }); + return payload; +} + +export async function login(userData: any) { + const expires = new Date(Date.now() + 20 * 60 * 1000); + const session = await encrypt({ userData, expires }); + cookies().set("session", session, { expires, httpOnly: true }); +} + +export async function logout() { + cookies().set("session", "", { expires: new Date(0) }); +} + +export async function getSession() { + const session = cookies().get("session")?.value; + if (!session) return null; + return await decrypt(session); +} + +export async function updateSession(request: NextRequest) { + const session = request.cookies.get("session")?.value; + if (!session) return null; + + const parsed = await decrypt(session); + parsed.expires = new Date(Date.now() + 20 * 60 * 1000); + const res = NextResponse.next(); + res.cookies.set({ + name: "session", + value: await encrypt(parsed), + expires: parsed.expires, + httpOnly: true, + }); + return res; +} diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..46f329d --- /dev/null +++ b/middleware.ts @@ -0,0 +1,6 @@ +import { NextRequest } from 'next/server'; +import { updateSession } from './lib'; + +export async function middleware(request: NextRequest) { + return await updateSession(request); +} diff --git a/package-lock.json b/package-lock.json index daca924..3a09b5c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "vo2-max-calculator", "version": "0.1.0", "dependencies": { + "jose": "^5.2.4", "next": "14.2.1", "react": "^18", "react-dom": "^18" @@ -16,6 +17,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "daisyui": "^4.10.1", "eslint": "^8", "eslint-config-next": "14.2.1", "postcss": "^8", @@ -1143,6 +1145,16 @@ "node": ">= 8" } }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -1161,6 +1173,34 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.10.1.tgz", + "integrity": "sha512-Ds0Z0Fv+Xf6ZEqV4Q5JIOeKfg83xxnww0Lzid0V94vPtlQ0yYmucEa33zSctsX2VEgBALtmk5zVEqd59pnUbuQ==", + "dev": true, + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1973,6 +2013,12 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -2866,6 +2912,14 @@ "jiti": "bin/jiti.js" } }, + "node_modules/jose": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz", + "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index a2d0aca..572b8c2 100644 --- a/package.json +++ b/package.json @@ -9,18 +9,20 @@ "lint": "next lint" }, "dependencies": { + "jose": "^5.2.4", + "next": "14.2.1", "react": "^18", - "react-dom": "^18", - "next": "14.2.1" + "react-dom": "^18" }, "devDependencies": { - "typescript": "^5", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "daisyui": "^4.10.1", + "eslint": "^8", + "eslint-config-next": "14.2.1", "postcss": "^8", "tailwindcss": "^3.4.1", - "eslint": "^8", - "eslint-config-next": "14.2.1" + "typescript": "^5" } } diff --git a/tailwind.config.ts b/tailwind.config.ts index 7e4bd91..2ac1e71 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -15,6 +15,9 @@ const config: Config = { }, }, }, - plugins: [], + plugins: [require("daisyui")], + daisyui: { + themes: ["night"], + }, }; export default config;