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
-
-
-
-
-
-
-
-
-
-
+ <>
+ 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;