diff --git a/.env.sample b/.env.sample index 2a4cdbb..4d136db 100644 --- a/.env.sample +++ b/.env.sample @@ -1,6 +1,17 @@ -DATABASE_URL = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +DATABASE_URL = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +JWT_SECRET = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -GITHUB_CLIENT_ID = xxxxxxxxxxxxxxxxxxxx -GITHUB_SECRET = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# It's used in the callback URL of the OAuth2 flow +# Define it as http://localhost:3000 if you're running the app locally +NEXTAUTH_URL = xxxxxxxxxxxxxxxxxxxxxxx + +# OAuth Apps used to fetch users' metrics +STACKAPPS_CLIENT_ID = xxxxx +STACKAPPS_SECRET = xxxxxxxxxxxxxxxxxxxxxxxx -JWT_SECRET = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file +WAKATIME_CLIENT_ID = xxxxxxxxxxxxxxxxxxxxxxxx +WAKATIME_SECRET = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + +GITHUB_CLIENT_ID = xxxxxxxxxxxxxxxxxxxx +GITHUB_CLIENT_SECRET = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +GITHUB_CLIENT_INSTALL_URL = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/.gitignore b/.gitignore index e50c531..a741597 100644 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,5 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts -.env \ No newline at end of file +.env +.env.staging \ No newline at end of file diff --git a/README.md b/README.md index 0af0223..a95d942 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,63 @@ ## devstats -_WIP_. +_You worked hard, it's time to manifest._ -## Supported platforms +[![Vercel](https://vercelbadge.vercel.app/api/sametcodes/devstats?style=flat-square)](https://vercelbadge.vercel.app/api/sametcodes/devstats?style=flat-square) -### GitHub +### Running locally -> TODO: mention definition of environmant variable for username and personal access token. +Make sure you install the dependencies first. -It uses GitHub GraphQL API to fetch user calendar activities. It requires to create personal access token with the following scopes: +```bash +npm install +``` + +You need the `.env` file that have environment variables. You can get it from the [Vercel](https://vercel.com) project settings, or just by running `vercel env pull .env` command if you have the access to the vercel project. If you don't have, ask admin to get it. + +Before running, you need to generate the database schema and prepare `husky` hooks. +```bash +npm run prepare ``` -repo -read:packages -read:org -read:public_key -read:repo_hook -user -read:discussion -read:enterprise -read:gpg_key + +```bash +npm run prisma:generate ``` -Create a new personal access token on [here](https://github.com/settings/tokens/new). +You are ready to go to run the development server. -### CodeWars +```bash +npm run dev +``` -> TODO: mention definition of environment variables for username +Good hacking. + +### Code linting and prettifying + +This project uses [ESLint](https://eslint.org/) and [Prettier](https://prettier.io/) for code linting and formatting. You can run the following command to lint the code and check if there are any errors. + +```bash +npm run lint +``` -### StackOverflow +If you want to fix the errors automatically, you can run the following command. + +```bash +npm run lint:fix +``` + +### Commit linting + +This project uses [Husky](https://typicode.github.io/husky/#/) to run the linting and formatting before every commit. If commits do not fit the conventional commit format, the commit will be rejected. Check the rules [here](https://www.conventionalcommits.org/en/v1.0.0/#specification). + +### Scripts + +The metric methods require name and descriptions on the database records, and they are provided on as JSDoc comments on the methods. You can run the following command to migrate metric methods to the related database records. + +```bash +npm run migrate:platform +``` -dc +## Versions and changelogs -> TODO: mention definition of environment variables for `user_id` +Please check the [releases page](https://github.com/sametcodes/devstats/releases) to see the versions and changelogs. \ No newline at end of file diff --git a/components/svgs/github/index.tsx b/components/svgs/github/index.tsx index 6519362..1e028aa 100644 --- a/components/svgs/github/index.tsx +++ b/components/svgs/github/index.tsx @@ -8,7 +8,7 @@ import { export const getCurrentYearContributions = (result: any, platform: any) => { const { totalContributions } = - result.data.user.contributionsCollection.contributionCalendar; + result.data.viewer.contributionsCollection.contributionCalendar; return ( @@ -25,7 +25,7 @@ export const getCurrentYearContributions = (result: any, platform: any) => { }; export const getPopularContributions = (result: any) => { - const popularContributions = result.data.user.contributionsCollection; + const popularContributions = result.data.viewer.contributionsCollection; const [popularIssue, popularPullrequest] = [ popularContributions.popularIssueContribution.issue, popularContributions.popularPullRequestContribution.pullRequest, @@ -58,7 +58,7 @@ export const getContributionsSummary = (result: any) => { totalRepositoriesWithContributedCommits, totalRepositoriesWithContributedPullRequests, totalRepositoriesWithContributedIssues, - } = result.data.user.contributionsCollection; + } = result.data.viewer.contributionsCollection; return ( diff --git a/package-lock.json b/package-lock.json index 36daa8f..27959c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@types/react-dom": "18.0.8", "@types/simple-oauth2": "^5.0.2", "chalk": "^4.1.0", + "env-cmd": "^10.1.0", "eslint": "8.34.0", "eslint-config-next": "13.2.0", "husky": "^8.0.0", @@ -1649,6 +1650,15 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -1974,6 +1984,22 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/env-cmd": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", + "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", + "dev": true, + "dependencies": { + "commander": "^4.0.0", + "cross-spawn": "^7.0.0" + }, + "bin": { + "env-cmd": "bin/env-cmd.js" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7225,6 +7251,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true + }, "compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", @@ -7469,6 +7501,16 @@ "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", "dev": true }, + "env-cmd": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/env-cmd/-/env-cmd-10.1.0.tgz", + "integrity": "sha512-mMdWTT9XKN7yNth/6N6g2GuKuJTsKMDHlQFUDacb/heQRRWOTIZ42t1rMHnQu4jYxU1ajdTeJM+9eEETlqToMA==", + "dev": true, + "requires": { + "commander": "^4.0.0", + "cross-spawn": "^7.0.0" + } + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", diff --git a/package.json b/package.json index 71eec2d..e9428bb 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,15 @@ "scripts": { "dev": "next dev", "build": "npm run prisma:generate && next build", + "build:staging": "env-cmd -f .env.staging npm run build && env-cmd -f .env.staging npm run start", "start": "next start", - "prisma:generate": "prisma generate --schema=./services/prisma/schema.prisma", "lint": "next lint --max-warnings=0", "lint:fix": "next lint --fix", "prettier": "prettier --check .", "prettier:fix": "prettier --write .", "prepare": "husky install", - "jsdoc:explain": "jsdoc -X $1", - "migrate:methods": "ts-node -r tsconfig-paths/register scripts/migrates/methods.ts" + "prisma:generate": "prisma generate --schema=./services/prisma/schema.prisma", + "migrate:platform": "ts-node -r tsconfig-paths/register scripts/migrates/platform.ts" }, "dependencies": { "@next-auth/prisma-adapter": "^1.0.5", @@ -32,6 +32,7 @@ "@types/react-dom": "18.0.8", "@types/simple-oauth2": "^5.0.2", "chalk": "^4.1.0", + "env-cmd": "^10.1.0", "eslint": "8.34.0", "eslint-config-next": "13.2.0", "husky": "^8.0.0", diff --git a/pages/api/auth/[...nextauth].ts b/pages/api/auth/[...nextauth].ts index ef7cd5e..15fa25e 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/pages/api/auth/[...nextauth].ts @@ -4,17 +4,20 @@ import prisma from "@services/prisma"; import callbacks from "@services/nextauth/callbacks"; import GithubProvider from "next-auth/providers/github"; -const { GITHUB_CLIENT_ID, GITHUB_SECRET } = process.env; +const { GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET } = process.env; -if (GITHUB_CLIENT_ID === undefined || GITHUB_SECRET === undefined) { - throw new Error("GITHUB_CLIENT_ID and GITHUB_SECRET must be defined"); +if (GITHUB_CLIENT_ID === undefined || GITHUB_CLIENT_SECRET === undefined) { + throw new Error("GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET must be defined"); } export const authOptions = { providers: [ GithubProvider({ clientId: GITHUB_CLIENT_ID, - clientSecret: GITHUB_SECRET, + clientSecret: GITHUB_CLIENT_SECRET, + client: { + redirect_uris: [`${process.env.NEXTAUTH_URL}/api/auth/callback/github`], + }, }), ], secret: process.env.JWT_SECRET, diff --git a/pages/api/oauth/[...route].ts b/pages/api/oauth/[...route].ts index ecdf047..adce828 100644 --- a/pages/api/oauth/[...route].ts +++ b/pages/api/oauth/[...route].ts @@ -3,7 +3,7 @@ import { unstable_getServerSession } from "next-auth"; import { generateRandomString } from "@utils"; import { authOptions } from "@pages/api/auth/[...nextauth]"; -import Providers from "@services/oauth/providers"; +import { providers } from "@services/oauth"; import actions from "@services/oauth/actions"; export default async function handler( @@ -16,11 +16,11 @@ export default async function handler( const [action, platform]: string[] = req.query.route as string[]; if (req.method !== "GET") return res.status(405).end(); - if (Object.keys(Providers).indexOf(platform) === -1) + if (Object.keys(providers).indexOf(platform) === -1) return res.status(404).send("Not Found"); if (action === "callback") { - const provider = Providers[platform]; + const provider = providers[platform]; const params = provider.getTokenParam(provider, req.query); try { @@ -40,7 +40,11 @@ export default async function handler( } if (action === "connect") { - const provider = Providers[platform]; + const provider = providers[platform]; + if (provider.connect_url) { + return res.redirect(provider.connect_url); + } + const redirect_uri = provider.authorization.authorizeURL({ redirect_uri: provider.redirect_uri, scope: provider.scope, diff --git a/scripts/migrates/methods.ts b/scripts/migrates/platform.ts similarity index 89% rename from scripts/migrates/methods.ts rename to scripts/migrates/platform.ts index 0cfb3ab..928c8f1 100644 --- a/scripts/migrates/methods.ts +++ b/scripts/migrates/platform.ts @@ -60,9 +60,16 @@ const migrate = async ({ docs: JSDocMinified[]; code: string; }): Promise => { - const platform = await prisma.platform.findUnique({ where: { code } }); - - if (!platform) return console.error(`Platform ${code} not found`); + let platform = await prisma.platform.findUnique({ where: { code } }); + + if (!platform) { + const name = Array.from(code) + .map((letter, i) => (i === 0 ? letter.toUpperCase() : letter)) + .join(""); + platform = await prisma.platform.create({ + data: { code, name, methods: [] }, + }); + } const methods = docs .map((doc) => ({ diff --git a/services/api/handler.ts b/services/api/handler.ts index 3daf923..cd95a62 100644 --- a/services/api/handler.ts +++ b/services/api/handler.ts @@ -18,6 +18,9 @@ const handlePlatformAPI: PlatformAPIHandler = ( .status(400) .json({ message: "Bad request: uid parameter is missing" }); + const user = await prisma.user.findFirst({ where: { id: uid } }); + if (!user) return res.status(404).json({ message: "User not found" }); + const platform = await prisma.platform.findFirst({ where: { code: platformCode }, }); @@ -28,18 +31,21 @@ const handlePlatformAPI: PlatformAPIHandler = ( select: { value: true, platform: { select: { name: true } } }, where: { userId: uid, platformId: platform.id }, }); + const connection = await prisma.connection.findFirst({ where: { userId: uid, platformId: platform.id }, }); if (!connection) - return res.status(404).json({ message: "No user config or connection" }); + return res + .status(404) + .json({ message: "User has no connection on this platform" }); const result = await getPlatformResponse( req.query, services, templates, - userConfig?.value, - connection + connection, + userConfig ); if (result.success === false) return res.status(result.status).json({ message: result.error }); diff --git a/services/oauth/actions.ts b/services/oauth/actions.ts index 8d1d5be..1dc6a65 100644 --- a/services/oauth/actions.ts +++ b/services/oauth/actions.ts @@ -1,7 +1,7 @@ import prisma from "@services/prisma"; import { AccessToken } from "simple-oauth2"; import { Session } from "next-auth"; -import { Provider, ConnectionProfile } from "./providers"; +import { Provider, ConnectionProfile } from "."; type ISignIn = { accessToken: AccessToken; diff --git a/services/oauth/index.ts b/services/oauth/index.ts new file mode 100644 index 0000000..b224cdc --- /dev/null +++ b/services/oauth/index.ts @@ -0,0 +1,44 @@ +import { AuthorizationCode, AccessToken, Token } from "simple-oauth2"; +import wakatime from "./providers/wakatime"; +import github from "./providers/github"; +import stackoverflow from "./providers/stackoverflow"; + +export type ConnectionProfile = { + name: string; + email: string; + image: string; +}; + +export type Provider = { + code: string; + client: { + id: string; + secret: string; + }; + authorization: AuthorizationCode; + getTokenParam: ( + provider: any, + params: any + ) => { + code: string; + redirect_uri: string; + + client_id?: string; + client_secret?: string; + + state?: string; + grant_type?: string; + login?: string; + allow_signup?: boolean; + }; + connect_url?: string; + getProfile: (token: Token) => Promise; + redirect_uri: string; + scope: string; +}; + +export const providers: { [key: string]: Provider } = { + wakatime, + github, + stackoverflow, +}; diff --git a/services/oauth/providers.ts b/services/oauth/providers.ts deleted file mode 100644 index a793676..0000000 --- a/services/oauth/providers.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { AuthorizationCode, AccessToken, Token } from "simple-oauth2"; - -export type ConnectionProfile = { - name: string; - email: string; - image: string; -}; - -export type Provider = { - code: string; - client: { - id: string; - secret: string; - }; - authorization: AuthorizationCode; - getTokenParam: ( - provider: any, - params: any - ) => { - code: string; - grant_type: string; - client_secret: string; - client_id: string; - redirect_uri: string; - }; - getProfile: (token: Token) => Promise; - redirect_uri: string; - scope: string; -}; - -const providers: { [key: string]: Provider } = { - wakatime: { - code: "wakatime", - client: { - id: process.env.WAKATIME_CLIENT_ID as string, - secret: process.env.WAKATIME_SECRET as string, - }, - authorization: new AuthorizationCode({ - client: { - id: process.env.WAKATIME_CLIENT_ID as string, - secret: process.env.WAKATIME_SECRET as string, - }, - auth: { - tokenHost: "https://wakatime.com", - tokenPath: "/oauth/token", - authorizePath: "/oauth/authorize", - }, - }), - getTokenParam: (provider, params) => { - return { - code: params.code as string, - grant_type: "authorization_code", - client_secret: provider.client.secret, - client_id: provider.client.id, - redirect_uri: provider.redirect_uri, - }; - }, - getProfile: async (token) => { - try { - const response = await fetch( - "https://wakatime.com/api/v1/users/current", - { - headers: { Authorization: `Bearer ${token.access_token}` }, - } - ); - const { data } = await response.json(); - - return { - name: data.full_name || data.display_name, - email: data.email, - image: data.photo, - }; - } catch (err) { - if (err instanceof Error) - console.error(`Error getting profile from wakatime: ${err.message}`); - } - }, - redirect_uri: `${process.env.AUTH_URL}/api/oauth/callback/wakatime`, - scope: "email,read_stats,read_logged_time", - }, -}; - -export default providers; diff --git a/services/oauth/providers/github.ts b/services/oauth/providers/github.ts new file mode 100644 index 0000000..24ded7d --- /dev/null +++ b/services/oauth/providers/github.ts @@ -0,0 +1,53 @@ +import { AuthorizationCode } from "simple-oauth2"; +import { Provider } from "@services/oauth"; + +const config: Provider = { + code: "github", + client: { + id: process.env.GITHUB_CLIENT_ID as string, + secret: process.env.GITHUB_CLIENT_SECRET as string, + }, + authorization: new AuthorizationCode({ + client: { + id: process.env.GITHUB_CLIENT_ID as string, + secret: process.env.GITHUB_CLIENT_SECRET as string, + }, + auth: { + tokenHost: "https://github.com", + authorizePath: "/login/oauth/authorize", + tokenPath: "/login/oauth/access_token", + }, + }), + getTokenParam: (provider, params) => { + return { + code: params.code as string, + grant_type: "authorization_code", + client_secret: provider.client.secret, + state: params.state as string, + client_id: provider.client.id, + redirect_uri: provider.redirect_uri, + }; + }, + getProfile: async (token) => { + try { + const response = await fetch("https://api.github.com/user", { + headers: { Authorization: `token ${token.access_token}` }, + }); + const data = await response.json(); + + return { + name: data.name || data.login, + email: data.email, + image: data.avatar_url, + }; + } catch (err) { + if (err instanceof Error) + console.error(`Error getting profile from github: ${err.message}`); + } + }, + connect_url: process.env.GITHUB_CLIENT_INSTALL_URL, + redirect_uri: `${process.env.NEXTAUTH_URL}/api/oauth/callback/github`, + scope: "read:user user:email repo", +}; + +export default config; diff --git a/services/oauth/providers/stackoverflow.ts b/services/oauth/providers/stackoverflow.ts new file mode 100644 index 0000000..7d6134d --- /dev/null +++ b/services/oauth/providers/stackoverflow.ts @@ -0,0 +1,40 @@ +import { AuthorizationCode } from "simple-oauth2"; +import { Provider } from "@services/oauth"; + +const config: Provider = { + code: "stackoverflow", + client: { + id: process.env.STACKAPPS_CLIENT_ID as string, + secret: process.env.STACKAPPS_SECRET as string, + }, + authorization: new AuthorizationCode({ + client: { + id: process.env.STACKAPPS_CLIENT_ID as string, + secret: process.env.STACKAPPS_SECRET as string, + }, + auth: { + tokenHost: "https://stackoverflow.com/", + authorizePath: "/oauth", + tokenPath: "/oauth/access_token", + }, + }), + getTokenParam: (provider, params) => { + return { + client_id: provider.client.id, + client_secret: provider.client.secret, + code: params.code, + redirect_uri: provider.redirect_uri, + }; + }, + getProfile: async (token) => { + return Promise.resolve({ + name: "test", + email: "", + image: "", + }); + }, + redirect_uri: `${process.env.NEXTAUTH_URL}/api/oauth/callback/stackoverflow`, + scope: "private_info", +}; + +export default config; diff --git a/services/oauth/providers/wakatime.ts b/services/oauth/providers/wakatime.ts new file mode 100644 index 0000000..f00d127 --- /dev/null +++ b/services/oauth/providers/wakatime.ts @@ -0,0 +1,53 @@ +import { AuthorizationCode } from "simple-oauth2"; +import { Provider } from "@services/oauth"; + +const config: Provider = { + code: "wakatime", + client: { + id: process.env.WAKATIME_CLIENT_ID as string, + secret: process.env.WAKATIME_SECRET as string, + }, + authorization: new AuthorizationCode({ + client: { + id: process.env.WAKATIME_CLIENT_ID as string, + secret: process.env.WAKATIME_SECRET as string, + }, + auth: { + tokenHost: "https://wakatime.com", + authorizePath: "/oauth/authorize", + tokenPath: "/oauth/token", + }, + }), + getTokenParam: (provider, params) => { + return { + code: params.code as string, + grant_type: "authorization_code", + client_secret: provider.client.secret, + state: params.state as string, + client_id: provider.client.id, + redirect_uri: provider.redirect_uri, + }; + }, + getProfile: async (token) => { + try { + const response = await fetch( + "https://wakatime.com/api/v1/users/current", + { headers: { Authorization: `Bearer ${token.access_token}` } } + ); + const { data } = await response.json(); + + return { + name: data.full_name || data.display_name, + email: data.email, + image: data.photo, + }; + } catch (err) { + if (err instanceof Error) + console.error(`Error getting profile from wakatime: ${err.message}`); + } + }, + redirect_uri: `${process.env.NEXTAUTH_URL}/api/oauth/callback/wakatime`, + scope: "email,read_stats,read_logged_time", +}; + +export default config; diff --git a/services/platform/codewars/index.ts b/services/platform/codewars/index.ts index 5a655b8..fedcfc5 100644 --- a/services/platform/codewars/index.ts +++ b/services/platform/codewars/index.ts @@ -1,6 +1,7 @@ import { ServiceResponse } from "@services/platform/types"; import request from "@services/platform/codewars/request"; import { CodewarsUserConfig } from "@services/platform/types"; +import { Connection } from "@prisma/client"; /** * @name getUser @@ -8,6 +9,7 @@ import { CodewarsUserConfig } from "@services/platform/types"; * @description Get a summary of your scores and top languages */ export const getUser = async ( + connection: Connection, userConfig: CodewarsUserConfig ): Promise => { const response = await request(`/users/${userConfig.username}`); diff --git a/services/platform/codewars/request.ts b/services/platform/codewars/request.ts index d74e300..a73923d 100644 --- a/services/platform/codewars/request.ts +++ b/services/platform/codewars/request.ts @@ -5,7 +5,7 @@ * @throws {Error} - If the response is an error */ -export default function request(path: string) { +export default function request(path: string): Promise { return fetch(`https://www.codewars.com/api/v1${path}`) .then((res) => { if (res.headers.get("content-type")?.includes("application/json")) { diff --git a/services/platform/github/index.ts b/services/platform/github/index.ts index 1142ae0..a99430a 100644 --- a/services/platform/github/index.ts +++ b/services/platform/github/index.ts @@ -1,9 +1,7 @@ import { ServiceResponse } from "@services/platform/types"; import request from "@services/platform/github/request"; import { GithubUserConfig } from "@services/platform/types"; - -// Github GraphQL API Explorer can be used to discover -// https://docs.github.com/en/graphql/overview/explorer +import { Connection } from "@prisma/client"; /** * @name getCurrentYearContributions @@ -11,16 +9,16 @@ import { GithubUserConfig } from "@services/platform/types"; * @description Get the total number of contributions for the current year */ export const getCurrentYearContributions = async ( - userConfig: GithubUserConfig + connection: Connection ): Promise => { - const query = `{ user(login: "${userConfig.username}") { + const query = `{ viewer { contributionsCollection { contributionCalendar { totalContributions } } } }`; - const response = await request(query, userConfig.token); + const response = await request(query, connection); if ("error" in response) return response; return { success: true, data: response.data, platform: "github" }; }; @@ -31,9 +29,9 @@ export const getCurrentYearContributions = async ( * @description List your most popular contributions overall */ export const getPopularContributions = async ( - userConfig: GithubUserConfig + connection: Connection ): Promise => { - const query = `{ user(login: "${userConfig.username}") { + const query = `{ viewer { contributionsCollection{ popularIssueContribution{ isRestricted @@ -47,7 +45,7 @@ export const getPopularContributions = async ( } }`; - const response = await request(query, userConfig.token); + const response = await request(query, connection); if ("error" in response) return response; return { success: true, data: response.data, platform: "github" }; }; @@ -58,9 +56,9 @@ export const getPopularContributions = async ( * @description Get a summary of your contributions, like count of commits, PRs and issues */ export const getContributionsSummary = async ( - userConfig: GithubUserConfig + connection: Connection ): Promise => { - const query = `{user(login: "${userConfig.username}") { + const query = `{viewer{ contributionsCollection{ totalRepositoryContributions totalRepositoriesWithContributedCommits @@ -70,7 +68,7 @@ export const getContributionsSummary = async ( } }`; - const response = await request(query, userConfig.token); + const response = await request(query, connection); if ("error" in response) return response; return { success: true, data: response.data, platform: "github" }; }; diff --git a/services/platform/github/request.ts b/services/platform/github/request.ts index d2b9541..21ea1ae 100644 --- a/services/platform/github/request.ts +++ b/services/platform/github/request.ts @@ -1,14 +1,15 @@ +import { Connection } from "@prisma/client"; /** * @param {string} query - The GraphQL query to send to the GitHub API * @returns {Promise} - The response from the GitHub API * @throws {Error} - If the response is not a 2xx status code */ -export default function request(query: string, token: string) { +export default function request(query: string, connection: Connection) { return fetch("https://api.github.com/graphql", { method: "POST", headers: { - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${connection.access_token}`, Accept: "application/vnd.github.v4.idl", "Content-Type": "application/json", }, diff --git a/services/platform/response.ts b/services/platform/response.ts index 3830b92..c3c88c9 100644 --- a/services/platform/response.ts +++ b/services/platform/response.ts @@ -1,12 +1,13 @@ import JSXRender from "@utils/render"; import { trimChars } from "@utils"; +import { Connection } from "@prisma/client"; export const getPlatformResponse = async ( query: any, services: any, templates: any, - userConfig: any | undefined, - connection: any + connection: Connection, + userConfig: any | undefined ) => { const { method, returnType } = query; if (method === undefined || typeof method !== "string") @@ -44,7 +45,7 @@ export const getPlatformResponse = async ( if (!template) return { success: false, status: 500, error: "Template not found" }; - const response = await service(userConfig, connection); + const response = await service(connection, userConfig); if (response.success === false) { return { success: false, status: 500, error: response.error }; diff --git a/services/platform/stackoverflow/index.ts b/services/platform/stackoverflow/index.ts index ae250dd..578eeac 100644 --- a/services/platform/stackoverflow/index.ts +++ b/services/platform/stackoverflow/index.ts @@ -1,6 +1,6 @@ import { ServiceResponse } from "@services/platform/types"; import request from "@services/platform/stackoverflow/request"; -import { StackoverflowUserConfig } from "@services/platform/types"; +import { Connection } from "@prisma/client"; /** * @name getReputation @@ -8,30 +8,14 @@ import { StackoverflowUserConfig } from "@services/platform/types"; * @description Get the total reputation of the user */ export const getReputation = async ( - userConfig: StackoverflowUserConfig + connection: Connection ): Promise => { - if (!userConfig.userId) - return { - success: false, - error: { - message: "User ID is missing in the user configuration", - code: 400, - }, - }; - - const response = await request( - `/users/${userConfig.userId}/reputation?site=stackoverflow` - ); + const response = await request("/2.3/me"); if ("error" in response) return response; - const reputation = response.data.items.reduce( - (acc: number, el: any) => acc + el.reputation_change, - 0 - ); - return { success: true, - data: { reputation }, + data: { reputation: 5 }, platform: "stackoverflow", }; }; diff --git a/services/platform/wakatime/index.ts b/services/platform/wakatime/index.ts index 874d2d8..292a783 100644 --- a/services/platform/wakatime/index.ts +++ b/services/platform/wakatime/index.ts @@ -9,8 +9,8 @@ import { Connection } from "@prisma/client"; * @description Get overall time since today */ export const getAllTimeSinceToday = async ( - userConfig: WakatimeUserConfig, - connection: Connection + connection: Connection, + userConfig: WakatimeUserConfig ): Promise => { const response = await request( "/users/current/all_time_since_today", diff --git a/services/prisma/schema.prisma b/services/prisma/schema.prisma index 91a659d..27390fe 100644 --- a/services/prisma/schema.prisma +++ b/services/prisma/schema.prisma @@ -11,13 +11,13 @@ generator client { } model Platform { - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId name String - code String @unique + code String @unique raw_configuration Json? methods Json - config Config[] + config Config[] } model User { @@ -28,11 +28,11 @@ model User { emailVerified DateTime? image String? - accounts Account[] - sessions Session[] - config Config[] - connections Connection[] - profiles ConnectionProfile[] + accounts Account[] + sessions Session[] + config Config[] + connections Connection[] + profiles ConnectionProfile[] } model Config { @@ -45,51 +45,52 @@ model Config { } model Account { - id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @db.ObjectId - type String - provider String - providerAccountId String - refresh_token String? @db.String - access_token String? @db.String - expires_at Int? - token_type String? - scope String? - id_token String? @db.String - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(auto()) @map("_id") @db.ObjectId + userId String @db.ObjectId + type String + provider String + providerAccountId String + refresh_token String? @db.String + access_token String? @db.String + expires_at Int? + token_type String? + scope String? + id_token String? @db.String + session_state String? + refresh_token_expires_in Int? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@unique([provider, providerAccountId]) } model Connection { - id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @db.ObjectId - platformId String @db.ObjectId - - refresh_token String? @db.String - access_token String? @db.String - expires_at Int? - type String - token_type String? - scope String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - profile ConnectionProfile? + id String @id @default(auto()) @map("_id") @db.ObjectId + userId String @db.ObjectId + platformId String @db.ObjectId + + refresh_token String @db.String + access_token String @db.String + expires_at Int + type String + token_type String + scope String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + profile ConnectionProfile? } model ConnectionProfile { - id String @id @default(auto()) @map("_id") @db.ObjectId - userId String @db.ObjectId - platformId String @db.ObjectId - connectionId String @db.ObjectId @unique + id String @id @default(auto()) @map("_id") @db.ObjectId + userId String @db.ObjectId + platformId String @db.ObjectId + connectionId String @unique @db.ObjectId - name String - email String - image String + name String + email String + image String - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + connection Connection @relation(fields: [connectionId], references: [id], onDelete: Cascade) } model Session {