Compare commits
3 Commits
main
...
feature/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f8d962edd | ||
|
|
929f0e38f6 | ||
|
|
bb00f74d17 |
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
|
||||
}
|
||||
30
README.md
30
README.md
@@ -1,3 +1,31 @@
|
||||
# Strava-API
|
||||
|
||||
API used to communicate with my strava account, will be used as a display project
|
||||
API used to communicate with my strava account, will be used as a display project
|
||||
|
||||
## WIP and strava oAuth management
|
||||
Right now the oAuth management is a bit manual, I need to get the refresh token manually from strava website, then store it in Infisical vault (or env variable for testing).
|
||||
|
||||
### Steps to get refresh token
|
||||
1. Go to this URL (replace client_id with your strava app client id, redirect_uri with your redirect uri, and scope with the scopes you need):
|
||||
```https://www.strava.com/oauth/authorize?client_id=181987&response_type=code&redirect_uri=https://louisemard.dev&approval_prompt=force&scope=read,activity:read_all
|
||||
```
|
||||
2. Authorize the application and you will be redirected to the redirect_uri with a code in the URL.
|
||||
3. Exchange the code for a refresh token by making a POST request to the Strava API:
|
||||
```
|
||||
POST https://www.strava.com/oauth/token
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
client_id=181987
|
||||
client_secret=YOUR_CLIENT_SECRET
|
||||
code=AUTHORIZATION_CODE
|
||||
grant_type=authorization_code
|
||||
```
|
||||
4. The response will contain the refresh token, which you can then store in your Infisical vault or as an environment variable.
|
||||
The `REFRESH_TOKEN` is then used by the application to get new access tokens when needed. It's linked to your Strava account and allows the application to access your data.
|
||||
|
||||
## Infisical setup
|
||||
To use infisical you need to have the following variables in your vault (environment "dev"):
|
||||
- `INFISICAL_CLIENT_SECRET`: your infisical client secret
|
||||
- `INFISICAL_CLIENT_ID`: your infisical client id
|
||||
- `INFISICAL_URL`: Infisical url, mine is `https://vault.louisemard.dev`
|
||||
- `INFISICAL_PROJECT_ID`: Project where the secrets are stored, right now only one project is supported
|
||||
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.2",
|
||||
"typescript": "^5.3.2"
|
||||
}
|
||||
}
|
||||
63
src/clients/Client.ts
Normal file
63
src/clients/Client.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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() {
|
||||
dotenv.config();
|
||||
this.infisicalClient = new InfisicalSDK({
|
||||
siteUrl: 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, checkEnv: boolean = false): Effect.Effect<string, Error, never> {
|
||||
const self = this;
|
||||
return Effect.gen(function* (this: Client) {
|
||||
if (checkEnv && process.env[key]) {
|
||||
yield* Effect.log('Using environment variable for key: ' + key);
|
||||
return process.env[key]!;
|
||||
}
|
||||
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;
|
||||
55
src/clients/strava/StravaClient.ts
Normal file
55
src/clients/strava/StravaClient.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
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;
|
||||
// Retrieve credentials from Infisical, for refresh token as it represents user account
|
||||
// There's an option to check env variables first
|
||||
const refreshToken = yield* self.getCredential('REFRESH_TOKEN', true);
|
||||
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