Merge branch 'api-keys'

This commit is contained in:
Johannes
2023-04-05 17:40:52 +02:00
33 changed files with 484 additions and 255 deletions

View File

@@ -10,6 +10,7 @@ import {
PaintBrushIcon,
UserCircleIcon,
UsersIcon,
KeyIcon,
} from "@heroicons/react/24/solid";
import clsx from "clsx";
import Link from "next/link";
@@ -54,6 +55,13 @@ export default function SettingsNavbar({ environmentId }: { environmentId: strin
current: pathname?.includes("/lookandfeel"),
hidden: false,
},
{
name: "API Keys",
href: `/environments/${environmentId}/settings/api-keys`,
icon: KeyIcon,
current: pathname?.includes("/api-keys"),
hidden: false,
},
],
},
{

View File

@@ -0,0 +1,70 @@
"use client";
import Modal from "@/components/shared/Modal";
import { Button, Input, Label } from "@formbricks/ui";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import { useForm } from "react-hook-form";
interface MemberModalProps {
open: boolean;
setOpen: (v: boolean) => void;
onSubmit: (data: { label: string; environment: string }) => void;
}
export default function AddMemberModal({ open, setOpen, onSubmit }: MemberModalProps) {
const { register, getValues, handleSubmit, reset } = useForm<{ label: string; environment: string }>();
const submitAPIKey = async () => {
const data = getValues();
onSubmit(data);
setOpen(false);
reset();
};
return (
<Modal open={open} setOpen={setOpen} noPadding closeOnOutsideClick={false}>
<div className="flex h-full flex-col rounded-lg">
<div className="rounded-t-lg bg-slate-100">
<div className="flex items-center justify-between p-6">
<div className="flex items-center space-x-2">
<div className="text-xl font-medium text-slate-700">Add API Key</div>
</div>
</div>
</div>
<form onSubmit={handleSubmit(submitAPIKey)}>
<div className="flex justify-between rounded-lg p-6">
<div className="w-full space-y-4">
<div>
<Label>API Key Label</Label>
<Input placeholder="e.g. GitHub, PostHog, Slack" {...register("label", { required: true })} />
</div>
<div className="flex items-center rounded-lg border border-slate-200 bg-slate-100 p-2 text-sm text-slate-700">
<ExclamationTriangleIcon className="mx-3 h-12 w-12 text-amber-500" />
<p>
For security reasons, the API key will only be <strong>shown once</strong> after creation.
Please copy it to your destination right away.
</p>
</div>
</div>
</div>
<div className="flex justify-end border-t border-slate-200 p-6">
<div className="flex space-x-2">
<Button
type="button"
variant="minimal"
onClick={() => {
setOpen(false);
}}>
Cancel
</Button>
<Button variant="primary" type="submit">
Add API Key
</Button>
</div>
</div>
</form>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,39 @@
"use client";
import { ErrorComponent } from "@formbricks/ui";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { useProduct } from "@/lib/products/products";
import EditApiKeys from "./EditApiKeys";
export default function ApiKeyList({
environmentId,
environmentType,
}: {
environmentId: string;
environmentType: string;
}) {
const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId);
const findEnvironmentByType = (environments, targetType) => {
for (const environment of environments) {
if (environment.type === targetType) {
return environment.id;
}
}
return null;
};
if (isLoadingProduct) {
return <LoadingSpinner />;
}
if (isErrorProduct) {
<ErrorComponent />;
}
const environmentTypeId = findEnvironmentByType(product?.environments, environmentType);
console.log(environmentTypeId);
return <EditApiKeys environmentTypeId={environmentTypeId} environmentType={environmentType} />;
}

View File

@@ -0,0 +1,113 @@
"use client";
import { timeSince } from "@/../../packages/lib/time";
import DeleteDialog from "@/components/shared/DeleteDialog";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { createApiKey, deleteApiKey, useApiKeys } from "@/lib/apiKeys";
import { capitalizeFirstLetter } from "@/lib/utils";
import { Button, ErrorComponent } from "@formbricks/ui";
import { TrashIcon } from "@heroicons/react/24/outline";
import { useState } from "react";
import toast from "react-hot-toast";
import AddAPIKeyModal from "./AddApiKeyModal";
export default function EditAPIKeys({
environmentTypeId,
environmentType,
}: {
environmentTypeId: string;
environmentType: string;
}) {
const { apiKeys, mutateApiKeys, isLoadingApiKeys, isErrorApiKeys } = useApiKeys(environmentTypeId);
const [isAddAPIKeyModalOpen, setOpenAddAPIKeyModal] = useState(false);
const [isDeleteKeyModalOpen, setOpenDeleteKeyModal] = useState(false);
const [activeKey, setActiveKey] = useState({} as any);
const handleOpenDeleteKeyModal = (e, apiKey) => {
e.preventDefault();
setActiveKey(apiKey);
setOpenDeleteKeyModal(true);
};
const handleDeleteKey = async () => {
await deleteApiKey(environmentTypeId, activeKey);
mutateApiKeys();
setOpenDeleteKeyModal(false);
toast.success("API Key deleted");
};
const handleAddAPIKey = async (data) => {
console.log(data);
const apiKey = await createApiKey(environmentTypeId, { label: data.label });
mutateApiKeys([...JSON.parse(JSON.stringify(apiKeys)), apiKey], false);
setOpenAddAPIKeyModal(false);
};
if (isLoadingApiKeys) {
return <LoadingSpinner />;
}
if (isErrorApiKeys) {
<ErrorComponent />;
}
return (
<>
<div className="mb-6 text-right">
<Button
variant="primary"
onClick={() => {
setOpenAddAPIKeyModal(true);
}}>
{`Add ${capitalizeFirstLetter(environmentType)} API Key`}
</Button>
</div>
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-9 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-2">Label</div>
<div className="col-span-2">API Key</div>
<div className="col-span-2">Last used</div>
<div className="col-span-2">Created at</div>
<div className=""></div>
</div>
<div className="grid-cols-9">
{apiKeys.length === 0 ? (
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400 ">
You don&apos;t have any API keys yet
</div>
) : (
apiKeys.map((apiKey) => (
<div
className="grid h-12 w-full grid-cols-9 content-center rounded-lg px-6 text-left text-sm text-slate-900"
key={apiKey.hashedKey}>
<div className="col-span-2 font-semibold">{apiKey.label}</div>
<div className="col-span-2">{apiKey.apiKey || <span className="italic">secret</span>}</div>
<div className="col-span-2">{apiKey.lastUsed && timeSince(apiKey.lastUsed)}</div>
<div className="col-span-2">{timeSince(apiKey.createdAt)}</div>
<div className="col-span-1 text-center">
<button onClick={(e) => handleOpenDeleteKeyModal(e, apiKey)}>
<TrashIcon className="h-5 w-5 text-slate-700 hover:text-slate-500" />
</button>
</div>
</div>
))
)}
</div>
</div>
<AddAPIKeyModal
open={isAddAPIKeyModalOpen}
setOpen={setOpenAddAPIKeyModal}
onSubmit={handleAddAPIKey}
/>
<DeleteDialog
open={isDeleteKeyModalOpen}
setOpen={setOpenDeleteKeyModal}
deleteWhat="API Key"
onDelete={handleDeleteKey}
/>
</>
);
}

View File

@@ -0,0 +1,21 @@
import SettingsCard from "../SettingsCard";
import SettingsTitle from "../SettingsTitle";
import ApiKeyList from "./ApiKeyList";
export default async function ProfileSettingsPage({ params }) {
return (
<div>
<SettingsTitle title="API Keys" />
<SettingsCard
title="Development Env Keys"
description="Add and remove API keys for your Development environment.">
<ApiKeyList environmentId={params.environmentId} environmentType="development" />
</SettingsCard>
<SettingsCard
title="Production Env Keys"
description="Add and remove API keys for your Production environment.">
<ApiKeyList environmentId={params.environmentId} environmentType="production" />
</SettingsCard>
</div>
);
}

View File

@@ -83,7 +83,7 @@ export function EditMemberships({ environmentId }) {
className="grid h-12 w-full grid-cols-7 content-center rounded-lg p-0.5 py-2 text-left text-sm text-slate-900"
key={member.email}>
<div className="h-58 px-6 ">
<ProfileAvatar userId={member.userId} />
<ProfileAvatar userId={member.userId || member.email} />
</div>
<div className="col-span-2 flex flex-col justify-center">
<p>{member.name}</p>

View File

@@ -6,28 +6,26 @@ import { getServerSession } from "next-auth";
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.email) {
return true;
} else {
export const hasEnvironmentAccess = async (req, res, environmentId) => {
if (req.headers["x-api-key"]) {
const ownership = await hasApiEnvironmentAccess(req.headers["x-api-key"].toString(), environmentId);
if (!ownership) {
return false;
}
} else {
const user = await getSessionUser(req, res);
if (!user) {
return false;
}
const ownership = await hasUserEnvironmentAccess(user, environmentId);
if (!ownership) {
return false;
}
} catch (e) {
console.error(`can't verify ownership: ${e}`);
return false;
}
return true;
};
export const hasEnvironmentAccess = async (user, environmentId) => {
export const hasUserEnvironmentAccess = async (user, environmentId) => {
const environment = await prisma.environment.findUnique({
where: {
id: environmentId,
@@ -55,6 +53,23 @@ export const hasEnvironmentAccess = async (user, environmentId) => {
return false;
};
export const hasApiEnvironmentAccess = async (apiKey, environmentId) => {
// write function to check if the API Key has access to the environment
const apiKeyData = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(apiKey),
},
select: {
environmentId: true,
},
});
if (apiKeyData?.environmentId === environmentId) {
return true;
}
return false;
};
export const hasTeamAccess = async (user, teamId) => {
const membership = await prisma.membership.findUnique({
where: {
@@ -70,22 +85,8 @@ export const hasTeamAccess = async (user, teamId) => {
return false;
};
export const getSessionOrUser = async (req: NextApiRequest, res: NextApiResponse) => {
export const getSessionUser = async (req: NextApiRequest, res: NextApiResponse) => {
// check for session (browser usage)
let session: any = await getServerSession(req, res, authOptions);
if (session && "user" in session) return session.user;
// check for api key
if (req.headers["x-api-key"]) {
const apiKey = await prisma.apiKey.findUnique({
where: {
hashedKey: hashApiKey(req.headers["x-api-key"].toString()),
},
include: {
user: true,
},
});
if (apiKey && apiKey.user) {
return apiKey.user;
}
}
};

62
apps/web/lib/apiKeys.ts Normal file
View File

@@ -0,0 +1,62 @@
import useSWR from "swr";
import { fetcher } from "@formbricks/lib/fetcher";
export const useApiKeys = (environmentId: string) => {
const { data, error, mutate } = useSWR(`/api/v1/environments/${environmentId}/api-keys`, fetcher);
return {
apiKeys: data,
isLoadingApiKeys: !error && !data,
isErrorApiKeys: error,
mutateApiKeys: mutate,
};
};
export const useApiKey = (environmentId: string, id: string) => {
const { data, error, mutate } = useSWR(`/api/v1/environments/${environmentId}/api-keys/${id}`, fetcher);
return {
apiKey: data,
isLoadingApiKey: !error && !data,
isErrorApiKey: error,
mutateApiKey: mutate,
};
};
export const persistApiKey = async (environmentId: string, apiKey) => {
try {
await fetch(`/api/v1/environments/${environmentId}/api-keys/${apiKey.id}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(apiKey),
});
} catch (error) {
console.error(error);
}
};
export const createApiKey = async (environmentId: string, apiKey = {}) => {
try {
const res = await fetch(`/api/v1/environments/${environmentId}/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 (environmentId: string, apiKey) => {
try {
const res = await fetch(`/api/v1/environments/${environmentId}/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}`);
}
};

View File

@@ -0,0 +1,32 @@
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const environmentId = req.query.environmentId?.toString();
const apiKeyId = req.query.apiKeyId?.toString();
if (!apiKeyId || !environmentId) {
return res.status(400).json({ message: "Missing apiKeyId or environmentId" });
}
if (!(await hasEnvironmentAccess(req, res, environmentId))) {
return res.status(401).json({ message: "Not authenticated" });
}
// DELETE /api/environments/:environmentId/api-keys/:apiKeyId
// Deletes an existing API Key
// Required fields in body: environmentId, apiKeyId
// Optional fields in body: -
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.`);
}
}

View File

@@ -1,39 +1,42 @@
import { getSessionOrUser, hashApiKey } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess, hashApiKey } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import { randomBytes } from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const session = await getSessionOrUser(req, res);
if (!session) {
const environmentId = req.query.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
if (!(await hasEnvironmentAccess(req, res, environmentId))) {
return res.status(401).json({ message: "Not authenticated" });
}
// GET /api/users/[userId]/api-keys/
// Gets all ApiKeys of a user
// GET /api/environments/[environmentId]/api-keys/
// Gets all ApiKeys of an environment
if (req.method === "GET") {
const apiKeys = await prisma.apiKey.findMany({
where: {
user: { email: session.email },
environmentId,
},
});
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;
// POST /api/environments/:environmentId/api-keys
// Creates an API Key
// Optional fields in body: label
if (req.method === "POST") {
const apiKey = req.body;
const key = randomBytes(16).toString("hex");
// create form in database
// Create API Key in the database
const result = await prisma.apiKey.create({
data: {
...apiKey,
hashedKey: hashApiKey(key),
user: { connect: { email: session?.email } },
environment: { connect: { id: environmentId } },
},
});
res.json({ ...result, apiKey: key });

View File

@@ -1,14 +1,8 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
if (environmentId === undefined) {
return res.status(400).json({ message: "Missing environmentId" });
@@ -19,8 +13,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Missing attributeClassId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,23 +1,16 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
return res.status(403).json({ message: "Not authorized" });
if (!(await hasEnvironmentAccess(req, res, environmentId))) {
return res.status(401).json({ message: "Not authenticated" });
}
// GET

View File

@@ -1,14 +1,8 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
const eventClassId = req.query.eventClassId?.toString();
@@ -20,8 +14,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Missing eventClassId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,23 +1,16 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
return res.status(403).json({ message: "Not authorized" });
if (!(await hasEnvironmentAccess(req, res, environmentId))) {
return res.status(401).json({ message: "Not authenticated" });
}
// GET

View File

@@ -1,22 +1,16 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,18 +1,12 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query?.environmentId?.toString();
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}
@@ -36,21 +30,6 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
if (environment === null) {
return res.status(404).json({ message: "This environment doesn't exist" });
}
// check if membership exists
const membership = await prisma.membership.findUnique({
where: {
userId_teamId: {
userId: user.id,
teamId: environment.product.teamId,
},
},
});
if (membership === null) {
return res
.status(403)
.json({ message: "You don't have access to this organisation or this organisation doesn't exist" });
}
return res.json(environment);
}

View File

@@ -1,10 +1,10 @@
import { getSessionOrUser } from "@/lib/api/apiHelper";
import { getSessionUser } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
const user: any = await getSessionUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}

View File

@@ -1,14 +1,8 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
const personId = req.query.personId?.toString();
@@ -20,8 +14,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Missing personId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,22 +1,16 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,21 +1,14 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query?.environmentId?.toString();
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}
// GET
if (req.method === "GET") {
const environment = await prisma.environment.findUnique({
@@ -33,6 +26,14 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
where: {
id: environment.productId,
},
include: {
environments: {
select: {
id: true,
type: true,
},
},
},
});
if (product === null) {

View File

@@ -1,14 +1,8 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
const surveyId = req.query.surveyId?.toString();
@@ -20,8 +14,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Missing surveyId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,20 +1,14 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
const surveyId = req.query.surveyId?.toString();
const responseId = req.query.responseId?.toString();
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,14 +1,8 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
const surveyId = req.query.surveyId?.toString();
@@ -19,8 +13,8 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
return res.status(400).json({ message: "Missing surveyId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,23 +1,17 @@
import { getSessionOrUser, hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { hasEnvironmentAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import { captureTelemetry } from "@formbricks/lib/telemetry";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}
const environmentId = req.query.environmentId?.toString();
if (!environmentId) {
return res.status(400).json({ message: "Missing environmentId" });
}
const hasAccess = await hasEnvironmentAccess(user, environmentId);
if (hasAccess === false) {
const hasAccess = await hasEnvironmentAccess(req, res, environmentId);
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,4 +1,4 @@
import { getSessionOrUser } from "@/lib/api/apiHelper";
import { getSessionUser } from "@/lib/api/apiHelper";
import { populateEnvironment } from "@/lib/populate";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
@@ -6,7 +6,7 @@ import { EnvironmentType } from "@prisma/client";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
const user: any = await getSessionUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}

View File

@@ -1,10 +1,10 @@
import { getSessionOrUser } from "@/lib/api/apiHelper";
import { getSessionUser } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const user: any = await getSessionOrUser(req, res);
const user: any = await getSessionUser(req, res);
if (!user) {
return res.status(401).json({ message: "Not authenticated" });
}

View File

@@ -1,11 +1,11 @@
import { getSessionOrUser } from "@/lib/api/apiHelper";
import { getSessionUser } from "@/lib/api/apiHelper";
import { sendInviteMemberEmail } from "@/lib/email";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const currentUser: any = await getSessionOrUser(req, res);
const currentUser: any = await getSessionUser(req, res);
if (!currentUser) {
return res.status(401).json({ message: "Not authenticated" });
}

View File

@@ -1,11 +1,11 @@
import { getSessionOrUser, hasTeamAccess } from "@/lib/api/apiHelper";
import { getSessionUser, hasTeamAccess } from "@/lib/api/apiHelper";
import { sendInviteMemberEmail } from "@/lib/email";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const currentUser: any = await getSessionOrUser(req, res);
const currentUser: any = await getSessionUser(req, res);
if (!currentUser) {
return res.status(401).json({ message: "Not authenticated" });
}
@@ -16,7 +16,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
}
const hasAccess = await hasTeamAccess(currentUser, teamId);
if (hasAccess === false) {
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}
// TODO check if User is ADMIN or OWNER

View File

@@ -1,10 +1,10 @@
import { getSessionOrUser, hasTeamAccess } from "@/lib/api/apiHelper";
import { getSessionUser, hasTeamAccess } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const currentUser: any = await getSessionOrUser(req, res);
const currentUser: any = await getSessionUser(req, res);
if (!currentUser) {
return res.status(401).json({ message: "Not authenticated" });
}
@@ -15,7 +15,7 @@ export default async function handle(req: NextApiRequest, res: NextApiResponse)
}
const hasAccess = await hasTeamAccess(currentUser, teamId);
if (hasAccess === false) {
if (!hasAccess) {
return res.status(403).json({ message: "Not authorized" });
}

View File

@@ -1,48 +0,0 @@
import { getSessionOrUser, hasOwnership } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const session = await getSessionOrUser(req, res);
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}
const apiKeyId = req.query.apiKeyId?.toString();
if (!apiKeyId) {
return res.status(400).json({ message: "Missing apiKeyId" });
}
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.`);
}
}

View File

@@ -1,10 +1,10 @@
import { getSessionOrUser } from "@/lib/api/apiHelper";
import { getSessionUser } from "@/lib/api/apiHelper";
import { prisma } from "@formbricks/database";
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
// Check Authentication
const session = await getSessionOrUser(req, res);
const session = await getSessionUser(req, res);
if (!session) {
return res.status(401).json({ message: "Not authenticated" });
}

View File

@@ -0,0 +1,16 @@
/*
Warnings:
- You are about to drop the column `userId` on the `ApiKey` table. All the data in the column will be lost.
- Added the required column `environmentId` to the `ApiKey` table without a default value. This is not possible if the table is not empty.
*/
-- DropForeignKey
ALTER TABLE "ApiKey" DROP CONSTRAINT "ApiKey_userId_fkey";
-- AlterTable
ALTER TABLE "ApiKey" DROP COLUMN "userId",
ADD COLUMN "environmentId" TEXT NOT NULL;
-- AddForeignKey
ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "Environment"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -196,6 +196,7 @@ model Environment {
people Person[]
eventClasses EventClass[]
attributeClasses AttributeClass[]
apiKeys ApiKey[]
}
model Product {
@@ -265,13 +266,13 @@ model Invite {
}
model ApiKey {
id String @id @unique @default(cuid())
createdAt DateTime @default(now())
lastUsedAt DateTime?
label String?
hashedKey String @unique()
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
id String @id @unique @default(cuid())
createdAt DateTime @default(now())
lastUsedAt DateTime?
label String?
hashedKey String @unique()
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
environmentId String
}
enum IdentityProvider {
@@ -311,7 +312,6 @@ model User {
identityProviderAccountId String?
memberships Membership[]
accounts Account[]
apiKeys ApiKey[]
groupId String?
invitesCreated Invite[] @relation("inviteCreatedBy")
invitesAccepted Invite[] @relation("inviteAcceptedBy")