diff --git a/.env.docker b/.env.docker
index d2787daafa..fa435a4fea 100644
--- a/.env.docker
+++ b/.env.docker
@@ -14,7 +14,7 @@ NEXTAUTH_URL=http://localhost:3000
# This should always be localhost:3000 (or whatever port your app is running on)
NEXTAUTH_URL_INTERNAL=http://localhost:3000
-DATABASE_URL='postgresql://postgres:postgres@postgres:5432/formbricks?schema=public'
+DATABASE_URL='postgresql://postgres:postgres@postgres:5432/postgres?schema=public'
################
# Mail Setup #
diff --git a/.env.example b/.env.example
index 62f695a150..b2fe0a58ff 100644
--- a/.env.example
+++ b/.env.example
@@ -15,7 +15,7 @@ NEXTAUTH_URL=http://localhost:3000
# This should always be localhost:3000 (or whatever port your app is running on)
NEXTAUTH_URL_INTERNAL=http://localhost:3000
-DATABASE_URL='postgresql://postgres:postgres@localhost:5432/formbricks?schema=public'
+DATABASE_URL='postgresql://postgres:postgres@localhost:5432/postgres?schema=public'
# For Docker Compose Production Setup use this Database URL:
# DATABASE_URL='postgresql://postgres:postgres@postgres:5432/formbricks?schema=public'
diff --git a/apps/hq/next.config.js b/apps/hq/next.config.js
index da150cd84b..8072815530 100644
--- a/apps/hq/next.config.js
+++ b/apps/hq/next.config.js
@@ -12,6 +12,14 @@ module.exports = {
appDir: true,
serverComponentsExternalPackages: ["@prisma/client"],
},
+ images: {
+ remotePatterns: [
+ {
+ protocol: "https",
+ hostname: "avatars.githubusercontent.com",
+ },
+ ],
+ },
webpack: (config) => {
config.externals = [...(config.externals || []), "@prisma/client"];
// Important: return the modified config
@@ -21,7 +29,7 @@ module.exports = {
return [
{
source: "/",
- destination: "/forms/",
+ destination: "/app/",
permanent: false,
},
];
diff --git a/apps/hq/package.json b/apps/hq/package.json
index bf30c9f477..0ab09da438 100644
--- a/apps/hq/package.json
+++ b/apps/hq/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@formbricks/hq",
- "version": "1.0.0",
+ "version": "0.0.0",
"scripts": {
"dev": "next dev",
"build": "next build",
@@ -9,10 +9,12 @@
"lint": "next lint"
},
"dependencies": {
+ "@formbricks/react": "workspace:*",
"@formbricks/ui": "workspace:*",
"@headlessui/react": "^1.7.4",
"@heroicons/react": "^2.0.13",
"bcryptjs": "^2.4.3",
+ "date-fns": "^2.29.3",
"jsonwebtoken": "^8.5.1",
"next": "^13.0.5",
"next-auth": "^4.17.0",
@@ -20,7 +22,9 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^4.6.0",
- "react-toastify": "^9.1.1"
+ "react-loader-spinner": "^5.3.4",
+ "react-toastify": "^9.1.1",
+ "swr": "^1.3.0"
},
"devDependencies": {
"@formbricks/database": "workspace:*",
diff --git a/apps/hq/src/app/LoadingSpinner.tsx b/apps/hq/src/app/LoadingSpinner.tsx
new file mode 100644
index 0000000000..f29dbec739
--- /dev/null
+++ b/apps/hq/src/app/LoadingSpinner.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import { TailSpin } from "react-loader-spinner";
+
+export default function LoadingSpinner() {
+ return ;
+}
diff --git a/apps/hq/src/app/Logo.jsx b/apps/hq/src/app/Logo.jsx
index 742ae2c83f..0afe42daf2 100644
--- a/apps/hq/src/app/Logo.jsx
+++ b/apps/hq/src/app/Logo.jsx
@@ -51,24 +51,24 @@ export function Logo(props) {
@@ -79,7 +79,7 @@ export function Logo(props) {
-
+
-
+ colorInterpolationFilters="sRGB">
+
-
+ colorInterpolationFilters="sRGB">
+
@@ -122,8 +122,8 @@ export function Logo(props) {
width="87.5471"
height="87.5471"
filterUnits="userSpaceOnUse"
- color-interpolation-filters="sRGB">
-
+ colorInterpolationFilters="sRGB">
+
@@ -134,8 +134,8 @@ export function Logo(props) {
x2="-0.00218275"
y2="110.964"
gradientUnits="userSpaceOnUse">
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
diff --git a/apps/hq/src/app/SessionProvider.tsx b/apps/hq/src/app/SessionProvider.tsx
new file mode 100644
index 0000000000..cee9a186f1
--- /dev/null
+++ b/apps/hq/src/app/SessionProvider.tsx
@@ -0,0 +1,7 @@
+"use client";
+
+import { SessionProvider, SessionProviderProps } from "next-auth/react";
+
+export default function ClientSessionProvider(props: SessionProviderProps) {
+ return ;
+}
diff --git a/apps/hq/src/app/forms/layout.tsx b/apps/hq/src/app/app/layout.tsx
similarity index 77%
rename from apps/hq/src/app/forms/layout.tsx
rename to apps/hq/src/app/app/layout.tsx
index 1c78d4bdd7..33dcb20583 100644
--- a/apps/hq/src/app/forms/layout.tsx
+++ b/apps/hq/src/app/app/layout.tsx
@@ -3,25 +3,19 @@
import { Disclosure, Menu, Transition } from "@headlessui/react";
import { Bars3Icon, BellIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { signOut, useSession } from "next-auth/react";
+import Image from "next/image";
import { useRouter } from "next/navigation";
import { Fragment } from "react";
+import { Logo } from "../Logo";
+import AvatarPlaceholder from "@/images/avatar-placeholder.png";
+import Link from "next/link";
+import LoadingSpinner from "../LoadingSpinner";
-const user = {
- name: "Tom Cook",
- email: "tom@example.com",
- imageUrl:
- "https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=2&w=256&h=256&q=80",
-};
const navigation = [
- { name: "Dashboard", href: "#", current: true },
+ /* { name: "Forms", href: "#", current: true },
{ name: "Team", href: "#", current: false },
{ name: "Projects", href: "#", current: false },
- { name: "Calendar", href: "#", current: false },
-];
-const userNavigation = [
- { name: "Your Profile", onClick: () => {} },
- { name: "Settings", onClick: () => {} },
- { name: "Sign out", onClick: () => signOut() },
+ { name: "Calendar", href: "#", current: false }, */
];
function classNames(...classes) {
@@ -29,11 +23,24 @@ function classNames(...classes) {
}
export default function ProjectsLayout({ children }) {
- const { data: session, status } = useSession();
const router = useRouter();
+ const userNavigation = [
+ {
+ name: "Settings",
+ onClick: () => {
+ router.push("/app/me/settings");
+ },
+ },
+ { name: "Sign out", onClick: () => signOut() },
+ ];
+ const { data: session, status } = useSession();
if (status === "loading") {
- return
Loading
;
+ return (
+
+
+
+ );
}
if (!session) {
router.push(`/auth/signin?callbackUrl=${encodeURIComponent(window.location.href)}`);
@@ -49,16 +56,9 @@ export default function ProjectsLayout({ children }) {
-
-
+
+
+
{navigation.map((item) => (
@@ -78,19 +78,25 @@ export default function ProjectsLayout({ children }) {
-
View notifications
-
+ */}
{/* Profile dropdown */}
-
+
Open user menu
-
+
{item.name}
@@ -155,18 +161,22 @@ export default function ProjectsLayout({ children }) {
-
+
-
{user.name}
-
{user.email}
+
{session.user.name}
+
{session.user.email}
-
View notifications
-
+ */}
{userNavigation.map((item) => (
@@ -186,17 +196,8 @@ export default function ProjectsLayout({ children }) {
-
-
- {/* Replace with your content */}
-
{children}
- {/* /End replace */}
-
+ {children}
diff --git a/apps/hq/src/app/app/me/settings/page.tsx b/apps/hq/src/app/app/me/settings/page.tsx
new file mode 100644
index 0000000000..eb213534b3
--- /dev/null
+++ b/apps/hq/src/app/app/me/settings/page.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import LoadingSpinner from "@/app/LoadingSpinner";
+import Modal from "@/components/Modal";
+import { createApiKey, deleteApiKey, useApiKeys } from "@/lib/apiKeys";
+import { convertDateString, convertDateTimeString } from "@/lib/utils";
+import { Form, Submit, Text } from "@formbricks/react";
+import { Button } from "@formbricks/ui";
+import { useState } from "react";
+
+export default function MeSettingsPage() {
+ const { apiKeys, mutateApiKeys, isLoadingApiKeys } = useApiKeys();
+ const [openNewApiKeyModal, setOpenNewApiKeyModal] = useState(false);
+
+ return (
+
+
+ {/* Payment details */}
+
+
+
+
+
+
+
Personal API Keys
+
+ These keys allow full access to your personal account through the API, as if you were
+ logged in. Try not to keep disused keys around. If you have any suspicion that one of
+ these may be compromised, delete it and use a new one.
+
+
+
+ setOpenNewApiKeyModal(true)}>Add API Key
+
+
+
+
+
+
+
+
+
+
+
+ Label
+
+
+ Value
+
+
+ Last Used
+
+
+ Created
+
+
+ Edit
+
+
+
+
+ {isLoadingApiKeys ? (
+
+ ) : apiKeys.length === 0 ? (
+
+
+ You don't have any API Keys yet
+
+
+ ) : (
+ apiKeys.map((apiKey) => (
+
+
+ {apiKey.label}
+
+
+ {apiKey.apiKey || secret }
+
+
+ {convertDateTimeString(apiKey.lastUsed)}
+
+
+ {convertDateTimeString(apiKey.createdAt)}
+
+
+ {
+ if (
+ confirm(
+ "Do you really want to delete this API Key? It can no longer be used to access the API and cannot be restored."
+ )
+ ) {
+ await deleteApiKey(apiKey);
+ mutateApiKeys();
+ }
+ }}
+ className="text-brand-dark hover:text-brand">
+ Delete, {apiKey.label}
+
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+
+ {openNewApiKeyModal && (
+
+
+ Create a Personal API Key
+
+
+
+
+ )}
+
+ );
+}
diff --git a/apps/hq/src/app/app/page.tsx b/apps/hq/src/app/app/page.tsx
new file mode 100644
index 0000000000..b6418aea15
--- /dev/null
+++ b/apps/hq/src/app/app/page.tsx
@@ -0,0 +1,7 @@
+export default function ProjectsPage() {
+ return (
+
+ );
+}
diff --git a/apps/hq/src/app/auth/signin/SigninForm.tsx b/apps/hq/src/app/auth/signin/SigninForm.tsx
index d242c06d15..c149612bc4 100644
--- a/apps/hq/src/app/auth/signin/SigninForm.tsx
+++ b/apps/hq/src/app/auth/signin/SigninForm.tsx
@@ -33,11 +33,7 @@ export const SigninForm = ({ callbackUrl, error }) => {
)}
-
>
);
diff --git a/apps/hq/src/app/auth/signup/SignupForm.tsx b/apps/hq/src/app/auth/signup/SignupForm.tsx
index 51c6f75ccd..7018e84b0d 100644
--- a/apps/hq/src/app/auth/signup/SignupForm.tsx
+++ b/apps/hq/src/app/auth/signup/SignupForm.tsx
@@ -133,15 +133,19 @@ export const SignupForm = () => {
)}
-
-
+ {process.env.NEXT_PUBLIC_GITHUB_AUTH_ENABLED === "1" && (
+ <>
+
+
+ >
+ )}
>
);
diff --git a/apps/hq/src/app/auth/verify/SignIn.tsx b/apps/hq/src/app/auth/verify/SignIn.tsx
index e0156a4fa7..779a970587 100644
--- a/apps/hq/src/app/auth/verify/SignIn.tsx
+++ b/apps/hq/src/app/auth/verify/SignIn.tsx
@@ -8,7 +8,7 @@ export const SignIn = ({ token }) => {
if (token) {
signIn("token", {
token: token,
- callbackUrl: `/projects`,
+ callbackUrl: `/app`,
});
}
}, [token]);
diff --git a/apps/hq/src/app/forms/page.tsx b/apps/hq/src/app/forms/page.tsx
deleted file mode 100644
index 6f9814305b..0000000000
--- a/apps/hq/src/app/forms/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function ProjectsPage() {
- return Projects
;
-}
diff --git a/apps/hq/src/app/layout.tsx b/apps/hq/src/app/layout.tsx
index 7a324e5a91..c68ba0e3b6 100644
--- a/apps/hq/src/app/layout.tsx
+++ b/apps/hq/src/app/layout.tsx
@@ -1,16 +1,16 @@
-"use client";
-
import "@/styles/globals.css";
import "@/styles/toastify.css";
// include styles from the ui package
-import { SessionProvider } from "next-auth/react";
+import SessionProvider from "./SessionProvider";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
-
-
-
+
+
+ Formbricks HQ
+
+
{children}
diff --git a/apps/hq/src/components/Modal.tsx b/apps/hq/src/components/Modal.tsx
new file mode 100644
index 0000000000..ca9c4043ec
--- /dev/null
+++ b/apps/hq/src/components/Modal.tsx
@@ -0,0 +1,49 @@
+/* This example requires Tailwind CSS v2.0+ */
+import { Dialog, Transition } from "@headlessui/react";
+import { XMarkIcon } from "@heroicons/react/24/outline";
+import { Fragment } from "react";
+
+export default function Modal({ open, setOpen, children }) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ setOpen(false)}>
+ Close
+
+
+
+ {children}
+
+
+
+
+
+
+ );
+}
diff --git a/apps/hq/src/images/avatar-placeholder.png b/apps/hq/src/images/avatar-placeholder.png
new file mode 100644
index 0000000000..09892098aa
Binary files /dev/null and b/apps/hq/src/images/avatar-placeholder.png differ
diff --git a/apps/hq/src/lib/apiHelper.ts b/apps/hq/src/lib/apiHelper.ts
new file mode 100644
index 0000000000..7e30f09795
--- /dev/null
+++ b/apps/hq/src/lib/apiHelper.ts
@@ -0,0 +1,25 @@
+import { createHash } from "crypto";
+import { prisma } from "@formbricks/database";
+
+export const hashApiKey = (key: string): string => createHash("sha256").update(key).digest("hex");
+
+export const hasOwnership = async (model, session, id) => {
+ try {
+ const entity = await prisma[model].findUnique({
+ where: { id: id },
+ include: {
+ user: {
+ select: { email: true },
+ },
+ },
+ });
+ if (entity.user.email === session.user.email) {
+ return true;
+ } else {
+ return false;
+ }
+ } catch (e) {
+ console.error(`can't verify ownership: ${e}`);
+ return false;
+ }
+};
diff --git a/apps/hq/src/lib/apiKeys.ts b/apps/hq/src/lib/apiKeys.ts
new file mode 100644
index 0000000000..05c2da196d
--- /dev/null
+++ b/apps/hq/src/lib/apiKeys.ts
@@ -0,0 +1,62 @@
+import useSWR from "swr";
+import { fetcher } from "@/lib/utils";
+
+export const useApiKeys = () => {
+ const { data, error, mutate } = useSWR(`/api/users/me/api-keys/`, fetcher);
+
+ return {
+ apiKeys: data,
+ isLoadingApiKeys: !error && !data,
+ isErrorApiKeys: error,
+ mutateApiKeys: mutate,
+ };
+};
+
+export const useApiKey = (id: string) => {
+ const { data, error, mutate } = useSWR(`/api/users/me/api-keys/${id}/`, fetcher);
+
+ return {
+ apiKey: data,
+ isLoadingApiKey: !error && !data,
+ isErrorApiKey: error,
+ mutateApiKey: mutate,
+ };
+};
+
+export const persistApiKey = async (apiKey) => {
+ try {
+ await fetch(`/api/users/me/api-keys/${apiKey.id}/`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(apiKey),
+ });
+ } catch (error) {
+ console.error(error);
+ }
+};
+
+export const createApiKey = async (apiKey = {}) => {
+ try {
+ const res = await fetch(`/api/users/me/api-keys`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(apiKey),
+ });
+ return await res.json();
+ } catch (error) {
+ console.error(error);
+ throw Error(`createApiKey: unable to create api-key: ${error.message}`);
+ }
+};
+
+export const deleteApiKey = async (apiKey) => {
+ try {
+ const res = await fetch(`/api/users/me/api-keys/${apiKey.id}`, {
+ method: "DELETE",
+ });
+ return await res.json();
+ } catch (error) {
+ console.error(error);
+ throw Error(`deleteApiKey: unable to delete api-key: ${error.message}`);
+ }
+};
diff --git a/apps/hq/src/lib/email.ts b/apps/hq/src/lib/email.ts
index 4ec8f5ae8b..b4d9c87526 100644
--- a/apps/hq/src/lib/email.ts
+++ b/apps/hq/src/lib/email.ts
@@ -21,7 +21,7 @@ export const sendEmail = async (emailData: sendEmailData) => {
// debug: true,
});
const emailDefaults = {
- from: `Cargoship <${process.env.MAIL_FROM || "noreply@formbricks.com"}>`,
+ from: `Formbricks <${process.env.MAIL_FROM || "noreply@formbricks.com"}>`,
};
await transporter.sendMail({ ...emailDefaults, ...emailData });
};
@@ -36,14 +36,14 @@ export const sendVerificationEmail = async (user) => {
}/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
await sendEmail({
to: user.email,
- subject: "Welcome to Cargoship",
- html: `Welcome to Cargoship! To verify your email address and start using Cargoship please click this link:
+ subject: "Welcome to Formbricks",
+ html: `Welcome to Formbricks! To verify your email address and start using Formbricks please click this link:
${verifyLink}
The link is valid for one day. If it has expired please request a new token here:
${verificationRequestLink}
- Your Cargoship Team`,
+ Your Formbricks Team`,
});
};
@@ -56,7 +56,7 @@ export const sendForgotPasswordEmail = async (user) => {
)}`;
await sendEmail({
to: user.email,
- subject: "Reset your Cargoship password",
+ subject: "Reset your Formbricks password",
html: `You have requested a link to change your password. You can do this through the link below:
${verifyLink}
@@ -64,16 +64,16 @@ export const sendForgotPasswordEmail = async (user) => {
Your password won't change until you access the link above and create a new one.
- Your Cargoship Team`,
+ Your Formbricks Team`,
});
};
export const sendPasswordResetNotifyEmail = async (user) => {
await sendEmail({
to: user.email,
- subject: "Your Cargoship password has been changed",
+ subject: "Your Formbricks password has been changed",
html: `We're contacting you to notify you that your password has been changed.
- Your Cargoship Team`,
+ Your Formbricks Team`,
});
};
diff --git a/apps/hq/src/lib/session.ts b/apps/hq/src/lib/session.ts
new file mode 100644
index 0000000000..42dc8948b2
--- /dev/null
+++ b/apps/hq/src/lib/session.ts
@@ -0,0 +1,14 @@
+import { Session } from "next-auth";
+
+export async function getSession(cookie: string): Promise {
+ const response = await fetch(`${process.env.NEXTAUTH_URL}/api/auth/session`, {
+ headers: { cookie },
+ });
+
+ if (!response?.ok) {
+ return null;
+ }
+
+ const session = await response.json();
+ return Object.keys(session).length > 0 ? session : null;
+}
diff --git a/apps/hq/src/lib/token.ts b/apps/hq/src/lib/token.ts
new file mode 100644
index 0000000000..808f3a6781
--- /dev/null
+++ b/apps/hq/src/lib/token.ts
@@ -0,0 +1,15 @@
+import { cookies } from "next/headers";
+import { decode } from "next-auth/jwt";
+
+if (!process.env.NEXTAUTH_SECRET) {
+ throw new Error("NEXTAUTH_SECRET is missing");
+}
+
+export const getToken = async () => {
+ return await decode({
+ token: cookies()
+ .getAll()
+ .find((cookie) => cookie.name.includes("next-auth.session-token"))?.value,
+ secret: process.env.NEXTAUTH_SECRET as string,
+ });
+};
diff --git a/apps/hq/src/lib/utils.ts b/apps/hq/src/lib/utils.ts
new file mode 100644
index 0000000000..184c717b78
--- /dev/null
+++ b/apps/hq/src/lib/utils.ts
@@ -0,0 +1,79 @@
+import intlFormat from "date-fns/intlFormat";
+import { formatDistance } from "date-fns";
+
+export const fetcher = async (url) => {
+ const res = await fetch(url);
+
+ // If the status code is not in the range 200-299,
+ // we still try to parse and throw it.
+ if (!res.ok) {
+ const error: any = new Error("An error occurred while fetching the data.");
+ // Attach extra info to the error object.
+ error.info = await res.json();
+ error.status = res.status;
+ throw error;
+ }
+
+ return res.json();
+};
+
+export const convertDateString = (dateString: string) => {
+ if (!dateString) {
+ return dateString;
+ }
+ const date = new Date(dateString);
+ return intlFormat(
+ date,
+ {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ },
+ {
+ locale: "en",
+ }
+ );
+};
+
+export const convertDateTimeString = (dateString: string) => {
+ if (!dateString) {
+ return dateString;
+ }
+ const date = new Date(dateString);
+ return intlFormat(
+ date,
+ {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ },
+ {
+ locale: "en",
+ }
+ );
+};
+
+export const convertTimeString = (dateString: string) => {
+ const date = new Date(dateString);
+ return intlFormat(
+ date,
+ {
+ hour: "numeric",
+ minute: "2-digit",
+ second: "2-digit",
+ },
+ {
+ locale: "en",
+ }
+ );
+};
+
+export const timeSince = (dateString: string) => {
+ const date = new Date(dateString);
+ return formatDistance(date, new Date(), {
+ addSuffix: true,
+ });
+};
diff --git a/apps/hq/src/pages/api/auth/[...nextauth].ts b/apps/hq/src/pages/api/auth/[...nextauth].ts
index d6363105f4..5ecc30733c 100644
--- a/apps/hq/src/pages/api/auth/[...nextauth].ts
+++ b/apps/hq/src/pages/api/auth/[...nextauth].ts
@@ -126,13 +126,31 @@ export const authOptions: NextAuthOptions = {
}),
],
callbacks: {
- async signIn({ user, account, profile, email, credentials }: any) {
- console.log("user", JSON.stringify(account, null, 2));
- console.log("account", JSON.stringify(account, null, 2));
- console.log("profile", JSON.stringify(profile, null, 2));
- console.log(JSON.stringify(user));
+ async jwt({ token, user, account }) {
+ const existingUser = await prisma.user.findFirst({
+ where: { email: token.email! },
+ select: {
+ id: true,
+ },
+ });
- if (account.provider === "credentials") {
+ if (!existingUser) {
+ return token;
+ }
+
+ return {
+ ...existingUser,
+ ...token,
+ };
+ },
+ async session({ session, token, user }) {
+ // @ts-ignore
+ session.user.id = token.id;
+
+ return session;
+ },
+ async signIn({ user, account }: any) {
+ if (account.provider === "credentials" || account.provider === "token") {
if (!user.emailVerified && process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") {
return `/auth/verification-requested?email=${encodeURIComponent(user.email)}`;
}
@@ -206,6 +224,19 @@ export const authOptions: NextAuthOptions = {
accounts: {
create: [{ ...account }],
},
+ teams: {
+ create: [
+ {
+ accepted: true,
+ role: "OWNER",
+ team: {
+ create: {
+ name: `${user.name}'s Team`,
+ },
+ },
+ },
+ ],
+ },
},
});
diff --git a/apps/hq/src/pages/api/users/index.tsx b/apps/hq/src/pages/api/users/index.tsx
index c8e543b8d6..f1b74439f2 100644
--- a/apps/hq/src/pages/api/users/index.tsx
+++ b/apps/hq/src/pages/api/users/index.tsx
@@ -20,6 +20,19 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
const userData = await prisma.user.create({
data: {
...user,
+ teams: {
+ create: [
+ {
+ accepted: true,
+ role: "OWNER",
+ team: {
+ create: {
+ name: `${user.name}'s Team`,
+ },
+ },
+ },
+ ],
+ },
},
});
if (process.env.NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED !== "1") await sendVerificationEmail(userData);
diff --git a/apps/hq/src/pages/api/users/me/api-keys/[apiKeyId]/index.ts b/apps/hq/src/pages/api/users/me/api-keys/[apiKeyId]/index.ts
new file mode 100644
index 0000000000..d141180c86
--- /dev/null
+++ b/apps/hq/src/pages/api/users/me/api-keys/[apiKeyId]/index.ts
@@ -0,0 +1,51 @@
+import type { NextApiResponse, NextApiRequest } from "next";
+import { getSession } from "next-auth/react";
+import { hasOwnership } from "@/lib/apiHelper";
+import { prisma } from "@formbricks/database";
+import { unstable_getServerSession } from "next-auth";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ // Check Authentication
+ const session = await unstable_getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401).json({ message: "Not authenticated" });
+ }
+
+ const apiKeyId = parseInt(req.query.apiKeyId.toString());
+
+ if (isNaN(apiKeyId)) {
+ return res.status(400).json({ message: "Invalid id" });
+ }
+
+ const ownership = await hasOwnership("apiKey", session, apiKeyId);
+ if (!ownership) {
+ return res.status(401).json({ message: "You are not authorized to access this apiKey" });
+ }
+
+ // GET /api/users/me/api-keys/:apiKeyId
+ // Get apiKey with specific id
+ if (req.method === "GET") {
+ const apiKey = await prisma.apiKey.findUnique({
+ where: {
+ id: apiKeyId,
+ },
+ });
+ if (apiKey === null) return res.status(404).json({ error: "not found" });
+ return res.json(apiKey);
+ }
+ // DELETE /api/users/me/api-keys/:apiKeyId
+ // Deletes an existing apiKey
+ // Required fields in body: -
+ // Optional fields in body: -
+ else if (req.method === "DELETE") {
+ const prismaRes = await prisma.apiKey.delete({
+ where: { id: apiKeyId },
+ });
+ return res.json(prismaRes);
+ }
+ // Unknown HTTP Method
+ else {
+ throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
+ }
+}
diff --git a/apps/hq/src/pages/api/users/me/api-keys/index.tsx b/apps/hq/src/pages/api/users/me/api-keys/index.tsx
new file mode 100644
index 0000000000..ed3797435a
--- /dev/null
+++ b/apps/hq/src/pages/api/users/me/api-keys/index.tsx
@@ -0,0 +1,52 @@
+import { hashApiKey } from "@/lib/apiHelper";
+import { authOptions } from "@/pages/api/auth/[...nextauth]";
+import { prisma } from "@formbricks/database";
+import { randomBytes } from "crypto";
+import type { NextApiRequest, NextApiResponse } from "next";
+import { unstable_getServerSession } from "next-auth";
+import { getSession } from "next-auth/react";
+
+export default async function handle(req: NextApiRequest, res: NextApiResponse) {
+ // Check Authentication
+ const session = await unstable_getServerSession(req, res, authOptions);
+ if (!session) {
+ return res.status(401).json({ message: "Not authenticated" });
+ }
+
+ // GET /api/users/[userId]/api-keys/
+ // Gets all ApiKeys of a user
+ if (req.method === "GET") {
+ const session = await getSession({ req });
+ const apiKeys = await prisma.apiKey.findMany({
+ where: {
+ user: { email: session.user.email },
+ },
+ });
+ return res.json(apiKeys);
+ }
+ // POST /api/users/[userId]/api-keys/
+ // Creates a ApiKey
+ // Required fields in body: -
+ // Optional fields in body: note
+ else if (req.method === "POST") {
+ const apiKey = req.body;
+
+ const key = randomBytes(16).toString("hex");
+
+ const session = await getSession({ req });
+ // create form in database
+ const result = await prisma.apiKey.create({
+ data: {
+ ...apiKey,
+ hashedKey: hashApiKey(key),
+ user: { connect: { email: session?.user?.email } },
+ },
+ });
+ res.json({ ...result, apiKey: key });
+ }
+
+ // Unknown HTTP Method
+ else {
+ throw new Error(`The HTTP ${req.method} method is not supported by this route.`);
+ }
+}
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
index 0cd17fd099..6121c895dd 100644
--- a/docker-compose.dev.yml
+++ b/docker-compose.dev.yml
@@ -5,7 +5,7 @@ services:
volumes:
- postgres:/var/lib/postgresql/data
environment:
- - POSTGRES_DB=formbricks
+ - POSTGRES_DB=postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
diff --git a/docker-compose.yml b/docker-compose.yml
index dc87194483..f798ea34e9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,7 +10,7 @@ services:
hostr:
build:
context: .
- dockerfile: ./apps/web/Dockerfile
+ dockerfile: ./apps/hq/Dockerfile
depends_on:
- postgres
ports:
diff --git a/packages/database/prisma/migrations/20221115091649_init/migration.sql b/packages/database/prisma/migrations/20221115091649_init/migration.sql
deleted file mode 100644
index 45e82c8cbf..0000000000
--- a/packages/database/prisma/migrations/20221115091649_init/migration.sql
+++ /dev/null
@@ -1,70 +0,0 @@
--- CreateEnum
-CREATE TYPE "MembershipRole" AS ENUM ('MEMBER', 'ADMIN', 'OWNER');
-
--- CreateEnum
-CREATE TYPE "IdentityProvider" AS ENUM ('EMAIL', 'GITHUB');
-
--- CreateTable
-CREATE TABLE "Project" (
- "id" SERIAL NOT NULL,
- "teamId" INTEGER NOT NULL,
-
- CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Team" (
- "id" SERIAL NOT NULL,
- "name" TEXT NOT NULL,
-
- CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "Membership" (
- "teamId" INTEGER NOT NULL,
- "userId" INTEGER NOT NULL,
- "accepted" BOOLEAN NOT NULL DEFAULT false,
- "role" "MembershipRole" NOT NULL,
-
- CONSTRAINT "Membership_pkey" PRIMARY KEY ("userId","teamId")
-);
-
--- CreateTable
-CREATE TABLE "Account" (
- "id" TEXT NOT NULL,
- "userId" INTEGER NOT NULL,
- "type" TEXT NOT NULL,
- "provider" TEXT NOT NULL,
- "providerAccountId" TEXT NOT NULL,
- "access_token" TEXT,
- "refresh_token" TEXT,
- "expires_at" INTEGER,
- "token_type" TEXT,
- "scope" TEXT,
- "id_token" TEXT,
- "session_state" TEXT,
-
- CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "users" (
- "id" SERIAL NOT NULL,
- "name" TEXT,
- "email" TEXT NOT NULL,
- "email_verified" TIMESTAMP(3),
- "password" TEXT,
- "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updated_at" TIMESTAMP(3) NOT NULL,
- "identityProvider" "IdentityProvider" NOT NULL DEFAULT 'EMAIL',
- "identityProviderAccountId" TEXT,
-
- CONSTRAINT "users_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
diff --git a/packages/database/prisma/migrations/20221126102828_init/migration.sql b/packages/database/prisma/migrations/20221126102828_init/migration.sql
new file mode 100644
index 0000000000..32ed206861
--- /dev/null
+++ b/packages/database/prisma/migrations/20221126102828_init/migration.sql
@@ -0,0 +1,136 @@
+-- CreateEnum
+CREATE TYPE "PipelineType" AS ENUM ('WEBHOOK', 'EMAIL_NOTIFICATION');
+
+-- CreateEnum
+CREATE TYPE "MembershipRole" AS ENUM ('MEMBER', 'ADMIN', 'OWNER');
+
+-- CreateEnum
+CREATE TYPE "IdentityProvider" AS ENUM ('EMAIL', 'GITHUB');
+
+-- CreateTable
+CREATE TABLE "Pipeline" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "name" TEXT NOT NULL,
+ "type" "PipelineType" NOT NULL,
+ "formId" INTEGER NOT NULL,
+ "enabled" BOOLEAN NOT NULL DEFAULT false,
+ "config" JSONB NOT NULL DEFAULT '{}',
+
+ CONSTRAINT "Pipeline_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Customer" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "teamId" INTEGER NOT NULL,
+ "data" JSONB NOT NULL DEFAULT '{}',
+
+ CONSTRAINT "Customer_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Form" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "teamId" INTEGER NOT NULL,
+ "schema" JSONB NOT NULL DEFAULT '{}',
+
+ CONSTRAINT "Form_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Submission" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "teamId" INTEGER,
+ "formId" INTEGER NOT NULL,
+ "customerId" INTEGER NOT NULL,
+ "data" JSONB NOT NULL DEFAULT '{}',
+
+ CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Team" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "name" TEXT NOT NULL,
+
+ CONSTRAINT "Team_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Membership" (
+ "teamId" INTEGER NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "accepted" BOOLEAN NOT NULL DEFAULT false,
+ "role" "MembershipRole" NOT NULL,
+
+ CONSTRAINT "Membership_pkey" PRIMARY KEY ("userId","teamId")
+);
+
+-- CreateTable
+CREATE TABLE "ApiKey" (
+ "id" SERIAL NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "lastUsedAt" TIMESTAMP(3),
+ "label" TEXT,
+ "hashedKey" TEXT NOT NULL,
+ "userId" INTEGER NOT NULL,
+
+ CONSTRAINT "ApiKey_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Account" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "userId" INTEGER NOT NULL,
+ "type" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "providerAccountId" TEXT NOT NULL,
+ "access_token" TEXT,
+ "refresh_token" TEXT,
+ "expires_at" INTEGER,
+ "token_type" TEXT,
+ "scope" TEXT,
+ "id_token" TEXT,
+ "session_state" TEXT,
+
+ CONSTRAINT "Account_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "users" (
+ "id" SERIAL NOT NULL,
+ "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updated_at" TIMESTAMP(3) NOT NULL,
+ "name" TEXT,
+ "email" TEXT NOT NULL,
+ "email_verified" TIMESTAMP(3),
+ "password" TEXT,
+ "identityProvider" "IdentityProvider" NOT NULL DEFAULT 'EMAIL',
+ "identityProviderAccountId" TEXT,
+
+ CONSTRAINT "users_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiKey_id_key" ON "ApiKey"("id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "ApiKey_hashedKey_key" ON "ApiKey"("hashedKey");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma
index 11aa4a954b..20137a4df8 100644
--- a/packages/database/prisma/schema.prisma
+++ b/packages/database/prisma/schema.prisma
@@ -12,17 +12,66 @@ generator client {
previewFeatures = ["referentialIntegrity"]
}
-model Project {
- id Int @id @default(autoincrement())
- organization Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
- teamId Int
+enum PipelineType {
+ WEBHOOK
+ EMAIL_NOTIFICATION
+}
+
+model Pipeline {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ name String
+ type PipelineType
+ form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
+ formId Int
+ enabled Boolean @default(false)
+ config Json @default("{}")
+}
+
+model Customer {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
+ teamId Int
+ Submissions Submission[]
+ data Json @default("{}")
+}
+
+model Form {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
+ teamId Int
+ schema Json @default("{}")
+ submission Submission[]
+ Pipeline Pipeline[]
+}
+
+model Submission {
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ Team Team? @relation(fields: [teamId], references: [id])
+ teamId Int?
+ form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
+ formId Int
+ customer Customer @relation(fields: [customerId], references: [id])
+ customerId Int
+ data Json @default("{}")
}
model Team {
- id Int @id @default(autoincrement())
- name String
- members Membership[]
- projects Project[]
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ name String
+ members Membership[]
+ forms Form[]
+ Customer Customer[]
+ Submission Submission[]
}
enum MembershipRole {
@@ -42,24 +91,36 @@ model Membership {
@@id([userId, teamId])
}
+model ApiKey {
+ id Int @id @unique @default(autoincrement())
+ createdAt DateTime @default(now())
+ lastUsedAt DateTime?
+ label String?
+ hashedKey String @unique()
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ userId Int
+}
+
enum IdentityProvider {
EMAIL
GITHUB
}
model Account {
- id String @id @default(cuid())
- user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
+ id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
+ user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
type String
provider String
providerAccountId String
- access_token String? @db.Text
- refresh_token String? @db.Text
+ access_token String? @db.Text
+ refresh_token String? @db.Text
expires_at Int?
token_type String?
scope String?
- id_token String? @db.Text
+ id_token String? @db.Text
session_state String?
@@unique([provider, providerAccountId])
@@ -67,16 +128,17 @@ model Account {
model User {
id Int @id @default(autoincrement())
+ createdAt DateTime @default(now()) @map(name: "created_at")
+ updatedAt DateTime @updatedAt @map(name: "updated_at")
name String?
email String @unique
emailVerified DateTime? @map(name: "email_verified")
password String?
- createdAt DateTime @default(now()) @map(name: "created_at")
- updatedAt DateTime @updatedAt @map(name: "updated_at")
identityProvider IdentityProvider @default(EMAIL)
identityProviderAccountId String?
teams Membership[]
accounts Account[]
+ ApiKey ApiKey[]
@@map(name: "users")
}
diff --git a/packages/ui/src/Button.tsx b/packages/ui/src/Button.tsx
index 886f68f606..2599c00bf8 100644
--- a/packages/ui/src/Button.tsx
+++ b/packages/ui/src/Button.tsx
@@ -43,7 +43,7 @@ export const Button = forwardRef`, otherwise it's a ` `
const isLink = typeof props.href !== "undefined";
- const elementType = isLink ? "a" : "button";
+ const elementType = isLink ? "span" : "button";
const element = React.createElement(
elementType,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 595dbd024b..1212afbca0 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -117,6 +117,7 @@ importers:
apps/hq:
specifiers:
'@formbricks/database': workspace:*
+ '@formbricks/react': workspace:*
'@formbricks/tailwind-config': workspace:*
'@formbricks/tsconfig': workspace:*
'@formbricks/ui': workspace:*
@@ -127,6 +128,7 @@ importers:
'@types/react-dom': ^18.0.9
autoprefixer: ^10.4.13
bcryptjs: ^2.4.3
+ date-fns: ^2.29.3
eslint: ^8.28.0
eslint-config-formbricks: workspace:*
jsonwebtoken: ^8.5.1
@@ -137,13 +139,17 @@ importers:
react: ^18.2.0
react-dom: ^18.2.0
react-icons: ^4.6.0
+ react-loader-spinner: ^5.3.4
react-toastify: ^9.1.1
+ swr: ^1.3.0
typescript: ^4.9.3
dependencies:
+ '@formbricks/react': link:../../packages/react
'@formbricks/ui': link:../../packages/ui
'@headlessui/react': 1.7.4_biqbaboplfbrettd7655fr4n2y
'@heroicons/react': 2.0.13_react@18.2.0
bcryptjs: 2.4.3
+ date-fns: 2.29.3
jsonwebtoken: 8.5.1
next: 13.0.5_biqbaboplfbrettd7655fr4n2y
next-auth: 4.17.0_2xoejpawkzgot77rbv5mbik6ve
@@ -151,7 +157,9 @@ importers:
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-icons: 4.6.0_react@18.2.0
+ react-loader-spinner: 5.3.4_biqbaboplfbrettd7655fr4n2y
react-toastify: 9.1.1_biqbaboplfbrettd7655fr4n2y
+ swr: 1.3.0_react@18.2.0
devDependencies:
'@formbricks/database': link:../../packages/database
'@formbricks/tailwind-config': link:../../packages/tailwind-config
@@ -520,7 +528,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
'@babel/types': 7.20.2
- dev: true
/@babel/helper-builder-binary-assignment-operator-visitor/7.18.9:
resolution: {integrity: sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==}
@@ -1873,6 +1880,24 @@ packages:
transitivePeerDependencies:
- supports-color
+ /@babel/traverse/7.20.1_supports-color@5.5.0:
+ resolution: {integrity: sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==}
+ engines: {node: '>=6.9.0'}
+ dependencies:
+ '@babel/code-frame': 7.18.6
+ '@babel/generator': 7.20.4
+ '@babel/helper-environment-visitor': 7.18.9
+ '@babel/helper-function-name': 7.19.0
+ '@babel/helper-hoist-variables': 7.18.6
+ '@babel/helper-split-export-declaration': 7.18.6
+ '@babel/parser': 7.20.3
+ '@babel/types': 7.20.2
+ debug: 4.3.4_supports-color@5.5.0
+ globals: 11.12.0
+ transitivePeerDependencies:
+ - supports-color
+ dev: false
+
/@babel/types/7.20.2:
resolution: {integrity: sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==}
engines: {node: '>=6.9.0'}
@@ -2126,6 +2151,24 @@ packages:
- '@algolia/client-search'
dev: false
+ /@emotion/is-prop-valid/1.2.0:
+ resolution: {integrity: sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==}
+ dependencies:
+ '@emotion/memoize': 0.8.0
+ dev: false
+
+ /@emotion/memoize/0.8.0:
+ resolution: {integrity: sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==}
+ dev: false
+
+ /@emotion/stylis/0.8.5:
+ resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==}
+ dev: false
+
+ /@emotion/unitless/0.7.5:
+ resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
+ dev: false
+
/@esbuild-kit/cjs-loader/2.3.3:
resolution: {integrity: sha512-Rt4O1mXlPEDVxvjsHLgbtHVdUXYK9C1/6ThpQnt7FaXIjUOsI6qhHYMgALhNnlIMZffag44lXd6Dqgx3xALbpQ==}
dependencies:
@@ -5587,6 +5630,23 @@ packages:
- supports-color
dev: true
+ /babel-plugin-styled-components/2.0.7_styled-components@5.3.6:
+ resolution: {integrity: sha512-i7YhvPgVqRKfoQ66toiZ06jPNA3p6ierpfUuEWxNF+fV27Uv5gxBkf8KZLHUCc1nFA9j6+80pYoIpqCeyW3/bA==}
+ peerDependencies:
+ styled-components: '>= 2'
+ dependencies:
+ '@babel/helper-annotate-as-pure': 7.18.6
+ '@babel/helper-module-imports': 7.18.6
+ babel-plugin-syntax-jsx: 6.18.0
+ lodash: 4.17.21
+ picomatch: 2.3.1
+ styled-components: 5.3.6_7i5myeigehqah43i5u7wbekgba
+ dev: false
+
+ /babel-plugin-syntax-jsx/6.18.0:
+ resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
+ dev: false
+
/bail/1.0.5:
resolution: {integrity: sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==}
dev: true
@@ -6085,6 +6145,10 @@ packages:
engines: {node: '>=14.16'}
dev: true
+ /camelize/1.0.1:
+ resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==}
+ dev: false
+
/caniuse-lite/1.0.30001418:
resolution: {integrity: sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==}
@@ -6754,6 +6818,11 @@ packages:
engines: {node: '>=8'}
dev: false
+ /css-color-keywords/1.0.0:
+ resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==}
+ engines: {node: '>=4'}
+ dev: false
+
/css-loader/3.6.0_webpack@4.46.0:
resolution: {integrity: sha512-M5lSukoWi1If8dhQAUCvj4H8vUt3vOnwbQBH9DdTm/s4Ym2B/3dPMtYZeJmq7Q3S3Pa+I94DcZ7pc9bP14cWIQ==}
engines: {node: '>= 8.9.0'}
@@ -6786,6 +6855,14 @@ packages:
nth-check: 2.1.1
dev: true
+ /css-to-react-native/3.0.0:
+ resolution: {integrity: sha512-Ro1yETZA813eoyUp2GDBhG2j+YggidUmzO1/v9eYBKR2EHVEniE2MI/NqpTQ954BMpTPZFsGNPm46qFB9dpaPQ==}
+ dependencies:
+ camelize: 1.0.1
+ css-color-keywords: 1.0.0
+ postcss-value-parser: 4.2.0
+ dev: false
+
/css-what/6.1.0:
resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
engines: {node: '>= 6'}
@@ -6841,7 +6918,6 @@ packages:
/date-fns/2.29.3:
resolution: {integrity: sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==}
engines: {node: '>=0.11'}
- dev: true
/debug/2.6.9:
resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
@@ -6874,6 +6950,19 @@ packages:
dependencies:
ms: 2.1.2
+ /debug/4.3.4_supports-color@5.5.0:
+ resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.1.2
+ supports-color: 5.5.0
+ dev: false
+
/decamelize-keys/1.1.0:
resolution: {integrity: sha512-ocLWuYzRPoS9bfiSdDd3cxvrzovVMZnRDVEzAs+hWIVXGDbHxWMECij2OBuyB/An0FFW/nLuq6Kv1i/YC5Qfzg==}
engines: {node: '>=0.10.0'}
@@ -9296,6 +9385,12 @@ packages:
minimalistic-crypto-utils: 1.0.1
dev: true
+ /hoist-non-react-statics/3.3.2:
+ resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
+ dependencies:
+ react-is: 16.13.1
+ dev: false
+
/homedir-polyfill/1.0.3:
resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==}
engines: {node: '>=0.10.0'}
@@ -12727,7 +12822,6 @@ packages:
/postcss-value-parser/4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- dev: true
/postcss/7.0.39:
resolution: {integrity: sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==}
@@ -13200,6 +13294,23 @@ packages:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
dev: true
+ /react-is/18.2.0:
+ resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
+ dev: false
+
+ /react-loader-spinner/5.3.4_biqbaboplfbrettd7655fr4n2y:
+ resolution: {integrity: sha512-G2vw4ssX+RDZ/vfaeva06yfNqyFViv/u+tVZ3kFLy5TKNlNx2DbuwreBSpRtPespQA+VxinxUJsigwLwG9erOg==}
+ peerDependencies:
+ react: ^16.0.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ react-is: 18.2.0
+ styled-components: 5.3.6_7i5myeigehqah43i5u7wbekgba
+ styled-tools: 1.7.2
+ dev: false
+
/react-refresh/0.11.0:
resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==}
engines: {node: '>=0.10.0'}
@@ -13989,6 +14100,10 @@ packages:
kind-of: 6.0.3
dev: true
+ /shallowequal/1.1.0:
+ resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==}
+ dev: false
+
/shebang-command/1.2.0:
resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==}
engines: {node: '>=0.10.0'}
@@ -14475,6 +14590,30 @@ packages:
dependencies:
inline-style-parser: 0.1.1
+ /styled-components/5.3.6_7i5myeigehqah43i5u7wbekgba:
+ resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==}
+ engines: {node: '>=10'}
+ requiresBuild: true
+ peerDependencies:
+ react: '>= 16.8.0'
+ react-dom: '>= 16.8.0'
+ react-is: '>= 16.8.0'
+ dependencies:
+ '@babel/helper-module-imports': 7.18.6
+ '@babel/traverse': 7.20.1_supports-color@5.5.0
+ '@emotion/is-prop-valid': 1.2.0
+ '@emotion/stylis': 0.8.5
+ '@emotion/unitless': 0.7.5
+ babel-plugin-styled-components: 2.0.7_styled-components@5.3.6
+ css-to-react-native: 3.0.0
+ hoist-non-react-statics: 3.3.2
+ react: 18.2.0
+ react-dom: 18.2.0_react@18.2.0
+ react-is: 18.2.0
+ shallowequal: 1.1.0
+ supports-color: 5.5.0
+ dev: false
+
/styled-jsx/5.1.0_3lzqd2prgnu7gkxqqdmtvzna5u:
resolution: {integrity: sha512-/iHaRJt9U7T+5tp6TRelLnqBqiaIT0HsO0+vgyj8hK2KUk7aejFqRrumqPUlAqDwAj8IbS/1hk3IhBAAK/FCUQ==}
engines: {node: '>= 12.0.0'}
@@ -14510,6 +14649,10 @@ packages:
react: 18.2.0
dev: false
+ /styled-tools/1.7.2:
+ resolution: {integrity: sha512-IjLxzM20RMwAsx8M1QoRlCG/Kmq8lKzCGyospjtSXt/BTIIcvgTonaxQAsKnBrsZNwhpHzO9ADx5te0h76ILVg==}
+ dev: false
+
/sucrase/3.28.0:
resolution: {integrity: sha512-TK9600YInjuiIhVM3729rH4ZKPOsGeyXUwY+Ugu9eilNbdTFyHr6XcAGYbRVZPDgWj6tgI7bx95aaJjHnbffag==}
engines: {node: '>=8'}
@@ -14561,6 +14704,14 @@ packages:
- supports-color
dev: true
+ /swr/1.3.0_react@18.2.0:
+ resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==}
+ peerDependencies:
+ react: ^16.11.0 || ^17.0.0 || ^18.0.0
+ dependencies:
+ react: 18.2.0
+ dev: false
+
/symbol.prototype.description/1.0.5:
resolution: {integrity: sha512-x738iXRYsrAt9WBhRCVG5BtIC3B7CUkFwbHW2zOvGtwM33s7JjrCDyq8V0zgMYVb5ymsL8+qkzzpANH63CPQaQ==}
engines: {node: '>= 0.11.15'}
diff --git a/turbo.json b/turbo.json
index a5e6efbcbd..6f1acc4ed8 100644
--- a/turbo.json
+++ b/turbo.json
@@ -9,6 +9,7 @@
"GITHUB_SECRET",
"MAIL_FROM",
"NEXT_PUBLIC_EMAIL_VERIFICATION_DISABLED",
+ "NEXT_PUBLIC_GITHUB_AUTH_ENABLED",
"NEXT_PUBLIC_PASSWORD_RESET_DISABLED",
"NEXT_PUBLIC_PRIVACY_URL",
"NEXT_PUBLIC_SIGNUP_DISABLED",