Skip to content

Commit

Permalink
Merge pull request #21 from gitautoai/stage
Browse files Browse the repository at this point in the history
Github Auth and Stripe/Dashboard integration
  • Loading branch information
nikitamalinov authored Apr 9, 2024
2 parents ef561ae + 33a4f33 commit 6cf75d8
Show file tree
Hide file tree
Showing 45 changed files with 2,122 additions and 410 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/gitleaks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: gitleaks
on: [push]
jobs:
scan:
name: gitleaks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE}}
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ next-env.d.ts
.sentryclirc

.env*.local
prisma/.env
prisma/.env

/utils/nikita*
31 changes: 5 additions & 26 deletions app/_app.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
"use client";
import type { AppProps } from "next/app";

// Local Styles - Tailwind + Chakra
import "@/styles/globals.css";
import theme from "../theme/styles";

// 3rd Party Styles
import { ChakraProvider } from "@chakra-ui/react";
import { AnimatePresence } from "framer-motion";
import { Comfortaa, Poppins, Lexend } from "next/font/google";

// Analytics
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/react";

import { Suspense } from "react";

const comfortaa = Comfortaa({
subsets: ["latin"],
variable: "--font-comfortaa",
Expand All @@ -36,18 +23,10 @@ export default function App({
pageProps: { session, ...pageProps },
}: AppProps) {
return (
<ChakraProvider theme={theme}>
<SpeedInsights />
<AnimatePresence initial={true}>
<Suspense fallback={<div>Loading...</div>}>
<div
className={`${comfortaa.variable} ${poppins.variable} ${lexend.variable} font-helvetica`}
>
<Component {...pageProps} />
<Analytics mode={"production"} />
</div>
</Suspense>
</AnimatePresence>
</ChakraProvider>
<div
className={`${comfortaa.variable} ${poppins.variable} ${lexend.variable} font-helvetica`}
>
<Component {...pageProps} />
</div>
);
}
42 changes: 42 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import prisma from "@/lib/client";

// Third party
import NextAuth from "next-auth";
import GithubProvider from "next-auth/providers/github";
import { sign } from "jsonwebtoken";

const handler = NextAuth({
providers: [
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
],
callbacks: {
async session({ session, token }) {
try {
session.user.userId = Number(token.user_id);
session.jwtToken = token.jwtToken as string;
} catch (err) {
console.error(err);
}
return session;
},
async jwt({ token, account, user }) {
if (user) {
token.jwtToken = sign(user, process.env.JWT_SECRET as string, {
algorithm: "HS256",
expiresIn: "100d",
});
}
if (account) {
token.user_id = account.providerAccountId;
}
return token;
},
},
debug: true,
secret: process.env.NEXTAUTH_SECRET,
});

export { handler as GET, handler as POST };
79 changes: 79 additions & 0 deletions app/api/stripe/create-portal-or-checkout-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { NextResponse, NextRequest } from "next/server";

import { z, ZodError } from "zod";
import { isValidToken } from "@/utils/auth";

import stripe from "@/lib/stripe";
import { createCheckoutSession, hasActiveSubscription } from "@/utils/stripe";
// import { NEXT_PUBLIC_SITE_URL } from "@/lib/constants";

const schema = z.object({
userId: z.number(),
jwtToken: z.string(),
customerId: z.string(),
email: z.string().email(),
ownerType: z.string(),
ownerId: z.number(),
ownerName: z.string(),
userName: z.string(),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const {
userId,
jwtToken,
customerId,
email,
ownerType,
ownerId,
ownerName,
userName,
} = schema.parse(body);

if (!isValidToken(userId.toString(), jwtToken)) {
return new NextResponse("Unauthorized", { status: 401 });
}

let session = null;
// If the customer has an active non-free subscription, redirect to the customer portal
// Otherwise, create a new checkout session
if (await hasActiveSubscription(customerId)) {
session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: process.env.NEXT_PUBLIC_SITE_URL,
});
if (!session.url) throw new Error("No billing portal URL found");
} else {
session = await createCheckoutSession({
customerId,
email: email,
priceId: process.env.STRIPE_STANDARD_PLAN_PRICE_ID || "",
metadata: {
userId: userId,
userName: userName,
ownerName: ownerName,
ownerId: ownerId,
},
});
if (!session.url) throw new Error("No checkout session URL found");
}

return NextResponse.json(session.url, { status: 200 });
} catch (err: any) {
console.error(err);
if (err instanceof ZodError) {
return NextResponse.json(
{ message: err.issues[0].message },
{
status: 400,
}
);
} else {
return new NextResponse(err, {
status: 400,
});
}
}
}
46 changes: 46 additions & 0 deletions app/api/stripe/create-portal-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { NextResponse, NextRequest } from "next/server";

import { z, ZodError } from "zod";
import { isValidToken } from "@/utils/auth";

import stripe from "@/lib/stripe";

const schema = z.object({
userId: z.number(),
jwtToken: z.string(),
customerId: z.string(),
});

export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { userId, jwtToken, customerId } = schema.parse(body);

if (!isValidToken(userId.toString(), jwtToken)) {
return new NextResponse("Unauthorized", { status: 401 });
}

const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: process.env.NEXT_PUBLIC_SITE_URL as string,
});

if (!session.url) throw new Error("No checkout session URL found");

return NextResponse.json(session.url, { status: 200 });
} catch (err: any) {
console.error(err);
if (err instanceof ZodError) {
return NextResponse.json(
{ message: err.issues[0].message },
{
status: 400,
}
);
} else {
return new NextResponse(err, {
status: 400,
});
}
}
}
61 changes: 61 additions & 0 deletions app/api/stripe/get-userinfo-subscriptions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"use server";
import { NextResponse, NextRequest } from "next/server";

import { z, ZodError } from "zod";
import { isValidToken } from "@/utils/auth";

import { hasActiveSubscription } from "@/utils/stripe";

const schema = z.object({
userId: z.number(),
jwtToken: z.string(),
customerIds: z.array(z.string()),
});

export async function GET(req: NextRequest) {
try {
const url = new URL(req.url);
const params = new URLSearchParams(url.searchParams);

const { userId, jwtToken, customerIds } = schema.parse({
userId: Number(params.get("userId")),
jwtToken: params.get("jwtToken"),
customerIds: params.getAll("customerIds"),
});

if (!isValidToken(userId.toString(), jwtToken)) {
return new NextResponse("Unauthorized", { status: 401 });
}

// If no customerIds are passed, return an empty array
if (customerIds.length === 1 && customerIds[0].length === 0) {
return NextResponse.json([], { status: 200 });
}

const booleanMapping = [];

// Passing an array through api query params results in ['string1,string2'] format
const customerIdsSplit = customerIds[0].split(",");

for (const customerId of customerIdsSplit) {
const myBool = await hasActiveSubscription(customerId);
booleanMapping.push(myBool);
}

return NextResponse.json(booleanMapping, { status: 200 });
} catch (err: any) {
console.error(err);
if (err instanceof ZodError) {
return NextResponse.json(
{ message: err.issues[0].message },
{
status: 400,
}
);
} else {
return new NextResponse(err, {
status: 400,
});
}
}
}
76 changes: 76 additions & 0 deletions app/api/users/get-user-info/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use server";
import { NextResponse, NextRequest } from "next/server";
import { prisma } from "@/lib/prisma";
import { z, ZodError } from "zod";

// Utils
import { isValidToken } from "@/utils/auth";
import { stringify } from "@/utils/transform";

const schema = z.object({
userId: z.string(),
jwtToken: z.string(),
});
export async function GET(req: NextRequest) {
try {
const url = new URL(req.url);
const params = new URLSearchParams(url.searchParams);
const { userId, jwtToken } = schema.parse({
userId: params.get("userId"),
jwtToken: params.get("jwtToken"),
});

if (!isValidToken(userId, jwtToken)) {
return new NextResponse("Unauthorized", { status: 401 });
}

const user = await prisma.user.findMany({
where: {
user_id: Number(userId),
installations: {
uninstalled_at: null,
},
},
include: {
installations: {
include: {
owners: true,
},
},
},
});

// owner_type == "U" comes first in the list of users
user.sort((a: any, b: any) => {
if (
a.installations.owner_type === "U" &&
b.installations.owner_type !== "U"
) {
return -1;
} else if (
a.installations.owner_type !== "U" &&
b.installations.owner_type === "U"
) {
return 1;
} else {
return a.installations.created_at - b.installations.created_at;
}
});

return new NextResponse(stringify(user), { status: 200 });
} catch (err: any) {
console.error(err);
if (err instanceof ZodError) {
return NextResponse.json(
{ message: err.issues[0].message },
{
status: 400,
}
);
} else {
return new NextResponse(err, {
status: 400,
});
}
}
}
Loading

0 comments on commit 6cf75d8

Please sign in to comment.