From 205431007fb37de23d531b37df23154385590bc6 Mon Sep 17 00:00:00 2001 From: Esteban Dalel R Date: Wed, 5 Jul 2023 19:31:48 -0500 Subject: [PATCH] Feature/team and navbar (#182) * add server-only * add client-only * Move to RSC * remove logging * fix typings * fix typings * add LoginGridProps * Fix possibly null * Add types * Check nulls * fix possible nulls * ignore errors in lightly used api * Set conditionally * Check null * check null * fix typings * fix null errors * fix null * add typings * fix props passing * Add typing * Add typing * fix typings * Fix typing errors * use nonnull assertion * Add styles * Move to app folder * Adhere to app api route * Make it non default * upgrade next-auth * restore session provider * Fixes in adapter * move auth to pages to test use * Fix login route * fix import * Pass authprovider * Added sidebar test * Add sidebar * Fix navbar, extract navbar * Fix layout * Hide elements if no session * Update Navbar.tsx * Fix layout * Make app dark * Extract form * Extract navbar * Move to app * Move layout out * Remove logging * REmove logging * Move logingrid to RSC * Remove logingrid * Add layout * Remove logging * Remove logging * Create getTeammates.ts * Create Team page * Fix heading * Delete github.tsx * Move to App router * Update README.md, add title (#180) * Update README.md, add title * Update README.md * Update README.md * Update README.md (#181) * Move to RSC * Fix Try app ui * Remove data logging * Make card details a page * Move layout to master layout * Fix type * Fix layout order * Remove billing link --- .vscode/settings.json | 4 + app/api/actions/github/route.ts | 454 +++++++++++++++++ .../api/auth/[...nextauth]/route.ts | 16 +- {pages => app}/billing/CheckoutForm.tsx | 34 +- app/billing/cardElement.tsx | 58 +++ {pages => app}/billing/index.tsx | 0 app/billing/page.tsx | 33 ++ {pages => app}/billing/paymentSuccess.tsx | 28 +- {pages => app}/billing/teammatesInvited.tsx | 0 app/layout.tsx | 34 ++ app/page.tsx | 72 +++ app/settings/form.tsx | 167 +++++++ app/settings/page.tsx | 34 ++ app/team/page.tsx | 50 ++ app/vscode-insiders/page.tsx | 41 ++ app/vscode/page.tsx | 39 ++ app/vscodium/page.tsx | 38 ++ components/Header.tsx | 18 +- components/HeaderSignOut.tsx | 9 + components/Navbar.tsx | 21 + components/login-btn.tsx | 1 + components/loginGrid.tsx | 96 ++-- components/redirect.tsx | 23 + components/sidebar.tsx | 16 + lib/auth/AuthProvider.tsx | 14 + next-env.d.ts | 1 + next.config.js | 8 + package.json | 4 +- pages/_app.tsx | 1 + pages/actions/github.tsx | 77 --- pages/api/actions/github.ts | 455 ------------------ pages/api/analytics/vsmarketplace/update.ts | 2 + pages/api/stripe/createSubscription.ts | 4 +- pages/billing/cardDetails.tsx | 101 ---- pages/discord.tsx | 2 +- pages/gitpod-code.tsx | 3 +- pages/index.tsx | 77 --- pages/settings.tsx | 202 -------- pages/vscode-insiders.tsx | 61 --- pages/vscode.tsx | 60 --- pages/vscodium.tsx | 60 --- tsconfig.json | 12 +- utils/api/getAllUserPublicData.ts | 6 + utils/auth/adapter.ts | 29 +- utils/db/azuredb.ts | 6 +- utils/db/teams/getTeammates.ts | 8 + 46 files changed, 1254 insertions(+), 1225 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 app/api/actions/github/route.ts rename pages/api/auth/[...nextauth].ts => app/api/auth/[...nextauth]/route.ts (75%) rename {pages => app}/billing/CheckoutForm.tsx (56%) create mode 100644 app/billing/cardElement.tsx rename {pages => app}/billing/index.tsx (100%) create mode 100644 app/billing/page.tsx rename {pages => app}/billing/paymentSuccess.tsx (86%) rename {pages => app}/billing/teammatesInvited.tsx (100%) create mode 100644 app/layout.tsx create mode 100644 app/page.tsx create mode 100644 app/settings/form.tsx create mode 100644 app/settings/page.tsx create mode 100644 app/team/page.tsx create mode 100644 app/vscode-insiders/page.tsx create mode 100644 app/vscode/page.tsx create mode 100644 app/vscodium/page.tsx create mode 100644 components/HeaderSignOut.tsx create mode 100644 components/Navbar.tsx create mode 100644 components/redirect.tsx create mode 100644 components/sidebar.tsx create mode 100644 lib/auth/AuthProvider.tsx create mode 100644 next.config.js delete mode 100644 pages/actions/github.tsx delete mode 100644 pages/api/actions/github.ts delete mode 100644 pages/billing/cardDetails.tsx delete mode 100644 pages/index.tsx delete mode 100644 pages/settings.tsx delete mode 100644 pages/vscode-insiders.tsx delete mode 100644 pages/vscode.tsx delete mode 100644 pages/vscodium.tsx create mode 100644 utils/api/getAllUserPublicData.ts create mode 100644 utils/db/teams/getTeammates.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..d0679104b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.enablePromptUseWorkspaceTsdk": true +} \ No newline at end of file diff --git a/app/api/actions/github/route.ts b/app/api/actions/github/route.ts new file mode 100644 index 000000000..89c5e16af --- /dev/null +++ b/app/api/actions/github/route.ts @@ -0,0 +1,454 @@ +"use client"; +import { App } from "@octokit/app"; +import { trackEvent } from "../../../../utils/analytics/azureAppInsights"; +import executeRequest from "../../../../utils/db/azuredb"; +import getGitHub from "../../../../utils/actions/getGitHub"; +import getJira from "../../../../utils/actions/getJira"; +import getSlack from "../../../../utils/actions/getSlack"; +import getOpenAISummary from "../../../../utils/actions/getOpenAISummary"; +import addActionCount from "../../../../utils/db/teams/addActionCount"; +import getNotion from "../../../../utils/actions/getNotion"; +import githubMarkdown from "../../../../utils/actions/markdownHelpers/github"; +import jiraMarkdown from "../../../../utils/actions/markdownHelpers/jira"; +import slackMarkdown from "../../../../utils/actions/markdownHelpers/slack"; +import notionMarkdown from "../../../../utils/actions/markdownHelpers/notion"; +import countMarkdown from "../../../../utils/actions/markdownHelpers/count"; +import { NextRequest, NextResponse } from "next/server"; + +const app = new App({ + appId: process.env.GITHUB_APP_ID!, + privateKey: process.env.GITHUB_PRIVATE_KEY!, +}); + +export async function POST(request: NextRequest) { + try { + // Verify and parse the webhook event + const req = await request.json(); + + const eventName = req.headers["x-github-event"]; + let payload = req.body; + + if ( + payload.action === "opened" || + payload.action === "reopened" || + payload.action === "synchronize" + ) { + const { installation, repository, pull_request } = payload; + const installationId = installation.id; + const { title, body } = payload.pull_request; + const owner = repository.owner.login; + const repo = repository.name; + const number = pull_request.number; + trackEvent({ + name: "gitHubApp", + properties: { + user: pull_request.user.login, + owner, + repo, + action: payload.action, + //@ts-ignore + issue_number: number, + }, + }); + + const octokit = await app.getInstallationOctokit(installationId); + + const query = `EXEC dbo.get_all_tokens_from_gh_username @github_user='${pull_request.user.login}'`; + const wmUserData = await executeRequest(query); + const { + github_token, + jira_token, + jira_refresh_token, + slack_token, + notion_token, + cloudId, + AISummary, + JiraTickets, + GitHubPRs, + SlackMessages, + NotionPages, + user_email, + watermelon_user, + } = wmUserData; + if (!watermelon_user) { + { + // Post a new comment if no existing comment was found + await octokit + .request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner, + issue_number: number, + repo, + body: "[Please login to Watermelon to see the results](https://app.watermelontools.com/)", + } + ) + .then((response) => { + console.log("post comment", response.data); + }) + .catch((error) => { + return console.error("posting comment error", error); + }); + return NextResponse.json("User not registered"); + } + } + let octoCommitList = await octokit.request( + "GET /repos/{owner}/{repo}/pulls/{pull_number}/commits", + { + repo: repository.name, + owner: repository.owner.login, + pull_number: number, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + } + ); + let commitList: string[] = []; + for (let index = 0; index < octoCommitList?.data?.length; index++) { + commitList.push(octoCommitList.data[index].commit.message); + } + const commitSet = new Set(commitList); + const stopwords = [ + "a", + "about", + "add comments", + "add", + "all", + "an", + "and", + "as", + "at", + "better", + "build", + "bump dependencies", + "bump version", + "but", + "by", + "call", + "cd", + "change", + "changed", + "changes", + "changing", + "chore", + "ci", + "cleanup", + "code review", + "comment out", + "commit", + "commits", + "config", + "config", + "create", + "critical", + "debug", + "delete", + "dependency update", + "deploy", + "docs", + "documentation", + "eslint", + "feat", + "feature", + "fix", + "fix", + "fixed", + "fixes", + "fixing", + "for", + "from", + "get", + "github", + "gitignore", + "hack", + "he", + "hotfix", + "husky", + "if", + "ignore", + "improve", + "improved", + "improvement", + "improvements", + "improves", + "improving", + "in", + "init", + "is", + "lint-staged", + "lint", + "linting", + "list", + "log", + "logging", + "logs", + "major", + "merge conflict", + "merge", + "minor", + "npm", + "of", + "on", + "oops", + "or", + "package-lock.json", + "package.json", + "prettier", + "print", + "quickfix", + "refactor", + "refactored", + "refactoring", + "refactors", + "release", + "remove comments", + "remove console", + "remove", + "remove", + "removed", + "removes", + "removing", + "revert", + "security", + "setup", + "squash", + "start", + "style", + "stylelint", + "temp", + "test", + "tested", + "testing", + "tests", + "the", + "to", + "try", + "typo", + "up", + "update dependencies", + "update", + "updated", + "updates", + "updating", + "use", + "version", + "was", + "wip", + "with", + "yarn.lock", + "master", + "main", + "dev", + "development", + "prod", + "production", + "staging", + "stage", + "[", + "]", + "!", + "{", + "}", + "(", + ")", + "''", + '"', + "``", + "-", + "_", + ":", + ";", + ",", + ".", + "?", + "/", + "|", + "&", + "*", + "^", + "%", + "$", + "#", + "##", + "###", + "####", + "#####", + "######", + "#######", + "@", + "\n", + "\t", + "\r", + "", + "/*", + "*/", + "[x]", + "[]", + "[ ]", + ]; + // create a string from the commitlist set and remove stopwords in lowercase + + const searchStringSet = Array.from(commitSet).join(" "); + // add the title and body to the search string, remove stopwords and remove duplicates + const searchStringSetWTitleABody = Array.from( + new Set( + searchStringSet + .concat(` ${title.split("/").join(" ")}`) + .concat(` ${body}`.split("\n").join(" ")) + .split("\n") + .flatMap((line) => line.split(",")) + .map((commit: string) => commit.toLowerCase()) + .filter((commit) => !stopwords.includes(commit)) + .join(" ") + .split(" ") + ) + ).join(" "); + // select six random words from the search string + const randomWords = searchStringSetWTitleABody + .split(" ") + .sort(() => Math.random() - 0.5) + .slice(0, 6); + + const [ghValue, jiraValue, slackValue, notionValue, count] = + await Promise.all([ + getGitHub({ + repo, + owner, + github_token, + randomWords, + amount: GitHubPRs, + }), + getJira({ + user: user_email, + title, + body, + jira_token, + jira_refresh_token, + randomWords, + amount: JiraTickets, + }), + getSlack({ + title, + body, + slack_token, + randomWords, + amount: SlackMessages, + }), + getNotion({ + notion_token, + randomWords, + amount: NotionPages, + }), + addActionCount({ watermelon_user }), + ]); + + let textToWrite = ""; + textToWrite += "### WatermelonAI Summary (BETA)"; + textToWrite += `\n`; + + let businessLogicSummary; + if (AISummary) { + businessLogicSummary = await getOpenAISummary({ + ghValue, + commitList, + jiraValue, + slackValue, + title, + body, + }); + + if (businessLogicSummary) { + textToWrite += businessLogicSummary; + } else { + textToWrite += "Error getting summary" + businessLogicSummary.error; + } + } else { + textToWrite += `AI Summary deactivated by ${pull_request.user.login}`; + } + + textToWrite += githubMarkdown({ + GitHubPRs, + ghValue, + userLogin: pull_request.user.login, + }); + textToWrite += jiraMarkdown({ + JiraTickets, + jiraValue, + userLogin: pull_request.user.login, + }); + textToWrite += slackMarkdown({ + SlackMessages, + slackValue, + userLogin: pull_request.user.login, + }); + textToWrite += notionMarkdown({ + NotionPages, + notionValue, + userLogin: pull_request.user.login, + }); + textToWrite += countMarkdown({ + count, + isPrivateRepo: repository.private, + repoName: repo, + }); + + // Fetch all comments on the PR + const comments = await octokit.request( + "GET /repos/{owner}/{repo}/issues/{issue_number}/comments?sort=created&direction=desc", + { + owner, + repo, + issue_number: number, + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + } + ); + console.log("comments.data.length", comments.data.length); + // Find our bot's comment + let botComment = comments.data.find((comment) => { + return comment.user.login.includes("watermelon-context"); + }); + if (botComment?.id) { + console.log("bcID", botComment.id); + // Update the existing comment + await octokit.request( + "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", + { + owner, + repo, + comment_id: botComment.id, + body: textToWrite, + } + ); + } else { + // Post a new comment if no existing comment was found + await octokit + .request( + "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", + { + owner, + issue_number: number, + repo, + body: textToWrite, + } + ) + .then((response) => { + console.log("post comment", { + url: response.data.html_url, + body: response.data.body, + user: response?.data?.user?.login, + }); + }) + .catch((error) => { + return console.error("posting comment error", error); + }); + } + } + return NextResponse.json("Webhook event processed"); + } catch (error) { + console.error("general action processing error", error); + NextResponse.json("Error processing webhook event"); + } +} diff --git a/pages/api/auth/[...nextauth].ts b/app/api/auth/[...nextauth]/route.ts similarity index 75% rename from pages/api/auth/[...nextauth].ts rename to app/api/auth/[...nextauth]/route.ts index 6c7be6135..556c2c6f0 100644 --- a/pages/api/auth/[...nextauth].ts +++ b/app/api/auth/[...nextauth]/route.ts @@ -1,13 +1,14 @@ -import NextAuth from "next-auth"; +import NextAuth, { NextAuthOptions } from "next-auth"; import EmailProvider from "next-auth/providers/email"; -import MyAdapter from "../../../utils/auth/adapter"; - -export default NextAuth({ +import MyAdapter from "../../../../utils/auth/adapter"; +export const authOptions: NextAuthOptions = { adapter: MyAdapter(), callbacks: { // add the id to the session as accessToken async session({ session, token, user }) { - session.user.name = user.id; + if (session.user) { + session.user.name = user.id; + } return session; }, }, @@ -36,4 +37,7 @@ export default NextAuth({ name: "Watermelon Auth", }), ], -}); +}; + +const handler = NextAuth(authOptions); +export { handler as GET, handler as POST }; diff --git a/pages/billing/CheckoutForm.tsx b/app/billing/CheckoutForm.tsx similarity index 56% rename from pages/billing/CheckoutForm.tsx rename to app/billing/CheckoutForm.tsx index c34bf294c..17a21311c 100644 --- a/pages/billing/CheckoutForm.tsx +++ b/app/billing/CheckoutForm.tsx @@ -6,32 +6,34 @@ import { } from "@stripe/react-stripe-js"; import dynamic from "next/dynamic"; -const CheckoutForm = ({numberOfSeats}) => {; +const CheckoutForm = ({ numberOfSeats }) => { const stripe = useStripe(); const elements = useElements(); - const [errorMessage, setErrorMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState( + null + ); const handleSubmit = async (event) => { // We don't want to let default form submission happen here, // which would refresh the page. event.preventDefault(); + if (stripe && elements) { + const { error } = await stripe.confirmPayment({ + //`Elements` instance that was used to create the Payment Element + elements, + confirmParams: { + return_url: `${process.env.NEXT_PUBLIC_BACKEND_URL}/billing/paymentSuccess/?seats=${numberOfSeats}`, + }, + }); - const { error } = await stripe.confirmPayment({ - //`Elements` instance that was used to create the Payment Element - elements, - confirmParams: { - return_url: `${process.env.NEXT_PUBLIC_BACKEND_URL}/billing/paymentSuccess/?seats=${numberOfSeats}` - }, - }); - - if (error) { - // This point will only be reached if there is an immediate error when - // confirming the payment. Show error to your customer (for example, payment - // details incomplete) - setErrorMessage(error.message); + if (error) { + // This point will only be reached if there is an immediate error when + // confirming the payment. Show error to your customer (for example, payment + // details incomplete) + setErrorMessage(error.message); + } } }; - return (
diff --git a/app/billing/cardElement.tsx b/app/billing/cardElement.tsx new file mode 100644 index 000000000..fbe5c9c9c --- /dev/null +++ b/app/billing/cardElement.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Elements } from "@stripe/react-stripe-js"; +import CheckoutForm from "./CheckoutForm"; +import { loadStripe, StripeElementsOptions } from "@stripe/stripe-js"; +import { useEffect, useState } from "react"; + +export default function CardElement({ userEmail }) { + const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!); + const [clientSecret, setClientSecret] = useState(""); + const fetchClientSecret = async () => { + const response = await fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/stripe/createSubscription`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + quantity: 4, + email: userEmail, + }), + } + ); + const { clientSecret } = await response.json(); + setClientSecret(clientSecret); + }; + useEffect(() => { + fetchClientSecret(); + }, []); + const options: StripeElementsOptions = { + clientSecret, + loader: "auto", + appearance: { + theme: "night", + labels: "floating", + + variables: { + colorPrimary: "#79c0ff", + colorBackground: "#0d1117", + colorText: "#c9d1d9", + colorDanger: "#df1b41", + fontFamily: "Ideal Sans, system-ui, sans-serif", + spacingUnit: "2px", + borderRadius: "4px", + }, + }, + }; + return ( +
+ {clientSecret && ( + + + + )} +
+ ); +} diff --git a/pages/billing/index.tsx b/app/billing/index.tsx similarity index 100% rename from pages/billing/index.tsx rename to app/billing/index.tsx diff --git a/app/billing/page.tsx b/app/billing/page.tsx new file mode 100644 index 000000000..0b3800efa --- /dev/null +++ b/app/billing/page.tsx @@ -0,0 +1,33 @@ +import { authOptions } from "../api/auth/[...nextauth]/route"; +import { getServerSession } from "next-auth"; +import CardElement from "./cardElement"; + +async function BillingPage() { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + + return ( +
+
+
+
+

+ Purchase your Watermelon subscription +

+ +
+
+
+
+ ); +} + +export default BillingPage; diff --git a/pages/billing/paymentSuccess.tsx b/app/billing/paymentSuccess.tsx similarity index 86% rename from pages/billing/paymentSuccess.tsx rename to app/billing/paymentSuccess.tsx index 46ec2fdf5..cc24c8432 100644 --- a/pages/billing/paymentSuccess.tsx +++ b/app/billing/paymentSuccess.tsx @@ -6,7 +6,7 @@ function Paymentsuccess() { const router = useRouter(); const [numberOfSeats, setNumberOfSeats] = useState(4); - const [emailArray, setEmailArray] = useState([]); + const [emailArray, setEmailArray] = useState<[] | string[]>([]); useEffect(() => { const { seats } = router.query as { seats: string }; @@ -19,15 +19,18 @@ function Paymentsuccess() { emailArray.forEach((email) => { // Add email to github query count table // addEmailToGitHubQueryCountTable(email); - fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/github/addEmailToGitHubQueryCountTable`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - email - }) - }); + fetch( + `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/github/addEmailToGitHubQueryCountTable`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + email, + }), + } + ); // Add email to the list of paying users fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/api/payments/addEmails`, { @@ -47,7 +50,10 @@ function Paymentsuccess() { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ emails: emailArray, sender: "info@watermelon.tools" }), + body: JSON.stringify({ + emails: emailArray, + sender: "info@watermelon.tools", + }), }); router.push("/billing/teammatesInvited"); diff --git a/pages/billing/teammatesInvited.tsx b/app/billing/teammatesInvited.tsx similarity index 100% rename from pages/billing/teammatesInvited.tsx rename to app/billing/teammatesInvited.tsx diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 000000000..76ea7dfbe --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,34 @@ +import "@primer/css/index.scss"; + +import AuthProvider from "../lib/auth/AuthProvider"; +import Navbar from "../components/Navbar"; +import { getServerSession } from "next-auth"; +import Header from "../components/Header"; +import { authOptions } from "./api/auth/[...nextauth]/route"; +import LogInBtn from "../components/login-btn"; +export const metadata = { + title: "Watermelon", + description: "Get context on each PR", +}; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + if (!session) return ; + + return ( + + +
+ + {children} + + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 000000000..fd47679a9 --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,72 @@ +import LogInBtn from "../components/login-btn"; +import LoginGrid from "../components/loginGrid"; +import DownloadExtension from "../components/dashboard/DownloadExtension"; +import { getServerSession } from "next-auth"; +import { authOptions } from "./api/auth/[...nextauth]/route"; +import Header from "../components/Header"; +import Navbar from "../components/Navbar"; +import getAllPublicUserData from "../utils/api/getAllUserPublicData"; + +async function HomePage({}) { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + // if not logged in, do not show anything + const data = await getAllPublicUserData({ userEmail }); + return ( +
+ <> + {userEmail ? ( + <> + +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+

Try our GitHub App

+

Context on each Pr

+
+
+
+ + ) : null} + +
+ ); +} + +export default HomePage; diff --git a/app/settings/form.tsx b/app/settings/form.tsx new file mode 100644 index 000000000..409ece5b2 --- /dev/null +++ b/app/settings/form.tsx @@ -0,0 +1,167 @@ +"use client"; + +import { useState } from "react"; +import getUserSettings from "../../utils/api/getUserSettings"; + +export default function form({ userEmail }) { + const [saveDisabled, setSaveDisabled] = useState(false); + + const setUserSettingsState = async (userEmail) => { + let settings = await getUserSettings(userEmail); + setFormState(settings); + }; + const [formState, setFormState] = useState({ + JiraTickets: 3, + SlackMessages: 3, + GitHubPRs: 3, + NotionPages: 3, + AISummary: 1, + }); + const handleSubmit = async () => { + setSaveDisabled(true); + try { + const response = await fetch("/api/user/updateSettings", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ userSettings: formState, user: userEmail }), + }).then((res) => res.json()); + if ( + response.AISummary === formState.AISummary || + response.JiraTickets === formState.JiraTickets || + response.SlackMessages === formState.SlackMessages || + response.GitHubPRs === formState.GitHubPRs || + response.NotionPages === formState.NotionPages + ) { + setUserSettingsState(userEmail); + } else { + console.error("Failed to save the form"); + } + } catch (error) { + console.error("An error occurred while saving the form", error); + } finally { + setSaveDisabled(false); + } + }; + + return ( + +
+ Jira Tickets: + +
+
+ Slack Messages: + +
+
+ GitHub PRs: + +
+ +
+ Notion Pages: + +
+ +
+ AI Summary: + +
+ + + ); +} diff --git a/app/settings/page.tsx b/app/settings/page.tsx new file mode 100644 index 000000000..8802f1637 --- /dev/null +++ b/app/settings/page.tsx @@ -0,0 +1,34 @@ +import { getServerSession } from "next-auth"; + +import LogInBtn from "../../components/login-btn"; + +import { authOptions } from "../api/auth/[...nextauth]/route"; +import Form from "./form"; + +async function Settings({}) { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + // if not logged in, do not show anything + if (!session) return ; + + return ( +
+
+

Settings

+
+
+ App Settings +
+ This controls how the GitHub App behaves. +
+
+ +
+
+
+
+ ); +} + +export default Settings; diff --git a/app/team/page.tsx b/app/team/page.tsx new file mode 100644 index 000000000..351e625f4 --- /dev/null +++ b/app/team/page.tsx @@ -0,0 +1,50 @@ +import { getServerSession } from "next-auth"; + +import LogInBtn from "../../components/login-btn"; +import getTeammates from "../../utils/db/teams/getTeammates"; + +import { authOptions } from "../api/auth/[...nextauth]/route"; + +async function Settings({}) { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + // if not logged in, do not show anything + if (!session) return ; + const teammates = await getTeammates({ watermelon_user: userName }); + return ( +
+
+

Team

+
+
+

My Account

+
Control your own account
+
+
+

{userEmail}

+
+
+
+
+

My Team

+
+ View and invite people you work with +
+
+
+ {teammates.map((teammate) => { + return ( +
+

{teammate.email}

+
+ ); + })} +
+
+
+
+ ); +} + +export default Settings; diff --git a/app/vscode-insiders/page.tsx b/app/vscode-insiders/page.tsx new file mode 100644 index 000000000..3e035899f --- /dev/null +++ b/app/vscode-insiders/page.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; +import LoginGrid from "../../components/loginGrid"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../api/auth/[...nextauth]/route"; +import TimeToRedirect from "../../components/redirect"; +import getAllPublicUserData from "../../utils/api/getAllUserPublicData"; + +async function VSCodeInsiders() { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + const data = await getAllPublicUserData({ userEmail }); + + let system = "vscode-insiders"; + let url: string = `${system}://watermelontools.watermelon-tools?email=${ + userEmail ?? "" + }&token=${userName ?? ""}`; + + return ( +
+ +
+
+

Open VSCode Insiders

+ + +
+
+ + +
+ ); +} + +export default VSCodeInsiders; diff --git a/app/vscode/page.tsx b/app/vscode/page.tsx new file mode 100644 index 000000000..28f4ae2ae --- /dev/null +++ b/app/vscode/page.tsx @@ -0,0 +1,39 @@ +import Link from "next/link"; +import LoginGrid from "../../components/loginGrid"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../api/auth/[...nextauth]/route"; +import TimeToRedirect from "../../components/redirect"; +import getAllPublicUserData from "../../utils/api/getAllUserPublicData"; + +async function VSCode() { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + const data = await getAllPublicUserData({ userEmail }); + let system = "vscode"; + let url: string = `${system}://watermelontools.watermelon-tools?email=${ + userEmail ?? "" + }&token=${userName ?? ""}`; + + return ( +
+ <> + +
+
+

Open VSCode

+ + +
+
+ + + +
+ ); +} + +export default VSCode; diff --git a/app/vscodium/page.tsx b/app/vscodium/page.tsx new file mode 100644 index 000000000..83b04ada5 --- /dev/null +++ b/app/vscodium/page.tsx @@ -0,0 +1,38 @@ +import Link from "next/link"; +import LoginGrid from "../../components/loginGrid"; +import { getServerSession } from "next-auth"; +import { authOptions } from "../api/auth/[...nextauth]/route"; +import TimeToRedirect from "../../components/redirect"; +import getAllPublicUserData from "../../utils/api/getAllUserPublicData"; + +async function VSCodium({}) { + const session = await getServerSession(authOptions); + let userEmail = session?.user?.email; + let userName = session?.user?.name; + const data = await getAllPublicUserData({ userEmail }); + + let system = "vscodium"; + let url: string = `${system}://watermelontools.watermelon-tools?email=${ + userEmail ?? "" + }&token=${userName ?? ""}`; + + return ( +
+ +
+
+

Open VSCodium

+ + +
+
+ + +
+ ); +} + +export default VSCodium; diff --git a/components/Header.tsx b/components/Header.tsx index fd47abb01..b815731e6 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -1,13 +1,7 @@ +"use client"; import Image from "next/image"; -import { useSession, signOut } from "next-auth/react"; -import { useEffect, useState } from "react"; -export default function Header() { - const [userEmail, setUserEmail] = useState(null); - const { data } = useSession(); - - useEffect(() => { - setUserEmail(data?.user?.email); - }, [data]); +import HeaderSignOut from "./HeaderSignOut"; +export default function Header({ userEmail, userToken }) { return (
@@ -47,7 +41,7 @@ export default function Header() { className="dropdown-item" href={`vscode://watermelontools.watermelon-tools?email=${ userEmail ?? "" - }&token=${data?.user?.name ? data.user.name : ""}`} + }&token=${userToken ? userToken : ""}`} > VSCode Extension @@ -69,9 +63,7 @@ export default function Header() {
  • - +
  • diff --git a/components/HeaderSignOut.tsx b/components/HeaderSignOut.tsx new file mode 100644 index 000000000..2ff554208 --- /dev/null +++ b/components/HeaderSignOut.tsx @@ -0,0 +1,9 @@ +"use client"; +import { signOut } from "next-auth/react"; +export default function HeaderSignOut() { + return ( + + ); +} diff --git a/components/Navbar.tsx b/components/Navbar.tsx new file mode 100644 index 000000000..13e7e1b49 --- /dev/null +++ b/components/Navbar.tsx @@ -0,0 +1,21 @@ +import Link from "next/link"; + +export default function Navbar({ children }: { children: React.ReactNode }) { + const links = [ + { href: "/", label: "Home" }, + { href: "/settings", label: "Settings" }, + { href: "/team", label: "Team" }, + ]; + return ( +
    + +
    {children}
    +
    + ); +} diff --git a/components/login-btn.tsx b/components/login-btn.tsx index 49dbad8a1..edc3d35c2 100644 --- a/components/login-btn.tsx +++ b/components/login-btn.tsx @@ -1,3 +1,4 @@ +"use client"; import Image from "next/image"; import { signIn } from "next-auth/react"; export default function LogInBtn() { diff --git a/components/loginGrid.tsx b/components/loginGrid.tsx index 2059afc5f..0f674950b 100644 --- a/components/loginGrid.tsx +++ b/components/loginGrid.tsx @@ -1,5 +1,3 @@ -import { useEffect, useState } from "react"; - import InfoPanel from "../components/dashboard/InfoPanel"; import JiraLoginLink from "../components/JiraLoginLink"; import SlackLoginLink from "../components/SlackLoginLink"; @@ -9,73 +7,45 @@ import BitbucketLoginLink from "../components/BitbucketLoginLink"; import LinearLoginLink from "../components/LinearLoginLink"; import DiscordLoginLink from "./DiscordLoginLink"; import NotionLoginLink from "./NotionLoginLink"; -import getAllUserData from "../utils/api/getAllUserData"; -import getPaymentInfo from "../utils/api/getPaymentInfo"; type LoginGridProps = { userEmail: string; user_displayname: string; }; +function LoginGrid({ userEmail, data }) { + let githubUserData: null | LoginGridProps = null; + let bitbucketUserData: null | LoginGridProps = null; + let gitlabUserData: null | LoginGridProps = null; + let slackUserData: null | LoginGridProps = null; + let jiraUserData: null | LoginGridProps = null; + let discordUserData: null | LoginGridProps = null; + let notionUserData: null | LoginGridProps = null; + let linearUserData: null | LoginGridProps = null; -function LoginGrid({ userEmail }) { - const [jiraUserData, setJiraUserData] = useState(null); - const [githubUserData, setGithubUserData] = useState( - null - ); - const [bitbucketUserData, setBitbucketUserData] = - useState(null); - const [gitlabUserData, setGitlabUserData] = useState( - null - ); - const [slackUserData, setSlackUserData] = useState( - null - ); - const [discordUserData, setDiscordUserData] = useState( - null - ); - const [linearUserData, setLinearUserData] = useState( - null - ); - const [notionUserData, setNotionUserData] = useState( - null - ); - const [hasPaid, setHasPaid] = useState(false); + if (data?.github_data) { + githubUserData = JSON.parse(data.github_data); + } + if (data?.bitbucket_data) { + bitbucketUserData = JSON.parse(data.bitbucket_data); + } + if (data?.gitlab_data) { + gitlabUserData = JSON.parse(data.gitlab_data); + } + if (data?.slack_data) { + slackUserData = JSON.parse(data.slack_data); + } + if (data?.jira_data) { + jiraUserData = JSON.parse(data.jira_data); + } + if (data?.discord_data) { + discordUserData = JSON.parse(data.discord_data); + } + if (data?.notion_data) { + notionUserData = JSON.parse(data.notion_data); + } + if (data?.linear_data) { + linearUserData = JSON.parse(data.linear_data); + } - useEffect(() => { - if (userEmail) { - getAllUserData(userEmail).then((data) => { - if (data?.github_data) { - setGithubUserData(JSON.parse(data.github_data)); - } - if (data?.bitbucket_data) { - setBitbucketUserData(JSON.parse(data.bitbucket_data)); - } - if (data?.gitlab_data) { - setGitlabUserData(JSON.parse(data.gitlab_data)); - } - if (data?.slack_data) { - setSlackUserData(JSON.parse(data.slack_data)); - } - if (data?.jira_data) { - setJiraUserData(JSON.parse(data.jira_data)); - } - if (data?.discord_data) { - setDiscordUserData(JSON.parse(data.discord_data)); - } - if (data?.notion_data) { - setNotionUserData(JSON.parse(data.notion_data)); - } - if (data?.linear_data) { - setLinearUserData(JSON.parse(data.linear_data)); - } - }); - // use getByEmail to check if user has paid - // TODO: As stated on Jira ticket WM-66, we'll refactor this later in order to not block render - // and have a perfect self-serve experience - getPaymentInfo(userEmail).then((data) => { - setHasPaid(data); - }); - } - }, [userEmail]); return (
    { + const interval = setInterval(() => { + setTimeToRedirect(timeToRedirect - 1); + if (timeToRedirect === 0) { + window.open(url, "_blank"); + } + }, 1000); + return () => clearInterval(interval); + }, [timeToRedirect]); + return ( +
    + {timeToRedirect > 0 ? ( +

    We will try opening it in {timeToRedirect}...

    + ) : null} +
    + ); +} diff --git a/components/sidebar.tsx b/components/sidebar.tsx new file mode 100644 index 000000000..eb8319097 --- /dev/null +++ b/components/sidebar.tsx @@ -0,0 +1,16 @@ +import Link from "next/link"; + +const sidebar = ( + +); +export default sidebar; diff --git a/lib/auth/AuthProvider.tsx b/lib/auth/AuthProvider.tsx new file mode 100644 index 000000000..f0beb5dde --- /dev/null +++ b/lib/auth/AuthProvider.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { SessionProvider } from "next-auth/react"; + +// Use of the is mandatory to allow components that call +// `useSession()` anywhere in your application to access the `session` object +// We use client hydration to establish the +export default function AuthProvider({ + children, +}: { + children?: React.ReactNode; +}) { + return {children}; +} diff --git a/next-env.d.ts b/next-env.d.ts index 4f11a03dc..fd36f9494 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/next.config.js b/next.config.js new file mode 100644 index 000000000..4436b22b5 --- /dev/null +++ b/next.config.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + experimental: { + appDir: true, + }, +}; + +module.exports = nextConfig; diff --git a/package.json b/package.json index 293bbac18..b26058b1f 100644 --- a/package.json +++ b/package.json @@ -19,14 +19,16 @@ "@vercel/analytics": "^1.0.0", "airtable": "^0.11.0", "applicationinsights": "^2.5.1", + "client-only": "0.0.1", "md-to-adf": "^0.6.4", "next": "latest", - "next-auth": "^4.10.3", + "next-auth": "^4.22.1", "nodemailer": "^6.7.8", "octokit": "^2.0.9", "openai": "^3.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "server-only": "0.0.1", "stripe": "^10.10.0", "zlib": "^1.0.5" }, diff --git a/pages/_app.tsx b/pages/_app.tsx index fc28de7e5..c687eac85 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,6 +1,7 @@ import { SessionProvider } from "next-auth/react"; import { Analytics } from "@vercel/analytics/react"; import "@primer/css/index.scss"; + export default function App({ Component, pageProps: { session, ...pageProps }, diff --git a/pages/actions/github.tsx b/pages/actions/github.tsx deleted file mode 100644 index f13c4f40f..000000000 --- a/pages/actions/github.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; - -import Header from "../../components/Header"; -import LogInBtn from "../../components/login-btn"; -import LoginGrid from "../../components/loginGrid"; -import DownloadExtension from "../../components/dashboard/DownloadExtension"; - -function HomePage({}) { - const [userEmail, setUserEmail] = useState(null); - - const { data: session, status } = useSession(); - useEffect(() => { - setUserEmail(session?.user?.email); - }, [session]); - - return ( -
    - {status === "loading" &&
    } - {status === "unauthenticated" && } - {status === "authenticated" && ( - <> - {session ?
    : } - {userEmail ? ( - <> - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -

    Try our GitHub App

    -

    Context on each Pr

    -
    -
    -
    - - ) : null} - - )} -
    - ); -} - -export default HomePage; diff --git a/pages/api/actions/github.ts b/pages/api/actions/github.ts deleted file mode 100644 index 595033911..000000000 --- a/pages/api/actions/github.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { App } from "@octokit/app"; -import { trackEvent } from "../../../utils/analytics/azureAppInsights"; -import executeRequest from "../../../utils/db/azuredb"; -import getGitHub from "../../../utils/actions/getGitHub"; -import getJira from "../../../utils/actions/getJira"; -import getSlack from "../../../utils/actions/getSlack"; -import getOpenAISummary from "../../../utils/actions/getOpenAISummary"; -import addActionCount from "../../../utils/db/teams/addActionCount"; -import getNotion from "../../../utils/actions/getNotion"; -import githubMarkdown from "../../../utils/actions/markdownHelpers/github"; -import jiraMarkdown from "../../../utils/actions/markdownHelpers/jira"; -import slackMarkdown from "../../../utils/actions/markdownHelpers/slack"; -import notionMarkdown from "../../../utils/actions/markdownHelpers/notion"; -import countMarkdown from "../../../utils/actions/markdownHelpers/count"; - -const app = new App({ - appId: process.env.GITHUB_APP_ID, - privateKey: process.env.GITHUB_PRIVATE_KEY, -}); - -export default async (req, res) => { - if (req.method === "POST") { - try { - // Verify and parse the webhook event - const eventName = req.headers["x-github-event"]; - let payload = req.body; - - if ( - payload.action === "opened" || - payload.action === "reopened" || - payload.action === "synchronize" - ) { - const { installation, repository, pull_request } = payload; - const installationId = installation.id; - const { title, body } = payload.pull_request; - const owner = repository.owner.login; - const repo = repository.name; - const number = pull_request.number; - trackEvent({ - name: "gitHubApp", - properties: { - user: pull_request.user.login, - owner, - repo, - action: payload.action, - //@ts-ignore - issue_number: number, - }, - }); - - const octokit = await app.getInstallationOctokit(installationId); - - const query = `EXEC dbo.get_all_tokens_from_gh_username @github_user='${pull_request.user.login}'`; - const wmUserData = await executeRequest(query); - const { - github_token, - jira_token, - jira_refresh_token, - slack_token, - notion_token, - cloudId, - AISummary, - JiraTickets, - GitHubPRs, - SlackMessages, - NotionPages, - user_email, - watermelon_user, - } = wmUserData; - if (!watermelon_user) { - { - // Post a new comment if no existing comment was found - await octokit - .request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - { - owner, - issue_number: number, - repo, - body: "[Please login to Watermelon to see the results](https://app.watermelontools.com/)", - } - ) - .then((response) => { - console.log("post comment", response.data); - }) - .catch((error) => { - return console.error("posting comment error", error); - }); - return res.status(401).send("User not registered"); - } - } - let octoCommitList = await octokit.request( - "GET /repos/{owner}/{repo}/pulls/{pull_number}/commits", - { - repo: repository.name, - owner: repository.owner.login, - pull_number: number, - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - } - ); - let commitList: string[] = []; - for (let index = 0; index < octoCommitList?.data?.length; index++) { - commitList.push(octoCommitList.data[index].commit.message); - } - const commitSet = new Set(commitList); - const stopwords = [ - "a", - "about", - "add comments", - "add", - "all", - "an", - "and", - "as", - "at", - "better", - "build", - "bump dependencies", - "bump version", - "but", - "by", - "call", - "cd", - "change", - "changed", - "changes", - "changing", - "chore", - "ci", - "cleanup", - "code review", - "comment out", - "commit", - "commits", - "config", - "config", - "create", - "critical", - "debug", - "delete", - "dependency update", - "deploy", - "docs", - "documentation", - "eslint", - "feat", - "feature", - "fix", - "fix", - "fixed", - "fixes", - "fixing", - "for", - "from", - "get", - "github", - "gitignore", - "hack", - "he", - "hotfix", - "husky", - "if", - "ignore", - "improve", - "improved", - "improvement", - "improvements", - "improves", - "improving", - "in", - "init", - "is", - "lint-staged", - "lint", - "linting", - "list", - "log", - "logging", - "logs", - "major", - "merge conflict", - "merge", - "minor", - "npm", - "of", - "on", - "oops", - "or", - "package-lock.json", - "package.json", - "prettier", - "print", - "quickfix", - "refactor", - "refactored", - "refactoring", - "refactors", - "release", - "remove comments", - "remove console", - "remove", - "remove", - "removed", - "removes", - "removing", - "revert", - "security", - "setup", - "squash", - "start", - "style", - "stylelint", - "temp", - "test", - "tested", - "testing", - "tests", - "the", - "to", - "try", - "typo", - "up", - "update dependencies", - "update", - "updated", - "updates", - "updating", - "use", - "version", - "was", - "wip", - "with", - "yarn.lock", - "master", - "main", - "dev", - "development", - "prod", - "production", - "staging", - "stage", - "[", - "]", - "!", - "{", - "}", - "(", - ")", - "''", - '"', - "``", - "-", - "_", - ":", - ";", - ",", - ".", - "?", - "/", - "|", - "&", - "*", - "^", - "%", - "$", - "#", - "##", - "###", - "####", - "#####", - "######", - "#######", - "@", - "\n", - "\t", - "\r", - "", - "/*", - "*/", - "[x]", - "[]", - "[ ]", - ]; - // create a string from the commitlist set and remove stopwords in lowercase - - const searchStringSet = Array.from(commitSet).join(" "); - // add the title and body to the search string, remove stopwords and remove duplicates - const searchStringSetWTitleABody = Array.from( - new Set( - searchStringSet - .concat(` ${title.split("/").join(" ")}`) - .concat(` ${body}`.split("\n").join(" ")) - .split("\n") - .flatMap((line) => line.split(",")) - .map((commit: string) => commit.toLowerCase()) - .filter((commit) => !stopwords.includes(commit)) - .join(" ") - .split(" ") - ) - ).join(" "); - // select six random words from the search string - const randomWords = searchStringSetWTitleABody - .split(" ") - .sort(() => Math.random() - 0.5) - .slice(0, 6); - - const [ghValue, jiraValue, slackValue, notionValue, count] = - await Promise.all([ - getGitHub({ - repo, - owner, - github_token, - randomWords, - amount: GitHubPRs, - }), - getJira({ - user: user_email, - title, - body, - jira_token, - jira_refresh_token, - randomWords, - amount: JiraTickets, - }), - getSlack({ - title, - body, - slack_token, - randomWords, - amount: SlackMessages, - }), - getNotion({ - notion_token, - randomWords, - amount: NotionPages, - }), - addActionCount({ watermelon_user }), - ]); - - let textToWrite = ""; - textToWrite += "### WatermelonAI Summary (BETA)"; - textToWrite += `\n`; - - let businessLogicSummary; - if (AISummary) { - businessLogicSummary = await getOpenAISummary({ - ghValue, - commitList, - jiraValue, - slackValue, - title, - body, - }); - - if (businessLogicSummary) { - textToWrite += businessLogicSummary; - } else { - textToWrite += "Error getting summary" + businessLogicSummary.error; - } - } else { - textToWrite += `AI Summary deactivated by ${pull_request.user.login}`; - } - - textToWrite += githubMarkdown({ - GitHubPRs, - ghValue, - userLogin: pull_request.user.login, - }); - textToWrite += jiraMarkdown({ - JiraTickets, - jiraValue, - userLogin: pull_request.user.login, - }); - textToWrite += slackMarkdown({ - SlackMessages, - slackValue, - userLogin: pull_request.user.login, - }); - textToWrite += notionMarkdown({ - NotionPages, - notionValue, - userLogin: pull_request.user.login, - }); - textToWrite += countMarkdown({ - count, - isPrivateRepo: repository.private, - repoName: repo, - }); - - // Fetch all comments on the PR - const comments = await octokit.request( - "GET /repos/{owner}/{repo}/issues/{issue_number}/comments?sort=created&direction=desc", - { - owner, - repo, - issue_number: number, - headers: { - "X-GitHub-Api-Version": "2022-11-28", - }, - } - ); - console.log("comments.data.length", comments.data.length); - // Find our bot's comment - let botComment = comments.data.find((comment) => { - return comment.user.login.includes("watermelon-context"); - }); - if (botComment?.id) { - console.log("bcID", botComment.id); - // Update the existing comment - await octokit.request( - "PATCH /repos/{owner}/{repo}/issues/comments/{comment_id}", - { - owner, - repo, - comment_id: botComment.id, - body: textToWrite, - } - ); - } else { - // Post a new comment if no existing comment was found - await octokit - .request( - "POST /repos/{owner}/{repo}/issues/{issue_number}/comments", - { - owner, - issue_number: number, - repo, - body: textToWrite, - } - ) - .then((response) => { - console.log("post comment", { - url: response.data.html_url, - body: response.data.body, - user: response.data.user.login, - }); - }) - .catch((error) => { - return console.error("posting comment error", error); - }); - } - } - return res.status(200).send("Webhook event processed"); - } catch (error) { - console.error("general action processing error", error); - res.status(500).send("Error processing webhook event"); - } - } else { - res.setHeader("Allow", "POST"); - res.status(405).end("Method Not Allowed"); - } -}; diff --git a/pages/api/analytics/vsmarketplace/update.ts b/pages/api/analytics/vsmarketplace/update.ts index 057c6eb53..9be51aeee 100644 --- a/pages/api/analytics/vsmarketplace/update.ts +++ b/pages/api/analytics/vsmarketplace/update.ts @@ -41,6 +41,7 @@ export default async function handler(req, res) { chunk = 10; for (i = 0, j = records.length; i < j; i += chunk) { temparray = records.slice(i, i + chunk); + // @ts-ignore chunkedRecords.push(temparray); } // now we have an array of arrays of 10 records each @@ -49,6 +50,7 @@ export default async function handler(req, res) { for (let index = 0; index < chunkedRecords.length; index++) { const element = chunkedRecords[index]; let createdRecord = await base("vscmarketplace").create(element); + // @ts-ignore createdRecords.push(createdRecord); } res.status(200).json(createdRecords); diff --git a/pages/api/stripe/createSubscription.ts b/pages/api/stripe/createSubscription.ts index 41319590f..5722c395a 100644 --- a/pages/api/stripe/createSubscription.ts +++ b/pages/api/stripe/createSubscription.ts @@ -1,7 +1,7 @@ import Stripe from "stripe"; -const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY, { - apiVersion: null, +const stripe = new Stripe(process.env.NEXT_PUBLIC_STRIPE_SECRET_KEY!, { + apiVersion: "2022-08-01", }); export default async function handler(req, res) { diff --git a/pages/billing/cardDetails.tsx b/pages/billing/cardDetails.tsx deleted file mode 100644 index e0ac010af..000000000 --- a/pages/billing/cardDetails.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import { Elements } from "@stripe/react-stripe-js"; -import { loadStripe } from "@stripe/stripe-js"; -import { useEffect, useState, useCallback } from "react"; -import CheckoutForm from "./CheckoutForm"; -import { useRouter } from "next/router"; -import LogInBtn from "../../components/login-btn"; -import { useSession } from "next-auth/react"; - -const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY); - -function BillingPage() { - const [retrievedClientSecret, setRetrievedClientSecret] = useState(""); - const { data: session, status } = useSession(); - const router = useRouter(); - - useEffect(() => { - const { quantity, email } = router.query; - - // create async function that fetches the client secret - const fetchClientSecret = async () => { - const response = await fetch( - `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/stripe/createSubscription`, - { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - quantity, - email, - }), - } - ); - const { clientSecret } = await response.json(); - setRetrievedClientSecret(clientSecret); - }; - - fetchClientSecret(); - }, [router.query]); - - const options = { - clientSecret: retrievedClientSecret, - - appearance: { - theme: "night", - labels: "floating", - - variables: { - colorPrimary: "#79c0ff", - colorBackground: "#0d1117", - colorText: "#c9d1d9", - colorDanger: "#df1b41", - fontFamily: "Ideal Sans, system-ui, sans-serif", - fontSize: "14-px", - spacingUnit: "2px", - borderRadius: "4px", - }, - }, - }; - - return ( -
    - {session ? ( -
    -
    -
    -

    - Purchase your Watermelon subscription -

    - {/* render if component already mounted */} - {retrievedClientSecret && stripePromise && options && ( -
    - {router.isFallback ? ( -
    Loading...
    - ) : ( - // @ts-ignore - - - - )} -
    - )} -
    -
    -
    - ) : ( - - )} -
    - ); -} - -export default BillingPage; diff --git a/pages/discord.tsx b/pages/discord.tsx index c2bf37900..62e933152 100644 --- a/pages/discord.tsx +++ b/pages/discord.tsx @@ -64,7 +64,7 @@ export async function getServerSideProps(context) { const response = await fetch(`${API_ENDPOINT}/oauth2/token`, { method: "POST", headers: headers, - body: new URLSearchParams(data), + body: new URLSearchParams(data.toString()), }); const json = await response.json(); const user = await fetch(`${API_ENDPOINT}/users/@me`, { diff --git a/pages/gitpod-code.tsx b/pages/gitpod-code.tsx index a832d0e13..596355768 100644 --- a/pages/gitpod-code.tsx +++ b/pages/gitpod-code.tsx @@ -16,7 +16,7 @@ function Gitpod() { let system = `https://${router.query.podURL}.gitpod.io`; let url: string = `${system}://watermelontools.watermelon-tools?email=${ data?.user?.email ?? "" - }&token=${data?.user.name ?? ""}`; + }&token=${data?.user?.name ?? ""}`; const [userEmail, setUserEmail] = useState(null); useEffect(() => { @@ -51,7 +51,6 @@ function Gitpod() {
    - )} diff --git a/pages/index.tsx b/pages/index.tsx deleted file mode 100644 index 45745bc9b..000000000 --- a/pages/index.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; - -import Header from "../components/Header"; -import LogInBtn from "../components/login-btn"; -import LoginGrid from "../components/loginGrid"; -import DownloadExtension from "../components/dashboard/DownloadExtension"; - -function HomePage({}) { - const [userEmail, setUserEmail] = useState(null); - - const { data: session, status } = useSession(); - useEffect(() => { - setUserEmail(session?.user?.email); - }, [session]); - - return ( -
    - {status === "loading" &&
    } - {status === "unauthenticated" && } - {status === "authenticated" && ( - <> - {session ?
    : } - {userEmail ? ( - <> - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    -

    Try our GitHub App

    -

    Context on each Pr

    -
    -
    -
    - - ) : null} - - )} -
    - ); -} - -export default HomePage; diff --git a/pages/settings.tsx b/pages/settings.tsx deleted file mode 100644 index 3c0f5d343..000000000 --- a/pages/settings.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import { useSession } from "next-auth/react"; -import { useEffect, useState } from "react"; - -import Header from "../components/Header"; -import LogInBtn from "../components/login-btn"; - -import getUserSettings from "../utils/api/getUserSettings"; - -function HomePage({}) { - const [userEmail, setUserEmail] = useState(null); - const [saveDisabled, setSaveDisabled] = useState(false); - const { data: session, status } = useSession(); - - const setUserSettingsState = async (userEmail) => { - let settings = await getUserSettings(userEmail); - setFormState(settings); - }; - useEffect(() => { - setUserEmail(session?.user?.email); - setUserSettingsState(session?.user?.email); - }, [session]); - const [formState, setFormState] = useState({ - JiraTickets: 3, - SlackMessages: 3, - GitHubPRs: 3, - NotionPages: 3, - AISummary: 1, - }); - - const handleSubmit = async () => { - setSaveDisabled(true); - try { - const response = await fetch("/api/user/updateSettings", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ userSettings: formState, user: userEmail }), - }).then((res) => res.json()); - if ( - response.AISummary === formState.AISummary || - response.JiraTickets === formState.JiraTickets || - response.SlackMessages === formState.SlackMessages || - response.GitHubPRs === formState.GitHubPRs || - response.NotionPages === formState.NotionPages - ) { - setUserSettingsState(session?.user?.email); - } else { - console.error("Failed to save the form"); - } - } catch (error) { - console.error("An error occurred while saving the form", error); - } finally { - setSaveDisabled(false); - } - }; - - return ( -
    - {status === "loading" &&
    } - {status === "unauthenticated" && } - {status === "authenticated" && ( - <> - {session ?
    : } - {userEmail ? ( -
    -

    Settings

    -
    -
    - App Settings -
    - This controls how the GitHub App behaves. -
    -
    - -
    - Jira Tickets: - -
    -
    - Slack Messages: - -
    -
    - GitHub PRs: - -
    - -
    - Notion Pages: - -
    - -
    - AI Summary: - -
    - - -
    -
    - ) : null} - - )} -
    - ); -} - -export default HomePage; diff --git a/pages/vscode-insiders.tsx b/pages/vscode-insiders.tsx deleted file mode 100644 index 7e25087eb..000000000 --- a/pages/vscode-insiders.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import Link from "next/link"; -import { useSession, signIn } from "next-auth/react"; -import { useEffect, useState } from "react"; -import LoginGrid from "../components/loginGrid"; - -function VSCodeInsiders() { - const { status, data } = useSession({ - required: true, - onUnauthenticated() { - signIn(); - }, - }); - const [timeToRedirect, setTimeToRedirect] = useState(10); - const [userEmail, setUserEmail] = useState(null); - - let system = "vscode-insiders"; - let url: string = `${system}://watermelontools.watermelon-tools?email=${ - data?.user?.email ?? "" - }&token=${data?.user.name ?? ""}`; - - useEffect(() => { - setUserEmail(data?.user?.email); - }, [data]); - - useEffect(() => { - const interval = setInterval(() => { - setTimeToRedirect(timeToRedirect - 1); - if (timeToRedirect === 0) { - window.open(url, "_blank"); - } - }, 1000); - return () => clearInterval(interval); - }, [timeToRedirect]); - - return ( -
    - {status !== "loading" && ( - -
    -
    -

    Open VSCode Insiders

    - - {timeToRedirect > 0 ? ( -

    We will try opening it in {timeToRedirect}...

    - ) : null} -
    -
    - - )} - -
    - ); -} - -export default VSCodeInsiders; diff --git a/pages/vscode.tsx b/pages/vscode.tsx deleted file mode 100644 index 0c03a50f6..000000000 --- a/pages/vscode.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Link from "next/link"; -import { useSession, signIn } from "next-auth/react"; -import { useEffect, useState } from "react"; -import LoginGrid from "../components/loginGrid"; - -function VSCode() { - const { status, data } = useSession({ - required: true, - onUnauthenticated() { - signIn(); - }, - }); - const [timeToRedirect, setTimeToRedirect] = useState(10); - const [userEmail, setUserEmail] = useState(null); - - let system = "vscode"; - let url: string = `${system}://watermelontools.watermelon-tools?email=${ - data?.user?.email ?? "" - }&token=${data?.user.name ?? ""}`; - - useEffect(() => { - setUserEmail(data?.user?.email); - }, [data]); - - useEffect(() => { - const interval = setInterval(() => { - setTimeToRedirect(timeToRedirect - 1); - if (timeToRedirect === 0) { - window.open(url, "_blank"); - } - }, 1000); - return () => clearInterval(interval); - }, [timeToRedirect]); - - return ( -
    - {status !== "loading" && ( - <> - -
    -
    -

    Open VSCode

    - - {timeToRedirect > 0 ? ( -

    We will try opening it in {timeToRedirect}...

    - ) : null}{" "} -
    -
    - - - - )} -
    - ); -} - -export default VSCode; diff --git a/pages/vscodium.tsx b/pages/vscodium.tsx deleted file mode 100644 index 6ce2d372d..000000000 --- a/pages/vscodium.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import Link from "next/link"; -import { useSession, signIn } from "next-auth/react"; -import { useEffect, useState } from "react"; -import LoginGrid from "../components/loginGrid"; - -function VSCodium({}) { - const { status, data } = useSession({ - required: true, - onUnauthenticated() { - signIn(); - }, - }); - const [timeToRedirect, setTimeToRedirect] = useState(10); - const [userEmail, setUserEmail] = useState(null); - - useEffect(() => { - setUserEmail(data?.user?.email); - }, [data]); - - let system = "vscode"; - let url: string = `${system}://watermelontools.watermelon-tools?email=${ - userEmail ?? "" - }&token=${data?.user.name ?? ""}`; - - useEffect(() => { - const interval = setInterval(() => { - setTimeToRedirect(timeToRedirect - 1); - if (timeToRedirect === 0) { - window.open(url, "_blank"); - } - }, 1000); - return () => clearInterval(interval); - }, [timeToRedirect]); - - return ( -
    - {status !== "loading" && ( -
    - -
    -
    -

    Open VSCodium

    - - {timeToRedirect > 0 ? ( -

    We will try opening it in {timeToRedirect}...

    - ) : null} -
    -
    - -
    - )} - -
    - ); -} - -export default VSCodium; diff --git a/tsconfig.json b/tsconfig.json index 3d9b95905..b88034ab1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,12 +17,20 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", - "incremental": true + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "strictNullChecks": true }, "include": [ "next-env.d.ts", "**/*.ts", - "**/*.tsx" ], + "**/*.tsx", + ".next/types/**/*.ts" + ], "exclude": [ "node_modules" ] diff --git a/utils/api/getAllUserPublicData.ts b/utils/api/getAllUserPublicData.ts new file mode 100644 index 000000000..3edeeca73 --- /dev/null +++ b/utils/api/getAllUserPublicData.ts @@ -0,0 +1,6 @@ +import "server-only"; +import getAllPublicUserData from "../db/user/getAllPublicUserData"; +export default async function getData({ userEmail }) { + let dbUser = await getAllPublicUserData(userEmail); + return dbUser; +} diff --git a/utils/auth/adapter.ts b/utils/auth/adapter.ts index a6ca75f34..f94b0c443 100644 --- a/utils/auth/adapter.ts +++ b/utils/auth/adapter.ts @@ -12,15 +12,20 @@ client.setApiKey(process.env.SENDGRID_API_KEY); function makeISO(date: string | Date) { return new Date(date).toISOString(); } - +const emptyUser = { + id: "", + name: null, + email: "", + image: null, + emailVerified: null, +}; export default function MyAdapter(): Adapter { return { async createUser(user): Promise { let createdUser = await executeRequest( `EXEC [dbo].[create_user] @email = '${user.email}',${ user.name ? ` @name = '${user.name}',` : "" - } @emailVerified = '${makeISO(user.emailVerified as any)}'; - ` + } @emailVerified = '${makeISO(user.emailVerified as any)}';` ); const request = await client @@ -53,8 +58,9 @@ export default function MyAdapter(): Adapter { let userData = await executeRequest( `EXEC [dbo].[get_user] @id = '${id}';` ); + console.log("getUser", userData); if (!userData.email) { - return null; + return emptyUser; } return { id: userData.id, @@ -69,7 +75,7 @@ export default function MyAdapter(): Adapter { `EXEC [dbo].[get_user_by_email] @email = '${email}';` ); if (!userData.email) { - return null; + return emptyUser; } return { id: userData.id, @@ -87,13 +93,13 @@ export default function MyAdapter(): Adapter { `EXEC [dbo].[get_user_by_account] @providerAccountId = '${providerAccountId}', @provider = '${provider}';` ); if (!userData.email) { - return null; + return emptyUser; } return userData; }, async updateUser(user): Promise { if (!user.emailVerified || !user.id) { - return null; + return emptyUser; } let updatedUser = await executeRequest( `EXEC [dbo].[update_user] @id = '${user.id}', ${ @@ -112,7 +118,7 @@ export default function MyAdapter(): Adapter { }, async deleteUser(userId): Promise { console.log("deleteUser", userId); - return; + return emptyUser; }, async linkAccount(account): Promise { await executeRequest( @@ -172,11 +178,10 @@ export default function MyAdapter(): Adapter { }): Promise { let updatedSession = await executeRequest( `EXEC [dbo].[update_session] @session_token = '${sessionToken}', @userId = '${userId}', @expires = '${new Date( - expires - ).toISOString()}';` + new Date(expires!).toISOString() + )}';` ); - const session = { - id: updatedSession.id as string, + const session: AdapterSession = { sessionToken: updatedSession.session_token as string, userId: updatedSession.user_id as string, expires: new Date(updatedSession.expires), diff --git a/utils/db/azuredb.ts b/utils/db/azuredb.ts index ba05d40a2..9c0f84492 100644 --- a/utils/db/azuredb.ts +++ b/utils/db/azuredb.ts @@ -1,11 +1,11 @@ -const executeRequest = async (query) => { +const executeRequest = async (query: string) => { const queryString = JSON.stringify({ query: query }); try { - let resp = await fetch(process.env.AZURE_WEBAPP_URL, { + let resp = await fetch(process.env.AZURE_WEBAPP_URL!, { method: "POST", headers: { "Content-Type": "application/json", - "x-vercel-azure-auth-token": process.env.AZURE_WEBAPP_TOKEN, + "x-vercel-azure-auth-token": process.env.AZURE_WEBAPP_TOKEN!, }, body: queryString, }) diff --git a/utils/db/teams/getTeammates.ts b/utils/db/teams/getTeammates.ts new file mode 100644 index 000000000..d73e0ca9e --- /dev/null +++ b/utils/db/teams/getTeammates.ts @@ -0,0 +1,8 @@ +import executeRequest from "../azuredb"; + +export default async ({ watermelon_user }) => { + let query = await executeRequest( + `EXEC dbo.get_user_teammates @watermelon_user = '${watermelon_user}'` + ); + return query; +};