finalizing team support
This commit is contained in:
parent
b09de5a8af
commit
665019dc59
|
@ -3,6 +3,7 @@ import Button from "../ui/Button";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import { useDeleteUser } from "@/hooks/store/admin/users";
|
import { useDeleteUser } from "@/hooks/store/admin/users";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onClose: Function;
|
onClose: Function;
|
||||||
|
@ -30,25 +31,33 @@ export default function DeleteUserModal({ onClose, userId }: Props) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { data } = useSession();
|
||||||
|
const isAdmin = data?.user?.id === Number(process.env.NEXT_PUBLIC_ADMIN);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal toggleModal={onClose}>
|
<Modal toggleModal={onClose}>
|
||||||
<p className="text-xl font-thin text-red-500">{t("delete_user")}</p>
|
<p className="text-xl font-thin text-red-500">
|
||||||
|
{isAdmin ? t("delete_user") : t("remove_user")}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="divider mb-3 mt-1"></div>
|
<div className="divider mb-3 mt-1"></div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<p>{t("confirm_user_deletion")}</p>
|
<p>{t("confirm_user_deletion")}</p>
|
||||||
|
<p>{t("confirm_user_removal_desc")}</p>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
<div role="alert" className="alert alert-warning">
|
<div role="alert" className="alert alert-warning">
|
||||||
<i className="bi-exclamation-triangle text-xl" />
|
<i className="bi-exclamation-triangle text-xl" />
|
||||||
<span>
|
<span>
|
||||||
<b>{t("warning")}:</b> {t("irreversible_action_warning")}
|
<b>{t("warning")}:</b> {t("irreversible_action_warning")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button className="ml-auto" intent="destructive" onClick={submit}>
|
<Button className="ml-auto" intent="destructive" onClick={submit}>
|
||||||
<i className="bi-trash text-xl" />
|
<i className="bi-trash text-xl" />
|
||||||
{t("delete_confirmation")}
|
{isAdmin ? t("delete_confirmation") : t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
|
@ -150,12 +150,12 @@ export default function EditCollectionSharingModal({
|
||||||
<TextInput
|
<TextInput
|
||||||
value={memberUsername || ""}
|
value={memberUsername || ""}
|
||||||
className="bg-base-200"
|
className="bg-base-200"
|
||||||
placeholder={t("members_username_placeholder")}
|
placeholder={t("add_member_placeholder")}
|
||||||
onChange={(e) => setMemberUsername(e.target.value)}
|
onChange={(e) => setMemberUsername(e.target.value)}
|
||||||
onKeyDown={(e) =>
|
onKeyDown={(e) =>
|
||||||
e.key === "Enter" &&
|
e.key === "Enter" &&
|
||||||
addMemberToCollection(
|
addMemberToCollection(
|
||||||
user.username as string,
|
user,
|
||||||
memberUsername || "",
|
memberUsername || "",
|
||||||
collection,
|
collection,
|
||||||
setMemberState,
|
setMemberState,
|
||||||
|
@ -167,7 +167,7 @@ export default function EditCollectionSharingModal({
|
||||||
<div
|
<div
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
addMemberToCollection(
|
addMemberToCollection(
|
||||||
user.username as string,
|
user,
|
||||||
memberUsername || "",
|
memberUsername || "",
|
||||||
collection,
|
collection,
|
||||||
setMemberState,
|
setMemberState,
|
||||||
|
|
|
@ -49,13 +49,13 @@ export default function InviteModal({ onClose }: Props) {
|
||||||
await addUser.mutateAsync(form, {
|
await addUser.mutateAsync(form, {
|
||||||
onSettled: () => {
|
onSettled: () => {
|
||||||
setSubmitLoader(false);
|
setSubmitLoader(false);
|
||||||
signIn("invite", {
|
},
|
||||||
|
onSuccess: async () => {
|
||||||
|
await signIn("invite", {
|
||||||
email: form.email,
|
email: form.email,
|
||||||
callbackUrl: "/member-onboarding",
|
callbackUrl: "/member-onboarding",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
});
|
});
|
||||||
},
|
|
||||||
onSuccess: () => {
|
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,7 +12,6 @@ export default function ProfileDropdown() {
|
||||||
const { data: user = {} } = useUser();
|
const { data: user = {} } = useUser();
|
||||||
|
|
||||||
const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
const isAdmin = user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
||||||
const DISABLE_INVITES = process.env.DISABLE_INVITES === "true";
|
|
||||||
|
|
||||||
const handleToggle = () => {
|
const handleToggle = () => {
|
||||||
const newTheme = settings.theme === "dark" ? "light" : "dark";
|
const newTheme = settings.theme === "dark" ? "light" : "dark";
|
||||||
|
@ -74,16 +73,16 @@ export default function ProfileDropdown() {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{!DISABLE_INVITES && (
|
{!user.parentSubscriptionId && (
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/team"
|
href="/settings/billing"
|
||||||
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
|
onClick={() => (document?.activeElement as HTMLElement)?.blur()}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
role="button"
|
role="button"
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{t("manage_team")}
|
{t("invite_users")}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
import clsx from "clsx";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Divider({ className }: Props) {
|
||||||
|
return <hr className={clsx("border-neutral-content border-t", className)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Divider;
|
|
@ -44,7 +44,6 @@ export default function AuthRedirect({ children }: Props) {
|
||||||
{ path: "/tags", isProtected: true },
|
{ path: "/tags", isProtected: true },
|
||||||
{ path: "/preserved", isProtected: true },
|
{ path: "/preserved", isProtected: true },
|
||||||
{ path: "/admin", isProtected: true },
|
{ path: "/admin", isProtected: true },
|
||||||
{ path: "/team", isProtected: true },
|
|
||||||
{ path: "/search", isProtected: true },
|
{ path: "/search", isProtected: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,12 @@ export default async function updateCollection(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uniqueMembers = data.members.filter(
|
||||||
|
(e, i, a) =>
|
||||||
|
a.findIndex((el) => el.userId === e.userId) === i &&
|
||||||
|
e.userId !== collectionIsAccessible.ownerId
|
||||||
|
);
|
||||||
|
|
||||||
const updatedCollection = await prisma.$transaction(async () => {
|
const updatedCollection = await prisma.$transaction(async () => {
|
||||||
await prisma.usersAndCollections.deleteMany({
|
await prisma.usersAndCollections.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -91,7 +97,7 @@ export default async function updateCollection(
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
members: {
|
members: {
|
||||||
create: data.members.map((e) => ({
|
create: uniqueMembers.map((e) => ({
|
||||||
user: { connect: { id: e.userId } },
|
user: { connect: { id: e.userId } },
|
||||||
canCreate: e.canCreate,
|
canCreate: e.canCreate,
|
||||||
canUpdate: e.canUpdate,
|
canUpdate: e.canUpdate,
|
||||||
|
|
|
@ -5,14 +5,21 @@ export default async function getPublicUser(
|
||||||
isId: boolean,
|
isId: boolean,
|
||||||
requestingId?: number
|
requestingId?: number
|
||||||
) {
|
) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findFirst({
|
||||||
where: isId
|
where: isId
|
||||||
? {
|
? {
|
||||||
id: Number(targetId) as number,
|
id: Number(targetId) as number,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
username: targetId as string,
|
username: targetId as string,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
email: targetId as string,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
include: {
|
include: {
|
||||||
whitelistedUsers: {
|
whitelistedUsers: {
|
||||||
select: {
|
select: {
|
||||||
|
@ -22,7 +29,7 @@ export default async function getPublicUser(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user)
|
if (!user || !user.id)
|
||||||
return { response: "User not found or profile is private.", status: 404 };
|
return { response: "User not found or profile is private.", status: 404 };
|
||||||
|
|
||||||
const whitelistedUsernames = user.whitelistedUsers?.map(
|
const whitelistedUsernames = user.whitelistedUsers?.map(
|
||||||
|
@ -31,7 +38,7 @@ export default async function getPublicUser(
|
||||||
|
|
||||||
const isInAPublicCollection = await prisma.collection.findFirst({
|
const isInAPublicCollection = await prisma.collection.findFirst({
|
||||||
where: {
|
where: {
|
||||||
["OR"]: [
|
OR: [
|
||||||
{ ownerId: user.id },
|
{ ownerId: user.id },
|
||||||
{
|
{
|
||||||
members: {
|
members: {
|
||||||
|
@ -73,6 +80,7 @@ export default async function getPublicUser(
|
||||||
id: lessSensitiveInfo.id,
|
id: lessSensitiveInfo.id,
|
||||||
name: lessSensitiveInfo.name,
|
name: lessSensitiveInfo.name,
|
||||||
username: lessSensitiveInfo.username,
|
username: lessSensitiveInfo.username,
|
||||||
|
email: lessSensitiveInfo.email,
|
||||||
image: lessSensitiveInfo.image,
|
image: lessSensitiveInfo.image,
|
||||||
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
archiveAsScreenshot: lessSensitiveInfo.archiveAsScreenshot,
|
||||||
archiveAsMonolith: lessSensitiveInfo.archiveAsMonolith,
|
archiveAsMonolith: lessSensitiveInfo.archiveAsMonolith,
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { prisma } from "@/lib/api/db";
|
import { prisma } from "@/lib/api/db";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
export default async function getUsers() {
|
export default async function getUsers(user: User) {
|
||||||
// Get all users
|
if (user.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1)) {
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
@ -17,5 +18,54 @@ export default async function getUsers() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return { response: users.sort((a: any, b: any) => a.id - b.id), status: 200 };
|
return {
|
||||||
|
response: users.sort((a: any, b: any) => a.id - b.id),
|
||||||
|
status: 200,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let subscriptionId = (
|
||||||
|
await prisma.subscription.findFirst({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)?.id;
|
||||||
|
|
||||||
|
if (!subscriptionId)
|
||||||
|
return {
|
||||||
|
response: "Subscription not found.",
|
||||||
|
status: 404,
|
||||||
|
};
|
||||||
|
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
parentSubscriptionId: subscriptionId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
subscriptions: {
|
||||||
|
id: subscriptionId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
username: true,
|
||||||
|
email: true,
|
||||||
|
emailVerified: true,
|
||||||
|
createdAt: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: users.sort((a: any, b: any) => a.id - b.id),
|
||||||
|
status: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,15 +4,19 @@ import removeFolder from "@/lib/api/storage/removeFolder";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import { DeleteUserBody } from "@/types/global";
|
import { DeleteUserBody } from "@/types/global";
|
||||||
import removeFile from "@/lib/api/storage/removeFile";
|
import removeFile from "@/lib/api/storage/removeFile";
|
||||||
|
import updateSeats from "@/lib/api/stripe/updateSeats";
|
||||||
|
|
||||||
export default async function deleteUserById(
|
export default async function deleteUserById(
|
||||||
userId: number,
|
userId: number,
|
||||||
body: DeleteUserBody,
|
body: DeleteUserBody,
|
||||||
isServerAdmin?: boolean
|
isServerAdmin: boolean,
|
||||||
|
queryId: number
|
||||||
) {
|
) {
|
||||||
// First, we retrieve the user from the database
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
|
include: {
|
||||||
|
subscriptions: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -23,22 +27,75 @@ export default async function deleteUserById(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isServerAdmin) {
|
if (!isServerAdmin) {
|
||||||
|
if (queryId === userId) {
|
||||||
if (user.password) {
|
if (user.password) {
|
||||||
const isPasswordValid = bcrypt.compareSync(body.password, user.password);
|
const isPasswordValid = bcrypt.compareSync(
|
||||||
|
body.password,
|
||||||
|
user.password
|
||||||
|
);
|
||||||
|
|
||||||
if (!isPasswordValid && !isServerAdmin) {
|
if (!isPasswordValid && !isServerAdmin) {
|
||||||
return {
|
return {
|
||||||
response: "Invalid credentials.",
|
response: "Invalid credentials.",
|
||||||
status: 401, // Unauthorized
|
status: 401,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
response:
|
response:
|
||||||
"User has no password. Please reset your password from the forgot password page.",
|
"User has no password. Please reset your password from the forgot password page.",
|
||||||
status: 401, // Unauthorized
|
status: 401,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (user.parentSubscriptionId) {
|
||||||
|
console.log(userId, user.parentSubscriptionId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: "Permission denied.",
|
||||||
|
status: 401,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
if (!user.subscriptions) {
|
||||||
|
return {
|
||||||
|
response: "User has no subscription.",
|
||||||
|
status: 401,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const findChild = await prisma.user.findFirst({
|
||||||
|
where: { id: queryId, parentSubscriptionId: user.subscriptions?.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!findChild)
|
||||||
|
return {
|
||||||
|
response: "Permission denied.",
|
||||||
|
status: 401,
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeUser = await prisma.user.update({
|
||||||
|
where: { id: findChild.id },
|
||||||
|
data: {
|
||||||
|
parentSubscription: {
|
||||||
|
disconnect: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSeats(
|
||||||
|
user.subscriptions.stripeSubscriptionId,
|
||||||
|
user.subscriptions.quantity - 1
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
response: removeUser,
|
||||||
|
status: 200,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the user and all related data within a transaction
|
// Delete the user and all related data within a transaction
|
||||||
|
@ -47,27 +104,27 @@ export default async function deleteUserById(
|
||||||
async (prisma) => {
|
async (prisma) => {
|
||||||
// Delete Access Tokens
|
// Delete Access Tokens
|
||||||
await prisma.accessToken.deleteMany({
|
await prisma.accessToken.deleteMany({
|
||||||
where: { userId },
|
where: { userId: queryId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete whitelisted users
|
// Delete whitelisted users
|
||||||
await prisma.whitelistedUser.deleteMany({
|
await prisma.whitelistedUser.deleteMany({
|
||||||
where: { userId },
|
where: { userId: queryId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete links
|
// Delete links
|
||||||
await prisma.link.deleteMany({
|
await prisma.link.deleteMany({
|
||||||
where: { collection: { ownerId: userId } },
|
where: { collection: { ownerId: queryId } },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete tags
|
// Delete tags
|
||||||
await prisma.tag.deleteMany({
|
await prisma.tag.deleteMany({
|
||||||
where: { ownerId: userId },
|
where: { ownerId: queryId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Find collections that the user owns
|
// Find collections that the user owns
|
||||||
const collections = await prisma.collection.findMany({
|
const collections = await prisma.collection.findMany({
|
||||||
where: { ownerId: userId },
|
where: { ownerId: queryId },
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
|
@ -86,29 +143,29 @@ export default async function deleteUserById(
|
||||||
|
|
||||||
// Delete collections after cleaning up related data
|
// Delete collections after cleaning up related data
|
||||||
await prisma.collection.deleteMany({
|
await prisma.collection.deleteMany({
|
||||||
where: { ownerId: userId },
|
where: { ownerId: queryId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete subscription
|
// Delete subscription
|
||||||
if (process.env.STRIPE_SECRET_KEY)
|
if (process.env.STRIPE_SECRET_KEY)
|
||||||
await prisma.subscription
|
await prisma.subscription
|
||||||
.delete({
|
.delete({
|
||||||
where: { userId },
|
where: { userId: queryId },
|
||||||
})
|
})
|
||||||
.catch((err) => console.log(err));
|
.catch((err) => console.log(err));
|
||||||
|
|
||||||
await prisma.usersAndCollections.deleteMany({
|
await prisma.usersAndCollections.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ userId: userId }, { collection: { ownerId: userId } }],
|
OR: [{ userId: queryId }, { collection: { ownerId: queryId } }],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete user's avatar
|
// Delete user's avatar
|
||||||
await removeFile({ filePath: `uploads/avatar/${userId}.jpg` });
|
await removeFile({ filePath: `uploads/avatar/${queryId}.jpg` });
|
||||||
|
|
||||||
// Finally, delete the user
|
// Finally, delete the user
|
||||||
await prisma.user.delete({
|
await prisma.user.delete({
|
||||||
where: { id: userId },
|
where: { id: queryId },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ timeout: 20000 }
|
{ timeout: 20000 }
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default async function getUserById(userId: number) {
|
||||||
whitelistedUsers: whitelistedUsernames,
|
whitelistedUsers: whitelistedUsernames,
|
||||||
subscription: {
|
subscription: {
|
||||||
active: subscriptions?.active,
|
active: subscriptions?.active,
|
||||||
|
quantity: subscriptions?.quantity,
|
||||||
},
|
},
|
||||||
parentSubscription: {
|
parentSubscription: {
|
||||||
active: parentSubscription?.active,
|
active: parentSubscription?.active,
|
||||||
|
|
|
@ -277,6 +277,7 @@ export default async function updateUserById(
|
||||||
image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "",
|
image: userInfo.image ? `${userInfo.image}?${Date.now()}` : "",
|
||||||
subscription: {
|
subscription: {
|
||||||
active: subscriptions?.active,
|
active: subscriptions?.active,
|
||||||
|
quantity: subscriptions?.quantity,
|
||||||
},
|
},
|
||||||
parentSubscription: {
|
parentSubscription: {
|
||||||
active: parentSubscription?.active,
|
active: parentSubscription?.active,
|
||||||
|
|
|
@ -2,9 +2,10 @@ import { CollectionIncludingMembersAndLinkCount, Member } from "@/types/global";
|
||||||
import getPublicUserData from "./getPublicUserData";
|
import getPublicUserData from "./getPublicUserData";
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import { TFunction } from "i18next";
|
import { TFunction } from "i18next";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
const addMemberToCollection = async (
|
const addMemberToCollection = async (
|
||||||
ownerUsername: string,
|
owner: User,
|
||||||
memberUsername: string,
|
memberUsername: string,
|
||||||
collection: CollectionIncludingMembersAndLinkCount,
|
collection: CollectionIncludingMembersAndLinkCount,
|
||||||
setMember: (newMember: Member) => null | undefined,
|
setMember: (newMember: Member) => null | undefined,
|
||||||
|
@ -12,7 +13,12 @@ const addMemberToCollection = async (
|
||||||
) => {
|
) => {
|
||||||
const checkIfMemberAlreadyExists = collection.members.find((e) => {
|
const checkIfMemberAlreadyExists = collection.members.find((e) => {
|
||||||
const username = (e.user.username || "").toLowerCase();
|
const username = (e.user.username || "").toLowerCase();
|
||||||
return username === memberUsername.toLowerCase();
|
const email = (e.user.email || "").toLowerCase();
|
||||||
|
|
||||||
|
return (
|
||||||
|
username === memberUsername.toLowerCase() ||
|
||||||
|
email === memberUsername.toLowerCase()
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -21,7 +27,8 @@ const addMemberToCollection = async (
|
||||||
// member can't be empty
|
// member can't be empty
|
||||||
memberUsername.trim() !== "" &&
|
memberUsername.trim() !== "" &&
|
||||||
// member can't be the owner
|
// member can't be the owner
|
||||||
memberUsername.trim().toLowerCase() !== ownerUsername.toLowerCase()
|
memberUsername.trim().toLowerCase() !== owner.username?.toLowerCase() &&
|
||||||
|
memberUsername.trim().toLowerCase() !== owner.email?.toLowerCase()
|
||||||
) {
|
) {
|
||||||
// Lookup, get data/err, list ...
|
// Lookup, get data/err, list ...
|
||||||
const user = await getPublicUserData(memberUsername.trim().toLowerCase());
|
const user = await getPublicUserData(memberUsername.trim().toLowerCase());
|
||||||
|
@ -37,12 +44,16 @@ const addMemberToCollection = async (
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
image: user.image,
|
image: user.image,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (checkIfMemberAlreadyExists) toast.error(t("user_already_member"));
|
} else if (checkIfMemberAlreadyExists) toast.error(t("user_already_member"));
|
||||||
else if (memberUsername.trim().toLowerCase() === ownerUsername.toLowerCase())
|
else if (
|
||||||
|
memberUsername.trim().toLowerCase() === owner.username?.toLowerCase() ||
|
||||||
|
memberUsername.trim().toLowerCase() === owner.email?.toLowerCase()
|
||||||
|
)
|
||||||
toast.error(t("you_are_already_collection_owner"));
|
toast.error(t("you_are_already_collection_owner"));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from "next-i18next";
|
||||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||||
import UserListing from "@/components/UserListing";
|
import UserListing from "@/components/UserListing";
|
||||||
import { useUsers } from "@/hooks/store/admin/users";
|
import { useUsers } from "@/hooks/store/admin/users";
|
||||||
|
import Divider from "@/components/ui/Divider";
|
||||||
|
|
||||||
interface User extends U {
|
interface User extends U {
|
||||||
subscriptions: {
|
subscriptions: {
|
||||||
|
@ -88,7 +89,7 @@ export default function Admin() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divider my-3"></div>
|
<Divider className="my-3" />
|
||||||
|
|
||||||
{filteredUsers && filteredUsers.length > 0 && searchQuery !== "" ? (
|
{filteredUsers && filteredUsers.length > 0 && searchQuery !== "" ? (
|
||||||
UserListing(filteredUsers, deleteUserModal, setDeleteUserModal, t)
|
UserListing(filteredUsers, deleteUserModal, setDeleteUserModal, t)
|
||||||
|
|
|
@ -11,6 +11,12 @@ const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
||||||
export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const token = await verifyToken({ req });
|
const token = await verifyToken({ req });
|
||||||
|
|
||||||
|
const queryId = Number(req.query.id);
|
||||||
|
|
||||||
|
if (!queryId) {
|
||||||
|
return res.status(400).json({ response: "Invalid request." });
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof token === "string") {
|
if (typeof token === "string") {
|
||||||
res.status(401).json({ response: token });
|
res.status(401).json({ response: token });
|
||||||
return null;
|
return null;
|
||||||
|
@ -24,12 +30,12 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|
||||||
const isServerAdmin = user?.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
const isServerAdmin = user?.id === Number(process.env.NEXT_PUBLIC_ADMIN || 1);
|
||||||
|
|
||||||
const userId = isServerAdmin ? Number(req.query.id) : token.id;
|
const userId = token.id;
|
||||||
|
|
||||||
if (userId !== Number(req.query.id) && !isServerAdmin)
|
|
||||||
return res.status(401).json({ response: "Permission denied." });
|
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
|
if (userId !== queryId && !isServerAdmin)
|
||||||
|
return res.status(401).json({ response: "Permission denied." });
|
||||||
|
|
||||||
const users = await getUserById(userId);
|
const users = await getUserById(userId);
|
||||||
return res.status(users.status).json({ response: users.response });
|
return res.status(users.status).json({ response: users.response });
|
||||||
}
|
}
|
||||||
|
@ -59,6 +65,9 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === "PUT") {
|
if (req.method === "PUT") {
|
||||||
|
if (userId !== queryId && !isServerAdmin)
|
||||||
|
return res.status(401).json({ response: "Permission denied." });
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_DEMO === "true")
|
if (process.env.NEXT_PUBLIC_DEMO === "true")
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
response:
|
response:
|
||||||
|
@ -74,7 +83,12 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
"This action is disabled because this is a read-only demo of Linkwarden.",
|
"This action is disabled because this is a read-only demo of Linkwarden.",
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = await deleteUserById(userId, req.body, isServerAdmin);
|
const updated = await deleteUserById(
|
||||||
|
userId,
|
||||||
|
req.body,
|
||||||
|
isServerAdmin,
|
||||||
|
queryId
|
||||||
|
);
|
||||||
return res.status(updated.status).json({ response: updated.response });
|
return res.status(updated.status).json({ response: updated.response });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,9 @@ export default async function users(req: NextApiRequest, res: NextApiResponse) {
|
||||||
} else if (req.method === "GET") {
|
} else if (req.method === "GET") {
|
||||||
const user = await verifyUser({ req, res });
|
const user = await verifyUser({ req, res });
|
||||||
|
|
||||||
if (!user || user.id !== Number(process.env.NEXT_PUBLIC_ADMIN || 1))
|
if (!user) return res.status(401).json({ response: "Unauthorized..." });
|
||||||
return res.status(401).json({ response: "Unauthorized..." });
|
|
||||||
|
|
||||||
const response = await getUsers();
|
const response = await getUsers(user);
|
||||||
return res.status(response.status).json({ response: response.response });
|
return res.status(response.status).json({ response: response.response });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import { toast } from "react-hot-toast";
|
||||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
import { useUpdateUser, useUser } from "@/hooks/store/user";
|
import { useUpdateUser, useUser } from "@/hooks/store/user";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
password: string;
|
password: string;
|
||||||
|
@ -28,8 +27,6 @@ export default function MemberOnboarding() {
|
||||||
const { data: user = {} } = useUser();
|
const { data: user = {} } = useUser();
|
||||||
const updateUser = useUpdateUser();
|
const updateUser = useUpdateUser();
|
||||||
|
|
||||||
const { status } = useSession();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
toast.success(t("accepted_invitation_please_fill"));
|
toast.success(t("accepted_invitation_please_fill"));
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -146,7 +143,7 @@ export default function MemberOnboarding() {
|
||||||
size="full"
|
size="full"
|
||||||
loading={submitLoader}
|
loading={submitLoader}
|
||||||
>
|
>
|
||||||
{t("sign_up")}
|
{t("continue_to_dashboard")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -67,10 +67,18 @@ export default function AccessTokens() {
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{new Date(token.createdAt || "").toLocaleDateString()}
|
{new Date(token.createdAt).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{new Date(token.expires || "").toLocaleDateString()}
|
{new Date(token.expires).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -1,8 +1,28 @@
|
||||||
import SettingsLayout from "@/layouts/SettingsLayout";
|
import SettingsLayout from "@/layouts/SettingsLayout";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import InviteModal from "@/components/ModalContent/InviteModal";
|
||||||
|
import { User as U } from "@prisma/client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
import getServerSideProps from "@/lib/client/getServerSideProps";
|
||||||
|
import { useUsers } from "@/hooks/store/admin/users";
|
||||||
|
import DeleteUserModal from "@/components/ModalContent/DeleteUserModal";
|
||||||
|
import { useUser } from "@/hooks/store/user";
|
||||||
|
import { dropdownTriggerer } from "@/lib/client/utils";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { signIn } from "next-auth/react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
|
||||||
|
interface User extends U {
|
||||||
|
subscriptions: {
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserModal = {
|
||||||
|
isOpen: boolean;
|
||||||
|
userId: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Billing() {
|
export default function Billing() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
@ -12,6 +32,25 @@ export default function Billing() {
|
||||||
if (!process.env.NEXT_PUBLIC_STRIPE) router.push("/settings/profile");
|
if (!process.env.NEXT_PUBLIC_STRIPE) router.push("/settings/profile");
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const { data: users = [] } = useUsers();
|
||||||
|
const { data: account } = useUser();
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [filteredUsers, setFilteredUsers] = useState<User[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (users.length > 0) {
|
||||||
|
setFilteredUsers(users);
|
||||||
|
}
|
||||||
|
}, [users]);
|
||||||
|
|
||||||
|
const [deleteUserModal, setDeleteUserModal] = useState<UserModal>({
|
||||||
|
isOpen: false,
|
||||||
|
userId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [inviteModal, setInviteModal] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsLayout>
|
<SettingsLayout>
|
||||||
<p className="capitalize text-3xl font-thin inline">
|
<p className="capitalize text-3xl font-thin inline">
|
||||||
|
@ -40,6 +79,190 @@ export default function Billing() {
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 w-full rounded-md h-8 mt-5">
|
||||||
|
<p className="truncate w-full pr-7 text-3xl font-thin">
|
||||||
|
{t("manage_seats")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider my-3"></div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between gap-2 mb-3 relative">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="search-box"
|
||||||
|
className="inline-flex items-center w-fit absolute left-1 pointer-events-none rounded-md p-1 text-primary"
|
||||||
|
>
|
||||||
|
<i className="bi-search"></i>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
id="search-box"
|
||||||
|
type="text"
|
||||||
|
placeholder={t("search_users")}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchQuery(e.target.value);
|
||||||
|
|
||||||
|
if (users) {
|
||||||
|
setFilteredUsers(
|
||||||
|
users.filter((user: any) =>
|
||||||
|
JSON.stringify(user)
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(e.target.value.toLowerCase())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="border border-neutral-content bg-base-200 focus:border-primary py-1 rounded-md pl-9 pr-2 w-full max-w-[15rem] md:w-[15rem] md:max-w-full duration-200 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<div
|
||||||
|
onClick={() => setInviteModal(true)}
|
||||||
|
className="flex items-center btn btn-accent dark:border-violet-400 text-white btn-sm px-2 h-[2.15rem] relative"
|
||||||
|
>
|
||||||
|
<p>{t("invite_user")}</p>
|
||||||
|
<i className="bi-plus text-2xl"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border rounded-md shadow border-neutral-content">
|
||||||
|
<table className="table bg-base-300 rounded-md">
|
||||||
|
<thead>
|
||||||
|
<tr className="sm:table-row hidden border-b-neutral-content">
|
||||||
|
{process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && (
|
||||||
|
<th>{t("email")}</th>
|
||||||
|
)}
|
||||||
|
{process.env.NEXT_PUBLIC_STRIPE === "true" && (
|
||||||
|
<th>{t("status")}</th>
|
||||||
|
)}
|
||||||
|
<th>{t("date_added")}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filteredUsers?.map((user, index) => (
|
||||||
|
<tr
|
||||||
|
key={index}
|
||||||
|
className={clsx(
|
||||||
|
"group border-b-neutral-content duration-100 w-full relative flex flex-col sm:table-row",
|
||||||
|
user.id !== account.id &&
|
||||||
|
"hover:bg-neutral-content hover:bg-opacity-30"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{process.env.NEXT_PUBLIC_EMAIL_PROVIDER === "true" && (
|
||||||
|
<td className="truncate max-w-full" title={user.email || ""}>
|
||||||
|
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
|
||||||
|
{t("email")}
|
||||||
|
</p>
|
||||||
|
<p>{user.email}</p>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
{process.env.NEXT_PUBLIC_STRIPE === "true" && (
|
||||||
|
<td>
|
||||||
|
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
|
||||||
|
{t("status")}
|
||||||
|
</p>
|
||||||
|
{user.emailVerified ? (
|
||||||
|
<p className="font-bold px-2 bg-green-600 text-white rounded-md w-fit">
|
||||||
|
{t("active")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="font-bold px-2 bg-neutral-content rounded-md w-fit">
|
||||||
|
{t("pending")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td>
|
||||||
|
<p className="sm:hidden block text-neutral text-xs font-bold mb-2">
|
||||||
|
{t("date_added")}
|
||||||
|
</p>
|
||||||
|
<p className="whitespace-nowrap">
|
||||||
|
{new Date(user.createdAt).toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
{user.id !== account.id && (
|
||||||
|
<td>
|
||||||
|
<div
|
||||||
|
className={`dropdown dropdown-bottom font-normal dropdown-end absolute right-[0.35rem] top-[0.35rem]`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onMouseDown={dropdownTriggerer}
|
||||||
|
className="btn btn-ghost btn-sm btn-square duration-100"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className={"bi bi-three-dots text-lg text-neutral"}
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
<ul className="dropdown-content z-[30] menu shadow bg-base-200 border border-neutral-content rounded-box mt-1">
|
||||||
|
{!user.emailVerified ? (
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(
|
||||||
|
document?.activeElement as HTMLElement
|
||||||
|
)?.blur();
|
||||||
|
signIn("invite", {
|
||||||
|
email: user.email,
|
||||||
|
callbackUrl: "/member-onboarding",
|
||||||
|
redirect: false,
|
||||||
|
}).then(() =>
|
||||||
|
toast.success(t("resend_invite_success"))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t("resend_invite")}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
) : undefined}
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={() => {
|
||||||
|
(document?.activeElement as HTMLElement)?.blur();
|
||||||
|
setDeleteUserModal({
|
||||||
|
isOpen: true,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
{t("remove_user")}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-center font-bold mt-3">
|
||||||
|
{t("seats_purchased", { count: account?.subscription?.quantity })}
|
||||||
|
</p>
|
||||||
|
{inviteModal && <InviteModal onClose={() => setInviteModal(false)} />}
|
||||||
|
{deleteUserModal.isOpen && deleteUserModal.userId && (
|
||||||
|
<DeleteUserModal
|
||||||
|
onClose={() => setDeleteUserModal({ isOpen: false, userId: null })}
|
||||||
|
userId={deleteUserModal.userId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</SettingsLayout>
|
</SettingsLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
108
pages/team.tsx
108
pages/team.tsx
|
@ -1,108 +0,0 @@
|
||||||
import InviteModal from "@/components/ModalContent/InviteModal";
|
|
||||||
import { User as U } from "@prisma/client";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import getServerSideProps from "@/lib/client/getServerSideProps";
|
|
||||||
import UserListing from "@/components/UserListing";
|
|
||||||
import { useUsers } from "@/hooks/store/admin/users";
|
|
||||||
|
|
||||||
interface User extends U {
|
|
||||||
subscriptions: {
|
|
||||||
active: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserModal = {
|
|
||||||
isOpen: boolean;
|
|
||||||
userId: number | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Admin() {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { data: users = [] } = useUsers();
|
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
|
||||||
const [filteredUsers, setFilteredUsers] = useState<User[]>();
|
|
||||||
|
|
||||||
const [deleteUserModal, setDeleteUserModal] = useState<UserModal>({
|
|
||||||
isOpen: false,
|
|
||||||
userId: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [inviteModal, setInviteModal] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-6xl mx-auto p-5">
|
|
||||||
<div className="flex sm:flex-row flex-col justify-between gap-2">
|
|
||||||
<div className="gap-2 inline-flex items-center">
|
|
||||||
<Link
|
|
||||||
href="/dashboard"
|
|
||||||
className="text-neutral btn btn-square btn-sm btn-ghost"
|
|
||||||
>
|
|
||||||
<i className="bi-chevron-left text-xl"></i>
|
|
||||||
</Link>
|
|
||||||
<p className="capitalize text-3xl font-thin inline">
|
|
||||||
{t("team_management")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center relative justify-between gap-2">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="search-box"
|
|
||||||
className="inline-flex items-center w-fit absolute left-1 pointer-events-none rounded-md p-1 text-primary"
|
|
||||||
>
|
|
||||||
<i className="bi-search"></i>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<input
|
|
||||||
id="search-box"
|
|
||||||
type="text"
|
|
||||||
placeholder={t("search_users")}
|
|
||||||
value={searchQuery}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearchQuery(e.target.value);
|
|
||||||
|
|
||||||
if (users) {
|
|
||||||
setFilteredUsers(
|
|
||||||
users.filter((user: any) =>
|
|
||||||
JSON.stringify(user)
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(e.target.value.toLowerCase())
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="border border-neutral-content bg-base-200 focus:border-primary py-1 rounded-md pl-9 pr-2 w-full max-w-[15rem] md:w-[15rem] md:max-w-full duration-200 outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
onClick={() => setInviteModal(true)}
|
|
||||||
className="flex items-center btn btn-accent dark:border-violet-400 text-white btn-sm px-2 aspect-square relative"
|
|
||||||
>
|
|
||||||
<i className="bi-plus text-3xl absolute"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divider my-3"></div>
|
|
||||||
|
|
||||||
{filteredUsers && filteredUsers.length > 0 && searchQuery !== "" ? (
|
|
||||||
UserListing(filteredUsers, deleteUserModal, setDeleteUserModal, t)
|
|
||||||
) : searchQuery !== "" ? (
|
|
||||||
<p>{t("no_user_found_in_search")}</p>
|
|
||||||
) : users && users.length > 0 ? (
|
|
||||||
UserListing(users, deleteUserModal, setDeleteUserModal, t)
|
|
||||||
) : (
|
|
||||||
<p>{t("no_users_found")}</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{inviteModal && <InviteModal onClose={() => setInviteModal(false)} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export { getServerSideProps };
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `teamRole` on the `User` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" DROP COLUMN "teamRole";
|
||||||
|
|
||||||
|
-- DropEnum
|
||||||
|
DROP TYPE "TeamRole";
|
|
@ -0,0 +1,8 @@
|
||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `teamId` on the `Collection` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Collection" DROP COLUMN "teamId";
|
|
@ -37,7 +37,6 @@ model User {
|
||||||
locale String @default("en")
|
locale String @default("en")
|
||||||
parentSubscription Subscription? @relation("ChildUsers", fields: [parentSubscriptionId], references: [id])
|
parentSubscription Subscription? @relation("ChildUsers", fields: [parentSubscriptionId], references: [id])
|
||||||
parentSubscriptionId Int?
|
parentSubscriptionId Int?
|
||||||
teamRole TeamRole @default(ADMIN)
|
|
||||||
accounts Account[]
|
accounts Account[]
|
||||||
collections Collection[]
|
collections Collection[]
|
||||||
tags Tag[]
|
tags Tag[]
|
||||||
|
@ -77,11 +76,6 @@ model WhitelistedUser {
|
||||||
updatedAt DateTime @default(now()) @updatedAt
|
updatedAt DateTime @default(now()) @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TeamRole {
|
|
||||||
MEMBER
|
|
||||||
ADMIN
|
|
||||||
}
|
|
||||||
|
|
||||||
model VerificationToken {
|
model VerificationToken {
|
||||||
identifier String
|
identifier String
|
||||||
token String @unique
|
token String @unique
|
||||||
|
@ -114,7 +108,6 @@ model Collection {
|
||||||
owner User @relation(fields: [ownerId], references: [id])
|
owner User @relation(fields: [ownerId], references: [id])
|
||||||
ownerId Int
|
ownerId Int
|
||||||
members UsersAndCollections[]
|
members UsersAndCollections[]
|
||||||
teamId Int?
|
|
||||||
createdBy User @relation("CreatedCollections", fields: [createdById], references: [id])
|
createdBy User @relation("CreatedCollections", fields: [createdById], references: [id])
|
||||||
createdById Int
|
createdById Int
|
||||||
links Link[]
|
links Link[]
|
||||||
|
|
|
@ -318,7 +318,7 @@
|
||||||
"sharable_link": "Sharable Link",
|
"sharable_link": "Sharable Link",
|
||||||
"copied": "Copied!",
|
"copied": "Copied!",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"members_username_placeholder": "Username (without the '@')",
|
"add_member_placeholder": "Add members by email or username",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
"admin": "Admin",
|
"admin": "Admin",
|
||||||
"contributor": "Contributor",
|
"contributor": "Contributor",
|
||||||
|
@ -397,14 +397,25 @@
|
||||||
"invalid_url_guide":"Please enter a valid Address for the Link. (It should start with http/https)",
|
"invalid_url_guide":"Please enter a valid Address for the Link. (It should start with http/https)",
|
||||||
"email_invalid": "Please enter a valid email address.",
|
"email_invalid": "Please enter a valid email address.",
|
||||||
"username_invalid_guide": "Username has to be at least 3 characters, no spaces and special characters are allowed.",
|
"username_invalid_guide": "Username has to be at least 3 characters, no spaces and special characters are allowed.",
|
||||||
"manage_team": "Manage Team",
|
|
||||||
"team_management": "Team Management",
|
"team_management": "Team Management",
|
||||||
"invite_user": "Invite User",
|
"invite_user": "Invite User",
|
||||||
|
"invite_users": "Invite Users",
|
||||||
"invite_user_desc": "To invite someone to your team, please enter their email address below:",
|
"invite_user_desc": "To invite someone to your team, please enter their email address below:",
|
||||||
"invite_user_note": "Please note that once the invitation is accepted, an additional seat will be used and your account will automatically be billed for this addition.",
|
"invite_user_note": "Please note that once the invitation is accepted, an additional seat will be purchased and your account will automatically be billed for this addition.",
|
||||||
"send_invitation": "Send Invitation",
|
"send_invitation": "Send Invitation",
|
||||||
"learn_more": "Learn more",
|
"learn_more": "Learn more",
|
||||||
"finalize_profile": "Finalize Your Profile",
|
"finalize_profile": "Finalize Your Profile",
|
||||||
"invitation_desc": "{{owner}} invited you to join Linkwarden. \nTo continue, please finish setting up your account.",
|
"invitation_desc": "{{owner}} invited you to join Linkwarden. \nTo continue, please finish setting up your account.",
|
||||||
"accepted_invitation_please_fill": "You've accepted the invitation to join Linkwarden. Please fill out the following to finalize your account."
|
"accepted_invitation_please_fill": "You've accepted the invitation to join Linkwarden. Please fill out the following to finalize your account.",
|
||||||
|
"status": "Status",
|
||||||
|
"pending": "Pending",
|
||||||
|
"active": "Active",
|
||||||
|
"manage_seats": "Manage Seats",
|
||||||
|
"seats_purchased": "{{count}} seats purchased",
|
||||||
|
"date_added": "Date Added",
|
||||||
|
"resend_invite": "Resend Invitation",
|
||||||
|
"resend_invite_success": "Invitation Resent!",
|
||||||
|
"remove_user": "Remove User",
|
||||||
|
"continue_to_dashboard": "Continue to Dashboard",
|
||||||
|
"confirm_user_removal_desc": "They will need to have a subscription to access Linkwarden again."
|
||||||
}
|
}
|
|
@ -32,7 +32,7 @@ export interface Member {
|
||||||
canCreate: boolean;
|
canCreate: boolean;
|
||||||
canUpdate: boolean;
|
canUpdate: boolean;
|
||||||
canDelete: boolean;
|
canDelete: boolean;
|
||||||
user: OptionalExcluding<User, "username" | "name" | "id">;
|
user: OptionalExcluding<User, "email" | "username" | "name" | "id">;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CollectionIncludingMembersAndLinkCount
|
export interface CollectionIncludingMembersAndLinkCount
|
||||||
|
|
Ŝarĝante…
Reference in New Issue