Initializes Strava API integration
Sets up the basic structure for integrating with the Strava API. This includes: - Adds .devcontainer configuration for consistent development environment. - Configures eslint and prettier for code quality and formatting. - Implements secure credential management using Infisical. - Creates basic Strava client and request classes with authentication handling. - Sets up basic express routes to return data from the Strava API This commit lays the foundation for future enhancements such as fetching and displaying activity data. Removes nodemon configuration Removes the nodemon.json file as its configuration is no longer needed. fix: remove clg
This commit is contained in:
8
.devcontainer/.dockerignore
Normal file
8
.devcontainer/.dockerignore
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
npm-debug.log
|
||||||
|
dist
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
35
.devcontainer/Dockerfile
Normal file
35
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
FROM node:22-bullseye
|
||||||
|
|
||||||
|
# Install additional tools
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
curl \
|
||||||
|
wget \
|
||||||
|
vim \
|
||||||
|
nano \
|
||||||
|
build-essential \
|
||||||
|
python3 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create a non-root user 'node' if not exists (usually already in official node image)
|
||||||
|
# Set up environment
|
||||||
|
ENV NODE_ENV=development
|
||||||
|
|
||||||
|
# Install global npm packages
|
||||||
|
RUN npm install -g \
|
||||||
|
@types/node \
|
||||||
|
typescript \
|
||||||
|
ts-node \
|
||||||
|
nodemon
|
||||||
|
|
||||||
|
WORKDIR /workspace
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 3000 5000
|
||||||
|
|
||||||
|
CMD ["npm", "run", "dev"]
|
||||||
45
.devcontainer/devcontainer.json
Normal file
45
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "TypeScript Node.js",
|
||||||
|
"dockerFile": "Dockerfile",
|
||||||
|
"context": "..",
|
||||||
|
"remoteUser": "node",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/git:1": {},
|
||||||
|
"ghcr.io/devcontainers/features/github-cli:1": {}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"ms-vscode.vscode-typescript-next",
|
||||||
|
"ESLint.eslint",
|
||||||
|
"Prettier.prettier-vscode",
|
||||||
|
"ms-vscode.makefile-tools",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||||
|
"typescript.format.enable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [
|
||||||
|
3000,
|
||||||
|
5000
|
||||||
|
],
|
||||||
|
"postCreateCommand": "npm install",
|
||||||
|
"mounts": [
|
||||||
|
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/node/.ssh,type=bind,consistency=cached"
|
||||||
|
],
|
||||||
|
"remoteEnv": {
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
}
|
||||||
|
}
|
||||||
22
.eslintrc.json
Normal file
22
.eslintrc.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"node": true,
|
||||||
|
"es2020": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module",
|
||||||
|
"project": "./tsconfig.json"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "warn",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
|
||||||
|
"no-console": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
||||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"editor.fontFamily": "Fira Code",
|
||||||
|
"editor.fontLigatures": true
|
||||||
|
}
|
||||||
5297
package-lock.json
generated
Normal file
5297
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
package.json
Normal file
38
package.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"name": "strava-api",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "API to communicate with Strava account",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "nodemon --exec ts-node src/index.ts",
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"lint": "eslint src --ext .ts",
|
||||||
|
"lint:fix": "eslint src --ext .ts --fix",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\"",
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"strava",
|
||||||
|
"api"
|
||||||
|
],
|
||||||
|
"author": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@infisical/sdk": "^4.0.6",
|
||||||
|
"@types/express": "^5.0.3",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"effect": "^3.18.4",
|
||||||
|
"express": "^5.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^6.13.0",
|
||||||
|
"@typescript-eslint/parser": "^6.13.0",
|
||||||
|
"eslint": "^8.54.0",
|
||||||
|
"nodemon": "^3.0.2",
|
||||||
|
"prettier": "^3.1.0",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"typescript": "^5.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/clients/Client.ts
Normal file
61
src/clients/Client.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { Effect } from "effect";
|
||||||
|
import { InfisicalSDK } from '@infisical/sdk'
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
/**
|
||||||
|
* Abstract base class for API clients that provides secure credential management through Infisical.
|
||||||
|
*
|
||||||
|
* This class handles authentication with Infisical and provides methods to retrieve secrets
|
||||||
|
* securely. Subclasses should extend this class to implement specific API client functionality.
|
||||||
|
*
|
||||||
|
* @abstract
|
||||||
|
* @remarks
|
||||||
|
* The client uses Effect for functional error handling and async operations.
|
||||||
|
* Requires the following environment variables:
|
||||||
|
* - `INFISICAL_URL`: The URL of the Infisical instance
|
||||||
|
* - `INFISICAL_CLIENT_ID`: The client ID for universal authentication
|
||||||
|
* - `INFISICAL_CLIENT_SECRET`: The client secret for universal authentication
|
||||||
|
* - `INFISICAL_PROJECT_ID`: The project ID in Infisical
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
abstract class Client {
|
||||||
|
|
||||||
|
// log env variables to verify they are loaded
|
||||||
|
|
||||||
|
private infisicalClient: InfisicalSDK;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.infisicalClient = new InfisicalSDK({
|
||||||
|
siteUrl: process.env.INFISICAL_URL,
|
||||||
|
});
|
||||||
|
dotenv.config();
|
||||||
|
console.log("INFISICAL_URL:", process.env.INFISICAL_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): Effect.Effect<void, Error, never> {
|
||||||
|
return Effect.tryPromise({
|
||||||
|
try: () => this.infisicalClient.auth().universalAuth.login({
|
||||||
|
clientId: process.env.INFISICAL_CLIENT_ID!,
|
||||||
|
clientSecret: process.env.INFISICAL_CLIENT_SECRET!,
|
||||||
|
}),
|
||||||
|
catch: (error) => new Error(`Failed to login to Infisical: ${error}`),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getCredential(key: string): Effect.Effect<string, Error, never> {
|
||||||
|
const self = this;
|
||||||
|
return Effect.gen(function* (this: Client) {
|
||||||
|
const secret = yield* Effect.tryPromise({
|
||||||
|
try: () => self.infisicalClient.secrets().getSecret({
|
||||||
|
environment: "dev",
|
||||||
|
projectId: process.env.INFISICAL_PROJECT_ID!,
|
||||||
|
secretName: key,
|
||||||
|
}),
|
||||||
|
catch: (error) => new Error(`Failed to get secret ${key} from Infisical: ${error}`),
|
||||||
|
})
|
||||||
|
return secret.secretValue;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Client;
|
||||||
184
src/clients/strava/StravaAuth.ts
Normal file
184
src/clients/strava/StravaAuth.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import { Effect, Context, Schema } from "effect";
|
||||||
|
|
||||||
|
|
||||||
|
const StravaAuthResponse = Schema.Struct({
|
||||||
|
access_token: Schema.String,
|
||||||
|
refresh_token: Schema.String,
|
||||||
|
expires_at: Schema.Number,
|
||||||
|
expires_in: Schema.Number,
|
||||||
|
token_type: Schema.String,
|
||||||
|
});
|
||||||
|
|
||||||
|
const decodeStravaAuthResponse = Schema.decodeUnknown(
|
||||||
|
StravaAuthResponse
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface StravaAuthImpl {
|
||||||
|
getAccessToken: () => Effect.Effect<string, Error, never>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeStravaAuth = (refreshToken: string, clientId: string, clientSecret: string): StravaAuthImpl => {
|
||||||
|
let _accessToken = "";
|
||||||
|
let _refreshToken = refreshToken;
|
||||||
|
let _expiresAt = 0;
|
||||||
|
// let _expiresIn = 0;
|
||||||
|
// let _tokenType = "";
|
||||||
|
|
||||||
|
const refreshAccessToken = (): Effect.Effect<string, Error, never> => {
|
||||||
|
return Effect.tryPromise({
|
||||||
|
try: () => fetch(`https://www.strava.com/oauth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: clientSecret,
|
||||||
|
refresh_token: _refreshToken,
|
||||||
|
grant_type: "refresh_token",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
catch: (error: unknown) => new Error(`Failed to refresh token: ${error}`)
|
||||||
|
}).pipe(
|
||||||
|
Effect.flatMap((res) => {
|
||||||
|
if (!res.ok) {
|
||||||
|
return Effect.fail(new Error(`Failed to refresh token: ${res.statusText}`));
|
||||||
|
}
|
||||||
|
return Effect.tryPromise(() => res.json());
|
||||||
|
}),
|
||||||
|
Effect.flatMap(decodeStravaAuthResponse),
|
||||||
|
Effect.map((data) => {
|
||||||
|
_accessToken = data.access_token;
|
||||||
|
_refreshToken = data.refresh_token;
|
||||||
|
_expiresAt = data.expires_at * 1000; // Convert to milliseconds
|
||||||
|
// _expiresIn = data.expires_in;
|
||||||
|
// _tokenType = data.token_type;
|
||||||
|
return data.access_token;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
getAccessToken: (): Effect.Effect<string, Error, never> => {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* Effect.log(`Current time: ${Date.now()}, token expires at: ${_expiresAt}`);
|
||||||
|
// log expiresAt and current time as a soustraction to see how much time is left
|
||||||
|
yield* Effect.log(`Time left until token expires: ${_expiresAt - Date.now()} ms`);
|
||||||
|
if (_expiresAt < Date.now()) {
|
||||||
|
yield* Effect.log("Access token expired, refreshing...");
|
||||||
|
yield* refreshAccessToken();
|
||||||
|
}
|
||||||
|
return _accessToken;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StravaAuth extends Context.Tag("StravaAuth")<
|
||||||
|
StravaAuth,
|
||||||
|
StravaAuthImpl
|
||||||
|
>() { }
|
||||||
|
|
||||||
|
// class StravaAuth {
|
||||||
|
|
||||||
|
// private _accessToken: string;
|
||||||
|
// private _refreshToken: string;
|
||||||
|
// private _expiresAt: number;
|
||||||
|
// private _expiresIn: number;
|
||||||
|
// private _tokenType: string;
|
||||||
|
|
||||||
|
// constructor(accessToken: string, refreshToken: string) {
|
||||||
|
// // super();
|
||||||
|
// this._accessToken = accessToken;
|
||||||
|
// this._refreshToken = refreshToken;
|
||||||
|
// this._expiresAt = 0;
|
||||||
|
// this._expiresIn = 0;
|
||||||
|
// this._tokenType = "";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Setters, we don't use effect here as there's no complex calls nor async operations
|
||||||
|
// setAccessToken(token: string): void {
|
||||||
|
// this._accessToken = token;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setRefreshToken(token: string): void {
|
||||||
|
// this._refreshToken = token;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setExpiresAt(expiresAt: number): void {
|
||||||
|
// this._expiresAt = expiresAt;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setExpiresIn(expiresIn: number): void {
|
||||||
|
// this._expiresIn = expiresIn;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setTokenType(tokenType: string): void {
|
||||||
|
// this._tokenType = tokenType;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Getters
|
||||||
|
// getAccessToken = (): Effect.Effect<string, Error, never> => {
|
||||||
|
// return Effect.gen(function* (this: StravaAuth) {
|
||||||
|
// if (this._expiresAt < Date.now()) {
|
||||||
|
// yield* Effect.log("Access token expired, refreshing...");
|
||||||
|
// yield* this.refreshAccessToken();
|
||||||
|
// }
|
||||||
|
// return this._accessToken;
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getRefreshToken(): Effect.Effect<string, Error, never> {
|
||||||
|
// return Effect.succeed(this._refreshToken);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getExpiresAt(): Effect.Effect<number, Error, never> {
|
||||||
|
// return Effect.succeed(this._expiresAt);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getExpiresIn(): Effect.Effect<number, Error, never> {
|
||||||
|
// return Effect.succeed(this._expiresIn);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// getTokenType(): Effect.Effect<string, Error, never> {
|
||||||
|
// return Effect.succeed(this._tokenType);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// // Strava access tokens expire after 6 hours, we do need to refresh them
|
||||||
|
// private refreshAccessToken(): Effect.Effect<string, Error, never> {
|
||||||
|
// return Effect.tryPromise({
|
||||||
|
// try: () => fetch(`https://www.strava.com/oauth/token`, {
|
||||||
|
// method: "POST",
|
||||||
|
// headers: {
|
||||||
|
// "Content-Type": "application/json",
|
||||||
|
// },
|
||||||
|
// body: JSON.stringify({
|
||||||
|
// client_id: "your_client_id",
|
||||||
|
// client_secret: "your_client_secret",
|
||||||
|
// refresh_token: this._refreshToken,
|
||||||
|
// grant_type: "refresh_token",
|
||||||
|
// }),
|
||||||
|
// }),
|
||||||
|
// catch: (error: unknown) => new Error(`Failed to refresh token: ${error}`)
|
||||||
|
// }).pipe(
|
||||||
|
// Effect.flatMap((res) => {
|
||||||
|
// if (!res.ok) {
|
||||||
|
// return Effect.fail(new Error(`Failed to refresh token: ${res.statusText}`));
|
||||||
|
// }
|
||||||
|
// return Effect.tryPromise(() => res.json() as Promise<StravaAuthResponse>);
|
||||||
|
// }),
|
||||||
|
// Effect.map((data: StravaAuthResponse) => {
|
||||||
|
// this.setAccessToken(data.access_token);
|
||||||
|
// this.setRefreshToken(data.refresh_token);
|
||||||
|
// this.setExpiresAt(data.expires_at);
|
||||||
|
// this.setExpiresIn(data.expires_in);
|
||||||
|
// this.setTokenType(data.token_type);
|
||||||
|
// return data.access_token;
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
|
export default StravaAuth;
|
||||||
53
src/clients/strava/StravaClient.ts
Normal file
53
src/clients/strava/StravaClient.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Client from '../Client'
|
||||||
|
import { Effect, Layer } from 'effect';
|
||||||
|
import { makeStravaAuth, StravaAuthImpl, StravaAuth } from './StravaAuth';
|
||||||
|
import StravaRequest from './StravaRequest';
|
||||||
|
|
||||||
|
class StravaClient extends Client {
|
||||||
|
|
||||||
|
private auth!: StravaAuthImpl;
|
||||||
|
private request!: StravaRequest;
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(): Effect.Effect<StravaClient, Error, never> {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const client = new StravaClient();
|
||||||
|
yield* client.init();
|
||||||
|
return client;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): Effect.Effect<void, Error, never> {
|
||||||
|
const self = this;
|
||||||
|
const superInit = super.init();
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
yield* superInit;
|
||||||
|
const refreshToken = yield* self.getCredential('REFRESH_TOKEN');
|
||||||
|
const clientId = yield* self.getCredential('CLIENT_ID');
|
||||||
|
const clientSecret = yield* self.getCredential('CLIENT_SECRET');
|
||||||
|
self.auth = makeStravaAuth(
|
||||||
|
refreshToken,
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
);
|
||||||
|
const authLayer = Layer.succeed(StravaAuth, self.auth);
|
||||||
|
self.request = yield* StravaRequest.create().pipe(
|
||||||
|
Effect.provide(authLayer)
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAthlete(): Effect.Effect<any, Error, never> {
|
||||||
|
return this.request.getAthlete();
|
||||||
|
}
|
||||||
|
|
||||||
|
getActivities(): Effect.Effect<any, Error, never> {
|
||||||
|
// return this.request.getActivities();
|
||||||
|
return Effect.succeed([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default StravaClient;
|
||||||
45
src/clients/strava/StravaRequest.ts
Normal file
45
src/clients/strava/StravaRequest.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { Effect } from "effect/index";
|
||||||
|
import StravaAuth, { StravaAuthImpl } from "./StravaAuth";
|
||||||
|
|
||||||
|
class StravaRequest {
|
||||||
|
|
||||||
|
private baseUrl: string;
|
||||||
|
private auth: StravaAuthImpl;
|
||||||
|
|
||||||
|
constructor(auth: StravaAuthImpl) {
|
||||||
|
this.baseUrl = 'https://www.strava.com/api/v3';
|
||||||
|
this.auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
static create(): Effect.Effect<StravaRequest, Error, StravaAuth> {
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const auth = yield* StravaAuth;
|
||||||
|
return new StravaRequest(auth);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAthlete(): Effect.Effect<any, Error, never> {
|
||||||
|
const self = this;
|
||||||
|
return Effect.gen(function* (this: StravaRequest) {
|
||||||
|
const accessToken = yield* self.auth.getAccessToken();
|
||||||
|
const response = yield* Effect.tryPromise({
|
||||||
|
try: () => fetch(`${self.baseUrl}/athlete`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
catch: (error) => new Error(`Failed to fetch athlete data: ${error}`),
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
return Effect.fail(new Error(`Error fetching athlete data: ${response.statusText}`));
|
||||||
|
}
|
||||||
|
const data = yield* Effect.tryPromise({
|
||||||
|
try: () => response.json(),
|
||||||
|
catch: (error) => new Error(`Failed to parse athlete data: ${error}`),
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default StravaRequest;
|
||||||
38
src/index.ts
Normal file
38
src/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import StravaClient from './clients/strava/StravaClient';
|
||||||
|
import { Effect } from 'effect'
|
||||||
|
/**
|
||||||
|
* Strava API
|
||||||
|
* Main entry point for the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
const stravaClientEffect = StravaClient.create();
|
||||||
|
const stravaClient = Effect.runPromise(stravaClientEffect);
|
||||||
|
// Basic routes
|
||||||
|
app.get('/', (req: express.Request, res: express.Response) => {
|
||||||
|
console.log(req)
|
||||||
|
res.json({ message: 'Strava API' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/health', (req: express.Request, res: express.Response) => {
|
||||||
|
console.log(req);
|
||||||
|
res.json({ status: 'ok' });
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/athlete', async (req: express.Request, res: express.Response) => {
|
||||||
|
console.log(req.baseUrl);
|
||||||
|
const client = await stravaClient;
|
||||||
|
const athlete = await Effect.runPromise(client.getAthlete());
|
||||||
|
res.json(athlete);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.listen(PORT, () => {
|
||||||
|
console.log(`Server running on port ${PORT}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default app;
|
||||||
25
tsconfig.json
Normal file
25
tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "commonjs",
|
||||||
|
"lib": ["ES2020", "DOM"],
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noImplicitReturns": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user